@ogcio/o11y-sdk-node 0.1.0-beta.1 → 0.1.0-beta.10

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 (83) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +222 -0
  3. package/dist/index.d.ts +3 -2
  4. package/dist/index.js +2 -0
  5. package/dist/lib/exporter/console.d.ts +3 -0
  6. package/dist/lib/exporter/console.js +20 -0
  7. package/dist/lib/exporter/grpc.d.ts +3 -0
  8. package/dist/lib/{grpc.js → exporter/grpc.js} +15 -9
  9. package/dist/lib/exporter/http.d.ts +3 -0
  10. package/dist/lib/{http.js → exporter/http.js} +15 -9
  11. package/dist/lib/exporter/index.d.ts +8 -0
  12. package/dist/lib/index.d.ts +29 -6
  13. package/dist/lib/instrumentation.node.js +32 -5
  14. package/dist/lib/metrics.d.ts +18 -0
  15. package/dist/lib/metrics.js +24 -0
  16. package/dist/lib/processor/enrich-logger-processor.d.ts +10 -0
  17. package/dist/lib/processor/enrich-logger-processor.js +19 -0
  18. package/dist/lib/processor/enrich-span-processor.d.ts +11 -0
  19. package/dist/lib/processor/enrich-span-processor.js +22 -0
  20. package/dist/lib/resource.d.ts +7 -0
  21. package/dist/lib/resource.js +18 -0
  22. package/dist/lib/traces.d.ts +1 -0
  23. package/dist/lib/traces.js +4 -0
  24. package/dist/lib/url-sampler.d.ts +10 -0
  25. package/dist/lib/url-sampler.js +25 -0
  26. package/dist/lib/utils.d.ts +4 -2
  27. package/dist/lib/utils.js +8 -3
  28. package/dist/package.json +57 -0
  29. package/dist/vitest.config.js +15 -1
  30. package/index.ts +4 -2
  31. package/lib/exporter/console.ts +31 -0
  32. package/lib/{grpc.ts → exporter/grpc.ts} +19 -11
  33. package/lib/{http.ts → exporter/http.ts} +19 -11
  34. package/lib/exporter/index.ts +9 -0
  35. package/lib/index.ts +37 -5
  36. package/lib/instrumentation.node.ts +42 -7
  37. package/lib/metrics.ts +75 -0
  38. package/lib/processor/enrich-logger-processor.ts +34 -0
  39. package/lib/processor/enrich-span-processor.ts +39 -0
  40. package/lib/resource.ts +30 -0
  41. package/lib/traces.ts +5 -0
  42. package/lib/url-sampler.ts +52 -0
  43. package/lib/utils.ts +16 -4
  44. package/package.json +32 -25
  45. package/test/index.test.ts +9 -0
  46. package/test/integration/README.md +26 -0
  47. package/test/integration/integration.test.ts +58 -0
  48. package/test/integration/run.sh +88 -0
  49. package/test/metrics.test.ts +142 -0
  50. package/test/node-config.test.ts +70 -31
  51. package/test/processor/enrich-logger-processor.test.ts +58 -0
  52. package/test/processor/enrich-span-processor.test.ts +104 -0
  53. package/test/resource.test.ts +33 -0
  54. package/test/url-sampler.test.ts +215 -0
  55. package/test/utils/alloy-log-parser.ts +46 -0
  56. package/test/validation.test.ts +31 -0
  57. package/tsconfig.json +2 -1
  58. package/vitest.config.ts +15 -1
  59. package/coverage/cobertura-coverage.xml +0 -199
  60. package/coverage/lcov-report/base.css +0 -224
  61. package/coverage/lcov-report/block-navigation.js +0 -87
  62. package/coverage/lcov-report/favicon.png +0 -0
  63. package/coverage/lcov-report/index.html +0 -131
  64. package/coverage/lcov-report/prettify.css +0 -1
  65. package/coverage/lcov-report/prettify.js +0 -2
  66. package/coverage/lcov-report/sdk-node/index.html +0 -116
  67. package/coverage/lcov-report/sdk-node/index.ts.html +0 -106
  68. package/coverage/lcov-report/sdk-node/lib/grpc.ts.html +0 -178
  69. package/coverage/lcov-report/sdk-node/lib/http.ts.html +0 -190
  70. package/coverage/lcov-report/sdk-node/lib/index.html +0 -191
  71. package/coverage/lcov-report/sdk-node/lib/index.ts.html +0 -265
  72. package/coverage/lcov-report/sdk-node/lib/instrumentation.node.ts.html +0 -310
  73. package/coverage/lcov-report/sdk-node/lib/options.ts.html +0 -109
  74. package/coverage/lcov-report/sdk-node/lib/utils.ts.html +0 -115
  75. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  76. package/coverage/lcov-report/sorter.js +0 -196
  77. package/coverage/lcov.info +0 -206
  78. package/dist/lib/grpc.d.ts +0 -3
  79. package/dist/lib/http.d.ts +0 -3
  80. package/dist/lib/options.d.ts +0 -7
  81. package/lib/options.ts +0 -8
  82. package/test-report.xml +0 -39
  83. /package/dist/lib/{options.js → exporter/index.js} +0 -0
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { LogRecord } from "@opentelemetry/sdk-logs";
3
+ import { EnrichLogProcessor } from "../../lib/processor/enrich-logger-processor.js";
4
+
5
+ const createMockLogRecord = () => {
6
+ return {
7
+ setAttribute: vi.fn(),
8
+ } as unknown as LogRecord;
9
+ };
10
+
11
+ describe("EnrichLogProcessor", () => {
12
+ it("should enrich log record with static attributes", () => {
13
+ const attributes = { key1: "value1", key2: 42 };
14
+ const processor = new EnrichLogProcessor(attributes);
15
+ const mockLogRecord = createMockLogRecord();
16
+
17
+ processor.onEmit(mockLogRecord);
18
+
19
+ expect(mockLogRecord.setAttribute).toHaveBeenCalledWith("key1", "value1");
20
+ expect(mockLogRecord.setAttribute).toHaveBeenCalledWith("key2", 42);
21
+ });
22
+
23
+ it("should enrich log record with dynamic attributes", () => {
24
+ const attributes = {
25
+ key1: () => "dynamicValue",
26
+ key2: () => 100,
27
+ };
28
+ const processor = new EnrichLogProcessor(attributes);
29
+ const mockLogRecord = createMockLogRecord();
30
+
31
+ processor.onEmit(mockLogRecord);
32
+
33
+ expect(mockLogRecord.setAttribute).toHaveBeenCalledWith(
34
+ "key1",
35
+ "dynamicValue",
36
+ );
37
+ expect(mockLogRecord.setAttribute).toHaveBeenCalledWith("key2", 100);
38
+ });
39
+
40
+ it("should not set attributes if no span attributes are provided", () => {
41
+ const processor = new EnrichLogProcessor();
42
+ const mockLogRecord = createMockLogRecord();
43
+
44
+ processor.onEmit(mockLogRecord);
45
+
46
+ expect(mockLogRecord.setAttribute).not.toHaveBeenCalled();
47
+ });
48
+
49
+ it("should reject forceFlush", async () => {
50
+ await expect(
51
+ new EnrichLogProcessor().forceFlush(),
52
+ ).resolves.toBeUndefined();
53
+ });
54
+
55
+ it("should resolve shutdown", async () => {
56
+ await expect(new EnrichLogProcessor().shutdown()).resolves.toBeUndefined();
57
+ });
58
+ });
@@ -0,0 +1,104 @@
1
+ import {
2
+ AttributeValue,
3
+ Context,
4
+ Exception,
5
+ Link,
6
+ Span,
7
+ SpanAttributes,
8
+ SpanContext,
9
+ SpanStatus,
10
+ TimeInput,
11
+ } from "@opentelemetry/api";
12
+ import { describe, expect, it } from "vitest";
13
+ 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
+ }
57
+
58
+ describe("EnrichSpanProcessor", () => {
59
+ it("should set static attributes on span", () => {
60
+ const spanAttributes = {
61
+ key1: "value1",
62
+ key2: 123,
63
+ };
64
+ const processor = new EnrichSpanProcessor(spanAttributes);
65
+ const mockSpan = new MockSpan();
66
+ const mockContext = {} as Context;
67
+
68
+ processor.onStart(mockSpan, mockContext);
69
+
70
+ expect(mockSpan.attributes["key1"]).toBe("value1");
71
+ expect(mockSpan.attributes["key2"]).toBe(123);
72
+ });
73
+
74
+ it("should set dynamic attributes on span", () => {
75
+ const spanAttributes = {
76
+ dynamicKey: () => "dynamicValue",
77
+ };
78
+ const processor = new EnrichSpanProcessor(spanAttributes);
79
+ const mockSpan = new MockSpan();
80
+ const mockContext = {} as Context;
81
+
82
+ processor.onStart(mockSpan, mockContext);
83
+
84
+ expect(mockSpan.attributes["dynamicKey"]).toBe("dynamicValue");
85
+ });
86
+
87
+ it("should not set attributes if none are provided", () => {
88
+ const processor = new EnrichSpanProcessor();
89
+ const mockSpan = new MockSpan();
90
+ const mockContext = {} as Context;
91
+
92
+ processor.onStart(mockSpan, mockContext);
93
+
94
+ expect(mockSpan.attributes["key1"]).toBeUndefined();
95
+ });
96
+
97
+ it("default method, should maintain default behaviour", async () => {
98
+ const processor = new EnrichSpanProcessor();
99
+
100
+ expect(processor.onEnd(null!)).toBeUndefined();
101
+ expect(await processor.shutdown()).toBeUndefined();
102
+ expect(await processor.forceFlush()).toBeUndefined();
103
+ });
104
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { ObservabilityResourceDetector } from "../lib/resource";
3
+
4
+ describe("ObservabilityResourceDetector", () => {
5
+ test("should return custom resource attribute", () => {
6
+ const detector = new ObservabilityResourceDetector({
7
+ first: "first_value",
8
+ second: "second_value",
9
+ });
10
+ const result = detector.detect();
11
+
12
+ expect(result.attributes).not.toBeNull();
13
+ expect(result.attributes).toHaveProperty("first");
14
+ expect(result.attributes!["first"]).eq("first_value");
15
+ expect(result.attributes).toHaveProperty("second");
16
+ expect(result.attributes!["second"]).eq("second_value");
17
+ // default
18
+ expect(result.attributes).toHaveProperty("o11y.sdk.name");
19
+ expect(result.attributes!["o11y.sdk.name"]).eq("@ogcio/o11y-sdk-node");
20
+ expect(result.attributes).toHaveProperty("o11y.sdk.version");
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
+ });
33
+ });
@@ -0,0 +1,215 @@
1
+ import { Context } from "@opentelemetry/api";
2
+ import {
3
+ SamplingDecision,
4
+ SamplingResult,
5
+ } from "@opentelemetry/sdk-trace-base";
6
+ import { describe, expect, test, vi } from "vitest";
7
+ import { UrlSampler } from "../lib/url-sampler";
8
+
9
+ describe("url sampler", () => {
10
+ // mock sampler to be sure every trace after UrlSamper has RECORD status
11
+ const mockSampler = {
12
+ shouldSample: vi
13
+ .fn()
14
+ .mockImplementation(
15
+ (_context, _traceId, _spanName, _spanKind, attributes, _links) => {
16
+ return {
17
+ decision: SamplingDecision.RECORD,
18
+ attributes: attributes,
19
+ } as SamplingResult;
20
+ },
21
+ ),
22
+ };
23
+
24
+ test("should add custom span attributes to trace", async () => {
25
+ const sampler: UrlSampler = new UrlSampler(
26
+ [
27
+ {
28
+ type: "endsWith",
29
+ url: "/health",
30
+ },
31
+ ],
32
+ mockSampler,
33
+ {
34
+ "signal.namespace": "unittest",
35
+ "signal.callback.result": () => "test",
36
+ },
37
+ );
38
+
39
+ expect(sampler).not.toBeNull();
40
+
41
+ const result = sampler.shouldSample(
42
+ {} as Context,
43
+ "traceId",
44
+ "span",
45
+ 0,
46
+ { "http.target": "/track" },
47
+ [],
48
+ );
49
+
50
+ expect(sampler.toString()).toBe("UrlSampler");
51
+ expect(result.decision).toBe(SamplingDecision.RECORD);
52
+ expect(result.attributes).not.toBeNull();
53
+ });
54
+
55
+ test("should not record trace about /health api", async () => {
56
+ const sampler: UrlSampler = new UrlSampler(
57
+ [
58
+ {
59
+ type: "endsWith",
60
+ url: "/health",
61
+ },
62
+ ],
63
+ mockSampler,
64
+ );
65
+
66
+ const result = sampler.shouldSample(
67
+ {} as Context,
68
+ "traceId",
69
+ "span",
70
+ 0,
71
+ { "http.target": "/health" },
72
+ [],
73
+ );
74
+
75
+ expect(sampler.toString()).toBe("UrlSampler");
76
+ expect(result.decision).toBe(SamplingDecision.NOT_RECORD);
77
+ });
78
+
79
+ test("should record every other trace which is not /health api", async () => {
80
+ const sampler: UrlSampler = new UrlSampler(
81
+ [
82
+ {
83
+ type: "endsWith",
84
+ url: "/health",
85
+ },
86
+ ],
87
+ mockSampler,
88
+ );
89
+
90
+ let result = sampler.shouldSample(
91
+ {} as Context,
92
+ "traceId",
93
+ "span",
94
+ 0,
95
+ { "http.target": "/test" },
96
+ [],
97
+ );
98
+
99
+ expect(result.decision).toBe(SamplingDecision.RECORD);
100
+
101
+ result = sampler.shouldSample(
102
+ {} as Context,
103
+ "traceId",
104
+ "span",
105
+ 0,
106
+ { "http.target": "/another/url" },
107
+ [],
108
+ );
109
+
110
+ expect(result.decision).toBe(SamplingDecision.RECORD);
111
+ });
112
+
113
+ test("operator 'includes', should not record every trace which include /block in url", async () => {
114
+ const sampler: UrlSampler = new UrlSampler(
115
+ [
116
+ {
117
+ type: "includes",
118
+ url: "/block",
119
+ },
120
+ ],
121
+ mockSampler,
122
+ );
123
+
124
+ expect(sampler).not.toBeNull();
125
+
126
+ const result = sampler.shouldSample(
127
+ {} as Context,
128
+ "traceId",
129
+ "span",
130
+ 0,
131
+ { "http.target": "/namespace/block/example/12" },
132
+ [],
133
+ );
134
+
135
+ expect(sampler.toString()).toBe("UrlSampler");
136
+ expect(result.decision).toBe(SamplingDecision.NOT_RECORD);
137
+ });
138
+
139
+ test("operator 'endsWith', should not record only trace which ends with /block in url", async () => {
140
+ const sampler: UrlSampler = new UrlSampler(
141
+ [
142
+ {
143
+ type: "endsWith",
144
+ url: "/block",
145
+ },
146
+ ],
147
+ mockSampler,
148
+ );
149
+
150
+ expect(sampler).not.toBeNull();
151
+
152
+ // expect traced with block in the URL middle
153
+ let result = sampler.shouldSample(
154
+ {} as Context,
155
+ "traceId",
156
+ "span",
157
+ 0,
158
+ { "http.target": "/namespace/block/example/12" },
159
+ [],
160
+ );
161
+
162
+ expect(sampler.toString()).toBe("UrlSampler");
163
+ expect(result.decision).toBe(SamplingDecision.RECORD);
164
+
165
+ // should stop trace with block at the end
166
+ result = sampler.shouldSample(
167
+ {} as Context,
168
+ "traceId",
169
+ "span",
170
+ 0,
171
+ { "http.target": "/namespace/example/block" },
172
+ [],
173
+ );
174
+
175
+ expect(sampler.toString()).toBe("UrlSampler");
176
+ expect(result.decision).toBe(SamplingDecision.NOT_RECORD);
177
+ });
178
+
179
+ test("operator 'equals', should not record trace which is equal to /block in url", async () => {
180
+ const sampler: UrlSampler = new UrlSampler(
181
+ [
182
+ {
183
+ type: "equals",
184
+ url: "/block",
185
+ },
186
+ ],
187
+ mockSampler,
188
+ );
189
+
190
+ expect(sampler).not.toBeNull();
191
+
192
+ let result = sampler.shouldSample(
193
+ {} as Context,
194
+ "traceId",
195
+ "span",
196
+ 0,
197
+ { "http.target": "/namespace/block/example/12" },
198
+ [],
199
+ );
200
+
201
+ expect(sampler.toString()).toBe("UrlSampler");
202
+ expect(result.decision).toBe(SamplingDecision.RECORD);
203
+
204
+ result = sampler.shouldSample(
205
+ {} as Context,
206
+ "traceId",
207
+ "span",
208
+ 0,
209
+ { "http.target": "/block" },
210
+ [],
211
+ );
212
+
213
+ expect(result.decision).toBe(SamplingDecision.NOT_RECORD);
214
+ });
215
+ });
@@ -0,0 +1,46 @@
1
+ export function parseLog(
2
+ log: string,
3
+ ): Record<string, object | string | number> {
4
+ const logArray = log
5
+ .split("\\n")
6
+ .map((line) => line.trim())
7
+ .filter((line) => line);
8
+
9
+ const jsonObject: Record<string, object | string | number> = {};
10
+ let currentSection: Record<string, object | string | number> = jsonObject;
11
+ const sectionStack: Record<string, object | string | number>[] = [];
12
+
13
+ logArray.forEach((line) => {
14
+ line = line.trim();
15
+
16
+ if (line.startsWith("->")) {
17
+ const match = line.match(/->\s+([^:]+):\s+(Str|Int)\((.+)\)/);
18
+ if (match) {
19
+ const [, key, type, value] = match;
20
+ const parsedValue = type === "Int" ? parseInt(value, 10) : value;
21
+
22
+ if (typeof currentSection === "object") {
23
+ currentSection[key] = parsedValue;
24
+ }
25
+ }
26
+ } else if (line.endsWith(":")) {
27
+ // new section
28
+ const sectionName = line
29
+ .slice(0, -1)
30
+ .trim()
31
+ .toLowerCase()
32
+ .replace(" ", "_");
33
+ jsonObject[sectionName] = {};
34
+ currentSection = jsonObject[sectionName] as Record<
35
+ string,
36
+ object | string | number
37
+ >;
38
+ sectionStack.push(currentSection);
39
+ } else if (line.startsWith('"')) {
40
+ // Additional metadata at the end, store it separately
41
+ jsonObject["metadata"] = line;
42
+ }
43
+ });
44
+
45
+ return jsonObject;
46
+ }
@@ -63,4 +63,35 @@ describe("validation config: should return without breaking the execution", () =
63
63
  );
64
64
  expect(consoleLogSpy).not.toHaveBeenCalled();
65
65
  });
