@ogcio/o11y-sdk-node 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +69 -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.js +14 -13
  7. package/dist/lib/exporter/http.d.ts +1 -1
  8. package/dist/lib/exporter/http.js +14 -13
  9. package/dist/lib/exporter/pii-exporter-decorator.d.ts +20 -0
  10. package/dist/lib/exporter/pii-exporter-decorator.js +104 -0
  11. package/dist/lib/exporter/processor-config.d.ts +5 -0
  12. package/dist/lib/exporter/processor-config.js +16 -0
  13. package/dist/lib/index.d.ts +18 -4
  14. package/dist/lib/instrumentation.node.js +13 -11
  15. package/dist/lib/internals/hooks.d.ts +3 -0
  16. package/dist/lib/internals/hooks.js +12 -0
  17. package/dist/lib/internals/pii-detection.d.ts +17 -0
  18. package/dist/lib/internals/pii-detection.js +116 -0
  19. package/dist/lib/internals/shared-metrics.d.ts +7 -0
  20. package/dist/lib/internals/shared-metrics.js +18 -0
  21. package/dist/lib/traces.d.ts +20 -1
  22. package/dist/lib/traces.js +47 -1
  23. package/dist/package.json +3 -2
  24. package/dist/vitest.config.js +7 -1
  25. package/lib/config-manager.ts +16 -0
  26. package/lib/exporter/console.ts +6 -4
  27. package/lib/exporter/grpc.ts +34 -21
  28. package/lib/exporter/http.ts +33 -20
  29. package/lib/exporter/pii-exporter-decorator.ts +152 -0
  30. package/lib/exporter/processor-config.ts +23 -0
  31. package/lib/index.ts +19 -4
  32. package/lib/instrumentation.node.ts +16 -16
  33. package/lib/internals/hooks.ts +14 -0
  34. package/lib/internals/pii-detection.ts +145 -0
  35. package/lib/internals/shared-metrics.ts +34 -0
  36. package/lib/traces.ts +74 -1
  37. package/package.json +3 -2
  38. package/test/config-manager.test.ts +34 -0
  39. package/test/exporter/pii-exporter-decorator.test.ts +88 -0
  40. package/test/integration/{integration.test.ts → http-tracing.integration.test.ts} +0 -2
  41. package/test/integration/pii.integration.test.ts +68 -0
  42. package/test/integration/run.sh +2 -2
  43. package/test/internals/hooks.test.ts +45 -0
  44. package/test/internals/pii-detection.test.ts +141 -0
  45. package/test/internals/shared-metrics.test.ts +34 -0
  46. package/test/node-config.test.ts +59 -14
  47. package/test/processor/enrich-span-processor.test.ts +2 -54
  48. package/test/traces/active-span.test.ts +28 -0
  49. package/test/traces/with-span.test.ts +340 -0
  50. package/test/utils/alloy-log-parser.ts +7 -0
  51. package/test/utils/mock-signals.ts +144 -0
  52. 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 {
5
+ PushMetricExporter,
6
+ ResourceMetrics,
7
+ } from "@opentelemetry/sdk-metrics";
8
+ import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
9
+ import { NodeSDKConfig } from "../index.js";
10
+ import {
11
+ _cleanLogBodyPII,
12
+ _cleanObjectPII,
13
+ _cleanStringPII,
14
+ } from "../internals/pii-detection.js";
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
+ item = this._redactLogRecord(item) as ReadableLogRecord;
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
+ return {
133
+ ...log,
134
+ body: _cleanLogBodyPII(log.body),
135
+ attributes: log.attributes && _cleanObjectPII(log.attributes, "log"),
136
+ resource: log.resource && {
137
+ ...log.resource,
138
+ attributes: _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 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.
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
- * 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
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 every logs and traces.
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
- process.on("SIGTERM", async () => {
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.2.0",
3
+ "version": "0.3.1",
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.3",
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
+ });