apcore-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.gitmessage +60 -0
  3. package/.pre-commit-config.yaml +28 -0
  4. package/CHANGELOG.md +47 -0
  5. package/CLAUDE.md +68 -0
  6. package/README.md +131 -0
  7. package/apcore-logo.svg +79 -0
  8. package/package.json +37 -0
  9. package/planning/acl-system/overview.md +54 -0
  10. package/planning/acl-system/plan.md +92 -0
  11. package/planning/acl-system/state.json +76 -0
  12. package/planning/acl-system/tasks/acl-core.md +226 -0
  13. package/planning/acl-system/tasks/acl-rule.md +92 -0
  14. package/planning/acl-system/tasks/conditional-rules.md +259 -0
  15. package/planning/acl-system/tasks/pattern-matching.md +152 -0
  16. package/planning/acl-system/tasks/yaml-loading.md +271 -0
  17. package/planning/core-executor/overview.md +53 -0
  18. package/planning/core-executor/plan.md +88 -0
  19. package/planning/core-executor/state.json +76 -0
  20. package/planning/core-executor/tasks/async-support.md +106 -0
  21. package/planning/core-executor/tasks/execution-pipeline.md +113 -0
  22. package/planning/core-executor/tasks/redaction.md +85 -0
  23. package/planning/core-executor/tasks/safety-checks.md +65 -0
  24. package/planning/core-executor/tasks/setup.md +75 -0
  25. package/planning/decorator-bindings/overview.md +62 -0
  26. package/planning/decorator-bindings/plan.md +104 -0
  27. package/planning/decorator-bindings/state.json +87 -0
  28. package/planning/decorator-bindings/tasks/binding-directory.md +79 -0
  29. package/planning/decorator-bindings/tasks/binding-loader.md +148 -0
  30. package/planning/decorator-bindings/tasks/explicit-schemas.md +85 -0
  31. package/planning/decorator-bindings/tasks/function-module.md +127 -0
  32. package/planning/decorator-bindings/tasks/module-factory.md +89 -0
  33. package/planning/decorator-bindings/tasks/schema-modes.md +142 -0
  34. package/planning/middleware-system/overview.md +48 -0
  35. package/planning/middleware-system/plan.md +102 -0
  36. package/planning/middleware-system/state.json +65 -0
  37. package/planning/middleware-system/tasks/adapters.md +170 -0
  38. package/planning/middleware-system/tasks/base.md +115 -0
  39. package/planning/middleware-system/tasks/logging-middleware.md +304 -0
  40. package/planning/middleware-system/tasks/manager.md +313 -0
  41. package/planning/observability/overview.md +53 -0
  42. package/planning/observability/plan.md +119 -0
  43. package/planning/observability/state.json +98 -0
  44. package/planning/observability/tasks/context-logger.md +201 -0
  45. package/planning/observability/tasks/exporters.md +121 -0
  46. package/planning/observability/tasks/metrics-collector.md +162 -0
  47. package/planning/observability/tasks/metrics-middleware.md +141 -0
  48. package/planning/observability/tasks/obs-logging-middleware.md +179 -0
  49. package/planning/observability/tasks/span-model.md +120 -0
  50. package/planning/observability/tasks/tracing-middleware.md +179 -0
  51. package/planning/overview.md +81 -0
  52. package/planning/registry-system/overview.md +57 -0
  53. package/planning/registry-system/plan.md +114 -0
  54. package/planning/registry-system/state.json +109 -0
  55. package/planning/registry-system/tasks/dependencies.md +157 -0
  56. package/planning/registry-system/tasks/entry-point.md +148 -0
  57. package/planning/registry-system/tasks/metadata.md +198 -0
  58. package/planning/registry-system/tasks/registry-core.md +323 -0
  59. package/planning/registry-system/tasks/scanner.md +172 -0
  60. package/planning/registry-system/tasks/schema-export.md +261 -0
  61. package/planning/registry-system/tasks/types.md +124 -0
  62. package/planning/registry-system/tasks/validation.md +177 -0
  63. package/planning/schema-system/overview.md +56 -0
  64. package/planning/schema-system/plan.md +121 -0
  65. package/planning/schema-system/state.json +98 -0
  66. package/planning/schema-system/tasks/exporter.md +153 -0
  67. package/planning/schema-system/tasks/loader.md +106 -0
  68. package/planning/schema-system/tasks/ref-resolver.md +133 -0
  69. package/planning/schema-system/tasks/strict-mode.md +140 -0
  70. package/planning/schema-system/tasks/typebox-generation.md +133 -0
  71. package/planning/schema-system/tasks/types-and-annotations.md +160 -0
  72. package/planning/schema-system/tasks/validator.md +149 -0
  73. package/src/acl.ts +188 -0
  74. package/src/bindings.ts +208 -0
  75. package/src/config.ts +24 -0
  76. package/src/context.ts +75 -0
  77. package/src/decorator.ts +110 -0
  78. package/src/errors.ts +369 -0
  79. package/src/executor.ts +348 -0
  80. package/src/index.ts +81 -0
  81. package/src/middleware/adapters.ts +54 -0
  82. package/src/middleware/base.ts +33 -0
  83. package/src/middleware/index.ts +6 -0
  84. package/src/middleware/logging.ts +103 -0
  85. package/src/middleware/manager.ts +105 -0
  86. package/src/module.ts +41 -0
  87. package/src/observability/context-logger.ts +201 -0
  88. package/src/observability/index.ts +4 -0
  89. package/src/observability/metrics.ts +212 -0
  90. package/src/observability/tracing.ts +187 -0
  91. package/src/registry/dependencies.ts +99 -0
  92. package/src/registry/entry-point.ts +64 -0
  93. package/src/registry/index.ts +8 -0
  94. package/src/registry/metadata.ts +111 -0
  95. package/src/registry/registry.ts +314 -0
  96. package/src/registry/scanner.ts +150 -0
  97. package/src/registry/schema-export.ts +177 -0
  98. package/src/registry/types.ts +32 -0
  99. package/src/registry/validation.ts +38 -0
  100. package/src/schema/annotations.ts +67 -0
  101. package/src/schema/exporter.ts +93 -0
  102. package/src/schema/index.ts +14 -0
  103. package/src/schema/loader.ts +270 -0
  104. package/src/schema/ref-resolver.ts +235 -0
  105. package/src/schema/strict.ts +128 -0
  106. package/src/schema/types.ts +73 -0
  107. package/src/schema/validator.ts +82 -0
  108. package/src/utils/index.ts +1 -0
  109. package/src/utils/pattern.ts +30 -0
  110. package/tests/helpers.ts +30 -0
  111. package/tests/integration/test-acl-safety.test.ts +268 -0
  112. package/tests/integration/test-binding-executor.test.ts +194 -0
  113. package/tests/integration/test-e2e-flow.test.ts +117 -0
  114. package/tests/integration/test-error-propagation.test.ts +259 -0
  115. package/tests/integration/test-middleware-chain.test.ts +120 -0
  116. package/tests/integration/test-observability-integration.test.ts +438 -0
  117. package/tests/observability/test-context-logger.test.ts +123 -0
  118. package/tests/observability/test-metrics.test.ts +89 -0
  119. package/tests/observability/test-tracing.test.ts +131 -0
  120. package/tests/registry/test-dependencies.test.ts +70 -0
  121. package/tests/registry/test-entry-point.test.ts +133 -0
  122. package/tests/registry/test-metadata.test.ts +265 -0
  123. package/tests/registry/test-registry.test.ts +140 -0
  124. package/tests/registry/test-scanner.test.ts +257 -0
  125. package/tests/registry/test-schema-export.test.ts +224 -0
  126. package/tests/registry/test-validation.test.ts +75 -0
  127. package/tests/schema/test-loader.test.ts +97 -0
  128. package/tests/schema/test-ref-resolver.test.ts +105 -0
  129. package/tests/schema/test-strict.test.ts +139 -0
  130. package/tests/schema/test-validator.test.ts +64 -0
  131. package/tests/test-acl.test.ts +206 -0
  132. package/tests/test-bindings.test.ts +227 -0
  133. package/tests/test-config.test.ts +76 -0
  134. package/tests/test-context.test.ts +151 -0
  135. package/tests/test-decorator.test.ts +173 -0
  136. package/tests/test-errors.test.ts +204 -0
  137. package/tests/test-executor.test.ts +252 -0
  138. package/tests/test-middleware-manager.test.ts +185 -0
  139. package/tests/test-middleware.test.ts +86 -0
  140. package/tsconfig.build.json +8 -0
  141. package/tsconfig.json +20 -0
  142. package/vitest.config.ts +18 -0
