@ogcio/o11y-sdk-node 0.3.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 +7 -0
- package/dist/lib/exporter/pii-exporter-decorator.d.ts +1 -1
- package/dist/lib/exporter/pii-exporter-decorator.js +7 -6
- package/dist/package.json +1 -1
- package/lib/exporter/pii-exporter-decorator.ts +11 -11
- package/package.json +1 -1
- package/test/exporter/pii-exporter-decorator.test.ts +75 -126
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-node@v0.3.0...@ogcio/o11y-sdk-node@v0.3.1) (2025-07-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **sdk-node:** replace Object.assign with spread operator on log redaction ([#186](https://github.com/ogcio/o11y/issues/186)) ([432f8d6](https://github.com/ogcio/o11y/commit/432f8d6aa493fd01d093f7ecdd0a9faccd0726e9))
|
|
9
|
+
|
|
3
10
|
## [0.3.0](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-node@v0.2.0...@ogcio/o11y-sdk-node@v0.3.0) (2025-07-28)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ExportResult } from "@opentelemetry/core";
|
|
2
2
|
import { OTLPExporterBase } from "@opentelemetry/otlp-exporter-base";
|
|
3
3
|
import { ReadableLogRecord } from "@opentelemetry/sdk-logs";
|
|
4
|
+
import { PushMetricExporter, ResourceMetrics } from "@opentelemetry/sdk-metrics";
|
|
4
5
|
import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
5
6
|
import { NodeSDKConfig } from "../index.js";
|
|
6
|
-
import { PushMetricExporter, ResourceMetrics } from "@opentelemetry/sdk-metrics";
|
|
7
7
|
export declare class PIIExporterDecorator extends OTLPExporterBase<(ReadableSpan | ReadableLogRecord)[] | ResourceMetrics> implements SpanExporter, PushMetricExporter {
|
|
8
8
|
private readonly _exporter;
|
|
9
9
|
private readonly _config;
|
|
@@ -25,7 +25,7 @@ export class PIIExporterDecorator extends OTLPExporterBase {
|
|
|
25
25
|
this._redactSpan(item);
|
|
26
26
|
}
|
|
27
27
|
else if (this._isReadableLogRecord(item)) {
|
|
28
|
-
this._redactLogRecord(item);
|
|
28
|
+
item = this._redactLogRecord(item);
|
|
29
29
|
}
|
|
30
30
|
return item;
|
|
31
31
|
});
|
|
@@ -83,14 +83,15 @@ export class PIIExporterDecorator extends OTLPExporterBase {
|
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
_redactLogRecord(log) {
|
|
86
|
-
|
|
86
|
+
return {
|
|
87
|
+
...log,
|
|
87
88
|
body: _cleanLogBodyPII(log.body),
|
|
88
89
|
attributes: log.attributes && _cleanObjectPII(log.attributes, "log"),
|
|
89
|
-
resource: {
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
resource: log.resource && {
|
|
91
|
+
...log.resource,
|
|
92
|
+
attributes: _cleanObjectPII(log.resource.attributes, "log"),
|
|
92
93
|
},
|
|
93
|
-
}
|
|
94
|
+
};
|
|
94
95
|
}
|
|
95
96
|
_redactResourceMetrics(metric) {
|
|
96
97
|
Object.assign(metric, {
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { ExportResult } from "@opentelemetry/core";
|
|
2
2
|
import { OTLPExporterBase } from "@opentelemetry/otlp-exporter-base";
|
|
3
3
|
import { ReadableLogRecord } from "@opentelemetry/sdk-logs";
|
|
4
|
+
import {
|
|
5
|
+
PushMetricExporter,
|
|
6
|
+
ResourceMetrics,
|
|
7
|
+
} from "@opentelemetry/sdk-metrics";
|
|
4
8
|
import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
5
9
|
import { NodeSDKConfig } from "../index.js";
|
|
6
10
|
import {
|
|
@@ -8,10 +12,6 @@ import {
|
|
|
8
12
|
_cleanObjectPII,
|
|
9
13
|
_cleanStringPII,
|
|
10
14
|
} from "../internals/pii-detection.js";
|
|
11
|
-
import {
|
|
12
|
-
PushMetricExporter,
|
|
13
|
-
ResourceMetrics,
|
|
14
|
-
} from "@opentelemetry/sdk-metrics";
|
|
15
15
|
|
|
16
16
|
export class PIIExporterDecorator
|
|
17
17
|
extends OTLPExporterBase<
|
|
@@ -55,7 +55,7 @@ export class PIIExporterDecorator
|
|
|
55
55
|
if (this._isReadableSpan(item)) {
|
|
56
56
|
this._redactSpan(item);
|
|
57
57
|
} else if (this._isReadableLogRecord(item)) {
|
|
58
|
-
this._redactLogRecord(item);
|
|
58
|
+
item = this._redactLogRecord(item) as ReadableLogRecord;
|
|
59
59
|
}
|
|
60
60
|
return item;
|
|
61
61
|
});
|
|
@@ -129,15 +129,15 @@ export class PIIExporterDecorator
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
private _redactLogRecord(log: ReadableLogRecord) {
|
|
132
|
-
|
|
132
|
+
return {
|
|
133
|
+
...log,
|
|
133
134
|
body: _cleanLogBodyPII(log.body),
|
|
134
135
|
attributes: log.attributes && _cleanObjectPII(log.attributes, "log"),
|
|
135
|
-
resource: {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
_cleanObjectPII(log.resource.attributes, "log"),
|
|
136
|
+
resource: log.resource && {
|
|
137
|
+
...log.resource,
|
|
138
|
+
attributes: _cleanObjectPII(log.resource.attributes, "log"),
|
|
139
139
|
},
|
|
140
|
-
}
|
|
140
|
+
};
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
private _redactResourceMetrics(metric: ResourceMetrics) {
|
package/package.json
CHANGED
|
@@ -1,139 +1,88 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { ReadableLogRecord } from "@opentelemetry/sdk-logs";
|
|
2
|
+
import { ResourceMetrics } from "@opentelemetry/sdk-metrics";
|
|
3
|
+
import { ReadableSpan } from "@opentelemetry/sdk-trace-base";
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import
|
|
5
|
-
import { PIIExporterDecorator } from "../../lib/exporter/pii-exporter-decorator.js";
|
|
6
|
-
import * as pii from "../../lib/internals/pii-detection.js";
|
|
7
|
-
|
|
8
|
-
const mockExporter = {
|
|
9
|
-
export: vi.fn(),
|
|
10
|
-
forceFlush: vi.fn().mockResolvedValue(undefined),
|
|
11
|
-
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
12
|
-
_delegate: {}, // required by OTLPExporterBase
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const mockCallback = vi.fn();
|
|
16
|
-
|
|
17
|
-
const baseSpan = (): ReadableSpan =>
|
|
18
|
-
({
|
|
19
|
-
name: "john.doe@example.com",
|
|
20
|
-
kind: 1,
|
|
21
|
-
spanContext: () => ({ traceId: "abc", spanId: "def" }),
|
|
22
|
-
attributes: { email: "john.doe@example.com" },
|
|
23
|
-
resource: {
|
|
24
|
-
attributes: { team: "dev@example.com" },
|
|
25
|
-
otherProp: "shouldBeLost",
|
|
26
|
-
},
|
|
27
|
-
links: [{ attributes: { collaborator: "co@example.com" } }],
|
|
28
|
-
events: [{ name: "event@email.com", attributes: { code: "123" } }],
|
|
29
|
-
}) as any;
|
|
30
|
-
|
|
31
|
-
const baseLog = (): ReadableLogRecord =>
|
|
32
|
-
({
|
|
33
|
-
body: "user: john.doe@example.com",
|
|
34
|
-
attributes: { email: "john.doe@example.com" },
|
|
35
|
-
resource: {
|
|
36
|
-
attributes: { service: "log@example.com" },
|
|
37
|
-
otherProp: "shouldBeLost",
|
|
38
|
-
},
|
|
39
|
-
severityText: "INFO",
|
|
40
|
-
severityNumber: 1,
|
|
41
|
-
}) as any;
|
|
42
|
-
|
|
43
|
-
vi.mock("../../lib/internals/pii-detection.js", async () => {
|
|
44
|
-
return {
|
|
45
|
-
_cleanStringPII: vi.fn((v: string) => `[REDACTED(${v})]`),
|
|
46
|
-
_cleanObjectPII: vi.fn((obj: any) =>
|
|
47
|
-
Object.fromEntries(
|
|
48
|
-
Object.entries(obj || {}).map(([k, v]) => [k, `[REDACTED(${v})]`]),
|
|
49
|
-
),
|
|
50
|
-
),
|
|
51
|
-
_cleanLogBodyPII: vi.fn((v: any) => `[REDACTED_BODY(${v})]`),
|
|
52
|
-
};
|
|
53
|
-
});
|
|
5
|
+
import { PIIExporterDecorator } from "../../lib/exporter/pii-exporter-decorator";
|
|
54
6
|
|
|
55
7
|
describe("PIIExporterDecorator", () => {
|
|
56
|
-
let
|
|
8
|
+
let exporterMock: any;
|
|
9
|
+
let config: any;
|
|
10
|
+
let piiExporter: PIIExporterDecorator;
|
|
57
11
|
|
|
58
12
|
beforeEach(() => {
|
|
59
|
-
|
|
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);
|
|
60
22
|
});
|
|
61
23
|
|
|
62
|
-
it("
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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]");
|
|
71
50
|
});
|
|
72
51
|
|
|
73
|
-
it("
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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]");
|
|
83
70
|
});
|
|
84
71
|
|
|
85
|
-
it("
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
expect(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// Resource is replaced with only attributes, old props lost
|
|
101
|
-
expect(span.resource).toEqual({
|
|
102
|
-
attributes: { team: "[REDACTED(dev@example.com)]" },
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
expect(span.events[0].name).toMatch(/^\[REDACTED\(/);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("redacts log record data if detection.email is true", () => {
|
|
109
|
-
const config: NodeSDKConfig = { detection: { email: true } };
|
|
110
|
-
decorator = new PIIExporterDecorator(mockExporter as any, config);
|
|
111
|
-
|
|
112
|
-
const log = baseLog();
|
|
113
|
-
decorator.export([log], mockCallback);
|
|
114
|
-
|
|
115
|
-
expect(pii._cleanLogBodyPII).toHaveBeenCalled();
|
|
116
|
-
expect(pii._cleanObjectPII).toHaveBeenCalled();
|
|
117
|
-
|
|
118
|
-
expect(log.body).toMatch(/^\[REDACTED_BODY\(/);
|
|
119
|
-
expect(log.attributes.email).toMatch(/^\[REDACTED\(/);
|
|
120
|
-
|
|
121
|
-
// Resource replaced with only attributes, other props lost
|
|
122
|
-
expect(log.resource).toEqual({
|
|
123
|
-
attributes: { service: "[REDACTED(log@example.com)]" },
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("handles a mix of spans and logs", () => {
|
|
128
|
-
const config: NodeSDKConfig = { detection: { email: true } };
|
|
129
|
-
decorator = new PIIExporterDecorator(mockExporter as any, config);
|
|
130
|
-
|
|
131
|
-
const items = [baseSpan(), baseLog()];
|
|
132
|
-
decorator.export(items, mockCallback);
|
|
133
|
-
|
|
134
|
-
expect(mockExporter.export).toHaveBeenCalledWith(items, mockCallback);
|
|
135
|
-
expect(pii._cleanLogBodyPII).toHaveBeenCalled();
|
|
136
|
-
expect(pii._cleanStringPII).toHaveBeenCalled();
|
|
137
|
-
expect(pii._cleanObjectPII).toHaveBeenCalled();
|
|
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
|
+
);
|
|
138
87
|
});
|
|
139
88
|
});
|