1
5import { describe, expect, it } from "vitest";
6import { Writable } from "node:stream";
7import { context, trace } from "@opentelemetry/api";
8import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
9import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
10import { makeOtelPino, otelLogBindings, requestChild } from "../pino-otel.js";
11
12
13
14const CM = new AsyncLocalStorageContextManager();
15CM.enable();
16context.setGlobalContextManager(CM);
17
18
19class Collector extends Writable {
20 lines: string[] = [];
21 override _write(chunk: string | Buffer, _enc: BufferEncoding, cb: (err?: Error) => void): void {
22 for (const line of String(chunk).split("\n")) {
23 if (line.trim()) this.lines.push(line);
24 }
25 cb();
26 }
27}
28
29describe("otelLogBindings", () => {
30 it("always carries requestId (unknown when absent)", () => {
31 expect(otelLogBindings(undefined)).toEqual({ requestId: "unknown" });
32 expect(otelLogBindings("req-123")).toEqual({ requestId: "req-123" });
33 });
34
35 it("omits trace fields when no span is active (exactOptionalPropertyTypes shape)", () => {
36 const b = otelLogBindings("r1");
37 expect(Object.keys(b).sort()).toEqual(["requestId"]);
38 });
39
40 it("carries traceId/spanId when a span is active + workspaceId when given", () => {
41 const provider = new BasicTracerProvider();
42 trace.setGlobalTracerProvider(provider);
43 try {
44 const tracer = trace.getTracer("t");
45 const bindings = tracer.startActiveSpan("op", (span) => {
46 const b = otelLogBindings("r1", "ws_9");
47 span.end();
48 return b;
49 });
50 expect(bindings.requestId).toBe("r1");
51 expect(bindings.workspaceId).toBe("ws_9");
52 expect(bindings.traceId).toMatch(/^[0-9a-f]{32}$/);
53 expect(bindings.spanId).toMatch(/^[0-9a-f]{16}$/);
54 } finally {
55 void provider.shutdown();
56 }
57 });
58});
59
60describe("makeOtelPino (the requestId log-capture test)", () => {
61 it("emits requestId on every line (the spec's log-capture assertion)", async () => {
62 const col = new Collector();
63 const logger = makeOtelPino({ service: "factory-test", dest: col });
64 const req = requestChild(logger, "req-abc");
65 req.info("hello world");
66 req.warn("watch out");
67 logger.info("background line");
68 await new Promise((r) => setTimeout(r, 50));
69 const lines = col.lines.map((l) => JSON.parse(l) as Record<string, unknown>);
70 expect(lines.length).toBe(3);
71 for (const line of lines) {
72 expect(line["service"]).toBe("factory-test");
73 expect(typeof line["requestId"]).toBe("string");
74 }
75
76 expect(lines[0]!["requestId"]).toBe("req-abc");
77 expect(lines[1]!["requestId"]).toBe("req-abc");
78 expect(lines[2]!["requestId"]).toBe("unknown");
79 });
80
81 it("carries traceId/spanId on lines written inside an active span", async () => {
82 trace.disable();
83 const provider = new BasicTracerProvider();
84 trace.setGlobalTracerProvider(provider);
85 const col = new Collector();
86 const logger = makeOtelPino({ service: "factory-test", dest: col });
87 const tracer = trace.getTracer("t");
88 tracer.startActiveSpan("op", (span) => {
89 logger.info("inside span");
90 span.end();
91 });
92 await new Promise((r) => setTimeout(r, 50));
93 const line = JSON.parse(col.lines[0]!) as Record<string, unknown>;
94 expect(line["traceId"]).toMatch(/^[0-9a-f]{32}$/);
95 expect(line["spanId"]).toMatch(/^[0-9a-f]{16}$/);
96 void provider.shutdown();
97 });
98});
99