@ogcio/o11y-sdk-node 0.1.0-beta.9 → 0.3.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 (60) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +158 -13
  3. package/dist/lib/config-manager.d.ts +3 -0
  4. package/dist/lib/config-manager.js +11 -0
  5. package/dist/lib/exporter/console.js +3 -4
  6. package/dist/lib/exporter/grpc.d.ts +1 -1
  7. package/dist/lib/exporter/grpc.js +24 -14
  8. package/dist/lib/exporter/http.d.ts +1 -1
  9. package/dist/lib/exporter/http.js +14 -13
  10. package/dist/lib/exporter/pii-exporter-decorator.d.ts +20 -0
  11. package/dist/lib/exporter/pii-exporter-decorator.js +103 -0
  12. package/dist/lib/exporter/processor-config.d.ts +5 -0
  13. package/dist/lib/exporter/processor-config.js +16 -0
  14. package/dist/lib/index.d.ts +25 -4
  15. package/dist/lib/instrumentation.node.d.ts +1 -1
  16. package/dist/lib/instrumentation.node.js +29 -19
  17. package/dist/lib/internals/hooks.d.ts +3 -0
  18. package/dist/lib/internals/hooks.js +12 -0
  19. package/dist/lib/internals/pii-detection.d.ts +17 -0
  20. package/dist/lib/internals/pii-detection.js +116 -0
  21. package/dist/lib/internals/shared-metrics.d.ts +7 -0
  22. package/dist/lib/internals/shared-metrics.js +18 -0
  23. package/dist/lib/resource.js +2 -2
  24. package/dist/lib/traces.d.ts +20 -1
  25. package/dist/lib/traces.js +47 -1
  26. package/dist/package.json +23 -21
  27. package/dist/vitest.config.js +8 -2
  28. package/lib/config-manager.ts +16 -0
  29. package/lib/exporter/console.ts +6 -4
  30. package/lib/exporter/grpc.ts +46 -20
  31. package/lib/exporter/http.ts +33 -20
  32. package/lib/exporter/pii-exporter-decorator.ts +152 -0
  33. package/lib/exporter/processor-config.ts +23 -0
  34. package/lib/index.ts +28 -4
  35. package/lib/instrumentation.node.ts +37 -22
  36. package/lib/internals/hooks.ts +14 -0
  37. package/lib/internals/pii-detection.ts +145 -0
  38. package/lib/internals/shared-metrics.ts +34 -0
  39. package/lib/resource.ts +3 -2
  40. package/lib/traces.ts +74 -1
  41. package/package.json +23 -21
  42. package/test/config-manager.test.ts +34 -0
  43. package/test/exporter/pii-exporter-decorator.test.ts +139 -0
  44. package/test/index.test.ts +44 -12
  45. package/test/integration/README.md +1 -1
  46. package/test/integration/{integration.test.ts → http-tracing.integration.test.ts} +0 -2
  47. package/test/integration/pii.integration.test.ts +68 -0
  48. package/test/integration/run.sh +2 -2
  49. package/test/internals/hooks.test.ts +45 -0
  50. package/test/internals/pii-detection.test.ts +141 -0
  51. package/test/internals/shared-metrics.test.ts +34 -0
  52. package/test/node-config.test.ts +68 -30
  53. package/test/processor/enrich-span-processor.test.ts +2 -54
  54. package/test/resource.test.ts +12 -1
  55. package/test/traces/active-span.test.ts +28 -0
  56. package/test/traces/with-span.test.ts +340 -0
  57. package/test/utils/alloy-log-parser.ts +7 -0
  58. package/test/utils/mock-signals.ts +144 -0
  59. package/test/validation.test.ts +22 -16
  60. package/vitest.config.ts +8 -2
