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