@ogcio/o11y-sdk-node 0.3.1 → 0.4.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/lib/exporter/pii-exporter-decorator.d.ts +2 -0
  3. package/dist/lib/exporter/pii-exporter-decorator.js +27 -12
  4. package/dist/lib/index.d.ts +5 -0
  5. package/dist/lib/instrumentation.node.js +0 -8
  6. package/dist/lib/internals/redaction/pii-detection.d.ts +25 -0
  7. package/dist/lib/internals/redaction/pii-detection.js +80 -0
  8. package/dist/lib/internals/redaction/redactors/email.d.ts +8 -0
  9. package/dist/lib/internals/redaction/redactors/email.js +48 -0
  10. package/dist/lib/internals/redaction/redactors/index.d.ts +4 -0
  11. package/dist/lib/internals/redaction/redactors/index.js +6 -0
  12. package/dist/lib/internals/redaction/redactors/ip.d.ts +10 -0
  13. package/dist/lib/internals/redaction/redactors/ip.js +54 -0
  14. package/dist/lib/processor/enrich-logger-processor.d.ts +2 -2
  15. package/dist/package.json +14 -14
  16. package/dist/vitest.config.js +4 -4
  17. package/lib/exporter/pii-exporter-decorator.ts +53 -18
  18. package/lib/index.ts +5 -0
  19. package/lib/instrumentation.node.ts +0 -10
  20. package/lib/internals/redaction/pii-detection.ts +113 -0
  21. package/lib/internals/redaction/redactors/email.ts +58 -0
  22. package/lib/internals/redaction/redactors/index.ts +12 -0
  23. package/lib/internals/redaction/redactors/ip.ts +68 -0
  24. package/lib/internals/shared-metrics.ts +1 -1
  25. package/lib/processor/enrich-logger-processor.ts +2 -2
  26. package/package.json +14 -14
  27. package/test/internals/pii-detection.test.ts +157 -33
  28. package/test/internals/redactors/email.test.ts +81 -0
  29. package/test/internals/redactors/ip.test.ts +93 -0
  30. package/test/traces/active-span.test.ts +1 -1
  31. package/vitest.config.ts +4 -4
  32. package/dist/lib/internals/pii-detection.d.ts +0 -17
  33. package/dist/lib/internals/pii-detection.js +0 -116
  34. package/lib/internals/pii-detection.ts +0 -145
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [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)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * 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))
9
+
10
+ ## [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)
11
+
12
+
13
+ ### Features
14
+
15
+ * o11y react-sdk IP pii redaction AB[#30758](https://github.com/ogcio/o11y/issues/30758) ([#196](https://github.com/ogcio/o11y/issues/196)) ([d6bb83f](https://github.com/ogcio/o11y/commit/d6bb83fa425ffdf3fb023ec62caeef070995d07a))
16
+ * pii redaction refactor and ip redaction support AB[#30042](https://github.com/ogcio/o11y/issues/30042) AB[#30043](https://github.com/ogcio/o11y/issues/30043) ([#195](https://github.com/ogcio/o11y/issues/195)) ([3074693](https://github.com/ogcio/o11y/commit/307469399a7b03f3b647d85a2249e3c86dce933f))
17
+
18
+
19
+ ### Miscellaneous Chores
20
+
21
+ * **deps:** bump the root-deps group across 1 directory with 16 updates ([#193](https://github.com/ogcio/o11y/issues/193)) ([c523565](https://github.com/ogcio/o11y/commit/c523565a6ba7f327473e8d93afc7d532ac220457))
22
+ * **deps:** bump the root-deps group across 3 directories with 34 updates ([#188](https://github.com/ogcio/o11y/issues/188)) ([c0059f3](https://github.com/ogcio/o11y/commit/c0059f33cbb5c39cf6df0f4640604796dbbd3f57))
23
+
3
24
  ## [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
25
 
5
26
 
@@ -7,6 +7,7 @@ import { NodeSDKConfig } from "../index.js";
7
7
  export declare class PIIExporterDecorator extends OTLPExporterBase<(ReadableSpan | ReadableLogRecord)[] | ResourceMetrics> implements SpanExporter, PushMetricExporter {
8
8
  private readonly _exporter;
9
9
  private readonly _config;
10
+ private readonly _redactors;
10
11
  constructor(exporter: OTLPExporterBase<(ReadableSpan | ReadableLogRecord)[] | ResourceMetrics>, config: NodeSDKConfig);
11
12
  forceFlush(): Promise<void>;
12
13
  shutdown(): Promise<void>;
@@ -17,4 +18,5 @@ export declare class PIIExporterDecorator extends OTLPExporterBase<(ReadableSpan
17
18
  private _redactSpan;
18
19
  private _redactLogRecord;
19
20
  private _redactResourceMetrics;
21
+ private _buildRedactors;
20
22
  }
@@ -1,12 +1,15 @@
1
1
  import { OTLPExporterBase } from "@opentelemetry/otlp-exporter-base";
2
- import { _cleanLogBodyPII, _cleanObjectPII, _cleanStringPII, } from "../internals/pii-detection.js";
2
+ import { _cleanStringPII, _recursiveObjectClean, } from "../internals/redaction/pii-detection.js";
3
+ import { redactors, } from "../internals/redaction/redactors/index.js";
3
4
  export class PIIExporterDecorator extends OTLPExporterBase {
4
5
  _exporter;
5
6
  _config;
7
+ _redactors;
6
8
  constructor(exporter, config) {
7
9
  super(exporter["_delegate"]);
8
10
  this._exporter = exporter;
9
11
  this._config = config;
12
+ this._redactors = this._buildRedactors(config.detection);
10
13
  }
11
14
  forceFlush() {
12
15
  return this._exporter.forceFlush();
@@ -15,7 +18,7 @@ export class PIIExporterDecorator extends OTLPExporterBase {
15
18
  return this._exporter.shutdown();
16
19
  }
17
20
  export(items, resultCallback) {
18
- if (!this._config.detection?.email) {
21
+ if (this._redactors.length === 0) {
19
22
  this._exporter.export(items, resultCallback);
20
23
  return;
21
24
  }
@@ -62,21 +65,24 @@ export class PIIExporterDecorator extends OTLPExporterBase {
62
65
  }
63
66
  _redactSpan(span) {
64
67
  Object.assign(span, {
65
- name: _cleanStringPII(span.name, "trace"),
66
- attributes: span.attributes && _cleanObjectPII(span.attributes, "trace"),
68
+ name: _cleanStringPII(span.name, "trace", this._redactors),
69
+ attributes: span.attributes &&
70
+ _recursiveObjectClean(span.attributes, "trace", this._redactors),
67
71
  resource: {
68
72
  attributes: span?.resource?.attributes &&
69
- _cleanObjectPII(span.resource.attributes, "trace"),
73
+ _recursiveObjectClean(span.resource.attributes, "trace", this._redactors),
70
74
  },
71
75
  links: span?.links?.map((link) => {
72
76
  Object.assign(link, {
73
- attributes: link?.attributes && _cleanObjectPII(link.attributes, "trace"),
77
+ attributes: link?.attributes &&
78
+ _recursiveObjectClean(link.attributes, "trace", this._redactors),
74
79
  });
75
80
  }),
76
81
  events: span?.events?.map((event) => {
77
82
  Object.assign(event, {
78
- name: _cleanStringPII(event.name, "trace"),
79
- attributes: event?.attributes && _cleanObjectPII(event.attributes, "trace"),
83
+ name: _cleanStringPII(event.name, "trace", this._redactors),
84
+ attributes: event?.attributes &&
85
+ _recursiveObjectClean(event.attributes, "trace", this._redactors),
80
86
  });
81
87
  return event;
82
88
  }),
@@ -85,11 +91,12 @@ export class PIIExporterDecorator extends OTLPExporterBase {
85
91
  _redactLogRecord(log) {
86
92
  return {
87
93
  ...log,
88
- body: _cleanLogBodyPII(log.body),
89
- attributes: log.attributes && _cleanObjectPII(log.attributes, "log"),
94
+ body: _recursiveObjectClean(log.body, "log", this._redactors),
95
+ attributes: log.attributes &&
96
+ _recursiveObjectClean(log.attributes, "log", this._redactors),
90
97
  resource: log.resource && {
91
98
  ...log.resource,
92
- attributes: _cleanObjectPII(log.resource.attributes, "log"),
99
+ attributes: _recursiveObjectClean(log.resource.attributes, "log", this._redactors),
93
100
  },
94
101
  };
95
102
  }
@@ -97,8 +104,16 @@ export class PIIExporterDecorator extends OTLPExporterBase {
97
104
  Object.assign(metric, {
98
105
  resource: {
99
106
  attributes: metric?.resource?.attributes &&
100
- _cleanObjectPII(metric.resource.attributes, "metric"),
107
+ _recursiveObjectClean(metric.resource.attributes, "metric", this._redactors),
101
108
  },
102
109
  });
103
110
  }
111
+ // Default opt-in every redactor available, excluding only those explicitly configured to false
112
+ _buildRedactors(redactorsConfig = {}) {
113
+ return Object.entries(redactors)
114
+ .filter(([key]) => {
115
+ return redactorsConfig[key] !== false;
116
+ })
117
+ .map(([_, value]) => value);
118
+ }
104
119
  }
@@ -78,6 +78,11 @@ export interface NodeSDKConfig {
78
78
  * @default true
79
79
  */
80
80
  email?: boolean;
81
+ /**
82
+ * Redact IPv4/IPv6 addresses
83
+ * @default true
84
+ */
85
+ ip?: boolean;
81
86
  };
82
87
  }
83
88
  export interface SamplerCondition {
@@ -23,14 +23,6 @@ export default async function buildNodeInstrumentation(config) {
23
23
  console.error("collectorUrl does not use a valid format. Skipping NodeJS OpenTelemetry instrumentation.");
24
24
  return;
25
25
  }
26
- if (!config.detection) {
27
- config.detection = {
28
- email: true,
29
- };
30
- }
31
- if (config.detection.email === undefined) {
32
- config.detection.email = true;
33
- }
34
26
  // Init configManager to make it available to all o11y utils.
35
27
  setNodeSdkConfig(config);
36
28
  const urlSampler = new UrlSampler(config.ignoreUrls, new TraceIdRatioBasedSampler(config.traceRatio ?? 1));
@@ -0,0 +1,25 @@
1
+ import type { AnyValue } from "@opentelemetry/api-logs";
2
+ import { Redactor } from "./redactors/index.js";
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;
11
+ /**
12
+ * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
13
+ *
14
+ * If the string is URL-encoded, it will be decoded before redaction.
15
+ *
16
+ * @template T
17
+ *
18
+ * @param {string} value - The input value to sanitize.
19
+ * @param {"trace" | "log"} source - The source context of the input, used in metrics.
20
+ * @param {Redactor[]} redactors - The string processors containing the redaction logic.
21
+ *
22
+ * @returns {string} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
23
+ */
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;
@@ -0,0 +1,80 @@
1
+ const decoder = new TextDecoder();
2
+ const encoder = new TextEncoder();
3
+ /**
4
+ * Checks whether a string contains URI-encoded components.
5
+ *
6
+ * @param {string} value - The string to inspect.
7
+ * @returns {boolean} `true` if the string is encoded, `false` otherwise.
8
+ */
9
+ export function _containsEncodedComponents(value) {
10
+ try {
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
+ }
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ return false;
24
+ }
25
+ /**
26
+ * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
27
+ *
28
+ * If the string is URL-encoded, it will be decoded before redaction.
29
+ *
30
+ * @template T
31
+ *
32
+ * @param {string} value - The input value to sanitize.
33
+ * @param {"trace" | "log"} source - The source context of the input, used in metrics.
34
+ * @param {Redactor[]} redactors - The string processors containing the redaction logic.
35
+ *
36
+ * @returns {string} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
37
+ */
38
+ export function _cleanStringPII(value, source, redactors) {
39
+ if (typeof value !== "string") {
40
+ return value;
41
+ }
42
+ let kind = "string";
43
+ let decodedValue = value;
44
+ if (_containsEncodedComponents(value)) {
45
+ decodedValue = decodeURIComponent(value);
46
+ kind = "url";
47
+ }
48
+ return redactors.reduce((redactedValue, currentRedactor) => currentRedactor(redactedValue, source, kind), decodedValue);
49
+ }
50
+ export function _recursiveObjectClean(value, source, redactors) {
51
+ if (typeof value === "string") {
52
+ return _cleanStringPII(value, source, redactors);
53
+ }
54
+ if (typeof value === "number" ||
55
+ typeof value === "boolean" ||
56
+ value == null) {
57
+ return value;
58
+ }
59
+ if (value instanceof Uint8Array) {
60
+ try {
61
+ const decoded = decoder.decode(value);
62
+ const sanitized = _cleanStringPII(decoded, source, redactors);
63
+ return encoder.encode(sanitized);
64
+ }
65
+ catch {
66
+ return value;
67
+ }
68
+ }
69
+ if (Array.isArray(value)) {
70
+ return value.map((value) => _recursiveObjectClean(value, source, redactors));
71
+ }
72
+ if (typeof value === "object") {
73
+ const sanitized = {};
74
+ for (const [key, val] of Object.entries(value)) {
75
+ sanitized[key] = _recursiveObjectClean(val, source, redactors);
76
+ }
77
+ return sanitized;
78
+ }
79
+ return value;
80
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Redacts provided input and collects metadata metrics about redacted email domains,
3
+ * data source and kind.
4
+ *
5
+ * @param {string} value The input string potentially containing email addresses.
6
+ * @returns {string} the redacted value
7
+ */
8
+ export declare const emailRedactor: (value: string, source: string, kind: string) => string;
@@ -0,0 +1,48 @@
1
+ import { _getPIICounterRedactionMetric } from "../../shared-metrics.js";
2
+ const EMAIL_REGEX = /[\p{L}\p{N}._%+-]+@((?:[\p{L}\p{N}-]+\.)+[\p{L}]{2,})/giu;
3
+ /**
4
+ * Redacts all email addresses in the input string and collects metadata.
5
+ *
6
+ * @param {string} value The input string potentially containing email addresses.
7
+ * @returns {{
8
+ * redacted: string,
9
+ * count: number,
10
+ * domains: Record<string, number>
11
+ * }}
12
+ *
13
+ * An object containing:
14
+ * - `redacted`: the string with email addresses replaced by `[REDACTED EMAIL]`
15
+ * - `count`: total number of email addresses redacted
16
+ * - `domains`: a map of domain names to the number of times they were redacted
17
+ */
18
+ function _redactEmails(value) {
19
+ let count = 0;
20
+ const domains = {};
21
+ const redacted = value.replace(EMAIL_REGEX, (_, domain) => {
22
+ count++;
23
+ domains[domain] = (domains[domain] || 0) + 1;
24
+ return "[REDACTED EMAIL]";
25
+ });
26
+ return { redacted, count, domains };
27
+ }
28
+ /**
29
+ * Redacts provided input and collects metadata metrics about redacted email domains,
30
+ * data source and kind.
31
+ *
32
+ * @param {string} value The input string potentially containing email addresses.
33
+ * @returns {string} the redacted value
34
+ */
35
+ export const emailRedactor = (value, source, kind) => {
36
+ const { redacted, count, domains } = _redactEmails(value);
37
+ if (count > 0) {
38
+ for (const [domain, domainCount] of Object.entries(domains)) {
39
+ _getPIICounterRedactionMetric().add(domainCount, {
40
+ pii_type: "email",
41
+ redaction_source: source,
42
+ pii_email_domain: domain,
43
+ pii_format: kind,
44
+ });
45
+ }
46
+ }
47
+ return redacted;
48
+ };
@@ -0,0 +1,4 @@
1
+ import { NodeSDKConfig } from "../../../index.js";
2
+ export type Redactor = (value: string, source: string, kind: string) => string;
3
+ export type RedactorKeys = keyof NonNullable<NodeSDKConfig["detection"]>;
4
+ export declare const redactors: Record<RedactorKeys, Redactor>;
@@ -0,0 +1,6 @@
1
+ import { emailRedactor } from "./email.js";
2
+ import { ipRedactor } from "./ip.js";
3
+ export const redactors = {
4
+ email: emailRedactor,
5
+ ip: ipRedactor,
6
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Redacts provided input and collects metadata metrics about redacted IPs,
3
+ * data source and kind.
4
+ *
5
+ * @param {string} value The input string potentially containing IP addresses.
6
+ * @param {string} source The source of the attribute being redacted (log, span, metric).
7
+ * @param {string} kind The type of the data structure containing the PII
8
+ * @returns {string} the redacted value
9
+ */
10
+ export declare const ipRedactor: (value: string, source: string, kind: string) => string;
@@ -0,0 +1,54 @@
1
+ import { _getPIICounterRedactionMetric } from "../../shared-metrics.js";
2
+ // Generous IP address matchers (might match some invalid addresses like 192.168.01.1)
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
+ /**
6
+ * Redacts all ip addresses in the input string and collects metadata.
7
+ *
8
+ * @param {string} value The input string potentially containing ip addresses.
9
+ * @returns {{
10
+ * redacted: string,
11
+ * count: number,
12
+ * domains: Record<string, number>
13
+ * }}
14
+ *
15
+ * An object containing:
16
+ * - `redacted`: the string with IP addresses replaced by `[REDACTED IPV*]`
17
+ * - `counters`: total number of addresses redacted by IPv* type
18
+ * - `domains`: a map of domain names to the number of times they were redacted
19
+ */
20
+ function _redactIps(value) {
21
+ const counters = {};
22
+ const redacted = value
23
+ .replace(IPV4_REGEX, () => {
24
+ counters["IPv4"] = (counters["IPv4"] || 0) + 1;
25
+ return "[REDACTED IPV4]";
26
+ })
27
+ .replace(IPV6_REGEX, () => {
28
+ counters["IPv4"] = (counters["IPv4"] || 0) + 1;
29
+ return "[REDACTED IPV6]";
30
+ });
31
+ return { redacted, counters };
32
+ }
33
+ /**
34
+ * Redacts provided input and collects metadata metrics about redacted IPs,
35
+ * data source and kind.
36
+ *
37
+ * @param {string} value The input string potentially containing IP addresses.
38
+ * @param {string} source The source of the attribute being redacted (log, span, metric).
39
+ * @param {string} kind The type of the data structure containing the PII
40
+ * @returns {string} the redacted value
41
+ */
42
+ export const ipRedactor = (value, source, kind) => {
43
+ const { redacted, counters } = _redactIps(value);
44
+ Object.entries(counters).forEach(([type, counter]) => {
45
+ if (counter > 0) {
46
+ _getPIICounterRedactionMetric().add(counter, {
47
+ pii_type: type,
48
+ redaction_source: source,
49
+ pii_format: kind,
50
+ });
51
+ }
52
+ });
53
+ return redacted;
54
+ };
@@ -1,10 +1,10 @@
1
- import { LogRecord, LogRecordProcessor } from "@opentelemetry/sdk-logs";
1
+ import { SdkLogRecord, LogRecordProcessor } from "@opentelemetry/sdk-logs";
2
2
  import { Context } from "@opentelemetry/api";
3
3
  import { SignalAttributeValue } from "../index.js";
4
4
  export declare class EnrichLogProcessor implements LogRecordProcessor {
5
5
  private _spanAttributes?;
6
6
  constructor(spanAttributes?: Record<string, SignalAttributeValue | (() => SignalAttributeValue)>);
7
7
  forceFlush(): Promise<void>;
8
- onEmit(logRecord: LogRecord, _context?: Context): void;
8
+ onEmit(logRecord: SdkLogRecord, _context?: Context): void;
9
9
  shutdown(): Promise<void>;
10
10
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -30,26 +30,26 @@
30
30
  "@grpc/grpc-js": "^1.13.4",
31
31
  "@opentelemetry/api": "^1.9.0",
32
32
  "@opentelemetry/api-logs": "^0.203.0",
33
- "@opentelemetry/auto-instrumentations-node": "^0.60.1",
33
+ "@opentelemetry/auto-instrumentations-node": "^0.62.1",
34
34
  "@opentelemetry/core": "^2.0.1",
35
- "@opentelemetry/exporter-logs-otlp-grpc": "^0.202.0",
36
- "@opentelemetry/exporter-logs-otlp-http": "^0.202.0",
37
- "@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0",
38
- "@opentelemetry/exporter-metrics-otlp-http": "^0.202.0",
39
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.202.0",
40
- "@opentelemetry/exporter-trace-otlp-http": "^0.202.0",
41
- "@opentelemetry/instrumentation": "^0.202.0",
42
- "@opentelemetry/otlp-exporter-base": "^0.202.0",
35
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0",
36
+ "@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
37
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.203.0",
38
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.203.0",
39
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0",
40
+ "@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
41
+ "@opentelemetry/instrumentation": "^0.203.0",
42
+ "@opentelemetry/otlp-exporter-base": "^0.203.0",
43
43
  "@opentelemetry/resources": "^2.0.1",
44
- "@opentelemetry/sdk-logs": "^0.202.0",
44
+ "@opentelemetry/sdk-logs": "^0.203.0",
45
45
  "@opentelemetry/sdk-metrics": "^2.0.1",
46
- "@opentelemetry/sdk-node": "^0.202.0",
46
+ "@opentelemetry/sdk-node": "^0.203.0",
47
47
  "@opentelemetry/sdk-trace-base": "^2.0.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@types/node": "^24.0.10",
50
+ "@types/node": "^24.3.0",
51
51
  "@vitest/coverage-v8": "^3.2.4",
52
- "tsx": "^4.20.3",
52
+ "tsx": "^4.20.5",
53
53
  "typescript": "^5.8.3",
54
54
  "vitest": "^3.2.4"
55
55
  },
@@ -26,10 +26,10 @@ export default defineConfig({
26
26
  test: {
27
27
  include: [
28
28
  "**/test/*.test.ts",
29
- "**/test/processor/*.test.ts",
30
- "**/test/traces/*.test.ts",
31
- "**/test/internals/*.test.ts",
32
- "**/test/exporter/*.test.ts",
29
+ "**/test/processor/**/*.test.ts",
30
+ "**/test/traces/**/*.test.ts",
31
+ "**/test/internals/**/*.test.ts",
32
+ "**/test/exporter/**/*.test.ts",
33
33
  ],
34
34
  name: "unit",
35
35
  },
@@ -8,10 +8,14 @@ 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,
14
- } from "../internals/pii-detection.js";
12
+ _recursiveObjectClean,
13
+ } from "../internals/redaction/pii-detection.js";
14
+ import {
15
+ Redactor,
16
+ RedactorKeys,
17
+ redactors,
18
+ } from "../internals/redaction/redactors/index.js";
15
19
 
16
20
  export class PIIExporterDecorator
17
21
  extends OTLPExporterBase<
@@ -21,6 +25,7 @@ export class PIIExporterDecorator
21
25
  {
22
26
  private readonly _exporter;
23
27
  private readonly _config;
28
+ private readonly _redactors;
24
29
 
25
30
  constructor(
26
31
  exporter: OTLPExporterBase<
@@ -31,6 +36,7 @@ export class PIIExporterDecorator
31
36
  super(exporter["_delegate"]);
32
37
  this._exporter = exporter;
33
38
  this._config = config;
39
+ this._redactors = this._buildRedactors(config.detection);
34
40
  }
35
41
 
36
42
  forceFlush(): Promise<void> {
@@ -45,7 +51,7 @@ export class PIIExporterDecorator
45
51
  items: (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics,
46
52
  resultCallback: (result: ExportResult) => void,
47
53
  ): void {
48
- if (!this._config.detection?.email) {
54
+ if (this._redactors.length === 0) {
49
55
  this._exporter.export(items, resultCallback);
50
56
  return;
51
57
  }
@@ -55,7 +61,7 @@ export class PIIExporterDecorator
55
61
  if (this._isReadableSpan(item)) {
56
62
  this._redactSpan(item);
57
63
  } else if (this._isReadableLogRecord(item)) {
58
- item = this._redactLogRecord(item) as ReadableLogRecord;
64
+ item = this._redactLogRecord(item);
59
65
  }
60
66
  return item;
61
67
  });
@@ -102,51 +108,80 @@ export class PIIExporterDecorator
102
108
  );
103
109
  }
104
110
 
105
- private _redactSpan(span: ReadableSpan) {
111
+ private _redactSpan(span: ReadableSpan): void {
106
112
  Object.assign(span, {
107
- name: _cleanStringPII(span.name, "trace"),
108
- attributes: span.attributes && _cleanObjectPII(span.attributes, "trace"),
113
+ name: _cleanStringPII(span.name, "trace", this._redactors),
114
+ attributes:
115
+ span.attributes &&
116
+ _recursiveObjectClean(span.attributes, "trace", this._redactors),
109
117
  resource: {
110
118
  attributes:
111
119
  span?.resource?.attributes &&
112
- _cleanObjectPII(span.resource.attributes, "trace"),
120
+ _recursiveObjectClean(
121
+ span.resource.attributes,
122
+ "trace",
123
+ this._redactors,
124
+ ),
113
125
  },
114
126
  links: span?.links?.map((link) => {
115
127
  Object.assign(link, {
116
128
  attributes:
117
- link?.attributes && _cleanObjectPII(link.attributes, "trace"),
129
+ link?.attributes &&
130
+ _recursiveObjectClean(link.attributes, "trace", this._redactors),
118
131
  });
119
132
  }),
120
133
  events: span?.events?.map((event) => {
121
134
  Object.assign(event, {
122
- name: _cleanStringPII(event.name, "trace"),
135
+ name: _cleanStringPII(event.name, "trace", this._redactors),
123
136
  attributes:
124
- event?.attributes && _cleanObjectPII(event.attributes, "trace"),
137
+ event?.attributes &&
138
+ _recursiveObjectClean(event.attributes, "trace", this._redactors),
125
139
  });
126
140
  return event;
127
141
  }),
128
142
  });
129
143
  }
130
144
 
131
- private _redactLogRecord(log: ReadableLogRecord) {
145
+ private _redactLogRecord(log: ReadableLogRecord): ReadableLogRecord {
132
146
  return {
133
147
  ...log,
134
- body: _cleanLogBodyPII(log.body),
135
- attributes: log.attributes && _cleanObjectPII(log.attributes, "log"),
148
+ body: _recursiveObjectClean(log.body, "log", this._redactors),
149
+ attributes:
150
+ log.attributes &&
151
+ _recursiveObjectClean(log.attributes, "log", this._redactors),
136
152
  resource: log.resource && {
137
153
  ...log.resource,
138
- attributes: _cleanObjectPII(log.resource.attributes, "log"),
154
+ attributes: _recursiveObjectClean(
155
+ log.resource.attributes,
156
+ "log",
157
+ this._redactors,
158
+ ),
139
159
  },
140
160
  };
141
161
  }
142
162
 
143
- private _redactResourceMetrics(metric: ResourceMetrics) {
163
+ private _redactResourceMetrics(metric: ResourceMetrics): void {
144
164
  Object.assign(metric, {
145
165
  resource: {
146
166
  attributes:
147
167
  metric?.resource?.attributes &&
148
- _cleanObjectPII(metric.resource.attributes, "metric"),
168
+ _recursiveObjectClean(
169
+ metric.resource.attributes,
170
+ "metric",
171
+ this._redactors,
172
+ ),
149
173
  },
150
174
  });
151
175
  }
176
+
177
+ // Default opt-in every redactor available, excluding only those explicitly configured to false
178
+ private _buildRedactors(
179
+ redactorsConfig: NodeSDKConfig["detection"] = {},
180
+ ): Redactor[] {
181
+ return Object.entries(redactors)
182
+ .filter(([key]) => {
183
+ return redactorsConfig[key as RedactorKeys] !== false;
184
+ })
185
+ .map(([_, value]) => value);
186
+ }
152
187
  }
package/lib/index.ts CHANGED
@@ -89,6 +89,11 @@ export interface NodeSDKConfig {
89
89
  * @default true
90
90
  */
91
91
  email?: boolean;
92
+ /**
93
+ * Redact IPv4/IPv6 addresses
94
+ * @default true
95
+ */
96
+ ip?: boolean;
92
97
  };
93
98
  }
94
99
 
@@ -41,16 +41,6 @@ export default async function buildNodeInstrumentation(
41
41
  return;
42
42
  }
43
43
 
44
- if (!config.detection) {
45
- config.detection = {
46
- email: true,
47
- };
48
- }
49
-
50
- if (config.detection.email === undefined) {
51
- config.detection.email = true;
52
- }
53
-
54
44
  // Init configManager to make it available to all o11y utils.
55
45
  setNodeSdkConfig(config);
56
46