@typokit/otel 0.1.4

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.
@@ -0,0 +1,310 @@
1
+ import { describe, it, expect } from "@rstest/core";
2
+ import { StructuredLogger, StdoutSink } from "./logger.js";
3
+ import { redactFields } from "./redact.js";
4
+ import type { LogEntry, LogSink } from "./types.js";
5
+
6
+ /** Test sink that captures log entries for assertions */
7
+ class TestSink implements LogSink {
8
+ entries: LogEntry[] = [];
9
+ write(entry: LogEntry): void {
10
+ this.entries.push(entry);
11
+ }
12
+ }
13
+
14
+ describe("StructuredLogger", () => {
15
+ it("should implement all six log levels", () => {
16
+ const sink = new TestSink();
17
+ const logger = new StructuredLogger({ level: "trace" }, {}, [sink]);
18
+
19
+ logger.trace("t");
20
+ logger.debug("d");
21
+ logger.info("i");
22
+ logger.warn("w");
23
+ logger.error("e");
24
+ logger.fatal("f");
25
+
26
+ expect(sink.entries).toHaveLength(6);
27
+ expect(sink.entries.map((e) => e.level)).toEqual([
28
+ "trace",
29
+ "debug",
30
+ "info",
31
+ "warn",
32
+ "error",
33
+ "fatal",
34
+ ]);
35
+ });
36
+
37
+ it("should produce structured JSON entries with timestamp and message", () => {
38
+ const sink = new TestSink();
39
+ const logger = new StructuredLogger({ level: "trace" }, {}, [sink]);
40
+
41
+ logger.info("hello world", { key: "value" });
42
+
43
+ const entry = sink.entries[0];
44
+ expect(entry.level).toBe("info");
45
+ expect(entry.message).toBe("hello world");
46
+ expect(entry.timestamp).toBeDefined();
47
+ expect(entry.data).toEqual({ key: "value" });
48
+ // Verify timestamp is ISO format
49
+ expect(() => new Date(entry.timestamp)).not.toThrow();
50
+ });
51
+
52
+ it("should include request metadata in log entries", () => {
53
+ const sink = new TestSink();
54
+ const logger = new StructuredLogger(
55
+ { level: "trace" },
56
+ {
57
+ traceId: "abc-123",
58
+ route: "POST /users",
59
+ phase: "handler",
60
+ requestId: "req-456",
61
+ serverAdapter: "native",
62
+ },
63
+ [sink],
64
+ );
65
+
66
+ logger.info("test");
67
+
68
+ const entry = sink.entries[0];
69
+ expect(entry.traceId).toBe("abc-123");
70
+ expect(entry.route).toBe("POST /users");
71
+ expect(entry.phase).toBe("handler");
72
+ expect(entry.requestId).toBe("req-456");
73
+ expect(entry.serverAdapter).toBe("native");
74
+ });
75
+
76
+ it("should omit undefined metadata fields from entries", () => {
77
+ const sink = new TestSink();
78
+ const logger = new StructuredLogger(
79
+ { level: "trace" },
80
+ { requestId: "req-1" },
81
+ [sink],
82
+ );
83
+
84
+ logger.info("test");
85
+
86
+ const entry = sink.entries[0];
87
+ expect(entry.requestId).toBe("req-1");
88
+ expect("traceId" in entry).toBe(false);
89
+ expect("route" in entry).toBe(false);
90
+ expect("phase" in entry).toBe(false);
91
+ expect("serverAdapter" in entry).toBe(false);
92
+ });
93
+
94
+ it("should filter log entries below the configured level", () => {
95
+ const sink = new TestSink();
96
+ const logger = new StructuredLogger({ level: "warn" }, {}, [sink]);
97
+
98
+ logger.trace("t");
99
+ logger.debug("d");
100
+ logger.info("i");
101
+ logger.warn("w");
102
+ logger.error("e");
103
+ logger.fatal("f");
104
+
105
+ expect(sink.entries).toHaveLength(3);
106
+ expect(sink.entries.map((e) => e.level)).toEqual([
107
+ "warn",
108
+ "error",
109
+ "fatal",
110
+ ]);
111
+ });
112
+
113
+ it("should default to info level in production", () => {
114
+ const proc = (
115
+ globalThis as unknown as {
116
+ process?: { env?: Record<string, string | undefined> };
117
+ }
118
+ ).process;
119
+ const originalEnv = proc?.env?.["NODE_ENV"];
120
+ if (proc?.env) {
121
+ proc.env["NODE_ENV"] = "production";
122
+ }
123
+
124
+ try {
125
+ const sink = new TestSink();
126
+ // No explicit level — should default to info in production
127
+ const logger = new StructuredLogger({}, {}, [sink]);
128
+
129
+ logger.trace("t");
130
+ logger.debug("d");
131
+ logger.info("i");
132
+ logger.warn("w");
133
+
134
+ expect(sink.entries).toHaveLength(2);
135
+ expect(sink.entries.map((e) => e.level)).toEqual(["info", "warn"]);
136
+ } finally {
137
+ if (proc?.env) {
138
+ if (originalEnv !== undefined) {
139
+ proc.env["NODE_ENV"] = originalEnv;
140
+ } else {
141
+ delete proc.env["NODE_ENV"];
142
+ }
143
+ }
144
+ }
145
+ });
146
+
147
+ it("should default to debug level in development", () => {
148
+ const proc = (
149
+ globalThis as unknown as {
150
+ process?: { env?: Record<string, string | undefined> };
151
+ }
152
+ ).process;
153
+ const originalEnv = proc?.env?.["NODE_ENV"];
154
+ if (proc?.env) {
155
+ proc.env["NODE_ENV"] = "development";
156
+ }
157
+
158
+ try {
159
+ const sink = new TestSink();
160
+ const logger = new StructuredLogger({}, {}, [sink]);
161
+
162
+ logger.trace("t");
163
+ logger.debug("d");
164
+ logger.info("i");
165
+
166
+ expect(sink.entries).toHaveLength(2);
167
+ expect(sink.entries.map((e) => e.level)).toEqual(["debug", "info"]);
168
+ } finally {
169
+ if (proc?.env) {
170
+ if (originalEnv !== undefined) {
171
+ proc.env["NODE_ENV"] = originalEnv;
172
+ } else {
173
+ delete proc.env["NODE_ENV"];
174
+ }
175
+ }
176
+ }
177
+ });
178
+
179
+ it("should redact sensitive fields in log data", () => {
180
+ const sink = new TestSink();
181
+ const logger = new StructuredLogger(
182
+ { level: "trace", redact: ["*.password", "*.token", "authorization"] },
183
+ {},
184
+ [sink],
185
+ );
186
+
187
+ logger.info("auth attempt", {
188
+ email: "user@example.com",
189
+ password: "secret123",
190
+ token: "jwt-abc",
191
+ authorization: "Bearer xyz",
192
+ });
193
+
194
+ const data = sink.entries[0].data as Record<string, unknown>;
195
+ expect(data["email"]).toBe("user@example.com");
196
+ expect(data["password"]).toBe("[REDACTED]");
197
+ expect(data["token"]).toBe("[REDACTED]");
198
+ expect(data["authorization"]).toBe("[REDACTED]");
199
+ });
200
+
201
+ it("should redact nested sensitive fields", () => {
202
+ const sink = new TestSink();
203
+ const logger = new StructuredLogger(
204
+ { level: "trace", redact: ["*.password"] },
205
+ {},
206
+ [sink],
207
+ );
208
+
209
+ logger.info("nested data", {
210
+ user: { name: "Alice", password: "secret" },
211
+ meta: { count: 1 },
212
+ });
213
+
214
+ const data = sink.entries[0].data as Record<string, unknown>;
215
+ const user = data["user"] as Record<string, unknown>;
216
+ expect(user["name"]).toBe("Alice");
217
+ expect(user["password"]).toBe("[REDACTED]");
218
+ const meta = data["meta"] as Record<string, unknown>;
219
+ expect(meta["count"]).toBe(1);
220
+ });
221
+
222
+ it("should create child logger with additional metadata", () => {
223
+ const sink = new TestSink();
224
+ const parent = new StructuredLogger(
225
+ { level: "trace" },
226
+ { requestId: "req-1", serverAdapter: "native" },
227
+ [sink],
228
+ );
229
+
230
+ const child = parent.child({ phase: "handler", route: "GET /items" });
231
+ child.info("from child");
232
+
233
+ const entry = sink.entries[0];
234
+ expect(entry.requestId).toBe("req-1");
235
+ expect(entry.serverAdapter).toBe("native");
236
+ expect(entry.phase).toBe("handler");
237
+ expect(entry.route).toBe("GET /items");
238
+ });
239
+
240
+ it("should write to multiple sinks", () => {
241
+ const sink1 = new TestSink();
242
+ const sink2 = new TestSink();
243
+ const logger = new StructuredLogger({ level: "trace" }, {}, [sink1, sink2]);
244
+
245
+ logger.info("multi-sink");
246
+
247
+ expect(sink1.entries).toHaveLength(1);
248
+ expect(sink2.entries).toHaveLength(1);
249
+ expect(sink1.entries[0].message).toBe("multi-sink");
250
+ });
251
+
252
+ it("should not include data field when no data is provided", () => {
253
+ const sink = new TestSink();
254
+ const logger = new StructuredLogger({ level: "trace" }, {}, [sink]);
255
+
256
+ logger.info("no data");
257
+
258
+ const entry = sink.entries[0];
259
+ expect("data" in entry).toBe(false);
260
+ });
261
+ });
262
+
263
+ describe("redactFields", () => {
264
+ it("should return data unchanged when no patterns", () => {
265
+ const data = { a: 1, b: "hello" };
266
+ const result = redactFields(data, []);
267
+ expect(result).toEqual(data);
268
+ });
269
+
270
+ it("should redact exact key matches", () => {
271
+ const result = redactFields({ password: "secret", name: "Alice" }, [
272
+ "password",
273
+ ]);
274
+ expect(result).toEqual({ password: "[REDACTED]", name: "Alice" });
275
+ });
276
+
277
+ it("should redact wildcard key matches at any depth", () => {
278
+ const result = redactFields(
279
+ { user: { password: "secret", name: "Alice" } },
280
+ ["*.password"],
281
+ );
282
+ expect(result).toEqual({
283
+ user: { password: "[REDACTED]", name: "Alice" },
284
+ });
285
+ });
286
+
287
+ it("should redact fields inside arrays of objects", () => {
288
+ const result = redactFields(
289
+ {
290
+ users: [
291
+ { name: "A", token: "t1" },
292
+ { name: "B", token: "t2" },
293
+ ],
294
+ },
295
+ ["*.token"],
296
+ );
297
+ const users = result["users"] as Array<Record<string, unknown>>;
298
+ expect(users[0]["token"]).toBe("[REDACTED]");
299
+ expect(users[1]["token"]).toBe("[REDACTED]");
300
+ expect(users[0]["name"]).toBe("A");
301
+ });
302
+ });
303
+
304
+ describe("StdoutSink", () => {
305
+ it("should be constructable", () => {
306
+ const sink = new StdoutSink();
307
+ expect(sink).toBeDefined();
308
+ expect(typeof sink.write).toBe("function");
309
+ });
310
+ });
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ // @typokit/otel — Structured Logger & Observability
2
+
3
+ export { StructuredLogger } from "./logger.js";
4
+ export { redactFields } from "./redact.js";
5
+ export { OtelLogSink, createOtelLogSink } from "./log-bridge.js";
6
+ export {
7
+ Tracer,
8
+ Span,
9
+ ConsoleSpanExporter,
10
+ OtlpSpanExporter,
11
+ NoopSpanExporter,
12
+ createRequestTracer,
13
+ resolveTracingConfig,
14
+ createExporter,
15
+ generateTraceId,
16
+ generateSpanId,
17
+ } from "./tracing.js";
18
+ export {
19
+ MetricsCollector,
20
+ ConsoleMetricExporter,
21
+ OtlpMetricExporter,
22
+ NoopMetricExporter,
23
+ resolveMetricsConfig,
24
+ createMetricExporter,
25
+ createMetricsCollector,
26
+ } from "./metrics.js";
27
+ export type {
28
+ LogLevel,
29
+ LogEntry,
30
+ LoggingConfig,
31
+ LogSink,
32
+ LogMetadata,
33
+ SpanData,
34
+ SpanStatus,
35
+ SpanExporter,
36
+ TracingConfig,
37
+ TelemetryConfig,
38
+ MetricsConfig,
39
+ MetricLabels,
40
+ MetricData,
41
+ MetricExporter,
42
+ HistogramDataPoint,
43
+ GaugeDataPoint,
44
+ CounterDataPoint,
45
+ } from "./types.js";