@ogcio/o11y-sdk-node 0.2.0 → 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 +62 -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.js +14 -13
- 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 +18 -4
- package/dist/lib/instrumentation.node.js +13 -11
- 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/traces.d.ts +20 -1
- package/dist/lib/traces.js +47 -1
- package/dist/package.json +3 -2
- package/dist/vitest.config.js +7 -1
- package/lib/config-manager.ts +16 -0
- package/lib/exporter/console.ts +6 -4
- package/lib/exporter/grpc.ts +34 -21
- 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 +19 -4
- package/lib/instrumentation.node.ts +16 -16
- 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/traces.ts +74 -1
- package/package.json +3 -2
- package/test/config-manager.test.ts +34 -0
- package/test/exporter/pii-exporter-decorator.test.ts +139 -0
- 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 +59 -14
- package/test/processor/enrich-span-processor.test.ts +2 -54
- 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/vitest.config.ts +7 -1
|
@@ -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
|
@@ -3,14 +3,18 @@ import type { Metadata } from "@grpc/grpc-js";
|
|
|
3
3
|
export interface NodeSDKConfig {
|
|
4
4
|
/**
|
|
5
5
|
* The opentelemetry collector entrypoint GRPC url.
|
|
6
|
-
* If the
|
|
6
|
+
* If the collectorUrl is null or undefined, the instrumentation will not be activated.
|
|
7
7
|
* @example http://alloy:4317
|
|
8
8
|
*/
|
|
9
9
|
collectorUrl: string;
|
|
10
10
|
/**
|
|
11
|
-
* 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
|
|
12
12
|
*/
|
|
13
13
|
serviceName?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Version of your application used for the collector to group logs and naming traces
|
|
16
|
+
*/
|
|
17
|
+
serviceVersion?: string;
|
|
14
18
|
/**
|
|
15
19
|
* Diagnostic log level for the internal runtime instrumentation
|
|
16
20
|
*
|
|
@@ -20,7 +24,7 @@ export interface NodeSDKConfig {
|
|
|
20
24
|
diagLogLevel?: SDKLogLevel;
|
|
21
25
|
/**
|
|
22
26
|
* Collector signals processing mode.
|
|
23
|
-
*
|
|
27
|
+
* single: makes an http/grpc request for each signal, and it is immediately processed inside grafana
|
|
24
28
|
* batch: sends multiple signals within a time window, optimized to reduce http/grpc calls in production
|
|
25
29
|
*
|
|
26
30
|
* @type string
|
|
@@ -36,7 +40,7 @@ export interface NodeSDKConfig {
|
|
|
36
40
|
ignoreUrls?: SamplerCondition[];
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
|
-
* 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.
|
|
40
44
|
*/
|
|
41
45
|
spanAttributes?: Record<
|
|
42
46
|
string,
|
|
@@ -75,6 +79,17 @@ export interface NodeSDKConfig {
|
|
|
75
79
|
* @default { waitForReady: true }
|
|
76
80
|
*/
|
|
77
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
|
+
};
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
export interface SamplerCondition {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import process from "process";
|
|
2
1
|
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
|
3
2
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
4
3
|
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
@@ -8,11 +7,13 @@ import {
|
|
|
8
7
|
ParentBasedSampler,
|
|
9
8
|
TraceIdRatioBasedSampler,
|
|
10
9
|
} from "@opentelemetry/sdk-trace-base";
|
|
10
|
+
import { setNodeSdkConfig } from "./config-manager.js";
|
|
11
11
|
import buildConsoleExporters from "./exporter/console.js";
|
|
12
12
|
import buildGrpcExporters from "./exporter/grpc.js";
|
|
13
13
|
import buildHttpExporters from "./exporter/http.js";
|
|
14
14
|
import type { Exporters } from "./exporter/index.js";
|
|
15
15
|
import type { NodeSDKConfig } from "./index.js";
|
|
16
|
+
import { _shutdownHook } from "./internals/hooks.js";
|
|
16
17
|
import { ObservabilityResourceDetector } from "./resource.js";
|
|
17
18
|
import { UrlSampler } from "./url-sampler.js";
|
|
18
19
|
|
|
@@ -40,6 +41,19 @@ export default async function buildNodeInstrumentation(
|
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
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
|
+
// Init configManager to make it available to all o11y utils.
|
|
55
|
+
setNodeSdkConfig(config);
|
|
56
|
+
|
|
43
57
|
const urlSampler = new UrlSampler(
|
|
44
58
|
config.ignoreUrls,
|
|
45
59
|
new TraceIdRatioBasedSampler(config.traceRatio ?? 1),
|
|
@@ -91,21 +105,7 @@ export default async function buildNodeInstrumentation(
|
|
|
91
105
|
sdk.start();
|
|
92
106
|
console.log("NodeJS OpenTelemetry instrumentation started successfully.");
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
// Flushing before shutdown is implemented on a per-exporter basis.
|
|
97
|
-
await sdk.shutdown();
|
|
98
|
-
console.log(
|
|
99
|
-
"NodeJS OpenTelemetry instrumentation shutdown successfully",
|
|
100
|
-
);
|
|
101
|
-
} catch (error) {
|
|
102
|
-
console.error(
|
|
103
|
-
"Error shutting down NodeJS OpenTelemetry instrumentation:",
|
|
104
|
-
error,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
108
|
+
_shutdownHook(sdk);
|
|
109
109
|
return sdk;
|
|
110
110
|
} catch (error) {
|
|
111
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/traces.ts
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
|
-
import { trace } from "@opentelemetry/api";
|
|
1
|
+
import { Span, SpanOptions, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
|
+
import { getNodeSdkConfig } from "./config-manager.js";
|
|
2
3
|
|
|
4
|
+
export type WithSpanParams<T> = {
|
|
5
|
+
/**
|
|
6
|
+
* The name of the trace the span should belong to.
|
|
7
|
+
* NOTE: If you want the new span to belong to an already existing trace, you should provide the same tracer name
|
|
8
|
+
*/
|
|
9
|
+
traceName?: string;
|
|
10
|
+
spanName: string;
|
|
11
|
+
spanOptions?: SpanOptions;
|
|
12
|
+
/** A function defining the task you want to be wrapped by this span */
|
|
13
|
+
fn: (span: Span) => T | Promise<T>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generates a function wrapping a given Callable `fn` into an error handling block.
|
|
18
|
+
* Setting Span status and recording any caught exception before bubbling it up.
|
|
19
|
+
*
|
|
20
|
+
* Marks the span as ended once the provided callable has ended or an error has been caught.
|
|
21
|
+
*
|
|
22
|
+
* @returns {Promise<T>} where T is the type returned by the Callable.
|
|
23
|
+
* @throws any error thrown by the original Callable `fn` provided.
|
|
24
|
+
*/
|
|
25
|
+
function selfContainedSpanHandlerGenerator<T>(
|
|
26
|
+
fn: (span: Span) => T | Promise<T>,
|
|
27
|
+
): (span: Span) => Promise<T> {
|
|
28
|
+
return async (span: Span) => {
|
|
29
|
+
try {
|
|
30
|
+
const fnResult = await fn(span);
|
|
31
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
32
|
+
return fnResult;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err instanceof Error) {
|
|
35
|
+
span.recordException(err);
|
|
36
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
span.recordException({ message: JSON.stringify(err) });
|
|
41
|
+
span.setStatus({
|
|
42
|
+
code: SpanStatusCode.ERROR,
|
|
43
|
+
message: JSON.stringify(err),
|
|
44
|
+
});
|
|
45
|
+
throw err;
|
|
46
|
+
} finally {
|
|
47
|
+
span.end();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Gets the currently active OpenTelemetry span.
|
|
54
|
+
*
|
|
55
|
+
* @returns {Span | undefined} The active span with redaction logic applied,
|
|
56
|
+
* or `undefined` if there is no active span in context.
|
|
57
|
+
*/
|
|
3
58
|
export function getActiveSpan() {
|
|
4
59
|
return trace.getActiveSpan();
|
|
5
60
|
}
|
|
61
|
+
|
|
62
|
+
export function withSpan<T>({
|
|
63
|
+
traceName,
|
|
64
|
+
spanName,
|
|
65
|
+
spanOptions = {},
|
|
66
|
+
fn,
|
|
67
|
+
}: WithSpanParams<T>) {
|
|
68
|
+
const sdkConfig = getNodeSdkConfig();
|
|
69
|
+
const tracer = trace.getTracer(
|
|
70
|
+
traceName ?? sdkConfig.serviceName ?? "o11y-sdk",
|
|
71
|
+
sdkConfig.serviceVersion,
|
|
72
|
+
);
|
|
73
|
+
return tracer.startActiveSpan(
|
|
74
|
+
spanName,
|
|
75
|
+
spanOptions,
|
|
76
|
+
selfContainedSpanHandlerGenerator<T>(fn),
|
|
77
|
+
);
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogcio/o11y-sdk-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@grpc/grpc-js": "^1.13.4",
|
|
23
23
|
"@opentelemetry/api": "^1.9.0",
|
|
24
|
+
"@opentelemetry/api-logs": "^0.203.0",
|
|
24
25
|
"@opentelemetry/auto-instrumentations-node": "^0.60.1",
|
|
25
26
|
"@opentelemetry/core": "^2.0.1",
|
|
26
27
|
"@opentelemetry/exporter-logs-otlp-grpc": "^0.202.0",
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"@opentelemetry/sdk-trace-base": "^2.0.1"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^24.0.
|
|
42
|
+
"@types/node": "^24.0.10",
|
|
42
43
|
"@vitest/coverage-v8": "^3.2.4",
|
|
43
44
|
"tsx": "^4.20.3",
|
|
44
45
|
"typescript": "^5.8.3",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getNodeSdkConfig, setNodeSdkConfig } from "../lib/config-manager";
|
|
3
|
+
import { NodeSDKConfig } from "../lib";
|
|
4
|
+
|
|
5
|
+
describe("Config Manager", () => {
|
|
6
|
+
it("throws if getConfig is called before initialization", () => {
|
|
7
|
+
expect(() => getNodeSdkConfig()).toThrow();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("sdk defined config is not pollutable", () => {
|
|
11
|
+
const config: NodeSDKConfig = {
|
|
12
|
+
collectorUrl: "http://example.com",
|
|
13
|
+
serviceName: "MyService",
|
|
14
|
+
spanAttributes: {
|
|
15
|
+
"my.attribute": "value",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
setNodeSdkConfig(config);
|
|
20
|
+
|
|
21
|
+
const cfg = getNodeSdkConfig();
|
|
22
|
+
|
|
23
|
+
// Top level
|
|
24
|
+
cfg.collectorUrl = "http://example.com/changed";
|
|
25
|
+
// Subfield
|
|
26
|
+
cfg.spanAttributes["my.attribute"] = "another-attribute";
|
|
27
|
+
|
|
28
|
+
// Ensure config values remain unchanged
|
|
29
|
+
expect(getNodeSdkConfig().collectorUrl).toStrictEqual(config.collectorUrl);
|
|
30
|
+
expect(getNodeSdkConfig().spanAttributes["my.attribute"]).toStrictEqual(
|
|
31
|
+
config.spanAttributes["my.attribute"],
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
});
|