66
+
67
+ test("node instrumentation: verify no instrumentation if exception occurs", () => {
68
+ let sdk: NodeSDK | undefined = undefined;
69
+
70
+ const consoleErrorSpy = vi
71
+ .spyOn(console, "error")
72
+ .mockImplementation(vi.fn());
73
+ const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(vi.fn());
74
+
75
+ vi.mock("@opentelemetry/auto-instrumentations-node", () => ({
76
+ getNodeAutoInstrumentations: vi.fn(() => {
77
+ throw new Error("Wanted error");
78
+ }),
79
+ }));
80
+
81
+ assert.doesNotThrow(() => {
82
+ sdk = buildNodeInstrumentation({
83
+ collectorUrl: "https://testurl.com",
84
+ serviceName: "test",
85
+ });
86
+ });
87
+
88
+ assert.equal(sdk, undefined);
89
+
90
+ expect(consoleErrorSpy).toHaveBeenCalled();
91
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
92
+ "Error starting NodeJS OpenTelemetry instrumentation:",
93
+ new Error("Wanted error"),
94
+ );
95
+ expect(consoleLogSpy).not.toHaveBeenCalled();
96
+ });
66
97
  });
package/tsconfig.json CHANGED
@@ -8,7 +8,8 @@
8
8
  "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
