@ogcio/o11y-sdk-node 0.4.0 → 0.4.2

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 (33) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/lib/config-manager.d.ts +1 -1
  3. package/dist/lib/config-manager.js +1 -4
  4. package/dist/lib/exporter/pii-exporter-decorator.d.ts +1 -1
  5. package/dist/lib/exporter/pii-exporter-decorator.js +10 -10
  6. package/dist/lib/internals/redaction/pii-detection.d.ts +11 -9
  7. package/dist/lib/internals/redaction/pii-detection.js +17 -24
  8. package/dist/lib/internals/redaction/redactors/ip.js +2 -2
  9. package/dist/lib/traces.js +1 -1
  10. package/dist/package.json +1 -1
  11. package/dist/vitest.config.js +1 -1
  12. package/lib/config-manager.ts +2 -6
  13. package/lib/exporter/pii-exporter-decorator.ts +15 -12
  14. package/lib/internals/redaction/pii-detection.ts +27 -40
  15. package/lib/internals/redaction/redactors/ip.ts +2 -2
  16. package/lib/traces.ts +2 -2
  17. package/package.json +1 -1
  18. package/test/config-manager.test.ts +2 -2
  19. package/test/integration/README.md +59 -11
  20. package/test/integration/docker-utils.sh +214 -0
  21. package/test/integration/main.sh +52 -0
  22. package/test/integration/teardown.sh +7 -0
  23. package/test/integration/{http-tracing.integration.test.ts → test_fastify-o11y-pii-enabled/http-tracing.integration.test.ts} +1 -1
  24. package/test/integration/{pii.integration.test.ts → test_fastify-o11y-pii-enabled/pii.integration.test.ts} +1 -1
  25. package/test/integration/test_fastify-o11y-pii-enabled/run.sh +42 -0
  26. package/test/integration/test_without-o11y/run.sh +30 -0
  27. package/test/integration/test_without-o11y/verify-status.integration.test.ts +32 -0
  28. package/test/internals/pii-detection.test.ts +142 -20
  29. package/test/internals/redactors/ip.test.ts +4 -0
  30. package/test/traces/active-span.test.ts +2 -4
  31. package/test/traces/with-span.test.ts +16 -0
  32. package/vitest.config.ts +1 -1
  33. package/test/integration/run.sh +0 -88
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.2](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-node@v0.4.1...@ogcio/o11y-sdk-node@v0.4.2) (2025-09-03)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * withSpan function throws error if sdk is not initialized AB[#30828](https://github.com/ogcio/o11y/issues/30828) ([#200](https://github.com/ogcio/o11y/issues/200)) ([407b616](https://github.com/ogcio/o11y/commit/407b616590024ca4131610c7a69bacd9699f23c5))
9
+
10
+ ## [0.4.1](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-node@v0.4.0...@ogcio/o11y-sdk-node@v0.4.1) (2025-09-03)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * nested attributes redaction node sdk AB[#30826](https://github.com/ogcio/o11y/issues/30826) ([#198](https://github.com/ogcio/o11y/issues/198)) ([b926ba5](https://github.com/ogcio/o11y/commit/b926ba54097fd6028c8aa78cbe1c276ce2cc2166))
16
+
3
17
  ## [0.4.0](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-node@v0.3.1...@ogcio/o11y-sdk-node@v0.4.0) (2025-09-02)
4
18
 
5
19
 
@@ -1,3 +1,3 @@
1
1
  import { NodeSDKConfig } from "./index.js";
2
2
  export declare const setNodeSdkConfig: (config: NodeSDKConfig) => void;
3
- export declare const getNodeSdkConfig: () => NodeSDKConfig;
3
+ export declare const getNodeSdkConfig: () => NodeSDKConfig | undefined;
@@ -3,9 +3,6 @@ export const setNodeSdkConfig = (config) => {
3
3
  nodeSDKConfig = config;
4
4
  };
5
5
  export const getNodeSdkConfig = () => {
6
- if (!nodeSDKConfig) {
7
- throw new Error("Node SDK Config was not initialized.");
8
- }
9
6
  // Ensure getters do not edit config.
10
- return JSON.parse(JSON.stringify(nodeSDKConfig));
7
+ return nodeSDKConfig ? JSON.parse(JSON.stringify(nodeSDKConfig)) : undefined;
11
8
  };
@@ -8,7 +8,7 @@ export declare class PIIExporterDecorator extends OTLPExporterBase<(ReadableSpan
8
8
  private readonly _exporter;
9
9
  private readonly _config;
10
10
  private readonly _redactors;
11
- constructor(exporter: OTLPExporterBase<(ReadableSpan | ReadableLogRecord)[] | ResourceMetrics>, config: NodeSDKConfig);
11
+ constructor(exporter: OTLPExporterBase<(ReadableSpan | ReadableLogRecord)[] | ResourceMetrics>, config: NodeSDKConfig | undefined);
12
12
  forceFlush(): Promise<void>;
13
13
  shutdown(): Promise<void>;
14
14
  export(items: (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics, resultCallback: (result: ExportResult) => void): void;
@@ -1,5 +1,5 @@
1
1
  import { OTLPExporterBase } from "@opentelemetry/otlp-exporter-base";
2
- import { _cleanLogBodyPII, _cleanObjectPII, _cleanStringPII, } from "../internals/redaction/pii-detection.js";
2
+ import { _cleanStringPII, _recursiveObjectClean, } from "../internals/redaction/pii-detection.js";
3
3
  import { redactors, } from "../internals/redaction/redactors/index.js";
4
4
  export class PIIExporterDecorator extends OTLPExporterBase {
5
5
  _exporter;
@@ -9,7 +9,7 @@ export class PIIExporterDecorator extends OTLPExporterBase {
9
9
  super(exporter["_delegate"]);
10
10
  this._exporter = exporter;
11
11
  this._config = config;
12
- this._redactors = this._buildRedactors(config.detection);
12
+ this._redactors = this._buildRedactors(config?.detection);
13
13
  }
14
14
  forceFlush() {
15
15
  return this._exporter.forceFlush();
@@ -67,22 +67,22 @@ export class PIIExporterDecorator extends OTLPExporterBase {
67
67
  Object.assign(span, {
68
68
  name: _cleanStringPII(span.name, "trace", this._redactors),
69
69
  attributes: span.attributes &&
70
- _cleanObjectPII(span.attributes, "trace", this._redactors),
70
+ _recursiveObjectClean(span.attributes, "trace", this._redactors),
71
71
  resource: {
72
72
  attributes: span?.resource?.attributes &&
73
- _cleanObjectPII(span.resource.attributes, "trace", this._redactors),
73
+ _recursiveObjectClean(span.resource.attributes, "trace", this._redactors),
74
74
  },
75
75
  links: span?.links?.map((link) => {
76
76
  Object.assign(link, {
77
77
  attributes: link?.attributes &&
78
- _cleanObjectPII(link.attributes, "trace", this._redactors),
78
+ _recursiveObjectClean(link.attributes, "trace", this._redactors),
79
79
  });
80
80
  }),
81
81
  events: span?.events?.map((event) => {
82
82
  Object.assign(event, {
83
83
  name: _cleanStringPII(event.name, "trace", this._redactors),
84
84
  attributes: event?.attributes &&
85
- _cleanObjectPII(event.attributes, "trace", this._redactors),
85
+ _recursiveObjectClean(event.attributes, "trace", this._redactors),
86
86
  });
87
87
  return event;
88
88
  }),
@@ -91,12 +91,12 @@ export class PIIExporterDecorator extends OTLPExporterBase {
91
91
  _redactLogRecord(log) {
92
92
  return {
93
93
  ...log,
94
- body: _cleanLogBodyPII(log.body, this._redactors),
94
+ body: _recursiveObjectClean(log.body, "log", this._redactors),
95
95
  attributes: log.attributes &&
96
- _cleanObjectPII(log.attributes, "log", this._redactors),
96
+ _recursiveObjectClean(log.attributes, "log", this._redactors),
97
97
  resource: log.resource && {
98
98
  ...log.resource,
99
- attributes: _cleanObjectPII(log.resource.attributes, "log", this._redactors),
99
+ attributes: _recursiveObjectClean(log.resource.attributes, "log", this._redactors),
100
100
  },
101
101
  };
102
102
  }
@@ -104,7 +104,7 @@ export class PIIExporterDecorator extends OTLPExporterBase {
104
104
  Object.assign(metric, {
105
105
  resource: {
106
106
  attributes: metric?.resource?.attributes &&
107
- _cleanObjectPII(metric.resource.attributes, "metric", this._redactors),
107
+ _recursiveObjectClean(metric.resource.attributes, "metric", this._redactors),
108
108
  },
109
109
  });
110
110
  }
@@ -1,23 +1,25 @@
1
1
  import type { AnyValue } from "@opentelemetry/api-logs";
2
2
  import { Redactor } from "./redactors/index.js";
3
- import { AttributeValue } from "@opentelemetry/api";
4
3
  export type PIISource = "trace" | "log" | "metric";
4
+ /**
5
+ * Checks whether a string contains URI-encoded components.
6
+ *
7
+ * @param {string} value - The string to inspect.
8
+ * @returns {boolean} `true` if the string is encoded, `false` otherwise.
9
+ */
10
+ export declare function _containsEncodedComponents(value: string): boolean;
5
11
  /**
6
12
  * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
7
13
  *
8
14
  * If the string is URL-encoded, it will be decoded before redaction.
9
- * Metrics are emitted for:
10
- * - each domain found in redacted email addresses.
11
- * - IPv4|IPv6 addresses redacted.
12
15
  *
13
16
  * @template T
14
17
  *
15
- * @param {T} value - The input value to sanitize.
18
+ * @param {string} value - The input value to sanitize.
16
19
  * @param {"trace" | "log"} source - The source context of the input, used in metrics.
17
20
  * @param {Redactor[]} redactors - The string processors containing the redaction logic.
18
21
  *
19
- * @returns {T} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
22
+ * @returns {string} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
20
23
  */
21
- export declare function _cleanStringPII<T extends AnyValue>(value: T, source: PIISource, redactors: Redactor[]): T;
22
- export declare function _cleanObjectPII(entry: object, source: PIISource, redactors: Redactor[]): Record<string, AttributeValue>;
23
- export declare function _cleanLogBodyPII<T extends AnyValue>(value: T, redactors: Redactor[]): T;
24
+ export declare function _cleanStringPII(value: string, source: PIISource, redactors: Redactor[]): string;
25
+ export declare function _recursiveObjectClean<T extends AnyValue>(value: T, source: PIISource, redactors: Redactor[]): T;
@@ -6,34 +6,36 @@ const encoder = new TextEncoder();
6
6
  * @param {string} value - The string to inspect.
7
7
  * @returns {boolean} `true` if the string is encoded, `false` otherwise.
8
8
  */
9
- function _containsEncodedComponents(value) {
9
+ export function _containsEncodedComponents(value) {
10
10
  try {
11
- return decodeURI(value) !== decodeURIComponent(value);
11
+ const decodedURIComponent = decodeURIComponent(value);
12
+ if (decodeURI(value) !== decodedURIComponent) {
13
+ return true;
14
+ }
15
+ if (value !== decodedURIComponent) {
16
+ return (encodeURIComponent(decodedURIComponent) === value ||
17
+ encodeURI(decodedURIComponent) === value);
18
+ }
12
19
  }
13
20
  catch {
14
21
  return false;
15
22
  }
23
+ return false;
16
24
  }
17
25
  /**
18
26
  * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
19
27
  *
20
28
  * If the string is URL-encoded, it will be decoded before redaction.
21
- * Metrics are emitted for:
22
- * - each domain found in redacted email addresses.
23
- * - IPv4|IPv6 addresses redacted.
24
29
  *
25
30
  * @template T
26
31
  *
27
- * @param {T} value - The input value to sanitize.
32
+ * @param {string} value - The input value to sanitize.
28
33
  * @param {"trace" | "log"} source - The source context of the input, used in metrics.
29
34
  * @param {Redactor[]} redactors - The string processors containing the redaction logic.
30
35
  *
31
- * @returns {T} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
36
+ * @returns {string} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
32
37
  */
33
38
  export function _cleanStringPII(value, source, redactors) {
34
- if (Array.isArray(value)) {
35
- return value.map((v) => _cleanStringPII(v, source, redactors));
36
- }
37
39
  if (typeof value !== "string") {
38
40
  return value;
39
41
  }
@@ -45,18 +47,9 @@ export function _cleanStringPII(value, source, redactors) {
45
47
  }
46
48
  return redactors.reduce((redactedValue, currentRedactor) => currentRedactor(redactedValue, source, kind), decodedValue);
47
49
  }
48
- export function _cleanObjectPII(entry, source, redactors) {
49
- if (!entry) {
50
- return entry;
51
- }
52
- return Object.fromEntries(Object.entries(entry).map(([k, v]) => [
53
- k,
54
- _cleanStringPII(v, source, redactors),
55
- ]));
56
- }
57
- export function _cleanLogBodyPII(value, redactors) {
50
+ export function _recursiveObjectClean(value, source, redactors) {
58
51
  if (typeof value === "string") {
59
- return _cleanStringPII(value, "log", redactors);
52
+ return _cleanStringPII(value, source, redactors);
60
53
  }
61
54
  if (typeof value === "number" ||
62
55
  typeof value === "boolean" ||
@@ -66,7 +59,7 @@ export function _cleanLogBodyPII(value, redactors) {
66
59
  if (value instanceof Uint8Array) {
67
60
  try {
68
61
  const decoded = decoder.decode(value);
69
- const sanitized = _cleanStringPII(decoded, "log", redactors);
62
+ const sanitized = _cleanStringPII(decoded, source, redactors);
70
63
  return encoder.encode(sanitized);
71
64
  }
72
65
  catch {
@@ -74,12 +67,12 @@ export function _cleanLogBodyPII(value, redactors) {
74
67
  }
75
68
  }
76
69
  if (Array.isArray(value)) {
77
- return value.map((value) => _cleanLogBodyPII(value, redactors));
70
+ return value.map((value) => _recursiveObjectClean(value, source, redactors));
78
71
  }
79
72
  if (typeof value === "object") {
80
73
  const sanitized = {};
81
74
  for (const [key, val] of Object.entries(value)) {
82
- sanitized[key] = _cleanLogBodyPII(val, redactors);
75
+ sanitized[key] = _recursiveObjectClean(val, source, redactors);
83
76
  }
84
77
  return sanitized;
85
78
  }
@@ -1,7 +1,7 @@
1
1
  import { _getPIICounterRedactionMetric } from "../../shared-metrics.js";
2
2
  // Generous IP address matchers (might match some invalid addresses like 192.168.01.1)
3
- const IPV4_REGEX = /(?<!\d)(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}(?!\d)/gi;
4
- const IPV6_REGEX = /(?<![0-9a-f:])((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|:(?::[0-9A-Fa-f]{1,4}){1,7}|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}|:(?::[0-9A-Fa-f]{1,4}){1,7}:?|(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})){3})(?![0-9a-f:])/gi;
3
+ const IPV4_REGEX = /(?<!\d)(?:%[0-9A-Fa-f]{2})?(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}(?:%[0-9A-Fa-f]{2})?(?!\d)/gi;
4
+ const IPV6_REGEX = /(?<![0-9a-f:])(?:%[0-9A-Fa-f]{2})?((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|:(?::[0-9A-Fa-f]{1,4}){1,7}|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}|:(?::[0-9A-Fa-f]{1,4}){1,7}:?|(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})){3})(?:%[0-9A-Fa-f]{2})?(?![0-9a-f:])/gi;
5
5
  /**
6
6
  * Redacts all ip addresses in the input string and collects metadata.
7
7
  *
@@ -45,6 +45,6 @@ export function getActiveSpan() {
45
45
  }
46
46
  export function withSpan({ traceName, spanName, spanOptions = {}, fn, }) {
47
47
  const sdkConfig = getNodeSdkConfig();
48
- const tracer = trace.getTracer(traceName ?? sdkConfig.serviceName ?? "o11y-sdk", sdkConfig.serviceVersion);
48
+ const tracer = trace.getTracer(traceName ?? sdkConfig?.serviceName ?? "o11y-sdk", sdkConfig?.serviceVersion);
49
49
  return tracer.startActiveSpan(spanName, spanOptions, selfContainedSpanHandlerGenerator(fn));
50
50
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -36,7 +36,7 @@ export default defineConfig({
36
36
  },
37
37
  {
38
38
  test: {
39
- include: ["**/test/integration/*.test.ts"],
39
+ include: ["**/test/integration/**/*.test.ts"],
40
40
  name: "integration",
41
41
  },
42
42
  },
@@ -6,11 +6,7 @@ export const setNodeSdkConfig = (config: NodeSDKConfig) => {
6
6
  nodeSDKConfig = config;
7
7
  };
8
8
 
9
- export const getNodeSdkConfig = (): NodeSDKConfig => {
10
- if (!nodeSDKConfig) {
11
- throw new Error("Node SDK Config was not initialized.");
12
- }
13
-
9
+ export const getNodeSdkConfig = (): NodeSDKConfig | undefined => {
14
10
  // Ensure getters do not edit config.
15
- return JSON.parse(JSON.stringify(nodeSDKConfig));
11
+ return nodeSDKConfig ? JSON.parse(JSON.stringify(nodeSDKConfig)) : undefined;
16
12
  };
@@ -8,9 +8,8 @@ import {
8
8
  import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
9
9
  import { NodeSDKConfig } from "../index.js";
10
10
  import {
11
- _cleanLogBodyPII,
12
- _cleanObjectPII,
13
11
  _cleanStringPII,
12
+ _recursiveObjectClean,
14
13
  } from "../internals/redaction/pii-detection.js";
15
14
  import {
16
15
  Redactor,
@@ -32,12 +31,12 @@ export class PIIExporterDecorator
32
31
  exporter: OTLPExporterBase<
33
32
  (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics
34
33
  >,
35
- config: NodeSDKConfig,
34
+ config: NodeSDKConfig | undefined,
36
35
  ) {
37
36
  super(exporter["_delegate"]);
38
37
  this._exporter = exporter;
39
38
  this._config = config;
40
- this._redactors = this._buildRedactors(config.detection);
39
+ this._redactors = this._buildRedactors(config?.detection);
41
40
  }
42
41
 
43
42
  forceFlush(): Promise<void> {
@@ -114,17 +113,21 @@ export class PIIExporterDecorator
114
113
  name: _cleanStringPII(span.name, "trace", this._redactors),
115
114
  attributes:
116
115
  span.attributes &&
117
- _cleanObjectPII(span.attributes, "trace", this._redactors),
116
+ _recursiveObjectClean(span.attributes, "trace", this._redactors),
118
117
  resource: {
119
118
  attributes:
120
119
  span?.resource?.attributes &&
121
- _cleanObjectPII(span.resource.attributes, "trace", this._redactors),
120
+ _recursiveObjectClean(
121
+ span.resource.attributes,
122
+ "trace",
123
+ this._redactors,
124
+ ),
122
125
  },
123
126
  links: span?.links?.map((link) => {
124
127
  Object.assign(link, {
125
128
  attributes:
126
129
  link?.attributes &&
127
- _cleanObjectPII(link.attributes, "trace", this._redactors),
130
+ _recursiveObjectClean(link.attributes, "trace", this._redactors),
128
131
  });
129
132
  }),
130
133
  events: span?.events?.map((event) => {
@@ -132,7 +135,7 @@ export class PIIExporterDecorator
132
135
  name: _cleanStringPII(event.name, "trace", this._redactors),
133
136
  attributes:
134
137
  event?.attributes &&
135
- _cleanObjectPII(event.attributes, "trace", this._redactors),
138
+ _recursiveObjectClean(event.attributes, "trace", this._redactors),
136
139
  });
137
140
  return event;
138
141
  }),
@@ -142,13 +145,13 @@ export class PIIExporterDecorator
142
145
  private _redactLogRecord(log: ReadableLogRecord): ReadableLogRecord {
143
146
  return {
144
147
  ...log,
145
- body: _cleanLogBodyPII(log.body, this._redactors),
148
+ body: _recursiveObjectClean(log.body, "log", this._redactors),
146
149
  attributes:
147
150
  log.attributes &&
148
- _cleanObjectPII(log.attributes, "log", this._redactors),
151
+ _recursiveObjectClean(log.attributes, "log", this._redactors),
149
152
  resource: log.resource && {
150
153
  ...log.resource,
151
- attributes: _cleanObjectPII(
154
+ attributes: _recursiveObjectClean(
152
155
  log.resource.attributes,
153
156
  "log",
154
157
  this._redactors,
@@ -162,7 +165,7 @@ export class PIIExporterDecorator
162
165
  resource: {
163
166
  attributes:
164
167
  metric?.resource?.attributes &&
165
- _cleanObjectPII(
168
+ _recursiveObjectClean(
166
169
  metric.resource.attributes,
167
170
  "metric",
168
171
  this._redactors,
@@ -1,6 +1,5 @@
1
1
  import type { AnyValue, AnyValueMap } from "@opentelemetry/api-logs";
2
2
  import { Redactor } from "./redactors/index.js";
3
- import { AttributeValue } from "@opentelemetry/api";
4
3
 
5
4
  const decoder = new TextDecoder();
6
5
  const encoder = new TextEncoder();
@@ -13,41 +12,44 @@ export type PIISource = "trace" | "log" | "metric";
13
12
  * @param {string} value - The string to inspect.
14
13
  * @returns {boolean} `true` if the string is encoded, `false` otherwise.
15
14
  */
16
- function _containsEncodedComponents(value: string): boolean {
15
+ export function _containsEncodedComponents(value: string): boolean {
17
16
  try {
18
- return decodeURI(value) !== decodeURIComponent(value);
17
+ const decodedURIComponent = decodeURIComponent(value);
18
+ if (decodeURI(value) !== decodedURIComponent) {
19
+ return true;
20
+ }
21
+
22
+ if (value !== decodedURIComponent) {
23
+ return (
24
+ encodeURIComponent(decodedURIComponent) === value ||
25
+ encodeURI(decodedURIComponent) === value
26
+ );
27
+ }
19
28
  } catch {
20
29
  return false;
21
30
  }
31
+
32
+ return false;
22
33
  }
23
34
 
24
35
  /**
25
36
  * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
26
37
  *
27
38
  * If the string is URL-encoded, it will be decoded before redaction.
28
- * Metrics are emitted for:
29
- * - each domain found in redacted email addresses.
30
- * - IPv4|IPv6 addresses redacted.
31
39
  *
32
40
  * @template T
33
41
  *
34
- * @param {T} value - The input value to sanitize.
42
+ * @param {string} value - The input value to sanitize.
35
43
  * @param {"trace" | "log"} source - The source context of the input, used in metrics.
36
44
  * @param {Redactor[]} redactors - The string processors containing the redaction logic.
37
45
  *
38
- * @returns {T} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
46
+ * @returns {string} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
39
47
  */
40
- export function _cleanStringPII<T extends AnyValue>(
41
- value: T,
48
+ export function _cleanStringPII(
49
+ value: string,
42
50
  source: PIISource,
43
51
  redactors: Redactor[],
44
- ): T {
45
- if (Array.isArray(value)) {
46
- return value.map((v) =>
47
- _cleanStringPII<typeof v>(v, source, redactors),
48
- ) as T;
49
- }
50
-
52
+ ): string {
51
53
  if (typeof value !== "string") {
52
54
  return value;
53
55
  }
@@ -59,37 +61,20 @@ export function _cleanStringPII<T extends AnyValue>(
59
61
  decodedValue = decodeURIComponent(value);
60
62
  kind = "url";
61
63
  }
62
-
63
64
  return redactors.reduce(
64
65
  (redactedValue: string, currentRedactor): string =>
65
66
  currentRedactor(redactedValue, source, kind),
66
67
  decodedValue,
67
- ) as T;
68
- }
69
-
70
- export function _cleanObjectPII(
71
- entry: object,
72
- source: PIISource,
73
- redactors: Redactor[],
74
- ): Record<string, AttributeValue> {
75
- if (!entry) {
76
- return entry;
77
- }
78
-
79
- return Object.fromEntries(
80
- Object.entries(entry).map(([k, v]) => [
81
- k,
82
- _cleanStringPII(v, source, redactors),
83
- ]),
84
68
  );
85
69
  }
86
70
 
87
- export function _cleanLogBodyPII<T extends AnyValue>(
71
+ export function _recursiveObjectClean<T extends AnyValue>(
88
72
  value: T,
73
+ source: PIISource,
89
74
  redactors: Redactor[],
90
75
  ): T {
91
76
  if (typeof value === "string") {
92
- return _cleanStringPII(value, "log", redactors);
77
+ return _cleanStringPII(value, source, redactors) as T;
93
78
  }
94
79
 
95
80
  if (
@@ -103,7 +88,7 @@ export function _cleanLogBodyPII<T extends AnyValue>(
103
88
  if (value instanceof Uint8Array) {
104
89
  try {
105
90
  const decoded = decoder.decode(value);
106
- const sanitized = _cleanStringPII(decoded, "log", redactors);
91
+ const sanitized = _cleanStringPII(decoded, source, redactors);
107
92
  return encoder.encode(sanitized) as T;
108
93
  } catch {
109
94
  return value;
@@ -111,13 +96,15 @@ export function _cleanLogBodyPII<T extends AnyValue>(
111
96
  }
112
97
 
113
98
  if (Array.isArray(value)) {
114
- return value.map((value) => _cleanLogBodyPII(value, redactors)) as T;
99
+ return value.map((value) =>
100
+ _recursiveObjectClean(value, source, redactors),
101
+ ) as T;
115
102
  }
116
103
 
117
104
  if (typeof value === "object") {
118
105
  const sanitized: AnyValueMap = {};
119
106
  for (const [key, val] of Object.entries(value)) {
120
- sanitized[key] = _cleanLogBodyPII(val, redactors);
107
+ sanitized[key] = _recursiveObjectClean(val, source, redactors);
121
108
  }
122
109
  return sanitized as T;
123
110
  }
@@ -2,9 +2,9 @@ import { _getPIICounterRedactionMetric } from "../../shared-metrics.js";
2
2
 
3
3
  // Generous IP address matchers (might match some invalid addresses like 192.168.01.1)
4
4
  const IPV4_REGEX =
5
- /(?<!\d)(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}(?!\d)/gi;
5
+ /(?<!\d)(?:%[0-9A-Fa-f]{2})?(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}(?:%[0-9A-Fa-f]{2})?(?!\d)/gi;
6
6
  const IPV6_REGEX =
7
- /(?<![0-9a-f:])((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|:(?::[0-9A-Fa-f]{1,4}){1,7}|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}|:(?::[0-9A-Fa-f]{1,4}){1,7}:?|(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})){3})(?![0-9a-f:])/gi;
7
+ /(?<![0-9a-f:])(?:%[0-9A-Fa-f]{2})?((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|:(?::[0-9A-Fa-f]{1,4}){1,7}|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}|:(?::[0-9A-Fa-f]{1,4}){1,7}:?|(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})){3})(?:%[0-9A-Fa-f]{2})?(?![0-9a-f:])/gi;
8
8
 
9
9
  /**
10
10
  * Redacts all ip addresses in the input string and collects metadata.
package/lib/traces.ts CHANGED
@@ -67,8 +67,8 @@ export function withSpan<T>({
67
67
  }: WithSpanParams<T>) {
68
68
  const sdkConfig = getNodeSdkConfig();
69
69
  const tracer = trace.getTracer(
70
- traceName ?? sdkConfig.serviceName ?? "o11y-sdk",
71
- sdkConfig.serviceVersion,
70
+ traceName ?? sdkConfig?.serviceName ?? "o11y-sdk",
71
+ sdkConfig?.serviceVersion,
72
72
  );
73
73
  return tracer.startActiveSpan(
74
74
  spanName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -3,8 +3,8 @@ import { getNodeSdkConfig, setNodeSdkConfig } from "../lib/config-manager";
3
3
  import { NodeSDKConfig } from "../lib";
4
4
 
5
5
  describe("Config Manager", () => {
6
- it("throws if getConfig is called before initialization", () => {
7
- expect(() => getNodeSdkConfig()).toThrow();
6
+ it("does not throw if getConfig is called before initialization", () => {
7
+ expect(() => getNodeSdkConfig()).not.toThrow();
8
8
  });
9
9
 
10
10
  it("sdk defined config is not pollutable", () => {
@@ -1,26 +1,74 @@
1
- # Integration Test
1
+ # Integration Test Runner
2
2
 
3
- This folder contains a setup for integration test with o11y node sdk.
3
+ This script is a **Bash-based test runner** that discovers and executes integration test scripts for the Node SDK (`./packages/sdk-node/test/integration`).
4
+ It ensures tests run inside an isolated Docker network and provides a summary of results.
4
5
 
5
- ## Workflow
6
+ ---
6
7
 
7
- - Docker must be in running state
8
- - Run the sh script `sh ./packages/sdk-node/test/integration/run.sh 1 .` from project root with following params
9
- 1. pipeline build number, for local development, any number or string is fine
10
- 2. root folder for docker context
11
- - Change dir to `packages/sdk-node/`
12
- - Run full test suite with `pnpm test`
8
+ ## Usage
13
9
 
14
- ## Script
10
+ ```bash
11
+ ./run-tests.sh <build-id> <root-path> <is-ci?>
12
+ ```
13
+
14
+ **Arguments**
15
+
16
+ <build-id>
17
+ A unique identifier for the test run. Used to name the Docker network (<build-id>_testnetwork).
18
+
19
+ <root-path>
20
+ The root path for the project or build environment.
21
+
22
+ [ci-flag] (optional)
23
+ Defaults to false.
24
+ If set to true, the script will remove all Docker images after the tests complete (intended for CI pipelines to save disk space).
25
+
26
+ ## How It Works
27
+
28
+ - Ensures at least two arguments are provided. Exits with usage instructions otherwise.
29
+ - Creates a temporary Docker network named <build-id>\_testnetwork.
30
+ - Finds all integration test scripts under: ./packages/sdk-node/test/integration/test\*/run.sh
31
+ - Runs each test script.
32
+ - Captures exit codes and tracks failures.
33
+ - Prints results per test.
34
+ - Removes the Docker network after all tests finish.
35
+ - If running in CI mode (ci-flag = true), removes all Docker images.
36
+ - Reports whether all tests passed or how many failed.
37
+ Exits with:
38
+ - 0 if all tests succeeded
39
+ - 1 if one or more tests failed
40
+
41
+ **Example**
42
+
43
+ ```
44
+ # Run tests locally
45
+ ./run-tests.sh my-build-id /path/to/project
46
+
47
+ # Run tests in CI mode (cleanup Docker images after)
48
+ ./run-tests.sh my-build-id /path/to/project true
49
+ ```
50
+
51
+ ## Tests
52
+
53
+ ### test_fastify-o11y-pii-enabled
15
54
 
16
55
  The `run.sh` script performs the following steps:
17
56
 
18
57
  - build a docker image of a fastify app `/examples/fastify`
19
- - setup an temporary test docker network
20
58
  - run grafana alloy inside a docker container with a test configuration `/alloy/integration-test.alloy`
21
59
  - ensure is running otherwise exit process
22
60
  - run fastify app in a docker container
23
61
  - ensure is running otherwise exit process
24
62
  - execute some curl to the fastify microservice
25
63
  - persist alloy log to a file and save to following path `/packages/sdk-node/test/integration/`
64
+ - verify pii data are removed and metrics are increased
26
65
  - docker turn down process (containers/network/image)
66
+
67
+ ### test_without-o11y
68
+
69
+ The `run.sh` script performs the following steps:
70
+
71
+ - build a docker image of a fastify app `/examples/fastify`
72
+ - verify service execution with o11y sdk disabled (otel collector empty)
73
+ - execute http requests for dummy route with otel trace customization function calls
74
+ - assert 200 http status without any error