@@ -0,0 +1,34 @@
1
+ vi.mock("../../lib/metrics", () => {
2
+ return {
3
+ getMetric: vi.fn(),
4
+ };
5
+ });
6
+
7
+ import { describe, it, expect, vi, beforeEach } from "vitest";
8
+ import { _getPIICounterRedactionMetric } from "../../lib/internals/shared-metrics.js";
9
+ import { getMetric } from "../../lib/metrics"; // Get the mocked function
10
+
11
+ describe("shared metrics", () => {
12
+ const mockMetric = { add: vi.fn() };
13
+
14
+ beforeEach(() => {
15
+ vi.resetModules(); // Clear module-level cache
16
+ vi.restoreAllMocks();
17
+ (getMetric as vi.Mock).mockClear(); // clear call history
18
+ (getMetric as vi.Mock).mockReturnValue(mockMetric);
19
+ });
20
+
21
+ it("calls getMetric with correct arguments and caches result", () => {
22
+ const metric1 = _getPIICounterRedactionMetric();
23
+ const metric2 = _getPIICounterRedactionMetric();
24
+
25
+ expect(getMetric).toHaveBeenCalledOnce();
26
+ expect(getMetric).toHaveBeenCalledWith("counter", {
27
+ meterName: "o11y",
28
+ metricName: "o11y_pii_redaction",
29
+ });
30
+
31
+ expect(metric1).toBe(mockMetric);
32
+ expect(metric2).toBe(mockMetric); // should be cached
33
+ });
34
+ });
@@ -1,25 +1,20 @@
1
- import { test, describe, assert, expect } from "vitest";
1
+ import { NodeSDK, metrics } from "@opentelemetry/sdk-node";
2
+ import { afterAll, assert, describe, expect, test } from "vitest";
2
3
  import buildNodeInstrumentation from "../lib/instrumentation.node.js";