9
9
  "strict": true /* Enable all strict type-checking options. */,
10
10
  "skipLibCheck": true /* Skip type checking all .d.ts files. */,
11
- "declaration": true
11
+ "declaration": true,
12
+ "resolveJsonModule": true
12
13
  },
13
14
  "exclude": ["**/test/**"]
14
15
  }
package/vitest.config.ts CHANGED
@@ -4,7 +4,6 @@ export default defineConfig({
4
4
  test: {
5
5
  globals: true,
6
6
  watch: false,
7
- include: ["**/test/*.test.ts"],
8
7
  exclude: ["**/fixtures/**", "**/dist/**"],
9
8
  poolOptions: {
10
9
  threads: {
@@ -22,5 +21,20 @@ export default defineConfig({
22
21
  },
23
22
  reporters: ["default", ["junit", { outputFile: "test-report.xml" }]],
24
23
  environment: "node",
24
+ pool: "threads",
25
+ workspace: [
26
+ {
27
+ test: {
28
+ include: ["**/test/*.test.ts", "**/test/processor/*.test.ts"],
29
+ name: "unit",
30
+ },
31
+ },
32
+ {
33
+ test: {
34
+ include: ["**/test/integration/*.test.ts"],
35
+ name: "integration",
36
+ },
37
+ },
38
+ ],
25
39
  },
26
40
  });