@ogcio/o11y-sdk-node 0.2.0 → 0.3.1
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.
- package/CHANGELOG.md +69 -0
- package/README.md +158 -13
- package/dist/lib/config-manager.d.ts +3 -0
- package/dist/lib/config-manager.js +11 -0
- package/dist/lib/exporter/console.js +3 -4
- package/dist/lib/exporter/grpc.js +14 -13
- package/dist/lib/exporter/http.d.ts +1 -1
- package/dist/lib/exporter/http.js +14 -13
- package/dist/lib/exporter/pii-exporter-decorator.d.ts +20 -0
- package/dist/lib/exporter/pii-exporter-decorator.js +104 -0
- package/dist/lib/exporter/processor-config.d.ts +5 -0
- package/dist/lib/exporter/processor-config.js +16 -0
- package/dist/lib/index.d.ts +18 -4
- package/dist/lib/instrumentation.node.js +13 -11
- package/dist/lib/internals/hooks.d.ts +3 -0
- package/dist/lib/internals/hooks.js +12 -0
- package/dist/lib/internals/pii-detection.d.ts +17 -0
- package/dist/lib/internals/pii-detection.js +116 -0
- package/dist/lib/internals/shared-metrics.d.ts +7 -0
- package/dist/lib/internals/shared-metrics.js +18 -0
- package/dist/lib/traces.d.ts +20 -1
- package/dist/lib/traces.js +47 -1
- package/dist/package.json +3 -2
- package/dist/vitest.config.js +7 -1
- package/lib/config-manager.ts +16 -0
- package/lib/exporter/console.ts +6 -4
- package/lib/exporter/grpc.ts +34 -21
- package/lib/exporter/http.ts +33 -20
- package/lib/exporter/pii-exporter-decorator.ts +152 -0
- package/lib/exporter/processor-config.ts +23 -0
- package/lib/index.ts +19 -4
- package/lib/instrumentation.node.ts +16 -16
- package/lib/internals/hooks.ts +14 -0
- package/lib/internals/pii-detection.ts +145 -0
- package/lib/internals/shared-metrics.ts +34 -0
- package/lib/traces.ts +74 -1
- package/package.json +3 -2
- package/test/config-manager.test.ts +34 -0
- package/test/exporter/pii-exporter-decorator.test.ts +88 -0
- package/test/integration/{integration.test.ts → http-tracing.integration.test.ts} +0 -2
- package/test/integration/pii.integration.test.ts +68 -0
- package/test/integration/run.sh +2 -2
- package/test/internals/hooks.test.ts +45 -0
- package/test/internals/pii-detection.test.ts +141 -0
- package/test/internals/shared-metrics.test.ts +34 -0
- package/test/node-config.test.ts +59 -14
- package/test/processor/enrich-span-processor.test.ts +2 -54
- package/test/traces/active-span.test.ts +28 -0
- package/test/traces/with-span.test.ts +340 -0
- package/test/utils/alloy-log-parser.ts +7 -0
- package/test/utils/mock-signals.ts +144 -0
- package/vitest.config.ts +7 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ReadableLogRecord } from "@opentelemetry/sdk-logs";
|
|
2
|
+
import { ResourceMetrics } from "@opentelemetry/sdk-metrics";
|
|
3
|
+
import { ReadableSpan } from "@opentelemetry/sdk-trace-base";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { PIIExporterDecorator } from "../../lib/exporter/pii-exporter-decorator";
|
|
6
|
+
|
|
7
|
+
describe("PIIExporterDecorator", () => {
|
|
8
|
+
let exporterMock: any;
|
|
9
|
+
let config: any;
|
|
10
|
+
let piiExporter: PIIExporterDecorator;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
exporterMock = {
|
|
14
|
+
export: vi.fn(),
|
|
15
|
+
shutdown: vi.fn(() => Promise.resolve()),
|
|
16
|
+
forceFlush: vi.fn(() => Promise.resolve()),
|
|
17
|
+
_delegate: {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
config = { detection: { email: true } };
|
|
21
|
+
piiExporter = new PIIExporterDecorator(exporterMock, config);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should redact emails in span name and attributes", () => {
|
|
25
|
+
const items: ReadableSpan[] = [
|
|
26
|
+
{
|
|
27
|
+
name: "user@example.com",
|
|
28
|
+
kind: 0,
|
|
29
|
+
spanContext: () => ({}),
|
|
30
|
+
attributes: { email: "user@example.com" },
|
|
31
|
+
resource: { attributes: { owner: "user@example.com" } },
|
|
32
|
+
events: [
|
|
33
|
+
{
|
|
34
|
+
name: "Login from user@example.com",
|
|
35
|
+
attributes: { email: "user@example.com" },
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
} as any,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const callback = vi.fn();
|
|
42
|
+
piiExporter.export(items, callback);
|
|
43
|
+
|
|
44
|
+
const exportedSpan = exporterMock.export.mock.calls[0][0][0];
|
|
45
|
+
expect(exportedSpan.name).toBe("[REDACTED EMAIL]");
|
|
46
|
+
expect(exportedSpan.attributes.email).toBe("[REDACTED EMAIL]");
|
|
47
|
+
expect(exportedSpan.resource.attributes.owner).toBe("[REDACTED EMAIL]");
|
|
48
|
+
expect(exportedSpan.events[0].name).toBe("Login from [REDACTED EMAIL]");
|
|
49
|
+
expect(exportedSpan.events[0].attributes.email).toBe("[REDACTED EMAIL]");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should redact emails in log records", () => {
|
|
53
|
+
const items: ReadableLogRecord[] = [
|
|
54
|
+
{
|
|
55
|
+
body: "Error from user@example.com",
|
|
56
|
+
attributes: { email: "user@example.com" },
|
|
57
|
+
severityText: "INFO",
|
|
58
|
+
severityNumber: 1,
|
|
59
|
+
resource: { attributes: { owner: "user@example.com" } },
|
|
60
|
+
} as any,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const callback = vi.fn();
|
|
64
|
+
piiExporter.export(items, callback);
|
|
65
|
+
|
|
66
|
+
const exportedLog = exporterMock.export.mock.calls[0][0][0];
|
|
67
|
+
expect(exportedLog.body).toBe("Error from [REDACTED EMAIL]");
|
|
68
|
+
expect(exportedLog.attributes.email).toBe("[REDACTED EMAIL]");
|
|
69
|
+
expect(exportedLog.resource.attributes.owner).toBe("[REDACTED EMAIL]");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should redact emails in resource metrics", () => {
|
|
73
|
+
const metrics: ResourceMetrics = {
|
|
74
|
+
resource: {
|
|
75
|
+
attributes: { maintainer: "user@example.com" },
|
|
76
|
+
},
|
|
77
|
+
scopeMetrics: [],
|
|
78
|
+
} as any;
|
|
79
|
+
|
|
80
|
+
const callback = vi.fn();
|
|
81
|
+
piiExporter.export(metrics, callback);
|
|
82
|
+
|
|
83
|
+
const exportedMetric = exporterMock.export.mock.calls[0][0];
|
|
84
|
+
expect(exportedMetric.resource.attributes.maintainer).toBe(
|
|
85
|
+
"[REDACTED EMAIL]",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -10,8 +10,6 @@ describe("instrumentation integration test", () => {
|
|
|
10
10
|
let health_traces_counter = 0;
|
|
11
11
|
let dummy_traces_counter = 0;
|
|
12
12
|
|
|
13
|
-
console.log(data.split(/\nts=/).length);
|
|
14
|
-
|
|
15
13
|
for (const line of data.split(/\nts=/)) {
|
|
16
14
|
const parsedLine: Record<string, object | string | number> =
|
|
17
15
|
parseLog(line);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, test, assert } from "vitest";
|
|
2
|
+
import { parseLog } from "../utils/alloy-log-parser";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
describe("pii integration test", () => {
|
|
7
|
+
test("should check redacted attributes and detect zero emails in logs", async () => {
|
|
8
|
+
const data = await readFile(join(__dirname, "logs.txt"), "utf-8");
|
|
9
|
+
|
|
10
|
+
let health_traces_counter = 0;
|
|
11
|
+
let dummy_traces_counter = 0;
|
|
12
|
+
let email_not_redacted = 0;
|
|
13
|
+
|
|
14
|
+
for (const line of data.split(/\nts=/)) {
|
|
15
|
+
const parsedLine: Record<string, object | string | number> =
|
|
16
|
+
parseLog(line);
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
parsedLine["attributes"] &&
|
|
20
|
+
parsedLine["attributes"]["span_kind"] &&
|
|
21
|
+
parsedLine["attributes"]["span_kind"] === "log"
|
|
22
|
+
) {
|
|
23
|
+
const matches = (parsedLine["log_body"] as string).match(
|
|
24
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
25
|
+
);
|
|
26
|
+
if (matches) {
|
|
27
|
+
email_not_redacted++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
parsedLine["attributes"] &&
|
|
33
|
+
parsedLine["attributes"]["span_kind"] &&
|
|
34
|
+
parsedLine["attributes"]["span_kind"] === "trace"
|
|
35
|
+
) {
|
|
36
|
+
if (parsedLine["attributes"]["http.target"] === "/api/dummy") {
|
|
37
|
+
dummy_traces_counter++;
|
|
38
|
+
|
|
39
|
+
// verify global sdk span resource
|
|
40
|
+
assert.equal(
|
|
41
|
+
parsedLine["resource_attributes"]["email"],
|
|
42
|
+
"[REDACTED EMAIL]",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// verify global sdk span attributes
|
|
46
|
+
assert.equal(parsedLine["attributes"]["email"], "[REDACTED EMAIL]");
|
|
47
|
+
// verify custom runtime span attributes
|
|
48
|
+
assert.equal(
|
|
49
|
+
parsedLine["attributes"]["multiple.email"],
|
|
50
|
+
"[REDACTED EMAIL]",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// verify global sdk span attributes
|
|
54
|
+
assert.equal(parsedLine["events"]["email"], "[REDACTED EMAIL]");
|
|
55
|
+
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (parsedLine["attributes"]["http.target"] === "/api/health") {
|
|
59
|
+
health_traces_counter++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
assert.equal(email_not_redacted, 0);
|
|
65
|
+
assert.equal(health_traces_counter, 0);
|
|
66
|
+
assert.equal(dummy_traces_counter, 2);
|
|
67
|
+
});
|
|
68
|
+
});
|
package/test/integration/run.sh
CHANGED
|
@@ -16,7 +16,7 @@ docker run -d \
|
|
|
16
16
|
--name $ALLOY_CONTAINER_NAME \
|
|
17
17
|
-p 4317:4317 \
|
|
18
18
|
-p 4318:4318 \
|
|
19
|
-
grafana/alloy \
|
|
19
|
+
grafana/alloy:v1.9.2 \
|
|
20
20
|
run --server.http.listen-addr=0.0.0.0:12345 --stability.level=experimental /etc/alloy/config.alloy
|
|
21
21
|
|
|
22
22
|
MAX_RETRIES=10
|
|
@@ -69,7 +69,7 @@ if [[ $ERROR_CODE -eq 0 ]]; then
|
|
|
69
69
|
fi
|
|
70
70
|
|
|
71
71
|
# sleep N seconds to await instrumentation flow send and receiving signals
|
|
72
|
-
sleep
|
|
72
|
+
sleep 1
|
|
73
73
|
|
|
74
74
|
# Copy logs from container to file
|
|
75
75
|
docker container logs $ALLOY_CONTAINER_NAME >&$ROOT_PATH/packages/sdk-node/test/integration/logs.txt
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { _shutdownHook } from "../../lib/internals/hooks";
|
|
3
|
+
|
|
4
|
+
describe("shutdown hook", () => {
|
|
5
|
+
it("should call sdk.shutdown and log success on SIGTERM", async () => {
|
|
6
|
+
const shutdownMock = vi.fn().mockResolvedValue(undefined);
|
|
7
|
+
const consoleLogMock = vi
|
|
8
|
+
.spyOn(console, "log")
|
|
9
|
+
.mockImplementation(() => {});
|
|
10
|
+
|
|
11
|
+
_shutdownHook({ shutdown: shutdownMock });
|
|
12
|
+
|
|
13
|
+
process.emit("SIGTERM");
|
|
14
|
+
// Wait for async logic
|
|
15
|
+
await Promise.resolve();
|
|
16
|
+
|
|
17
|
+
expect(shutdownMock).toHaveBeenCalled();
|
|
18
|
+
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
19
|
+
"NodeJS OpenTelemetry instrumentation shutdown successfully",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
consoleLogMock.mockRestore();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should catch and log errors on shutdown failure", async () => {
|
|
26
|
+
const error = new Error("Shutdown failed");
|
|
27
|
+
const shutdownMock = vi.fn().mockRejectedValue(error);
|
|
28
|
+
const consoleErrorMock = vi
|
|
29
|
+
.spyOn(console, "error")
|
|
30
|
+
.mockImplementation(() => {});
|
|
31
|
+
|
|
32
|
+
_shutdownHook({ shutdown: shutdownMock });
|
|
33
|
+
|
|
34
|
+
process.emit("SIGTERM");
|
|
35
|
+
// Wait for async logic
|
|
36
|
+
await Promise.resolve();
|
|
37
|
+
|
|
38
|
+
expect(consoleErrorMock).toHaveBeenCalledWith(
|
|
39
|
+
"Error shutting down NodeJS OpenTelemetry instrumentation:",
|
|
40
|
+
error,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
consoleErrorMock.mockRestore();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
_cleanStringPII,
|
|
4
|
+
_cleanLogBodyPII,
|
|
5
|
+
} from "../../lib/internals/pii-detection.js";
|
|
6
|
+
import * as sharedMetrics from "../../lib/internals/shared-metrics.js";
|
|
7
|
+
|
|
8
|
+
describe("PII Detection Utils", () => {
|
|
9
|
+
const mockMetricAdd = vi.fn();
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
vi.spyOn(sharedMetrics, "_getPIICounterRedactionMetric").mockReturnValue({
|
|
14
|
+
add: mockMetricAdd,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("_cleanStringPII", () => {
|
|
19
|
+
it("redacts plain email", () => {
|
|
20
|
+
const input = "admin@example.com";
|
|
21
|
+
const output = _cleanStringPII(input, "log");
|
|
22
|
+
|
|
23
|
+
expect(output).toBe("[REDACTED EMAIL]");
|
|
24
|
+
expect(mockMetricAdd).toHaveBeenCalledWith(
|
|
25
|
+
1,
|
|
26
|
+
expect.objectContaining({
|
|
27
|
+
pii_email_domain: "example.com",
|
|
28
|
+
pii_type: "email",
|
|
29
|
+
redaction_source: "log",
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("redacts email in URL-encoded string", () => {
|
|
35
|
+
const input = "user%40gmail.com";
|
|
36
|
+
const output = _cleanStringPII(input, "log");
|
|
37
|
+
|
|
38
|
+
expect(output).toBe("[REDACTED EMAIL]");
|
|
39
|
+
expect(mockMetricAdd).toHaveBeenCalledWith(
|
|
40
|
+
1,
|
|
41
|
+
expect.objectContaining({
|
|
42
|
+
pii_format: "url",
|
|
43
|
+
pii_email_domain: "gmail.com",
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("handles strings without email unchanged", () => {
|
|
49
|
+
const input = "hello world";
|
|
50
|
+
const output = _cleanStringPII(input, "log");
|
|
51
|
+
|
|
52
|
+
expect(output).toBe("hello world");
|
|
53
|
+
expect(mockMetricAdd).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("handles array of strings", () => {
|
|
57
|
+
const input = ["one@gmail.com", "two@example.com"];
|
|
58
|
+
const output = _cleanStringPII(input, "log");
|
|
59
|
+
|
|
60
|
+
expect(output).toEqual(["[REDACTED EMAIL]", "[REDACTED EMAIL]"]);
|
|
61
|
+
expect(mockMetricAdd).toHaveBeenCalledTimes(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("ignores non-string input", () => {
|
|
65
|
+
expect(_cleanStringPII(1234, "trace")).toBe(1234);
|
|
66
|
+
expect(_cleanStringPII(true, "trace")).toBe(true);
|
|
67
|
+
expect(_cleanStringPII(undefined, "trace")).toBeUndefined();
|
|
68
|
+
expect(mockMetricAdd).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("_cleanLogBodyPII", () => {
|
|
73
|
+
it("cleans string email", () => {
|
|
74
|
+
const result = _cleanLogBodyPII("demo@abc.com");
|
|
75
|
+
expect(result).toBe("[REDACTED EMAIL]");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("cleans deeply nested object", () => {
|
|
79
|
+
const input = {
|
|
80
|
+
user: {
|
|
81
|
+
email: "test@gmail.com",
|
|
82
|
+
profile: {
|
|
83
|
+
contact: "foo@example.com",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
status: "active",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const result = _cleanLogBodyPII(input);
|
|
90
|
+
|
|
91
|
+
expect(result).toEqual({
|
|
92
|
+
user: {
|
|
93
|
+
email: "[REDACTED EMAIL]",
|
|
94
|
+
profile: {
|
|
95
|
+
contact: "[REDACTED EMAIL]",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
status: "active",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("cleans Uint8Array input", () => {
|
|
103
|
+
const str = "admin@gmail.com";
|
|
104
|
+
const buffer = new TextEncoder().encode(str);
|
|
105
|
+
const result = _cleanLogBodyPII(buffer);
|
|
106
|
+
const decoded = new TextDecoder().decode(result as Uint8Array);
|
|
107
|
+
|
|
108
|
+
expect(decoded).toBe("[REDACTED EMAIL]");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("skips malformed Uint8Array decode", () => {
|
|
112
|
+
const corrupted = new Uint8Array([0xff, 0xfe, 0xfd]);
|
|
113
|
+
const result = _cleanLogBodyPII(corrupted);
|
|
114
|
+
|
|
115
|
+
// Should return a Uint8Array, but unmodified/redaction should not happen
|
|
116
|
+
expect(result).toBeInstanceOf(Uint8Array);
|
|
117
|
+
expect(result).not.toEqual(expect.arrayContaining([91, 82, 69]));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("cleans arrays of values", () => {
|
|
121
|
+
const result = _cleanLogBodyPII([
|
|
122
|
+
"bob@abc.com",
|
|
123
|
+
123,
|
|
124
|
+
{ nested: "jane@example.com" },
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
expect(result).toEqual([
|
|
128
|
+
"[REDACTED EMAIL]",
|
|
129
|
+
123,
|
|
130
|
+
{ nested: "[REDACTED EMAIL]" },
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("passes null and boolean through", () => {
|
|
135
|
+
expect(_cleanLogBodyPII(null)).toBeNull();
|
|
136
|
+
expect(_cleanLogBodyPII(undefined)).toBeUndefined();
|
|
137
|
+
expect(_cleanLogBodyPII(true)).toBe(true);
|
|
138
|
+
expect(_cleanLogBodyPII(false)).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -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
|
+
});
|
package/test/node-config.test.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import { test, describe, assert, expect } from "vitest";
|
|
2
|
-
import buildNodeInstrumentation from "../lib/instrumentation.node.js";
|
|
3
1
|
import { NodeSDK, metrics } from "@opentelemetry/sdk-node";
|
|
2
|
+
import { afterAll, assert, describe, expect, test } from "vitest";
|
|
3
|
+
import buildNodeInstrumentation from "../lib/instrumentation.node.js";
|
|
4
4
|
|
|
5
|
-
import { NodeSDKConfig } from "../lib/index.js";
|
|
6
|
-
import buildHttpExporters from "../lib/exporter/http.js";
|
|
7
|
-
import {
|
|
8
|
-
BatchSpanProcessor,
|
|
9
|
-
SimpleSpanProcessor,
|
|
10
|
-
} from "@opentelemetry/sdk-trace-base";
|
|
11
5
|
import {
|
|
12
6
|
BatchLogRecordProcessor,
|
|
13
7
|
SimpleLogRecordProcessor,
|
|
14
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";
|
|
15
16
|
import { EnrichSpanProcessor } from "../lib/processor/enrich-span-processor.js";
|
|
17
|
+
import { PIIExporterDecorator } from "../lib/exporter/pii-exporter-decorator.js";
|
|
16
18
|
|
|
17
19
|
describe("verify config settings", () => {
|
|
18
20
|
const commonConfig = {
|
|
@@ -37,14 +39,13 @@ describe("verify config settings", () => {
|
|
|
37
39
|
|
|
38
40
|
const logs = _configuration.logRecordProcessors;
|
|
39
41
|
|
|
40
|
-
assert.equal(logs.length,
|
|
41
|
-
assert.ok(logs[1] instanceof BatchLogRecordProcessor);
|
|
42
|
+
assert.equal(logs.length, 1);
|
|
43
|
+
assert.ok(logs[logs.length - 1] instanceof BatchLogRecordProcessor);
|
|
42
44
|
|
|
43
45
|
const spans = _configuration.spanProcessors;
|
|
44
46
|
|
|
45
|
-
assert.equal(spans.length,
|
|
47
|
+
assert.equal(spans.length, 1);
|
|
46
48
|
assert.ok(spans[0] instanceof BatchSpanProcessor);
|
|
47
|
-
assert.ok(spans[1] instanceof EnrichSpanProcessor);
|
|
48
49
|
|
|
49
50
|
assert.ok(
|
|
50
51
|
_configuration.metricReader instanceof
|
|
@@ -57,6 +58,9 @@ describe("verify config settings", () => {
|
|
|
57
58
|
...commonConfig,
|
|
58
59
|
protocol: "http",
|
|
59
60
|
diagLogLevel: "NONE",
|
|
61
|
+
spanAttributes: {
|
|
62
|
+
name: "custom-value",
|
|
63
|
+
},
|
|
60
64
|
};
|
|
61
65
|
|
|
62
66
|
const sdk: NodeSDK | undefined = await buildNodeInstrumentation(config);
|
|
@@ -68,6 +72,7 @@ describe("verify config settings", () => {
|
|
|
68
72
|
const logs = _configuration.logRecordProcessors;
|
|
69
73
|
|
|
70
74
|
assert.equal(logs.length, 2);
|
|
75
|
+
assert.ok(logs[0] instanceof EnrichLogProcessor);
|
|
71
76
|
assert.ok(logs[1] instanceof BatchLogRecordProcessor);
|
|
72
77
|
|
|
73
78
|
const spans = _configuration.spanProcessors;
|
|
@@ -87,6 +92,9 @@ describe("verify config settings", () => {
|
|
|
87
92
|
...commonConfig,
|
|
88
93
|
protocol: "console",
|
|
89
94
|
diagLogLevel: "NONE",
|
|
95
|
+
spanAttributes: {
|
|
96
|
+
name: "custom-name",
|
|
97
|
+
},
|
|
90
98
|
};
|
|
91
99
|
|
|
92
100
|
const sdk: NodeSDK = await buildNodeInstrumentation(config)!;
|
|
@@ -99,6 +107,7 @@ describe("verify config settings", () => {
|
|
|
99
107
|
|
|
100
108
|
// verify simple log processor for instant console logging
|
|
101
109
|
assert.equal(logs.length, 2);
|
|
110
|
+
assert.ok(logs[0] instanceof EnrichLogProcessor);
|
|
102
111
|
assert.ok(logs[1] instanceof SimpleLogRecordProcessor);
|
|
103
112
|
|
|
104
113
|
const spans = _configuration.spanProcessors;
|
|
@@ -129,8 +138,39 @@ describe("verify config settings", () => {
|
|
|
129
138
|
const _configuration = sdk["_configuration"];
|
|
130
139
|
|
|
131
140
|
const logRecordProcessors = _configuration.logRecordProcessors;
|
|
132
|
-
assert.equal(logRecordProcessors.length,
|
|
133
|
-
assert.ok(
|
|
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 () => {
|
|
149
|
+
const config: NodeSDKConfig = {
|
|
150
|
+
...commonConfig,
|
|
151
|
+
protocol: "grpc",
|
|
152
|
+
diagLogLevel: "NONE",
|
|
153
|
+
collectorMode: "single",
|
|
154
|
+
detection: {},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const sdk: NodeSDK | undefined = await buildNodeInstrumentation(config);
|
|
158
|
+
|
|
159
|
+
assert.ok(sdk);
|
|
160
|
+
|
|
161
|
+
const _configuration = sdk["_configuration"];
|
|
162
|
+
|
|
163
|
+
const logRecordProcessors = _configuration.logRecordProcessors;
|
|
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
|
+
);
|
|
134
174
|
});
|
|
135
175
|
|
|
136
176
|
test("check if clear base endpoint final slash", () => {
|
|
@@ -142,4 +182,9 @@ describe("verify config settings", () => {
|
|
|
142
182
|
|
|
143
183
|
expect(config.collectorUrl).toBe("http://example.com");
|
|
144
184
|
});
|
|
185
|
+
|
|
186
|
+
afterAll(() => {
|
|
187
|
+
// shutdown every instrumentation
|
|
188
|
+
process.emit("SIGTERM");
|
|
189
|
+
});
|
|
145
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", () => {
|
|
@@ -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
|
+
});
|