@@ -0,0 +1,119 @@
1
+ # Implementation Plan: Observability
2
+
3
+ ## Goal
4
+
5
+ Implement the three pillars of observability -- tracing, metrics, and logging -- as middleware-integrated components that provide runtime visibility into the apcore execution pipeline. Each pillar operates through a `Middleware` subclass that uses stack-based state in the shared `context.data` record to handle nested module-to-module calls correctly.
6
+
7
+ ## Architecture Design
8
+
9
+ ### Component Structure
10
+
11
+ The observability module is organized into three source files, each housing one pillar and its associated middleware:
12
+
13
+ - **Tracing** (`observability/tracing.ts`, ~190 lines) -- `Span` interface describing a trace span with `traceId`, `spanId`, `parentSpanId`, timing, status, attributes, and events. `createSpan()` factory function generates spans with `randomBytes(8).toString('hex')` for span IDs. `SpanExporter` interface with two implementations: `StdoutExporter` (JSON.stringify to console.log) and `InMemoryExporter` (bounded array, shift()-based eviction, configurable max 10,000). `TracingMiddleware` extends `Middleware` with stack-based span management and 4 sampling strategies.
14
+
15
+ - **Metrics** (`observability/metrics.ts`, ~210 lines) -- `MetricsCollector` class with counter (`increment`) and histogram (`observe`) primitives. Uses string-encoded composite keys in the format `name|key1=val1,key2=val2` stored in `Map<string, number>`. No thread locking required (Node.js single-threaded). Histogram support with configurable buckets (13 Prometheus-standard defaults), `+Inf` bucket, and full Prometheus text format export. Convenience methods `incrementCalls()`, `incrementErrors()`, `observeDuration()` for apcore-standard metrics. `MetricsMiddleware` extends `Middleware` with stack-based `performance.now()` timing.
16
+
17
+ - **Logging** (`observability/context-logger.ts`, ~200 lines) -- `ContextLogger` class with 6 log levels (trace/debug/info/warn/error/fatal mapped to numeric values 0-50), JSON and text output formats, automatic `_secret_`-prefixed key redaction, and `fromContext()` static factory that binds trace ID, module ID, and caller ID. Output targets a `WritableOutput` interface (defaults to `process.stderr`). `ObsLoggingMiddleware` extends `Middleware` with stack-based timing and configurable input/output logging.
18
+
19
+ ### Three Pillars Architecture
20
+
21
+ ```
22
+ Executor Pipeline
23
+ |
24
+ +--------------+--------------+
25
+ | | |
26
+ TracingMiddleware MetricsMiddleware ObsLoggingMiddleware
27
+ | | |
28
+ SpanExporter MetricsCollector ContextLogger
29
+ / \ | |
30
+ StdoutExp InMemoryExp Prometheus JSON/Text
31
+ Export Output
32
+ ```
33
+
34
+ ### Data Flow
35
+
36
+ Each middleware hooks into the executor pipeline at three points: `before()`, `after()`, and `onError()`. State is carried across nested calls via the shared `context.data` record using reserved keys:
37
+
38
+ 1. **TracingMiddleware** stores a span stack at `context.data['_tracing_spans']` and a sampling decision at `context.data['_tracing_sampled']`. On `before()`, a new span is created and pushed onto the stack with the current top-of-stack span as parent. On `after()`/`onError()`, the span is popped, finalized with end time and status, and conditionally exported based on the sampling decision.
39
+
40
+ 2. **MetricsMiddleware** stores a timing stack at `context.data['_metrics_starts']`. On `before()`, `performance.now()` is pushed. On `after()`/`onError()`, the start time is popped and duration computed as `(performance.now() - startTime) / 1000` (converted to seconds for Prometheus conventions).
41
+
42
+ 3. **ObsLoggingMiddleware** stores a timing stack at `context.data['_obs_logging_starts']`. On `before()`, `performance.now()` is pushed and a "Module call started" log entry is emitted. On `after()`, the start time is popped, duration computed in milliseconds, and "Module call completed" is emitted. On `onError()`, "Module call failed" is emitted with error details.
43
+
44
+ ### Technical Choices and Rationale
45
+
46
+ - **`performance.now()` for timing**: Provides monotonic millisecond resolution. Unlike `Date.now()`, it is not affected by system clock adjustments. Unlike Python's `time.time()` (wall-clock seconds), this returns monotonic milliseconds, so MetricsMiddleware divides by 1000 for Prometheus-standard seconds.
47
+
48
+ - **String-encoded composite keys**: `MetricsCollector` stores counters and histogram data using keys in the format `name|key1=val1,key2=val2`. This avoids the overhead of nested Maps or object hashing and allows simple Map lookups. Labels are sorted alphabetically for deterministic key generation.
49
+
50
+ - **No thread locking**: Node.js runs JavaScript on a single thread. Unlike the Python implementation which requires threading locks for metric state, no synchronization primitives are needed here.
51
+
52
+ - **`randomBytes(8).toString('hex')` for span IDs**: Produces 16-character hex strings (64 bits of entropy). Uses Node.js `node:crypto` for cryptographic randomness rather than `crypto.randomUUID().slice(0,16)` to avoid partial UUID collisions.
53
+
54
+ - **Bounded array with `shift()` for InMemoryExporter**: Simple FIFO eviction. Python uses `collections.deque(maxlen=...)` which is O(1) for left-pop; JavaScript `Array.shift()` is O(n) but acceptable for the expected span volumes. A ring buffer could be a future optimization.
55
+
56
+ - **No OTel bridge**: An `OTLPExporter` is not yet implemented. The `SpanExporter` interface is designed to be compatible with future OpenTelemetry integration, but no OTel SDK dependency exists today.
57
+
58
+ - **`WritableOutput` interface for logger**: Abstracts the output target to support both `process.stderr` (default) and test buffers. Avoids coupling to Node.js streams directly.
59
+
60
+ ## Task Breakdown
61
+
62
+ ```mermaid
63
+ graph TD
64
+ T1[span-model] --> T2[exporters]
65
+ T1 --> T3[tracing-middleware]
66
+ T2 --> T3
67
+ T4[metrics-collector] --> T5[metrics-middleware]
68
+ T6[context-logger] --> T7[obs-logging-middleware]
69
+ ```
70
+
71
+ | Task ID | Title | Estimated Time | Dependencies |
72
+ |---------|-------|---------------|--------------|
73
+ | span-model | Span interface and createSpan() factory | 1h | none |
74
+ | exporters | StdoutExporter and InMemoryExporter | 1.5h | span-model |
75
+ | tracing-middleware | TracingMiddleware with sampling strategies | 3h | span-model, exporters |
76
+ | metrics-collector | MetricsCollector with Prometheus export | 3h | none |
77
+ | metrics-middleware | MetricsMiddleware with stack-based timing | 2h | metrics-collector |
78
+ | context-logger | ContextLogger with formats and redaction | 2h | none |
79
+ | obs-logging-middleware | ObsLoggingMiddleware with structured logging | 2h | context-logger |
80
+
81
+ ## Risks and Considerations
82
+
83
+ - **`Array.shift()` performance in InMemoryExporter**: Eviction via `shift()` is O(n) per call when the buffer is full. For high-throughput tracing with maxSpans in the thousands, this is acceptable. If maxSpans grows to tens of thousands, consider a ring buffer implementation.
84
+ - **Missing OTLPExporter**: Production deployments expecting OpenTelemetry Protocol export will need a custom `SpanExporter` implementation or a future `OTLPExporter` addition. The `SpanExporter` interface is stable and ready for this extension.
85
+ - **Prometheus export is in-memory only**: `MetricsCollector.exportPrometheus()` generates text on demand. There is no HTTP endpoint or push gateway integration. Consumers must call `exportPrometheus()` and serve or ship the output themselves.
86
+ - **Sampling decision propagation**: The `_tracing_sampled` flag is set once per context (on first `before()` call) and inherited by all nested spans. This means proportional sampling is all-or-nothing per trace, not per span. The `error_first` strategy overrides this by always exporting error spans regardless of the sampling decision.
87
+ - **Logger output blocking**: `process.stderr.write()` is synchronous for small payloads on most platforms. High-volume logging could introduce backpressure in latency-sensitive paths.
88
+
89
+ ## Acceptance Criteria
90
+
91
+ - [x] `Span` interface defines all required fields (traceId, spanId, parentSpanId, name, startTime, endTime, status, attributes, events)
92
+ - [x] `createSpan()` generates unique 16-hex-char span IDs via `randomBytes(8).toString('hex')`
93
+ - [x] `StdoutExporter` serializes spans via `JSON.stringify` to `console.log`
94
+ - [x] `InMemoryExporter` respects `maxSpans` limit with FIFO eviction via `shift()`
95
+ - [x] `InMemoryExporter.getSpans()` returns a defensive copy (spread into new array)
96
+ - [x] `TracingMiddleware` supports all 4 sampling strategies: full, proportional, error_first, off
97
+ - [x] `TracingMiddleware` validates sampling rate (0.0-1.0) and strategy on construction
98
+ - [x] Nested spans correctly chain via `parentSpanId` using stack-based management
99
+ - [x] `error_first` strategy always exports error spans even when not sampled
100
+ - [x] `MetricsCollector` supports counter increment and histogram observe operations
101
+ - [x] String-encoded keys use sorted labels for deterministic composite key generation
102
+ - [x] `exportPrometheus()` produces valid Prometheus text format with HELP, TYPE, buckets, sum, count
103
+ - [x] `MetricsMiddleware` records calls, errors, and duration via stack-based timing
104
+ - [x] Duration is converted from milliseconds to seconds for Prometheus conventions
105
+ - [x] `ContextLogger` supports JSON and text output formats with level filtering
106
+ - [x] `_secret_`-prefixed keys are automatically redacted to `***REDACTED***`
107
+ - [x] `fromContext()` binds traceId, moduleId, and callerId from a `Context` instance
108
+ - [x] `ObsLoggingMiddleware` emits structured start/complete/error log entries with timing
109
+ - [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
110
+
111
+ ## References
112
+
113
+ - `src/observability/tracing.ts` -- Span model, exporters, and TracingMiddleware
114
+ - `src/observability/metrics.ts` -- MetricsCollector and MetricsMiddleware
115
+ - `src/observability/context-logger.ts` -- ContextLogger and ObsLoggingMiddleware
116
+ - `src/observability/index.ts` -- Public API barrel exports
117
+ - `tests/observability/test-tracing.test.ts` -- Tracing tests
118
+ - `tests/observability/test-metrics.test.ts` -- Metrics tests
119
+ - `tests/observability/test-context-logger.test.ts` -- Logger tests
@@ -0,0 +1,98 @@
1
+ {
2
+ "feature": "observability",
3
+ "created": "2026-02-16T00:00:00Z",
4
+ "updated": "2026-02-16T00:00:00Z",
5
+ "status": "completed",
6
+ "execution_order": [
7
+ "span-model",
8
+ "exporters",
9
+ "tracing-middleware",
10
+ "metrics-collector",
11
+ "metrics-middleware",
12
+ "context-logger",
13
+ "obs-logging-middleware"
14
+ ],
15
+ "progress": {
16
+ "total_tasks": 7,
17
+ "completed": 7,
18
+ "in_progress": 0,
19
+ "pending": 0
20
+ },
21
+ "tasks": [
22
+ {
23
+ "id": "span-model",
24
+ "file": "tasks/span-model.md",
25
+ "title": "Span Interface and createSpan() Factory",
26
+ "status": "completed",
27
+ "started_at": "2026-02-16T08:00:00Z",
28
+ "completed_at": "2026-02-16T09:00:00Z",
29
+ "assignee": null,
30
+ "commits": []
31
+ },
32
+ {
33
+ "id": "exporters",
34
+ "file": "tasks/exporters.md",
35
+ "title": "StdoutExporter and InMemoryExporter",
36
+ "status": "completed",
37
+ "started_at": "2026-02-16T09:00:00Z",
38
+ "completed_at": "2026-02-16T10:30:00Z",
39
+ "assignee": null,
40
+ "commits": []
41
+ },
42
+ {
43
+ "id": "tracing-middleware",
44
+ "file": "tasks/tracing-middleware.md",
45
+ "title": "TracingMiddleware with Sampling Strategies",
46
+ "status": "completed",
47
+ "started_at": "2026-02-16T10:30:00Z",
48
+ "completed_at": "2026-02-16T13:30:00Z",
49
+ "assignee": null,
50
+ "commits": []
51
+ },
52
+ {
53
+ "id": "metrics-collector",
54
+ "file": "tasks/metrics-collector.md",
55
+ "title": "MetricsCollector with Counters, Histograms, Prometheus Export",
56
+ "status": "completed",
57
+ "started_at": "2026-02-16T08:00:00Z",
58
+ "completed_at": "2026-02-16T11:00:00Z",
59
+ "assignee": null,
60
+ "commits": []
61
+ },
62
+ {
63
+ "id": "metrics-middleware",
64
+ "file": "tasks/metrics-middleware.md",
65
+ "title": "MetricsMiddleware with Stack-Based Timing",
66
+ "status": "completed",
67
+ "started_at": "2026-02-16T11:00:00Z",
68
+ "completed_at": "2026-02-16T13:00:00Z",
69
+ "assignee": null,
70
+ "commits": []
71
+ },
72
+ {
73
+ "id": "context-logger",
74
+ "file": "tasks/context-logger.md",
75
+ "title": "ContextLogger with JSON/Text Formats and Redaction",
76
+ "status": "completed",
77
+ "started_at": "2026-02-16T08:00:00Z",
78
+ "completed_at": "2026-02-16T10:00:00Z",
79
+ "assignee": null,
80
+ "commits": []
81
+ },
82
+ {
83
+ "id": "obs-logging-middleware",
84
+ "file": "tasks/obs-logging-middleware.md",
85
+ "title": "ObsLoggingMiddleware with Structured Logging",
86
+ "status": "completed",
87
+ "started_at": "2026-02-16T10:00:00Z",
88
+ "completed_at": "2026-02-16T12:00:00Z",
89
+ "assignee": null,
90
+ "commits": []
91
+ }
92
+ ],
93
+ "metadata": {
94
+ "source_doc": "planning/features/observability.md",
95
+ "created_by": "code-forge",
96
+ "version": "1.0"
97
+ }
98
+ }
@@ -0,0 +1,201 @@
1
+ # Task: ContextLogger with JSON/Text Formats and Redaction
2
+
3
+ ## Goal
4
+
5
+ Implement `ContextLogger` that provides structured logging with JSON and text output formats, numeric level filtering (trace/debug/info/warn/error/fatal), automatic redaction of `_secret_`-prefixed keys, and a `fromContext()` static factory method that binds trace, module, and caller metadata from a `Context` instance.
6
+
7
+ ## Files Involved
8
+
9
+ - `src/observability/context-logger.ts` -- ContextLogger class, LEVELS map, WritableOutput interface
10
+ - `src/context.ts` -- Context class (dependency for `fromContext()`)
11
+ - `tests/observability/test-context-logger.test.ts` -- Unit tests for ContextLogger
12
+
13
+ ## Steps (TDD)
14
+
15
+ ### 1. Write failing tests for basic logging
16
+
17
+ ```typescript
18
+ function createBufferOutput() {
19
+ const lines: string[] = [];
20
+ return {
21
+ output: { write: (s: string) => lines.push(s) },
22
+ lines,
23
+ };
24
+ }
25
+
26
+ describe('ContextLogger', () => {
27
+ it('logs JSON format by default', () => {
28
+ const { output, lines } = createBufferOutput();
29
+ const logger = new ContextLogger({ output });
30
+ logger.info('test message');
31
+ expect(lines).toHaveLength(1);
32
+ const parsed = JSON.parse(lines[0]);
33
+ expect(parsed.level).toBe('info');
34
+ expect(parsed.message).toBe('test message');
35
+ expect(parsed.timestamp).toBeDefined();
36
+ expect(parsed.logger).toBe('apcore');
37
+ });
38
+
39
+ it('logs text format', () => {
40
+ const { output, lines } = createBufferOutput();
41
+ const logger = new ContextLogger({ format: 'text', output });
42
+ logger.info('test message');
43
+ expect(lines[0]).toContain('[INFO]');
44
+ expect(lines[0]).toContain('test message');
45
+ });
46
+ });
47
+ ```
48
+
49
+ ### 2. Define WritableOutput interface and level map
50
+
51
+ ```typescript
52
+ interface WritableOutput {
53
+ write(s: string): void;
54
+ }
55
+
56
+ const LEVELS: Record<string, number> = {
57
+ trace: 0,
58
+ debug: 10,
59
+ info: 20,
60
+ warn: 30,
61
+ error: 40,
62
+ fatal: 50,
63
+ };
64
+ ```
65
+
66
+ ### 3. Implement ContextLogger constructor with options
67
+
68
+ ```typescript
69
+ export class ContextLogger {
70
+ constructor(options?: {
71
+ name?: string; // default: 'apcore'
72
+ format?: string; // 'json' (default) or 'text'
73
+ level?: string; // minimum level, default: 'info'
74
+ redactSensitive?: boolean; // default: true
75
+ output?: WritableOutput; // default: process.stderr
76
+ }) { /* ... */ }
77
+ }
78
+ ```
79
+
80
+ ### 4. Write failing tests for level filtering
81
+
82
+ ```typescript
83
+ it('respects log level filtering', () => {
84
+ const { output, lines } = createBufferOutput();
85
+ const logger = new ContextLogger({ level: 'warn', output });
86
+ logger.debug('should not appear');
87
+ logger.info('should not appear');
88
+ logger.warn('should appear');
89
+ logger.error('should appear');
90
+ expect(lines).toHaveLength(2);
91
+ });
92
+ ```
93
+
94
+ ### 5. Implement _emit() with level check and dual format output
95
+
96
+ The internal `_emit()` method:
97
+ 1. Checks if the message level meets the minimum threshold
98
+ 2. Redacts `_secret_`-prefixed keys in the `extra` record if `redactSensitive` is true
99
+ 3. Builds a log entry record with `timestamp`, `level`, `message`, `trace_id`, `module_id`, `caller_id`, `logger`, `extra`
100
+ 4. Serializes as JSON (`JSON.stringify + newline`) or text (`timestamp [LEVEL] [trace=...] [module=...] message extras`)
101
+
102
+ ### 6. Write failing tests for redaction
103
+
104
+ ```typescript
105
+ it('redacts _secret_ prefix keys', () => {
106
+ const { output, lines } = createBufferOutput();
107
+ const logger = new ContextLogger({ output });
108
+ logger.info('test', { _secret_token: 'abc123', name: 'Bob' });
109
+ const parsed = JSON.parse(lines[0]);
110
+ expect(parsed.extra._secret_token).toBe('***REDACTED***');
111
+ expect(parsed.extra.name).toBe('Bob');
112
+ });
113
+
114
+ it('does not redact when disabled', () => {
115
+ const { output, lines } = createBufferOutput();
116
+ const logger = new ContextLogger({ output, redactSensitive: false });
117
+ logger.info('test', { _secret_token: 'abc123' });
118
+ const parsed = JSON.parse(lines[0]);
119
+ expect(parsed.extra._secret_token).toBe('abc123');
120
+ });
121
+ ```
122
+
123
+ ### 7. Implement _secret_ redaction in _emit()
124
+
125
+ ```typescript
126
+ if (extra != null && this._redactSensitive) {
127
+ redactedExtra = {};
128
+ for (const [k, v] of Object.entries(extra)) {
129
+ redactedExtra[k] = k.startsWith('_secret_') ? '***REDACTED***' : v;
130
+ }
131
+ }
132
+ ```
133
+
134
+ ### 8. Write failing tests for fromContext()
135
+
136
+ ```typescript
137
+ it('fromContext sets trace/module/caller', () => {
138
+ const { output, lines } = createBufferOutput();
139
+ const ctx = Context.create(undefined, createIdentity('user1'));
140
+ const childCtx = ctx.child('mod.test');
141
+ const logger = ContextLogger.fromContext(childCtx, 'test-logger', { output });
142
+ logger.info('context log');
143
+ const parsed = JSON.parse(lines[0]);
144
+ expect(parsed.trace_id).toBe(ctx.traceId);
145
+ expect(parsed.module_id).toBe('mod.test');
146
+ expect(parsed.logger).toBe('test-logger');
147
+ });
148
+ ```
149
+
150
+ ### 9. Implement fromContext() static factory
151
+
152
+ ```typescript
153
+ static fromContext(context: Context, name: string, options?: {
154
+ format?: string;
155
+ level?: string;
156
+ redactSensitive?: boolean;
157
+ output?: WritableOutput;
158
+ }): ContextLogger {
159
+ const logger = new ContextLogger({ name, ...options });
160
+ logger._traceId = context.traceId;
161
+ logger._moduleId = context.callChain.length > 0
162
+ ? context.callChain[context.callChain.length - 1]
163
+ : null;
164
+ logger._callerId = context.callerId;
165
+ return logger;
166
+ }
167
+ ```
168
+
169
+ ### 10. Implement all 6 log level methods
170
+
171
+ ```typescript
172
+ trace(message: string, extra?: Record<string, unknown>): void { this._emit('trace', message, extra); }
173
+ debug(message: string, extra?: Record<string, unknown>): void { this._emit('debug', message, extra); }
174
+ info(message: string, extra?: Record<string, unknown>): void { this._emit('info', message, extra); }
175
+ warn(message: string, extra?: Record<string, unknown>): void { this._emit('warn', message, extra); }
176
+ error(message: string, extra?: Record<string, unknown>): void { this._emit('error', message, extra); }
177
+ fatal(message: string, extra?: Record<string, unknown>): void { this._emit('fatal', message, extra); }
178
+ ```
179
+
180
+ ### 11. Run tests and verify all pass
181
+
182
+ ## Acceptance Criteria
183
+
184
+ - [x] `ContextLogger` supports JSON (default) and text output formats
185
+ - [x] 6 log levels: trace (0), debug (10), info (20), warn (30), error (40), fatal (50)
186
+ - [x] Level filtering suppresses messages below the configured minimum level
187
+ - [x] `_secret_`-prefixed extra keys redacted to `***REDACTED***` when `redactSensitive` is true (default)
188
+ - [x] Redaction can be disabled via `redactSensitive: false`
189
+ - [x] `fromContext()` binds `traceId`, `moduleId` (last in callChain), and `callerId` from a Context
190
+ - [x] JSON format includes: timestamp, level, message, trace_id, module_id, caller_id, logger, extra
191
+ - [x] Text format: `timestamp [LEVEL] [trace=...] [module=...] message extras`
192
+ - [x] Default output is `process.stderr`; custom `WritableOutput` accepted via options
193
+ - [x] All tests pass with `vitest`
194
+
195
+ ## Dependencies
196
+
197
+ - None (independent pillar, but uses `Context` from core-executor for `fromContext()`)
198
+
199
+ ## Estimated Time
200
+
201
+ 2 hours
@@ -0,0 +1,121 @@
1
+ # Task: StdoutExporter and InMemoryExporter
2
+
3
+ ## Goal
4
+
5
+ Implement two `SpanExporter` implementations: `StdoutExporter` that serializes spans to stdout via `JSON.stringify`, and `InMemoryExporter` that stores spans in a bounded array with FIFO eviction. Note: `OTLPExporter` (OpenTelemetry Protocol) is not implemented -- this is a known gap for future work.
6
+
7
+ ## Files Involved
8
+
9
+ - `src/observability/tracing.ts` -- StdoutExporter and InMemoryExporter classes
10
+ - `tests/observability/test-tracing.test.ts` -- Unit tests for both exporters
11
+
12
+ ## Steps (TDD)
13
+
14
+ ### 1. Write failing tests for InMemoryExporter
15
+
16
+ ```typescript
17
+ describe('InMemoryExporter', () => {
18
+ it('collects and retrieves spans', () => {
19
+ const exporter = new InMemoryExporter();
20
+ const span = createSpan({ traceId: 't1', name: 'test', startTime: 0 });
21
+ exporter.export(span);
22
+ expect(exporter.getSpans()).toHaveLength(1);
23
+ expect(exporter.getSpans()[0].traceId).toBe('t1');
24
+ });
25
+
26
+ it('respects max_spans limit', () => {
27
+ const exporter = new InMemoryExporter(3);
28
+ for (let i = 0; i < 5; i++) {
29
+ exporter.export(createSpan({ traceId: `t${i}`, name: 'test', startTime: i }));
30
+ }
31
+ const spans = exporter.getSpans();
32
+ expect(spans).toHaveLength(3);
33
+ expect(spans[0].traceId).toBe('t2'); // oldest evicted
34
+ });
35
+
36
+ it('clear removes all spans', () => {
37
+ const exporter = new InMemoryExporter();
38
+ exporter.export(createSpan({ traceId: 't1', name: 'test', startTime: 0 }));
39
+ exporter.clear();
40
+ expect(exporter.getSpans()).toHaveLength(0);
41
+ });
42
+
43
+ it('getSpans returns defensive copy', () => {
44
+ const exporter = new InMemoryExporter();
45
+ exporter.export(createSpan({ traceId: 't1', name: 'test', startTime: 0 }));
46
+ const spans = exporter.getSpans();
47
+ spans.pop(); // mutate the copy
48
+ expect(exporter.getSpans()).toHaveLength(1); // original unaffected
49
+ });
50
+ });
51
+ ```
52
+
53
+ ### 2. Implement StdoutExporter
54
+
55
+ ```typescript
56
+ export class StdoutExporter implements SpanExporter {
57
+ export(span: Span): void {
58
+ console.log(JSON.stringify(span));
59
+ }
60
+ }
61
+ ```
62
+
63
+ Simple passthrough: serialize the span as a JSON string and write to stdout via `console.log`. No buffering or batching.
64
+
65
+ ### 3. Implement InMemoryExporter
66
+
67
+ ```typescript
68
+ export class InMemoryExporter implements SpanExporter {
69
+ private _spans: Span[] = [];
70
+ private _maxSpans: number;
71
+
72
+ constructor(maxSpans: number = 10_000) {
73
+ this._maxSpans = maxSpans;
74
+ }
75
+
76
+ export(span: Span): void {
77
+ this._spans.push(span);
78
+ while (this._spans.length > this._maxSpans) {
79
+ this._spans.shift(); // FIFO eviction
80
+ }
81
+ }
82
+
83
+ getSpans(): Span[] {
84
+ return [...this._spans]; // defensive copy
85
+ }
86
+
87
+ clear(): void {
88
+ this._spans = [];
89
+ }
90
+ }
91
+ ```
92
+
93
+ Key design decisions:
94
+ - Default `maxSpans` of 10,000 (configurable via constructor)
95
+ - `shift()` for FIFO eviction (O(n) but acceptable for expected volumes, unlike Python's O(1) `deque.popleft()`)
96
+ - `getSpans()` returns a spread copy to prevent external mutation of internal state
97
+ - No thread locking needed (Node.js single-threaded)
98
+
99
+ ### 4. Run tests and verify all pass
100
+
101
+ ## Known Gap: OTLPExporter
102
+
103
+ The Python implementation includes an `OTLPExporter` for shipping spans to OpenTelemetry-compatible backends. This is **not yet implemented** in the TypeScript version. The `SpanExporter` interface is designed to be forward-compatible -- a future `OTLPExporter` would implement `export(span: Span): void` and handle HTTP/gRPC transport to an OTLP collector.
104
+
105
+ ## Acceptance Criteria
106
+
107
+ - [x] `StdoutExporter` implements `SpanExporter` and calls `console.log(JSON.stringify(span))`
108
+ - [x] `InMemoryExporter` stores spans in a bounded array with configurable `maxSpans` (default 10,000)
109
+ - [x] FIFO eviction via `shift()` when array exceeds `maxSpans`
110
+ - [x] `getSpans()` returns a defensive copy via spread operator
111
+ - [x] `clear()` empties the internal span array
112
+ - [x] OTLPExporter absence is documented as a known gap
113
+ - [x] All tests pass with `vitest`
114
+
115
+ ## Dependencies
116
+
117
+ - **span-model** -- Requires `Span` interface and `SpanExporter` interface
118
+
119
+ ## Estimated Time
120
+
121
+ 1.5 hours