@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.
- package/CHANGELOG.md +128 -0
- package/README.md +158 -13
- package/dist/lib/config-manager.d.ts +3 -0
- package/dist/lib/config-manager.js +11 -0
- package/dist/lib/exporter/console.js +3 -4
- package/dist/lib/exporter/grpc.d.ts +1 -1
- package/dist/lib/exporter/grpc.js +24 -14
- package/dist/lib/exporter/http.d.ts +1 -1
- package/dist/lib/exporter/http.js +14 -13
- package/dist/lib/exporter/pii-exporter-decorator.d.ts +20 -0
- package/dist/lib/exporter/pii-exporter-decorator.js +103 -0
- package/dist/lib/exporter/processor-config.d.ts +5 -0
- package/dist/lib/exporter/processor-config.js +16 -0
- package/dist/lib/index.d.ts +25 -4
- package/dist/lib/instrumentation.node.d.ts +1 -1
- package/dist/lib/instrumentation.node.js +29 -19
- package/dist/lib/internals/hooks.d.ts +3 -0
- package/dist/lib/internals/hooks.js +12 -0
- package/dist/lib/internals/pii-detection.d.ts +17 -0
- package/dist/lib/internals/pii-detection.js +116 -0
- package/dist/lib/internals/shared-metrics.d.ts +7 -0
- package/dist/lib/internals/shared-metrics.js +18 -0
- package/dist/lib/resource.js +2 -2
- package/dist/lib/traces.d.ts +20 -1
- package/dist/lib/traces.js +47 -1
- package/dist/package.json +23 -21
- package/dist/vitest.config.js +8 -2
- package/lib/config-manager.ts +16 -0
- package/lib/exporter/console.ts +6 -4
- package/lib/exporter/grpc.ts +46 -20
- package/lib/exporter/http.ts +33 -20
- package/lib/exporter/pii-exporter-decorator.ts +152 -0
- package/lib/exporter/processor-config.ts +23 -0
- package/lib/index.ts +28 -4
- package/lib/instrumentation.node.ts +37 -22
- package/lib/internals/hooks.ts +14 -0
- package/lib/internals/pii-detection.ts +145 -0
- package/lib/internals/shared-metrics.ts +34 -0
- package/lib/resource.ts +3 -2
- package/lib/traces.ts +74 -1
- package/package.json +23 -21
- package/test/config-manager.test.ts +34 -0
- package/test/exporter/pii-exporter-decorator.test.ts +139 -0
- package/test/index.test.ts +44 -12
- package/test/integration/README.md +1 -1
- package/test/integration/{integration.test.ts → http-tracing.integration.test.ts} +0 -2
- package/test/integration/pii.integration.test.ts +68 -0
- package/test/integration/run.sh +2 -2
- package/test/internals/hooks.test.ts +45 -0
- package/test/internals/pii-detection.test.ts +141 -0
- package/test/internals/shared-metrics.test.ts +34 -0
- package/test/node-config.test.ts +68 -30
- package/test/processor/enrich-span-processor.test.ts +2 -54
- package/test/resource.test.ts +12 -1
- package/test/traces/active-span.test.ts +28 -0
- package/test/traces/with-span.test.ts +340 -0
- package/test/utils/alloy-log-parser.ts +7 -0
- package/test/utils/mock-signals.ts +144 -0
- package/test/validation.test.ts +22 -16
- package/vitest.config.ts +8 -2
package/lib/exporter/http.ts
CHANGED
|
@@ -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 {
|
|
8
|
+
import {
|
|
9
|
+
_logsProcessorConfig,
|
|
10
|
+
_spansProcessorConfig,
|
|
11
|
+
} from "./processor-config.js";
|
|
8
12
|
import { Exporters } from "./index.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
new PIIExporterDecorator(
|
|
25
|
+
new OTLPTraceExporter({
|
|
26
|
+
url: `${config.collectorUrl}/v1/traces`,
|
|
27
|
+
compression: CompressionAlgorithm.GZIP,
|
|
28
|
+
}),
|
|
29
|
+
getNodeSdkConfig(),
|
|
30
|
+
),
|
|
24
31
|
),
|
|
25
|
-
|
|
32
|
+
..._spansProcessorConfig(config),
|
|
26
33
|
],
|
|
27
34
|
metrics: new metrics.PeriodicExportingMetricReader({
|
|
28
|
-
exporter: new
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
44
|
+
..._logsProcessorConfig(config),
|
|
35
45
|
new LogRecordProcessorMap[config.collectorMode ?? "batch"](
|
|
36
|
-
new
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
44
|
+
if (!config.detection) {
|
|
45
|
+
config.detection = {
|
|
46
|
+
email: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
43
49
|
|
|
44
|
-
if (config.
|
|
45
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
}
|