@ogcio/o11y-sdk-node 0.1.0-beta.9 → 0.3.0

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 (60) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +158 -13
  3. package/dist/lib/config-manager.d.ts +3 -0
  4. package/dist/lib/config-manager.js +11 -0
  5. package/dist/lib/exporter/console.js +3 -4
  6. package/dist/lib/exporter/grpc.d.ts +1 -1
  7. package/dist/lib/exporter/grpc.js +24 -14
  8. package/dist/lib/exporter/http.d.ts +1 -1
  9. package/dist/lib/exporter/http.js +14 -13
  10. package/dist/lib/exporter/pii-exporter-decorator.d.ts +20 -0
  11. package/dist/lib/exporter/pii-exporter-decorator.js +103 -0
  12. package/dist/lib/exporter/processor-config.d.ts +5 -0
  13. package/dist/lib/exporter/processor-config.js +16 -0
  14. package/dist/lib/index.d.ts +25 -4
  15. package/dist/lib/instrumentation.node.d.ts +1 -1
  16. package/dist/lib/instrumentation.node.js +29 -19
  17. package/dist/lib/internals/hooks.d.ts +3 -0
  18. package/dist/lib/internals/hooks.js +12 -0
  19. package/dist/lib/internals/pii-detection.d.ts +17 -0
  20. package/dist/lib/internals/pii-detection.js +116 -0
  21. package/dist/lib/internals/shared-metrics.d.ts +7 -0
  22. package/dist/lib/internals/shared-metrics.js +18 -0
  23. package/dist/lib/resource.js +2 -2
  24. package/dist/lib/traces.d.ts +20 -1
  25. package/dist/lib/traces.js +47 -1
  26. package/dist/package.json +23 -21
  27. package/dist/vitest.config.js +8 -2
  28. package/lib/config-manager.ts +16 -0
  29. package/lib/exporter/console.ts +6 -4
  30. package/lib/exporter/grpc.ts +46 -20
  31. package/lib/exporter/http.ts +33 -20
  32. package/lib/exporter/pii-exporter-decorator.ts +152 -0
  33. package/lib/exporter/processor-config.ts +23 -0
  34. package/lib/index.ts +28 -4
  35. package/lib/instrumentation.node.ts +37 -22
  36. package/lib/internals/hooks.ts +14 -0
  37. package/lib/internals/pii-detection.ts +145 -0
  38. package/lib/internals/shared-metrics.ts +34 -0
  39. package/lib/resource.ts +3 -2
  40. package/lib/traces.ts +74 -1
  41. package/package.json +23 -21
  42. package/test/config-manager.test.ts +34 -0
  43. package/test/exporter/pii-exporter-decorator.test.ts +139 -0
  44. package/test/index.test.ts +44 -12
  45. package/test/integration/README.md +1 -1
  46. package/test/integration/{integration.test.ts → http-tracing.integration.test.ts} +0 -2
  47. package/test/integration/pii.integration.test.ts +68 -0
  48. package/test/integration/run.sh +2 -2
  49. package/test/internals/hooks.test.ts +45 -0
  50. package/test/internals/pii-detection.test.ts +141 -0
  51. package/test/internals/shared-metrics.test.ts +34 -0
  52. package/test/node-config.test.ts +68 -30
  53. package/test/processor/enrich-span-processor.test.ts +2 -54
  54. package/test/resource.test.ts +12 -1
  55. package/test/traces/active-span.test.ts +28 -0
  56. package/test/traces/with-span.test.ts +340 -0
  57. package/test/utils/alloy-log-parser.ts +7 -0
  58. package/test/utils/mock-signals.ts +144 -0
  59. package/test/validation.test.ts +22 -16
  60. package/vitest.config.ts +8 -2
@@ -1,13 +1,17 @@
1
- import { metrics } from "@opentelemetry/sdk-node";
2
- import { CompressionAlgorithm } from "@opentelemetry/otlp-exporter-base";
3
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
4
1
  import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
5
2
  import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
