@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 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
- Object.assign(log, {
86
+ return {
87
+ ...log,
87
88
  body: _cleanLogBodyPII(log.body),
88
89
  attributes: log.attributes && _cleanObjectPII(log.attributes, "log"),
89
- resource: {
90
- attributes: log?.resource?.attributes &&
91
- _cleanObjectPII(log.resource.attributes, "log"),
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,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -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
- Object.assign(log, {
132
+ return {
133
+ ...log,
133
134
  body: _cleanLogBodyPII(log.body),
134
135
  attributes: log.attributes && _cleanObjectPII(log.attributes, "log"),
135
- resource: {
136
- attributes:
137
- log?.resource?.attributes &&
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1,139 +1,88 @@
1
- import type { ReadableLogRecord } from "@opentelemetry/sdk-logs";
2
- import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";
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 type { NodeSDKConfig } from "../../index.js";
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 decorator: PIIExporterDecorator;
8
+ let exporterMock: any;
9
+ let config: any;
10
+ let piiExporter: PIIExporterDecorator;
57
11
 
58
12
  beforeEach(() => {
59
- vi.clearAllMocks();
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("delegates forceFlush and shutdown", async () => {
63
- const config: NodeSDKConfig = {};
64
- decorator = new PIIExporterDecorator(mockExporter as any, config);
65
-
66
- await decorator.forceFlush();
67
- await decorator.shutdown();
68
-
69
- expect(mockExporter.forceFlush).toHaveBeenCalled();
70
- expect(mockExporter.shutdown).toHaveBeenCalled();
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("skips redaction if detection.email is false", () => {
74
- const config: NodeSDKConfig = { detection: { email: false } };
75
- decorator = new PIIExporterDecorator(mockExporter as any, config);
76
-
77
- const spans = [baseSpan()];
78
- decorator.export(spans, mockCallback);
79
-
80
- expect(mockExporter.export).toHaveBeenCalledWith(spans, mockCallback);
81
- expect(pii._cleanStringPII).not.toHaveBeenCalled();
82
- expect(pii._cleanObjectPII).not.toHaveBeenCalled();
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("redacts span data if detection.email is true", () => {
86
- const config: NodeSDKConfig = { detection: { email: true } };
87
- decorator = new PIIExporterDecorator(mockExporter as any, config);
88
-
89
- const span = baseSpan();
90
- decorator.export([span], mockCallback);
91
-
92
- expect(pii._cleanStringPII).toHaveBeenCalled();
93
- expect(pii._cleanObjectPII).toHaveBeenCalled();
94
- expect(mockExporter.export).toHaveBeenCalledWith([span], mockCallback);
95
-
96
- // verify name and nested structure were touched
97
- expect(span.name).toMatch(/^\[REDACTED\(/);
98
- expect(span.attributes.email).toMatch(/^\[REDACTED\(/);
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
  });