3
- import { NodeSDK, logs, metrics, tracing } from "@opentelemetry/sdk-node";
4
- import { OTLPTraceExporter as GRPC_OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
5
- import { OTLPMetricExporter as GRPC_OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
6
- import { OTLPLogExporter as GRPC_OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-grpc";
7
-
8
- import { OTLPTraceExporter as HTTP_OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
9
- import { OTLPMetricExporter as HTTP_OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
10
- import { OTLPLogExporter as HTTP_OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
11
- import { NodeSDKConfig } from "../lib/index.js";
12
- import buildHttpExporters from "../lib/exporter/http.js";
13
- import {
14
- BatchSpanProcessor,
15
- ConsoleSpanExporter,
16
- SimpleSpanProcessor,
17
- } from "@opentelemetry/sdk-trace-base";
4
+
18
5
  import {
19
6
  BatchLogRecordProcessor,
20
7
  SimpleLogRecordProcessor,
21
8
  } from "@opentelemetry/sdk-logs";
9
+ import {
10
+ BatchSpanProcessor,
11
+ SimpleSpanProcessor,
12
+ } from "@opentelemetry/sdk-trace-base";
13
+ import buildHttpExporters from "../lib/exporter/http.js";
14
+ import { NodeSDKConfig } from "../lib/index.js";
15
+ import { EnrichLogProcessor } from "../lib/processor/enrich-logger-processor.js";
22
16
  import { EnrichSpanProcessor } from "../lib/processor/enrich-span-processor.js";
17
+ import { PIIExporterDecorator } from "../lib/exporter/pii-exporter-decorator.js";
23
18
 
24
19
  describe("verify config settings", () => {
25
20
  const commonConfig = {
@@ -27,7 +22,7 @@ describe("verify config settings", () => {
27
22
  serviceName: "test",
28
23
  };
29
24
 
30
- test("grpc config", () => {
25
+ test("grpc config", async () => {
31
26
  const config: NodeSDKConfig = {
32
27
  ...commonConfig,
33
28
  protocol: "grpc",
@@ -35,7 +30,7 @@ describe("verify config settings", () => {
35
30
  diagLogLevel: "NONE",
36
31
  };
37
32
 
38
- const sdk: NodeSDK | undefined = buildNodeInstrumentation(config);
33
+ const sdk: NodeSDK | undefined = await buildNodeInstrumentation(config);
39
34
 
40
35
  assert.ok(sdk);
41
36
 
@@ -44,14 +39,13 @@ describe("verify config settings", () => {
44
39
 
45
40
  const logs = _configuration.logRecordProcessors;
46
41
 
47
- assert.equal(logs.length, 2);
48
- assert.ok(logs[1] instanceof BatchLogRecordProcessor);
42
+ assert.equal(logs.length, 1);
43
+ assert.ok(logs[logs.length - 1] instanceof BatchLogRecordProcessor);
49
44
 
50
45
  const spans = _configuration.spanProcessors;
51
46
 
52
- assert.equal(spans.length, 2);
47
+ assert.equal(spans.length, 1);
53
48
  assert.ok(spans[0] instanceof BatchSpanProcessor);
54
- assert.ok(spans[1] instanceof EnrichSpanProcessor);
55
49
 
56
50
  assert.ok(
57
51
  _configuration.metricReader instanceof
@@ -59,14 +53,17 @@ describe("verify config settings", () => {
59
53
  );
60
54
  });
61
55
 
62
- test("http config", () => {
56
+ test("http config", async () => {
63
57
  const config: NodeSDKConfig = {
64
58
  ...commonConfig,
65
59
  protocol: "http",
66
60
  diagLogLevel: "NONE",
61
+ spanAttributes: {
62
+ name: "custom-value",
63
+ },
67
64
  };
68
65
 
69
- const sdk: NodeSDK | undefined = buildNodeInstrumentation(config);
66
+ const sdk: NodeSDK | undefined = await buildNodeInstrumentation(config);
70
67
  assert.ok(sdk);
71
68
 
72
69
  const _configuration = sdk["_configuration"];
@@ -75,6 +72,7 @@ describe("verify config settings", () => {
75
72
  const logs = _configuration.logRecordProcessors;
76
73
 
77
74
  assert.equal(logs.length, 2);
75
+ assert.ok(logs[0] instanceof EnrichLogProcessor);
78
76
  assert.ok(logs[1] instanceof BatchLogRecordProcessor);
79
77
 
80
78
  const spans = _configuration.spanProcessors;
@@ -89,14 +87,17 @@ describe("verify config settings", () => {
89
87
  );
90
88
  });
91
89
 
92
- test("console - console config", () => {
90
+ test("console - console config", async () => {
93
91
  const config: NodeSDKConfig = {
94
92
  ...commonConfig,
95
93
  protocol: "console",
96
94
  diagLogLevel: "NONE",
95
+ spanAttributes: {
96
+ name: "custom-name",
97
+ },
97
98
  };
98
99
 
99
- const sdk: NodeSDK = buildNodeInstrumentation(config)!;
100
+ const sdk: NodeSDK = await buildNodeInstrumentation(config)!;
100
101
  assert.ok(sdk);
101
102
 
102
103
  const _configuration = sdk["_configuration"];
@@ -106,6 +107,7 @@ describe("verify config settings", () => {
106
107
 
107
108
  // verify simple log processor for instant console logging
108
109
  assert.equal(logs.length, 2);
110
+ assert.ok(logs[0] instanceof EnrichLogProcessor);
109
111
  assert.ok(logs[1] instanceof SimpleLogRecordProcessor);
110
112
 
111
113
  const spans = _configuration.spanProcessors;
@@ -121,23 +123,54 @@ describe("verify config settings", () => {
121
123
  );
122
124
  });
123
125
 
124
- test("single log sending config", () => {
126
+ test("single log sending config", async () => {
127
+ const config: NodeSDKConfig = {
128
+ ...commonConfig,
129
+ protocol: "grpc",
130
+ diagLogLevel: "NONE",
131
+ collectorMode: "single",
132
+ };
133
+
134
+ const sdk: NodeSDK | undefined = await buildNodeInstrumentation(config);
135
+
136
+ assert.ok(sdk);
137
+
138
+ const _configuration = sdk["_configuration"];
139
+
140
+ const logRecordProcessors = _configuration.logRecordProcessors;
141
+ assert.equal(logRecordProcessors.length, 1);
142
+ assert.ok(
143
+ logRecordProcessors[logRecordProcessors.length - 1] instanceof
144
+ SimpleLogRecordProcessor,
145
+ );
146
+ });
147
+
148
+ test("pii exporter decorator injected by default", async () => {
125
149
  const config: NodeSDKConfig = {
126
150
  ...commonConfig,
127
151
  protocol: "grpc",
128
152
  diagLogLevel: "NONE",
129
153
  collectorMode: "single",
154
+ detection: {},
130
155
  };
131
156
 
132
- const sdk: NodeSDK | undefined = buildNodeInstrumentation(config);
157
+ const sdk: NodeSDK | undefined = await buildNodeInstrumentation(config);
133
158
 
134
159
  assert.ok(sdk);
135
160
 
136
161
  const _configuration = sdk["_configuration"];
137
162
 
138
163
  const logRecordProcessors = _configuration.logRecordProcessors;
139
- assert.equal(logRecordProcessors.length, 2);
140
- assert.ok(logRecordProcessors[1] instanceof SimpleLogRecordProcessor);
164
+ assert.equal(logRecordProcessors.length, 1);
165
+ assert.ok(
166
+ logRecordProcessors[logRecordProcessors.length - 1] instanceof
167
+ SimpleLogRecordProcessor,
168
+ );
169
+ assert.ok(
170
+ logRecordProcessors[logRecordProcessors.length - 1][
171
+ "_exporter"
172
+ ] instanceof PIIExporterDecorator,
173
+ );
141
174
  });
142
175
 
143
176
  test("check if clear base endpoint final slash", () => {
@@ -149,4 +182,9 @@ describe("verify config settings", () => {
149
182
 
150
183
  expect(config.collectorUrl).toBe("http://example.com");
151
184
  });
185
+
186
+ afterAll(() => {
187
+ // shutdown every instrumentation
188
+ process.emit("SIGTERM");
189
+ });
152
190
  });
@@ -1,59 +1,7 @@
1
- import {
2
- AttributeValue,
3
- Context,
4
- Exception,
5
- Link,
6
- Span,
7
- SpanAttributes,
8
- SpanContext,
9
- SpanStatus,
10
- TimeInput,
11
- } from "@opentelemetry/api";
1
+ import { Context } from "@opentelemetry/api";
12
2
  import { describe, expect, it } from "vitest";
13
3
  import { EnrichSpanProcessor } from "../../lib/processor/enrich-span-processor.js";
14
-
15
- class MockSpan implements Span {
16
- public attributes: Record<string, AttributeValue> = {};
17
-
18
- spanContext(): SpanContext {
19
- throw new Error("Method not implemented.");
20
- }
21
- setAttribute(key: string, value: AttributeValue): this {
22
- this.attributes[key] = value;
23
- return this;
24
- }
25
- setAttributes(attributes: SpanAttributes): this {
26
- throw new Error("Method not implemented.");
27
- }
28
- addEvent(
29
- name: string,
30
- attributesOrStartTime?: SpanAttributes | TimeInput,
31
- startTime?: TimeInput,
32
- ): this {
33
- throw new Error("Method not implemented.");
34
- }
35
- addLink(link: Link): this {
36
- throw new Error("Method not implemented.");
37
- }
38
- addLinks(links: Link[]): this {
39
- throw new Error("Method not implemented.");
40
- }
41
- setStatus(status: SpanStatus): this {
42
- throw new Error("Method not implemented.");
43
- }
44
- updateName(name: string): this {
45
- throw new Error("Method not implemented.");
46
- }
47
- end(endTime?: TimeInput): void {
48
- throw new Error("Method not implemented.");
49
- }
50
- isRecording(): boolean {
51
- throw new Error("Method not implemented.");
52
- }
53
- recordException(exception: Exception, time?: TimeInput): void {
54
- throw new Error("Method not implemented.");
55
- }
56
- }
4
+ import { MockSpan } from "../utils/mock-signals.js";
57
5
 
58
6
  describe("EnrichSpanProcessor", () => {
59
7
  it("should set static attributes on span", () => {
@@ -1,4 +1,4 @@
1
- import { describe, test, expect, vi } from "vitest";
1
+ import { describe, test, expect } from "vitest";
2
2
  import { ObservabilityResourceDetector } from "../lib/resource";
3
3
 
4
4
  describe("ObservabilityResourceDetector", () => {
@@ -19,4 +19,15 @@ describe("ObservabilityResourceDetector", () => {
19
19
  expect(result.attributes!["o11y.sdk.name"]).eq("@ogcio/o11y-sdk-node");
20
20
  expect(result.attributes).toHaveProperty("o11y.sdk.version");
21
21
  });
22
+
23
+ test("should return default resource attribute", () => {
24
+ const detector = new ObservabilityResourceDetector();
25
+ const result = detector.detect();
26
+
27
+ expect(result.attributes).not.toBeNull();
28
+ // default
29
+ expect(result.attributes).toHaveProperty("o11y.sdk.name");
30
+ expect(result.attributes!["o11y.sdk.name"]).eq("@ogcio/o11y-sdk-node");
31
+ expect(result.attributes).toHaveProperty("o11y.sdk.version");
32
+ });
22
33
  });
@@ -0,0 +1,28 @@
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import * as piiDetection from "../../lib/internals/pii-detection.js";
3
+ import { getActiveSpan } from "../../lib/traces.js";
4
+ import { MockSpan } from "../utils/mock-signals.js";
5
+ import { setNodeSdkConfig } from "../../lib/config-manager.js";
6
+
7
+ describe("getActiveSpan", () => {
8
+ it("returns undefined if no active span", async () => {
9
+ setNodeSdkConfig({
10
+ serviceName: "unit-test",
11
+ collectorUrl: "http://collector",
12
+ detection: {
13
+ email: false,
14
+ },
15
+ });
16
+
17
+ // Temporarily override
18
+ const opentelemetry = await import("@opentelemetry/api");
19
+
20
+ vi.spyOn(opentelemetry.trace, "getActiveSpan").mockImplementationOnce(
21
+ () => undefined,
22
+ );
23
+
24
+ const span = getActiveSpan();
25
+
26
+ expect(span).toBeUndefined();
27
+ });
28
+ });
@@ -0,0 +1,340 @@
1
+ import { Span, SpanOptions, SpanStatusCode, trace } from "@opentelemetry/api";
2
+ import { TraceState } from "@opentelemetry/core";
3
+ import { NodeSDK } from "@opentelemetry/sdk-node";
4
+ import {
5
+ InMemorySpanExporter,
6
+ SimpleSpanProcessor,
7
+ SpanProcessor,
8
+ } from "@opentelemetry/sdk-trace-base";
9
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
10
+ import { setNodeSdkConfig } from "../../lib/config-manager.js";
11
+ import { getActiveSpan, withSpan } from "../../lib/traces.js";
12
+
13
+ describe("withSpan", () => {
14
+ let memoryExporter: InMemorySpanExporter;
15
+ let spanProcessor: SpanProcessor;
16
+ let sdk: NodeSDK;
17
+
18
+ beforeAll(() => {
19
+ memoryExporter = new InMemorySpanExporter();
20
+ spanProcessor = new SimpleSpanProcessor(memoryExporter);
21
+ sdk = new NodeSDK({
22
+ spanProcessors: [spanProcessor],
23
+ instrumentations: [],
24
+ });
25
+
26
+ setNodeSdkConfig({
27
+ collectorUrl: "http://localhost:4317",
28
+ });
29
+ sdk.start();
30
+ });
31
+
32
+ afterEach(async () => {
33
+ // Flush any remaining spans
34
+ await spanProcessor.forceFlush();
35
+ // Clean up
36
+ memoryExporter.reset();
37
+ });
38
+
39
+ afterAll(async () => {
40
+ await sdk.shutdown();
41
+ });
42
+
43
+ it("should handle simple synchronous usage", async ({}) => {
44
+ let capturedSpan: Span;
45
+
46
+ await withSpan({
47
+ spanName: "test-sync-span",
48
+ fn: (span: Span) => {
49
+ capturedSpan = span;
50
+ },
51
+ });
52
+
53
+ await spanProcessor.forceFlush();
54
+ const spans = memoryExporter.getFinishedSpans();
55
+ expect(spans).toHaveLength(1);
56
+ expect(spans[0].name).toBe("test-sync-span");
57
+ expect(spans[0].status.code).toBe(SpanStatusCode.OK);
58
+ expect(capturedSpan).toBeTruthy();
59
+ expect(capturedSpan.spanContext().traceId).toBe(
60
+ spans[0].spanContext().traceId,
61
+ );
62
+ });
63
+
64
+ it("should handle synchronous functions that throw errors", async ({}) => {
65
+ const error = new Error("Sync error");
66
+
67
+ await expect(
68
+ withSpan({
69
+ spanName: "test-sync-error-span",
70
+ fn: () => {
71
+ throw error;
72
+ },
73
+ }),
74
+ ).rejects.toThrow(error.message);
75
+
76
+ await spanProcessor.forceFlush();
77
+ const spans = memoryExporter.getFinishedSpans();
78
+ expect(spans).toHaveLength(1);
79
+ expect(spans[0].name).toBe("test-sync-error-span");
80
+ expect(spans[0].status.code).toBe(SpanStatusCode.ERROR);
81
+ expect(spans[0].status.message).toBe(error.message);
82
+ expect(spans[0].events).toHaveLength(1);
83
+ expect(spans[0].events[0].name).toBe("exception");
84
+ });
85
+
86
+ it("should handle asynchronous functions correctly", async ({}) => {
87
+ let capturedSpan: Span;
88
+
89
+ await withSpan({
90
+ spanName: "test-async-span",
91
+ fn: async (span: Span) => {
92
+ capturedSpan = span;
93
+ await new Promise((resolve) => setTimeout(resolve, 10));
94
+ return "async-result";
95
+ },
96
+ });
97
+
98
+ await spanProcessor.forceFlush();
99
+ const spans = memoryExporter.getFinishedSpans();
100
+ expect(spans).toHaveLength(1);
101
+ expect(spans[0].name).toBe("test-async-span");
102
+ expect(spans[0].status.code).toBe(SpanStatusCode.OK);
103
+ expect(capturedSpan).toBeTruthy();
104
+ expect(capturedSpan.spanContext().traceId).toBe(
105
+ spans[0].spanContext().traceId,
106
+ );
107
+ });
108
+
109
+ it("should handle asynchronous functions that reject", async () => {
110
+ const error = new Error("Async error");
111
+
112
+ await expect(
113
+ withSpan({
114
+ spanName: "test-async-error-span",
115
+ fn: async () => {
116
+ await new Promise((resolve) => setTimeout(resolve, 10));
117
+ throw error;
118
+ },
119
+ }),
120
+ ).rejects.toThrow(error.message);
121
+
122
+ await spanProcessor.forceFlush();
123
+ const spans = memoryExporter.getFinishedSpans();
124
+ expect(spans).toHaveLength(1);
125
+ expect(spans[0].name).toBe("test-async-error-span");
126
+ expect(spans[0].status.code).toBe(SpanStatusCode.ERROR);
127
+ expect(spans[0].status.message).toBe(error.message);
128
+ expect(spans[0].events).toHaveLength(1);
129
+ expect(spans[0].events[0].name).toBe("exception");
130
+ });
131
+
132
+ it("should handle non-Error exceptions", async () => {
133
+ const nonErrorException = { message: "Not an error object", code: 500 };
134
+
135
+ await expect(
136
+ withSpan({
137
+ spanName: "test-non-error-span",
138
+ fn: () => {
139
+ throw nonErrorException;
140
+ },
141
+ }),
142
+ ).rejects.toEqual(nonErrorException);
143
+
144
+ const spans = memoryExporter.getFinishedSpans();
145
+ expect(spans).toHaveLength(1);
146
+ expect(spans[0].status.code).toBe(SpanStatusCode.ERROR);
147
+ expect(spans[0].status.message).toBe(JSON.stringify(nonErrorException));
148
+ expect(spans[0].events).toHaveLength(1);
149
+ expect(spans[0].events[0].name).toBe("exception");
150
+ });
151
+
152
+ it("should ensure span is ended even when errors occur", async () => {
153
+ const error = new Error("Test error");
154
+
155
+ await expect(
156
+ withSpan({
157
+ spanName: "test-finally-span",
158
+ fn: () => {
159
+ throw error;
160
+ },
161
+ }),
162
+ ).rejects.toThrow("Test error");
163
+
164
+ const spans = memoryExporter.getFinishedSpans();
165
+ expect(spans).toHaveLength(1);
166
+ expect(spans[0].ended).toBe(true);
167
+ });
168
+
169
+ it("should pass span options to tracer", async () => {
170
+ const spanOptions: SpanOptions = {
171
+ attributes: { "custom.attribute": "custom-value" },
172
+ kind: 1,
173
+ };
174
+
175
+ await withSpan({
176
+ spanName: "test-options-span",
177
+ spanOptions,
178
+ fn: () => "result",
179
+ });
180
+
181
+ await spanProcessor.forceFlush();
182
+ const spans = memoryExporter.getFinishedSpans();
183
+ expect(spans).toHaveLength(1);
184
+ expect(spans[0].attributes["custom.attribute"]).toBe("custom-value");
185
+ expect(spans[0].kind).toBe(1);
186
+ });
187
+
188
+ it("should use custom tracer name", async () => {
189
+ const customTracerName = "custom-tracer";
190
+
191
+ await withSpan({
192
+ traceName: customTracerName,
193
+ spanName: "test-custom-tracer-span",
194
+ fn: () => "result",
195
+ });
196
+
197
+ await spanProcessor.forceFlush();
198
+ const spans = memoryExporter.getFinishedSpans();
199
+ expect(spans).toHaveLength(1);
200
+ expect(spans[0].name).toBe("test-custom-tracer-span");
201
+ expect(spans[0].instrumentationScope.name).toBe(customTracerName);
202
+ });
203
+
204
+ it("should use default tracer name when not specified", async () => {
205
+ await withSpan({
206
+ spanName: "test-default-tracer-span",
207
+ fn: () => "result",
208
+ });
209
+
210
+ await spanProcessor.forceFlush();
211
+ const defaultSpans = memoryExporter.getFinishedSpans();
212
+ expect(defaultSpans).toHaveLength(1);
213
+ expect(defaultSpans[0].name).toBe("test-default-tracer-span");
214
+ expect(defaultSpans[0].instrumentationScope.name).toBe("o11y-sdk");
215
+
216
+ memoryExporter.reset();
217
+
218
+ setNodeSdkConfig({
219
+ collectorUrl: "",
220
+ serviceName: "test-service",
221
+ serviceVersion: "v1.0.0",
222
+ });
223
+
224
+ await withSpan({
225
+ spanName: "test-default-tracer-span",
226
+ fn: () => "result",
227
+ });
228
+ await spanProcessor.forceFlush();
229
+ const spans = memoryExporter.getFinishedSpans();
230
+ expect(spans).toHaveLength(1);
231
+ expect(spans[0].name).toBe("test-default-tracer-span");
232
+ expect(spans[0].instrumentationScope.name).toBe("test-service");
233
+ expect(spans[0].instrumentationScope.version).toBe("v1.0.0");
234
+ });
235
+
236
+ it("should allow function to interact with span context", async () => {
237
+ let receivedSpan: Span;
238
+
239
+ await withSpan({
240
+ spanName: "test-span-context",
241
+ fn: (span: Span) => {
242
+ receivedSpan = span;
243
+ span.spanContext().traceState = new TraceState(
244
+ "alpha=aaaaaaaaaaaa,bravo=bbbbbbbbbbbb",
245
+ );
246
+ },
247
+ });
248
+
249
+ await spanProcessor.forceFlush();
250
+ const spans = memoryExporter.getFinishedSpans();
251
+ expect(spans).toHaveLength(1);
252
+ expect(receivedSpan).toBeTruthy();
253
+ expect(receivedSpan.spanContext().traceId).toBe(
254
+ spans[0].spanContext().traceId,
255
+ );
256
+ expect(receivedSpan.spanContext().spanId).toBe(
257
+ spans[0].spanContext().spanId,
258
+ );
259
+ expect(spans[0].spanContext().traceState.serialize()).toStrictEqual(
260
+ "alpha=aaaaaaaaaaaa,bravo=bbbbbbbbbbbb",
261
+ );
262
+ });
263
+
264
+ it("should preserve context across setTimeout", async () => {
265
+ await withSpan({
266
+ spanName: "test-timeout-context",
267
+ fn: async (span: Span) => {
268
+ return new Promise((resolve) => {
269
+ setTimeout(() => {
270
+ getActiveSpan().addEvent("promise-resolved", {
271
+ result: "timeout-result",
272
+ });
273
+ resolve("timeout-result");
274
+ }, 10);
275
+ });
276
+ },
277
+ });
278
+
279
+ let newSpan: Span;
280
+ await trace
281
+ .getTracer("some-tracer")
282
+ .startActiveSpan("other-context", async (span) => {
283
+ newSpan = span;
284
+ span.addEvent("other-context-event", {
285
+ result: "other-context-result",
286
+ });
287
+ });
288
+
289
+ newSpan.addEvent("another-context-event", {
290
+ result: "another-context-result",
291
+ });
292
+ newSpan.setStatus({ code: SpanStatusCode.OK });
293
+ newSpan.end();
294
+ await spanProcessor.forceFlush();
295
+ const spans = memoryExporter.getFinishedSpans();
296
+ expect(spans).toHaveLength(2);
297
+
298
+ const timeoutSpan = spans.find((s) => s.name === "test-timeout-context");
299
+ expect(timeoutSpan.status.code).toBe(SpanStatusCode.OK);
300
+ expect(timeoutSpan.events).toHaveLength(1);
301
+ expect(timeoutSpan.events[0].name).toStrictEqual("promise-resolved");
302
+
303
+ const otherTrackedSpan = spans.find((s) => s.name === "other-context");
304
+ expect(otherTrackedSpan.status.code).toBe(SpanStatusCode.OK);
305
+ expect(otherTrackedSpan.events).toHaveLength(2);
306
+ expect(otherTrackedSpan.events[0].name).toStrictEqual(
307
+ "other-context-event",
308
+ );
309
+ expect(otherTrackedSpan.events[1].name).toStrictEqual(
310
+ "another-context-event",
311
+ );
312
+ });
313
+
314
+ it("should handle nested spans correctly", async () => {
315
+ await withSpan({
316
+ spanName: "outer-span",
317
+ fn: async () => {
318
+ await withSpan({
319
+ spanName: "inner-span",
320
+ fn: async () => {
321
+ await new Promise((resolve) => setTimeout(resolve, 1));
322
+ },
323
+ });
324
+ },
325
+ });
326
+
327
+ await spanProcessor.forceFlush();
328
+ const spans = memoryExporter.getFinishedSpans();
329
+ expect(spans).toHaveLength(2);
330
+
331
+ const innerSpan = spans.find((s) => s.name === "inner-span");
332
+ const outerSpan = spans.find((s) => s.name === "outer-span");
333
+
334
+ expect(innerSpan).toBeTruthy();
335
+ expect(outerSpan).toBeTruthy();
336
+ expect(innerSpan!.parentSpanContext.spanId).toBe(
337
+ outerSpan!.spanContext().spanId,
338
+ );
339
+ });
340
+ });
@@ -40,6 +40,13 @@ export function parseLog(
40
40
  // Additional metadata at the end, store it separately
41
41
  jsonObject["metadata"] = line;
42
42
  }
43
+
44
+ if (line.includes("Body:")) {
45
+ const match = line.match(/Body:\s+(\w+)\((.+)\)/);
46
+ if (match) {
47
+ jsonObject["log_body"] = match[2];
48
+ }
49
+ }
43
50
  });
44
51
 
45
52
  return jsonObject;