Initial Commit
This commit is contained in:
113
openspec/archive/2026-04-18-enterprise-logger/proposal.md
Normal file
113
openspec/archive/2026-04-18-enterprise-logger/proposal.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# OpenSpec Change: enterprise-logger
|
||||
|
||||
- **Change ID**: enterprise-logger
|
||||
- **Package**: `@suaveplan/logger`
|
||||
- **Category**: universal
|
||||
- **Target path**: `packages/universal/logger`
|
||||
- **Status**: IN PROGRESS
|
||||
- **Date**: 2026-04-18
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/logger.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Greenfield implementation of `@suaveplan/logger` for the genesis branch. The existing main-branch logger (v1.23) provides the baseline public-API surface to preserve; this change adds enterprise-grade structured JSON output, declarative redaction, head/tail/rule-based sampling, OpenTelemetry correlation, pluggable sinks, property-based tests, and benchmarks.
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build.
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Available |
|
||||
|---------|-----------|
|
||||
| `@suaveplan/error` | ✅ |
|
||||
| `@suaveplan/events` | ✅ |
|
||||
| `@suaveplan/types` | ✅ |
|
||||
| `@suaveplan/timing` | ❌ not yet built — use `Clock` from `@suaveplan/types` |
|
||||
| `@suaveplan/storage` | ❌ not yet built — keep `StorageInterface` locally |
|
||||
| `@suaveplan/streams` | ❌ not yet built — implement bounded buffer internally |
|
||||
| `@suaveplan/opentelemetry` | ❌ not yet built — use `@opentelemetry/api` directly |
|
||||
| `@suaveplan/feature-flags` | ❌ not yet built — skip |
|
||||
| `@suaveplan/schema-registry` | ❌ not yet built — export Zod schemas directly |
|
||||
|
||||
External deps (via catalog):
|
||||
|
||||
- `zod` → `catalog:forms`
|
||||
- `@opentelemetry/api` → `catalog:observability`
|
||||
- `tslib` → `catalog:build`
|
||||
|
||||
Dev deps (via catalog):
|
||||
|
||||
- `vitest`, `@vitest/coverage-istanbul` → `catalog:testing`
|
||||
- `fast-check` → `catalog:testing`
|
||||
- `mitata` → `catalog:testing`
|
||||
|
||||
## Preserved public API surface (from main branch)
|
||||
|
||||
All of the following must be exported from the genesis build:
|
||||
|
||||
### Types
|
||||
|
||||
- `LogLevel`, `LogEntry<TMetadata>`, `LoggerConfig<TMetadata>`, `LoggerMetrics`
|
||||
- `ConsoleLoggerConfig<TMetadata>`
|
||||
- `EventLoggerConfig<TMetadata>`, `LoggerEvents<TMetadata>`
|
||||
- `StorageLoggerConfig<TMetadata>`, `StorageInterface`
|
||||
- `ConsoleLoggerFactoryConfig<TMetadata>`, `StorageLoggerFactoryConfig<TMetadata>`, `EventLoggerFactoryConfig<TMetadata>`, `LoggerFactoryConfig<TMetadata>`
|
||||
- `InitLoggerOptions<TMetadata>`, `InitLoggerResult`
|
||||
- `SeverityLevelMap`, `UninstallErrorInterceptor`, `InstallErrorInterceptorOptions`
|
||||
|
||||
### Classes
|
||||
|
||||
- `AbstractLogger<TMetadata>` (abstract)
|
||||
- `ConsoleLogger<TMetadata>`
|
||||
- `EventLogger<TMetadata>`
|
||||
- `StorageLogger<TMetadata>`
|
||||
|
||||
### Interfaces
|
||||
|
||||
- `Logger<TMetadata>`
|
||||
- `LogLevels`
|
||||
|
||||
### Functions
|
||||
|
||||
- `createLogger(config)` (overloaded factory)
|
||||
- `initLogger(options?)` → `InitLoggerResult`
|
||||
- `installSuaveplanErrorInterceptor(logger?)` → `UninstallErrorInterceptor`
|
||||
- `__resetDefaultLoggerForTests()`
|
||||
|
||||
## New additions
|
||||
|
||||
### Modules
|
||||
|
||||
- **`record/`** — `LogRecord` structured JSON schema (Zod), `ts`/`level`/`msg`/`ctx`/`trace_id`/`span_id`/`error` fields
|
||||
- **`errors/`** — `LoggerError` extending `SuaveplanError`; codes: `LOGGER.SINK`, `LOGGER.SERIALIZE`, `LOGGER.CONFIG`, `LOGGER.FLUSH`
|
||||
- **`redaction/`** — `RedactionEngine` with field-path rules (dot-notation + glob), regex rules, depth limit; `@sensitive`-aware via `isSensitive()` from `@suaveplan/error`
|
||||
- **`sampling/`** — `HeadSampler` (deterministic per trace_id hash), `TailSampler` (bounded buffer + decision callback), `RuleSampler` (predicate-based); `SamplingPolicy` union type
|
||||
- **`sink/`** — `Sink` interface; `ConsoleSink` (ANSI dev output); `JsonSink` (NDJSON writer); `BufferedSink` (bounded queue, drop/block policy, `AbortSignal` flush)
|
||||
- **`otel/`** — `getOtelTraceContext()` reads `trace_id`/`span_id` from active OTel span (no-op when no exporter)
|
||||
- **`schemas/`** — Zod schemas for `LoggerConfig`, `LogRecord`, all sink configs (re-exported)
|
||||
|
||||
### Cross-cutting enterprise requirements
|
||||
|
||||
- Injectable `clock` (from `@suaveplan/types#Clock`) and `rng` on all time/random usages
|
||||
- `AbortSignal` support on `flush()` and `getLogs()`
|
||||
- `sideEffects: false` + tree-shake test
|
||||
- Property-based tests (fast-check) for redaction invariants and sampling determinism
|
||||
- mitata benchmarks in `src/__benchmarks__/`
|
||||
- OTel counters + histograms on `log()` calls (via `@opentelemetry/api`, no-op when unregistered)
|
||||
- Zero `any`, zero `ts-ignore`, zero mock usage in tests
|
||||
|
||||
## Dependency-layer check
|
||||
|
||||
`@suaveplan/logger` depends on `@suaveplan/error` and `@suaveplan/events`, both of which are enterprise-complete on genesis. ✅
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `bunx turbo lint typecheck test:coverage --filter=@suaveplan/logger` green with ≥ 100% coverage on touched modules
|
||||
- [x] `bun run docs:check --filter=@suaveplan/logger` reports ≥ 90%
|
||||
- [x] All preserved public API surface exported from `src/index.ts`
|
||||
- [x] New `LogRecord` schema, `LoggerError`, redaction, sampling, sink, OTel modules implemented
|
||||
- [x] Zero `any`, `ts-ignore`, `TODO`, `FIXME`, `@deprecated`, `.skip`, `.only` in `src/`
|
||||
- [x] Property-based tests for redaction invariants and sampling determinism
|
||||
- [x] mitata benchmarks in `src/__benchmarks__/`
|
||||
- [x] `sideEffects: false` confirmed, tree-shake test passes
|
||||
@@ -0,0 +1,158 @@
|
||||
# Capability Spec: Logging
|
||||
|
||||
**Package**: `@suaveplan/logger`
|
||||
**Version**: 0.0.0 (genesis)
|
||||
**Status**: ADDED
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### LG-01 — Log levels
|
||||
|
||||
The logger MUST support exactly four severity levels in ascending order: `debug < info < warn < error`. Messages below the configured minimum level are silently dropped. The level is runtime-configurable via `setLevel(level)`.
|
||||
|
||||
### LG-02 — Structured log record
|
||||
|
||||
Every emitted log record MUST conform to the `LogRecord` Zod schema:
|
||||
|
||||
```ts
|
||||
{
|
||||
ts: string; // ISO-8601 UTC timestamp (from injected clock)
|
||||
level: LogLevel; // "debug" | "info" | "warn" | "error"
|
||||
msg: string; // human-readable message
|
||||
ctx: Record<string, unknown>; // merged context (tags, metadata)
|
||||
trace_id?: string; // hex-encoded OTel trace ID (16 bytes)
|
||||
span_id?: string; // hex-encoded OTel span ID (8 bytes)
|
||||
error?: unknown; // serialized error payload (from @suaveplan/error toJSON)
|
||||
}
|
||||
```
|
||||
|
||||
### LG-03 — Pluggable sinks
|
||||
|
||||
The logger MUST route each `LogRecord` through an ordered array of `Sink` instances. A `Sink` MUST implement:
|
||||
|
||||
```ts
|
||||
interface Sink {
|
||||
write(record: LogRecord): void | Promise<void>;
|
||||
flush?(signal?: AbortSignal): Promise<void>;
|
||||
close?(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### LG-04 — Console sink
|
||||
|
||||
`ConsoleSink` MUST produce human-readable output to `console` with:
|
||||
|
||||
- ANSI colour-coding per level (configurable; off in non-TTY/CI)
|
||||
- ISO timestamp prefix (configurable)
|
||||
- Tags inline as `#tag` tokens
|
||||
- Metadata as a compact JSON suffix
|
||||
|
||||
### LG-05 — JSON (NDJSON) sink
|
||||
|
||||
`JsonSink` MUST serialize each `LogRecord` as a single line of JSON terminated by `\n` and pass it to a pluggable `WritableStream`-compatible writer or `(line: string) => void` callback.
|
||||
|
||||
### LG-06 — Buffered sink
|
||||
|
||||
`BufferedSink` MUST wrap any `Sink` with:
|
||||
|
||||
- A configurable maximum queue depth (default: 1 000 records)
|
||||
- Two overflow policies: `"drop"` (silently discard; increment counter) and `"block"` (await drain before accepting next record)
|
||||
- `flush(signal?: AbortSignal)` that drains the queue, honouring the `AbortSignal`
|
||||
|
||||
### LG-07 — Redaction engine
|
||||
|
||||
`RedactionEngine` MUST:
|
||||
|
||||
- Accept a list of `RedactionRule` items specifying field paths (dot-notation, glob `*` wildcard) to replace with the string `"[REDACTED]"` before the record reaches any sink
|
||||
- Detect `Sensitive<T>` wrappers (from `@suaveplan/error/redact`) and redact them automatically
|
||||
- Never mutate the original context object — always return a deep-cloned, redacted copy
|
||||
- Apply redaction before OTel context attachment and sink dispatch
|
||||
|
||||
### LG-08 — Sampling
|
||||
|
||||
The logger MUST support a pluggable `SamplingPolicy`:
|
||||
|
||||
| Sampler | Behaviour |
|
||||
|---------|-----------|
|
||||
| `HeadSampler` | Deterministic: hash `trace_id` against a `rate ∈ [0,1]`; no-op if no `trace_id` |
|
||||
| `TailSampler` | Buffer up to `N` records per trace; invoke user-supplied `decide(records)` callback and emit/drop the buffer |
|
||||
| `RuleSampler` | Ordered predicates `(record) => boolean | undefined`; first match wins; fallback to`SampleAll` |
|
||||
|
||||
When a sampling policy returns `"drop"`, the record is not passed to redaction or sinks but all OTel metrics are still incremented.
|
||||
|
||||
### LG-09 — OpenTelemetry correlation
|
||||
|
||||
When `@opentelemetry/api` has an active span, `getOtelTraceContext()` MUST return `{ trace_id, span_id }` as lowercase hex strings. When no span is active or `@opentelemetry/api` is not installed, it MUST return `{}` (no error thrown).
|
||||
|
||||
### LG-10 — Injectable clock and RNG
|
||||
|
||||
`AbstractLogger` MUST accept optional `clock: Clock` (from `@suaveplan/types`) and `rng: Rng` (from `@suaveplan/types`) in its config. Defaults: `Date` for clock, `crypto.getRandomValues` for rng. No `Date.now()` or `Math.random()` at module scope.
|
||||
|
||||
### LG-11 — Metrics
|
||||
|
||||
`AbstractLogger` MUST expose `getMetrics()` returning:
|
||||
|
||||
```ts
|
||||
{
|
||||
totalLogs: number;
|
||||
logsByLevel: Record<LogLevel, number>;
|
||||
droppedBySampling: number;
|
||||
droppedByBackpressure: number;
|
||||
lastLogTime?: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### LG-12 — OTel instrumentation
|
||||
|
||||
For every `log()` call the implementation MUST:
|
||||
|
||||
- Increment a `logger.records_total` counter (attributes: `level`)
|
||||
- Record a `logger.record_size_bytes` histogram (attributes: `level`)
|
||||
- Increment `logger.dropped_total` when a record is dropped by sampling or backpressure (attributes: `reason: "sampling" | "backpressure"`)
|
||||
|
||||
All via `@opentelemetry/api` — no-op when no exporter is registered.
|
||||
|
||||
### LG-13 — Structured errors
|
||||
|
||||
All errors thrown by `@suaveplan/logger` MUST be instances of `LoggerError extends SuaveplanError` with stable codes registered via `defineCodes`:
|
||||
|
||||
| Code constant | Trigger |
|
||||
|---------------|---------|
|
||||
| `LOGGER.SINK` | A sink's `write()` or `flush()` threw |
|
||||
| `LOGGER.SERIALIZE` | JSON serialisation of a log record failed |
|
||||
| `LOGGER.CONFIG` | Invalid configuration detected at construction time |
|
||||
| `LOGGER.FLUSH` | `flush()` aborted via `AbortSignal` |
|
||||
|
||||
### LG-14 — i18n / keyed messages
|
||||
|
||||
Log messages MUST be plain strings. Callers MAY pass a message key plus interpolation args as `{ key: string; args?: Record<string, unknown> }` — the logger resolves them via the `MessageResolver` from `@suaveplan/types` if one is installed. Unresolved keys are emitted as-is.
|
||||
|
||||
### LG-15 — Error bridge
|
||||
|
||||
`installSuaveplanErrorInterceptor(logger?)` MUST register an interceptor with `@suaveplan/error` that routes every `SuaveplanError` to the logger at the severity dictated by `err.severity`. The registration MUST be idempotent — re-calling returns the existing uninstall function.
|
||||
|
||||
### LG-16 — Factory
|
||||
|
||||
`createLogger(config)` MUST be an overloaded factory returning the concrete logger type (`ConsoleLogger | StorageLogger | EventLogger`) based on the `type` discriminant in config.
|
||||
|
||||
### LG-17 — Init helper
|
||||
|
||||
`initLogger(options?)` MUST construct a `ConsoleLogger` (or accept a pre-built logger), wire the error bridge, and return `{ logger, uninstall }`.
|
||||
|
||||
### LG-18 — Property-based tests
|
||||
|
||||
The following MUST have fast-check property tests:
|
||||
|
||||
- Redaction: `∀ record, rules → redacted record contains no unredacted sensitive fields`
|
||||
- Sampling: `HeadSampler is deterministic: same trace_id + rate always gives same decision`
|
||||
- JSON round-trip: `∀ record → JSON.parse(JSON.stringify(record))` satisfies the `LogRecord` Zod schema
|
||||
|
||||
### LG-19 — Benchmarks
|
||||
|
||||
`src/__benchmarks__/logger.bench.ts` MUST exercise the hot path (`log()` → redaction → sampling → `JsonSink`) with mitata and record throughput (records/s). CI regression gate: > 10% slowdown vs baseline fails.
|
||||
|
||||
### LG-20 — Tree-shake safety
|
||||
|
||||
`sideEffects: false` MUST be set. A test importing only `import type { Logger } from "@suaveplan/logger"` MUST produce a zero-byte bundle (or type-only import). The concrete classes MUST be importable individually via subpath exports without pulling in the full bundle.
|
||||
45
openspec/archive/2026-04-18-enterprise-logger/tasks.md
Normal file
45
openspec/archive/2026-04-18-enterprise-logger/tasks.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Tasks: enterprise-logger
|
||||
|
||||
Status key: ✅ done | 🔄 in progress | ⬜ pending
|
||||
|
||||
## Wave 1 — Scaffold
|
||||
|
||||
- ⬜ 1.1 Create `packages/universal/logger/` with `package.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `biome.json`
|
||||
- ⬜ 1.2 Create `src/index.ts` barrel (empty, filled incrementally)
|
||||
|
||||
## Wave 2 — Foundation modules
|
||||
|
||||
- ⬜ 2.1 `src/errors/` — `LoggerError` + `LOGGER_CODES`
|
||||
- ⬜ 2.2 `src/record/` — `LogRecord` type + Zod schemas
|
||||
- ⬜ 2.3 `src/otel/` — `getOtelTraceContext()`
|
||||
- ⬜ 2.4 `src/redaction/` — `RedactionEngine` + rules + property tests
|
||||
- ⬜ 2.5 `src/sampling/` — `HeadSampler`, `TailSampler`, `RuleSampler` + property tests
|
||||
|
||||
## Wave 3 — Sink layer
|
||||
|
||||
- ⬜ 3.1 `src/sink/sink` — `Sink` interface + `SinkOptions`
|
||||
- ⬜ 3.2 `src/sink/console-sink` — ANSI dev output
|
||||
- ⬜ 3.3 `src/sink/json-sink` — NDJSON writer
|
||||
- ⬜ 3.4 `src/sink/buffered-sink` — bounded queue, drop/block, `AbortSignal` flush
|
||||
|
||||
## Wave 4 — Logger core (refactored)
|
||||
|
||||
- ⬜ 4.1 `src/abstract/` — `Logger` interface + `AbstractLogger` with Sink[], redaction, sampling, OTel, clock injection
|
||||
- ⬜ 4.2 `src/console/` — `ConsoleLogger` (wraps `ConsoleSink`)
|
||||
- ⬜ 4.3 `src/event/` — `EventLogger` (emits via `@suaveplan/events`)
|
||||
- ⬜ 4.4 `src/storage/` — `StorageLogger` (batched `StorageInterface` writes)
|
||||
|
||||
## Wave 5 — High-level API
|
||||
|
||||
- ⬜ 5.1 `src/factory/` — `createLogger()` overloaded factory
|
||||
- ⬜ 5.2 `src/error-bridge/` — `installSuaveplanErrorInterceptor()`
|
||||
- ⬜ 5.3 `src/init/` — `initLogger()`
|
||||
|
||||
## Wave 6 — Quality
|
||||
|
||||
- ⬜ 6.1 Wire all exports in `src/index.ts`
|
||||
- ⬜ 6.2 Add `src/__benchmarks__/logger.bench.ts` (mitata)
|
||||
- ⬜ 6.3 Add tree-shake test
|
||||
- ⬜ 6.4 Run pre-publish gate: `bunx turbo lint typecheck test:coverage --filter=@suaveplan/logger`
|
||||
- ⬜ 6.5 Fix any issues found in gate
|
||||
- ⬜ 6.6 Update `CLAUDE.md` package listing + root `README.md`
|
||||
Reference in New Issue
Block a user