3
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
4
+ import { CompressionAlgorithm } from "@opentelemetry/otlp-exporter-base";
5
+ import { metrics } from "@opentelemetry/sdk-node";
6
+ import { NodeSDKConfig } from "../index.js";
6
7
  import { LogRecordProcessorMap, SpanProcessorMap } from "../utils.js";
7
- import { EnrichSpanProcessor } from "../processor/enrich-span-processor.js";
8
+ import {
9
+ _logsProcessorConfig,
10
+ _spansProcessorConfig,
11
+ } from "./processor-config.js";
8
12
  import { Exporters } from "./index.js";
9
- import { NodeSDKConfig } from "../index.js";
10
- import { EnrichLogProcessor } from "../processor/enrich-logger-processor.js";
13
+ import { getNodeSdkConfig } from "../config-manager.js";
14
+ import { PIIExporterDecorator } from "./pii-exporter-decorator.js";
11
15
 
12
16
  export default function buildHttpExporters(config: NodeSDKConfig): Exporters {
13
17
  if (config.collectorUrl.endsWith("/")) {
@@ -17,26 +21,35 @@ export default function buildHttpExporters(config: NodeSDKConfig): Exporters {
17
21
  return {
18
22
  spans: [
19
23
  new SpanProcessorMap[config.collectorMode ?? "batch"](
20
- new OTLPTraceExporter({
21
- url: `${config.collectorUrl}/v1/traces`,
22
- compression: CompressionAlgorithm.GZIP,
23
- }),
24
+ new PIIExporterDecorator(
25
+ new OTLPTraceExporter({
26
+ url: `${config.collectorUrl}/v1/traces`,
27
+ compression: CompressionAlgorithm.GZIP,
28
+ }),
29
+ getNodeSdkConfig(),
30
+ ),
24
31
  ),
25
- new EnrichSpanProcessor(config.spanAttributes),
32
+ ..._spansProcessorConfig(config),
26
33
  ],
27
34
  metrics: new metrics.PeriodicExportingMetricReader({
28
- exporter: new OTLPMetricExporter({
29
- url: `${config.collectorUrl}/v1/metrics`,
30
- compression: CompressionAlgorithm.GZIP,
31
- }),
35
+ exporter: new PIIExporterDecorator(
36
+ new OTLPMetricExporter({
37
+ url: `${config.collectorUrl}/v1/metrics`,
38
+ compression: CompressionAlgorithm.GZIP,
39
+ }),
40
+ getNodeSdkConfig(),
41
+ ),
32
42
  }),
33
43
  logs: [
34
- new EnrichLogProcessor(config.spanAttributes),
44
+ ..._logsProcessorConfig(config),
35
45
  new LogRecordProcessorMap[config.collectorMode ?? "batch"](
36
- new OTLPLogExporter({
37
- url: `${config.collectorUrl}/v1/logs`,
38
- compression: CompressionAlgorithm.GZIP,
39
- }),
46
+ new PIIExporterDecorator(
47
+ new OTLPLogExporter({
48
+ url: `${config.collectorUrl}/v1/logs`,
49
+ compression: CompressionAlgorithm.GZIP,
50
+ }),
51
+ getNodeSdkConfig(),
52
+ ),
40
53
  ),
41
54
  ],
42
55
  };
@@ -0,0 +1,152 @@
1
+ import { ExportResult } from "@opentelemetry/core";
2
+ import { OTLPExporterBase } from "@opentelemetry/otlp-exporter-base";
3
+ import { ReadableLogRecord } from "@opentelemetry/sdk-logs";
4
+ import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
5
+ import { NodeSDKConfig } from "../index.js";
6
+ import {
7
+ _cleanLogBodyPII,
8
+ _cleanObjectPII,
9
+ _cleanStringPII,
10
+ } from "../internals/pii-detection.js";
11
+ import {
12
+ PushMetricExporter,
13
+ ResourceMetrics,
14
+ } from "@opentelemetry/sdk-metrics";
15
+
16
+ export class PIIExporterDecorator
17
+ extends OTLPExporterBase<
18
+ (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics
19
+ >
20
+ implements SpanExporter, PushMetricExporter
21
+ {
22
+ private readonly _exporter;
23
+ private readonly _config;
24
+
25
+ constructor(
26
+ exporter: OTLPExporterBase<
27
+ (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics
28
+ >,
29
+ config: NodeSDKConfig,
30
+ ) {
31
+ super(exporter["_delegate"]);
32
+ this._exporter = exporter;
33
+ this._config = config;
34
+ }
35
+
36
+ forceFlush(): Promise<void> {
37
+ return this._exporter.forceFlush();
38
+ }
39
+
40
+ shutdown(): Promise<void> {
41
+ return this._exporter.shutdown();
42
+ }
43
+
44
+ export(
45
+ items: (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics,
46
+ resultCallback: (result: ExportResult) => void,
47
+ ): void {
48
+ if (!this._config.detection?.email) {
49
+ this._exporter.export(items, resultCallback);
50
+ return;
51
+ }
52
+
53
+ if (Array.isArray(items)) {
54
+ const redactedItem = items.map((item) => {
55
+ if (this._isReadableSpan(item)) {
56
+ this._redactSpan(item);
57
+ } else if (this._isReadableLogRecord(item)) {
58
+ this._redactLogRecord(item);
59
+ }
60
+ return item;
61
+ });
62
+
63
+ this._exporter.export(redactedItem, resultCallback);
64
+ return;
65
+ }
66
+
67
+ if (this._isResourceMetrics(items)) {
68
+ this._redactResourceMetrics(items);
69
+ this._exporter.export(items, resultCallback);
70
+ }
71
+ }
72
+
73
+ private _isReadableSpan(span: unknown): span is ReadableSpan {
74
+ return (
75
+ typeof span === "object" &&
76
+ span !== null &&
77
+ "name" in span &&
78
+ "kind" in span &&
79
+ "spanContext" in span &&
80
+ "attributes" in span
81
+ );
82
+ }
83
+
84
+ private _isReadableLogRecord(span: unknown): span is ReadableLogRecord {
85
+ return (
86
+ typeof span === "object" &&
87
+ span !== null &&
88
+ "body" in span &&
89
+ "attributes" in span &&
90
+ "severityText" in span &&
91
+ "severityNumber" in span
92
+ );
93
+ }
94
+
95
+ private _isResourceMetrics(obj: unknown): obj is ResourceMetrics {
96
+ return (
97
+ typeof obj === "object" &&
98
+ obj !== null &&
99
+ !Array.isArray(obj) &&
100
+ "resource" in obj &&
101
+ "scopeMetrics" in obj
102
+ );
103
+ }
104
+
105
+ private _redactSpan(span: ReadableSpan) {
106
+ Object.assign(span, {
107
+ name: _cleanStringPII(span.name, "trace"),
108
+ attributes: span.attributes && _cleanObjectPII(span.attributes, "trace"),
109
+ resource: {
110
+ attributes:
111
+ span?.resource?.attributes &&
112
+ _cleanObjectPII(span.resource.attributes, "trace"),
113
+ },
114
+ links: span?.links?.map((link) => {
115
+ Object.assign(link, {
116
+ attributes:
117
+ link?.attributes && _cleanObjectPII(link.attributes, "trace"),
118
+ });
119
+ }),
120
+ events: span?.events?.map((event) => {
121
+ Object.assign(event, {
122
+ name: _cleanStringPII(event.name, "trace"),
123
+ attributes:
124
+ event?.attributes && _cleanObjectPII(event.attributes, "trace"),
125
+ });
126
+ return event;
127
+ }),
128
+ });
129
+ }
130
+
131
+ private _redactLogRecord(log: ReadableLogRecord) {
132
+ Object.assign(log, {
133
+ body: _cleanLogBodyPII(log.body),
134
+ attributes: log.attributes && _cleanObjectPII(log.attributes, "log"),
135
+ resource: {
136
+ attributes:
137
+ log?.resource?.attributes &&
138
+ _cleanObjectPII(log.resource.attributes, "log"),
139
+ },
140
+ });
141
+ }
142
+
143
+ private _redactResourceMetrics(metric: ResourceMetrics) {
144
+ Object.assign(metric, {
145
+ resource: {
146
+ attributes:
147
+ metric?.resource?.attributes &&
148
+ _cleanObjectPII(metric.resource.attributes, "metric"),
149
+ },
150
+ });
151
+ }
152
+ }
@@ -0,0 +1,23 @@
1
+ import { NodeSDKConfig } from "../index.js";
2
+ import { EnrichLogProcessor } from "../processor/enrich-logger-processor.js";
3
+ import { EnrichSpanProcessor } from "../processor/enrich-span-processor.js";
4
+
5
+ export function _spansProcessorConfig(config: NodeSDKConfig) {
6
+ const _processor = [];
7
+
8
+ if (config.spanAttributes) {
9
+ _processor.push(new EnrichSpanProcessor(config.spanAttributes));
10
+ }
11
+
12
+ return _processor;
13
+ }
14
+
15
+ export function _logsProcessorConfig(config: NodeSDKConfig) {
16
+ const _processor = [];
17
+
18
+ if (config.spanAttributes) {
19
+ _processor.push(new EnrichLogProcessor(config.spanAttributes));
20
+ }
21
+
22
+ return _processor;
23
+ }
package/lib/index.ts CHANGED
@@ -1,14 +1,20 @@
1
+ import type { Metadata } from "@grpc/grpc-js";
2
+
1
3
  export interface NodeSDKConfig {
2
4
  /**
3
5
  * The opentelemetry collector entrypoint GRPC url.
4
- * If the collectoUrl is null or undefined, the instrumentation will not be activated.
6
+ * If the collectorUrl is null or undefined, the instrumentation will not be activated.
5
7
  * @example http://alloy:4317
6
8
  */
7
9
  collectorUrl: string;
8
10
  /**
9
- * Name of your application used for the collector to group logs
11
+ * Name of your application used for the collector to group logs and naming traces
10
12
  */
11
13
  serviceName?: string;
14
+ /**
15
+ * Version of your application used for the collector to group logs and naming traces
16
+ */
17
+ serviceVersion?: string;
12
18
  /**
13
19
  * Diagnostic log level for the internal runtime instrumentation
14
20
  *
@@ -18,7 +24,7 @@ export interface NodeSDKConfig {
18
24
  diagLogLevel?: SDKLogLevel;
19
25
  /**
20
26
  * Collector signals processing mode.
21
- * signle: makes an http/grpc request for each signal and it is immediately processed inside grafana
27
+ * single: makes an http/grpc request for each signal, and it is immediately processed inside grafana
22
28
  * batch: sends multiple signals within a time window, optimized to reduce http/grpc calls in production
23
29
  *
24
30
  * @type string
@@ -34,7 +40,7 @@ export interface NodeSDKConfig {
34
40
  ignoreUrls?: SamplerCondition[];
35
41
 
36
42
  /**
37
- * Object containing static properties or functions used to evaluate custom attributes for every logs and traces.
43
+ * Object containing static properties or functions used to evaluate custom attributes for all logs and traces.
38
44
  */
39
45
  spanAttributes?: Record<
40
46
  string,
@@ -66,6 +72,24 @@ export interface NodeSDKConfig {
66
72
  * @default grpc
67
73
  */
68
74
  protocol?: SDKProtocol;
75
+
76
+ /**
77
+ * Grpc Metadata for the grpc-js client.
78
+ *
79
+ * @default { waitForReady: true }
80
+ */
81
+ grpcMetadata?: Metadata;
82
+
83
+ /**
84
+ * Enable/Disable PII detection for GDPR data
85
+ */
86
+ detection?: {
87
+ /**
88
+ * Redact email address
89
+ * @default true
90
+ */
91
+ email?: boolean;
92
+ };
69
93
  }
70
94
 
71
95
  export interface SamplerCondition {
@@ -7,17 +7,19 @@ import {
7
7
  ParentBasedSampler,
8
8
  TraceIdRatioBasedSampler,
9
9
  } from "@opentelemetry/sdk-trace-base";
10
+ import { setNodeSdkConfig } from "./config-manager.js";
10
11
  import buildConsoleExporters from "./exporter/console.js";
11
12
  import buildGrpcExporters from "./exporter/grpc.js";
12
13
  import buildHttpExporters from "./exporter/http.js";
13
14
  import type { Exporters } from "./exporter/index.js";
14
15
  import type { NodeSDKConfig } from "./index.js";
16
+ import { _shutdownHook } from "./internals/hooks.js";
15
17
  import { ObservabilityResourceDetector } from "./resource.js";
16
18
  import { UrlSampler } from "./url-sampler.js";
17
19
 
18
- export default function buildNodeInstrumentation(
20
+ export default async function buildNodeInstrumentation(
19
21
  config?: NodeSDKConfig,
20
- ): NodeSDK | undefined {
22
+ ): Promise<NodeSDK | undefined> {
21
23
  if (!config) {
22
24
  console.warn(
23
25
  "observability config not set. Skipping NodeJS OpenTelemetry instrumentation.",
@@ -39,16 +41,19 @@ export default function buildNodeInstrumentation(
39
41
  return;
40
42
  }
41
43
 
42
- let exporter: Exporters;
44
+ if (!config.detection) {
45
+ config.detection = {
46
+ email: true,
47
+ };
48
+ }
43
49
 
44
- if (config.protocol === "http") {
45
- exporter = buildHttpExporters(config);
46
- } else if (config.protocol === "console") {
47
- exporter = buildConsoleExporters(config);
48
- } else {
49
- exporter = buildGrpcExporters(config);
50
+ if (config.detection.email === undefined) {
51
+ config.detection.email = true;
50
52
  }
51
53
 
54
+ // Init configManager to make it available to all o11y utils.
55
+ setNodeSdkConfig(config);
56
+
52
57
  const urlSampler = new UrlSampler(
53
58
  config.ignoreUrls,
54
59
  new TraceIdRatioBasedSampler(config.traceRatio ?? 1),
@@ -62,13 +67,27 @@ export default function buildNodeInstrumentation(
62
67
  localParentNotSampled: new AlwaysOffSampler(),
63
68
  });
64
69
 
70
+ diag.setLogger(
71
+ new DiagConsoleLogger(),
72
+ config.diagLogLevel ? DiagLogLevel[config.diagLogLevel] : DiagLogLevel.INFO,
73
+ );
74
+
65
75
  try {
66
- diag.setLogger(
67
- new DiagConsoleLogger(),
68
- config.diagLogLevel
69
- ? DiagLogLevel[config.diagLogLevel]
70
- : DiagLogLevel.INFO,
71
- );
76
+ const nodeSdkInstrumentation = getNodeAutoInstrumentations({
77
+ "@opentelemetry/instrumentation-fs": {
78
+ enabled: config.enableFS ?? false,
79
+ },
80
+ });
81
+
82
+ let exporter: Exporters;
83
+
84
+ if (config.protocol === "http") {
85
+ exporter = buildHttpExporters(config);
86
+ } else if (config.protocol === "console") {
87
+ exporter = buildConsoleExporters(config);
88
+ } else {
89
+ exporter = await buildGrpcExporters(config);
90
+ }
72
91
 
73
92
  const sdk = new NodeSDK({
74
93
  resourceDetectors: [
@@ -80,17 +99,13 @@ export default function buildNodeInstrumentation(
80
99
  logRecordProcessors: exporter.logs,
81
100
  sampler: mainSampler,
82
101
  textMapPropagator: new W3CTraceContextPropagator(),
83
- instrumentations: [
84
- getNodeAutoInstrumentations({
85
- "@opentelemetry/instrumentation-fs": {
86
- enabled: config.enableFS ?? false,
87
- },
88
- }),
89
- ],
102
+ instrumentations: [nodeSdkInstrumentation],
90
103
  });
91
104
 
92
105
  sdk.start();
93
106
  console.log("NodeJS OpenTelemetry instrumentation started successfully.");
107
+
108
+ _shutdownHook(sdk);
94
109
  return sdk;
95
110
  } catch (error) {
96
111
  console.error(
@@ -0,0 +1,14 @@
1
+ export function _shutdownHook(sdk: { shutdown: () => Promise<void> }) {
2
+ process.on("SIGTERM", async () => {
3
+ try {
4
+ // Flushing before shutdown is implemented on a per-exporter basis.
5
+ await sdk.shutdown();
6
+ console.log("NodeJS OpenTelemetry instrumentation shutdown successfully");
7
+ } catch (error) {
8
+ console.error(
9
+ "Error shutting down NodeJS OpenTelemetry instrumentation:",
10
+ error,
11
+ );
12
+ }
13
+ });
14
+ }
@@ -0,0 +1,145 @@
1
+ import type { AnyValue, AnyValueMap } from "@opentelemetry/api-logs";
2
+ import { _getPIICounterRedactionMetric } from "./shared-metrics.js";
3
+
4
+ const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@([a-zA-Z0-9.-]+\.[a-z]{2,})/gi;
5
+
6
+ const decoder = new TextDecoder();
7
+ const encoder = new TextEncoder();
8
+
9
+ export type PIISource = "trace" | "log" | "metric";
10
+
11
+ /**
12
+ * Redacts all email addresses in the input string and collects metadata.
13
+ *
14
+ * @param {string} value The input string potentially containing email addresses.
15
+ * @returns {{
16
+ * redacted: string,
17
+ * count: number,
18
+ * domains: Record<string, number>
19
+ * }}
20
+ *
21
+ * An object containing:
22
+ * - `redacted`: the string with email addresses replaced by `[REDACTED EMAIL]`
23
+ * - `count`: total number of email addresses redacted
24
+ * - `domains`: a map of domain names to the number of times they were redacted
25
+ */
26
+ function _redactEmails(value: string): {
27
+ redacted: string;
28
+ count: number;
29
+ domains: Record<string, number>;
30
+ } {
31
+ let count = 0;
32
+ const domains: Record<string, number> = {};
33
+
34
+ const redacted = value.replace(EMAIL_REGEX, (_, domain) => {
35
+ count++;
36
+ domains[domain] = (domains[domain] || 0) + 1;
37
+ return "[REDACTED EMAIL]";
38
+ });
39
+
40
+ return { redacted, count, domains };
41
+ }
42
+
43
+ /**
44
+ * Checks whether a string contains URI-encoded components.
45
+ *
46
+ * @param {string} value - The string to inspect.
47
+ * @returns {boolean} `true` if the string is encoded, `false` otherwise.
48
+ */
49
+ function _containsEncodedComponents(value: string) {
50
+ try {
51
+ return decodeURI(value) !== decodeURIComponent(value);
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Cleans a string by redacting email addresses and emitting metrics for PII.
59
+ *
60
+ * If the string is URL-encoded, it will be decoded before redaction.
61
+ * Metrics are emitted for each domain found in redacted email addresses.
62
+ *
63
+ * @param {string} value - The input string to sanitize.
64
+ * @param {"trace" | "log"} source - The source context of the input, used in metrics.
65
+ * @returns {string} The cleaned string with any email addresses replaced by `[REDACTED EMAIL]`.
66
+ */
67
+ export function _cleanStringPII(value: AnyValue, source: PIISource): AnyValue {
68
+ if (Array.isArray(value)) {
69
+ return value.map((v) => _cleanStringPII(v, source));
70
+ }
71
+
72
+ if (typeof value !== "string") {
73
+ return value;
74
+ }
75
+
76
+ let kind: "string" | "url" = "string";
77
+ let decodedValue = value;
78
+
79
+ if (_containsEncodedComponents(value)) {
80
+ decodedValue = decodeURIComponent(value);
81
+ kind = "url";
82
+ }
83
+
84
+ const { redacted, count, domains } = _redactEmails(decodedValue);
85
+
86
+ if (count > 0) {
87
+ for (const [domain, domainCount] of Object.entries(domains)) {
88
+ _getPIICounterRedactionMetric().add(domainCount, {
89
+ pii_type: "email",
90
+ redaction_source: source,
91
+ pii_email_domain: domain,
92
+ pii_format: kind,
93
+ });
94
+ }
95
+ }
96
+ return redacted;
97
+ }
98
+
99
+ export function _cleanObjectPII(entry: object, source: PIISource) {
100
+ if (!entry) {
101
+ return entry;
102
+ }
103
+
104
+ return Object.fromEntries(
105
+ Object.entries(entry).map(([k, v]) => [k, _cleanStringPII(v, source)]),
106
+ );
107
+ }
108
+
109
+ export function _cleanLogBodyPII(value: AnyValue): AnyValue {
110
+ if (typeof value === "string") {
111
+ return _cleanStringPII(value, "log");
112
+ }
113
+
114
+ if (
115
+ typeof value === "number" ||
116
+ typeof value === "boolean" ||
117
+ value == null
118
+ ) {
119
+ return value;
120
+ }
121
+
122
+ if (value instanceof Uint8Array) {
123
+ try {
124
+ const decoded = decoder.decode(value);
125
+ const sanitized = _cleanStringPII(decoded, "log") as string;
126
+ return encoder.encode(sanitized);
127
+ } catch {
128
+ return value;
129
+ }
130
+ }
131
+
132
+ if (Array.isArray(value)) {
133
+ return value.map(_cleanLogBodyPII);
134
+ }
135
+
136
+ if (typeof value === "object") {
137
+ const sanitized: AnyValueMap = {};
138
+ for (const [key, val] of Object.entries(value)) {
139
+ sanitized[key] = _cleanLogBodyPII(val);
140
+ }
141
+ return sanitized;
142
+ }
143
+
144
+ return value;
145
+ }
@@ -0,0 +1,34 @@
1
+ import { Attributes, Counter } from "@opentelemetry/api";
2
+ import { getMetric } from "../metrics.js";
3
+ import { PIISource } from "./pii-detection.js";
4
+
5
+ interface RedactionMetric extends Attributes {
6
+ /** Type of PII redacted (e.g., "email", "phone"). */
7
+ pii_type: string;
8
+ /** Domain part of the redacted PII (e.g., "gmail.com"). */
9
+ pii_email_domain?: string;
10
+ /** Source of the redaction (trace, log or metric). */
11
+ redaction_source: PIISource;
12
+ /** Format or structure of the redacted value. */
13
+ pii_format: "string" | "url";
14
+ }
15
+
16
+ // Cached singleton instance of the redaction counter metric
17
+ let _redactedCounter: undefined | Counter;
18
+
19
+ /**
20
+ * Returns a singleton OpenTelemetry counter metric used to record occurrences of PII redactions.
21
+ *
22
+ * @returns {Counter} The singleton OpenTelemetry counter metric for PII redactions.
23
+ */
24
+ export function _getPIICounterRedactionMetric() {
25
+ if (_redactedCounter) {
26
+ return _redactedCounter;
27
+ }
28
+
29
+ _redactedCounter = getMetric<"counter", RedactionMetric>("counter", {
30
+ meterName: "o11y",
31
+ metricName: "o11y_pii_redaction",
32
+ });
33
+ return _redactedCounter;
34
+ }
package/lib/resource.ts CHANGED
@@ -19,11 +19,12 @@ export class ObservabilityResourceDetector implements ResourceDetector {
19
19
  if (this._resourceAttributes) {
20
20
  attributes = {
21
21
  ...this._resourceAttributes,
22
- "o11y.sdk.name": packageJson.name,
23
- "o11y.sdk.version": packageJson.version,
24
22
  };
25
23
  }
26
24
 
25
+ attributes["o11y.sdk.name"] = packageJson.name;
26
+ attributes["o11y.sdk.version"] = packageJson.version;
27
+
27
28
  return { attributes };
28
29
  }
29
30
  }