@ogcio/o11y-sdk-node 0.1.0-beta.6 → 0.1.0-beta.8

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 (51) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +59 -0
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +1 -0
  5. package/dist/lib/exporter/console.d.ts +3 -0
  6. package/dist/lib/exporter/console.js +16 -0
  7. package/dist/lib/exporter/grpc.d.ts +3 -0
  8. package/dist/lib/{grpc.js → exporter/grpc.js} +9 -5
  9. package/dist/lib/exporter/http.d.ts +3 -0
  10. package/dist/lib/{http.js → exporter/http.js} +9 -5
  11. package/dist/lib/index.d.ts +17 -5
  12. package/dist/lib/instrumentation.node.js +27 -9
  13. package/dist/lib/metrics.d.ts +12 -12
  14. package/dist/lib/metrics.js +6 -10
  15. package/dist/lib/options.d.ts +5 -3
  16. package/dist/lib/processor/enrich-span-processor.d.ts +11 -0
  17. package/dist/lib/processor/enrich-span-processor.js +22 -0
  18. package/dist/lib/traces.d.ts +1 -0
  19. package/dist/lib/traces.js +4 -0
  20. package/dist/lib/url-sampler.d.ts +3 -2
  21. package/dist/lib/url-sampler.js +5 -8
  22. package/dist/lib/utils.d.ts +4 -2
  23. package/dist/lib/utils.js +8 -3
  24. package/dist/package.json +21 -17
  25. package/dist/vitest.config.js +15 -1
  26. package/index.ts +2 -2
  27. package/lib/exporter/console.ts +27 -0
  28. package/lib/{grpc.ts → exporter/grpc.ts} +13 -7
  29. package/lib/{http.ts → exporter/http.ts} +13 -7
  30. package/lib/index.ts +24 -4
  31. package/lib/instrumentation.node.ts +49 -11
  32. package/lib/metrics.ts +34 -29
  33. package/lib/options.ts +5 -3
  34. package/lib/processor/enrich-span-processor.ts +39 -0
  35. package/lib/traces.ts +5 -0
  36. package/lib/url-sampler.ts +13 -14
  37. package/lib/utils.ts +16 -4
  38. package/package.json +21 -17
  39. package/test/enrich-span-processor.test.ts +105 -0
  40. package/test/index.test.ts +9 -0
  41. package/test/integration/integration.test.ts +21 -0
  42. package/test/integration/run.sh +5 -2
  43. package/test/metrics.test.ts +9 -9
  44. package/test/node-config.test.ts +49 -36
  45. package/test/url-sampler.test.ts +215 -0
  46. package/vitest.config.ts +15 -1
  47. package/dist/lib/console.d.ts +0 -3
  48. package/dist/lib/console.js +0 -12
  49. package/dist/lib/grpc.d.ts +0 -3
  50. package/dist/lib/http.d.ts +0 -3
  51. package/lib/console.ts +0 -15
@@ -1,18 +1,24 @@
1
- import type { NodeSDKConfig } from "./index.js";
2
- import type { Exporters } from "./options.js";
3
- import { LogRecordProcessorMap } from "./utils.js";
4
1
  import { metrics } from "@opentelemetry/sdk-node";
5
2
  import { CompressionAlgorithm } from "@opentelemetry/otlp-exporter-base";
6
3
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
7
4
  import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-grpc";
8
5
  import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
6
+ import { NodeSDKConfig } from "../index.js";
7
+ import { Exporters } from "../options.js";
8
+ import { LogRecordProcessorMap, SpanProcessorMap } from "../utils.js";
9
+ import { EnrichSpanProcessor } from "../processor/enrich-span-processor.js";
9
10
 
10
11
  export default function buildGrpcExporters(config: NodeSDKConfig): Exporters {
11
12
  return {
12
- traces: new OTLPTraceExporter({
13
- url: `${config.collectorUrl}`,
14
- compression: CompressionAlgorithm.GZIP,
15
- }),
13
+ spans: [
14
+ new SpanProcessorMap[config.collectorMode ?? "batch"](
15
+ new OTLPTraceExporter({
16
+ url: `${config.collectorUrl}`,
17
+ compression: CompressionAlgorithm.GZIP,
18
+ }),
19
+ ),
20
+ new EnrichSpanProcessor(config.spanAttributes),
21
+ ],
16
22
  metrics: new metrics.PeriodicExportingMetricReader({
17
23
  exporter: new OTLPMetricExporter({
18
24
  url: `${config.collectorUrl}`,
@@ -1,11 +1,12 @@
1
- import type { NodeSDKConfig } from "./index.js";
2
- import type { Exporters } from "./options.js";
3
- import { LogRecordProcessorMap } from "./utils.js";
4
1
  import { metrics } from "@opentelemetry/sdk-node";
5
2
  import { CompressionAlgorithm } from "@opentelemetry/otlp-exporter-base";
6
3
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
7
4
  import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
8
5
  import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
6
+ import { LogRecordProcessorMap, SpanProcessorMap } from "../utils.js";
7
+ import { EnrichSpanProcessor } from "../processor/enrich-span-processor.js";
8
+ import { Exporters } from "../options.js";
9
+ import { NodeSDKConfig } from "../index.js";
9
10
 
10
11
  export default function buildHttpExporters(config: NodeSDKConfig): Exporters {
11
12
  if (config.collectorUrl.endsWith("/")) {
@@ -13,10 +14,15 @@ export default function buildHttpExporters(config: NodeSDKConfig): Exporters {
13
14
  }
14
15
 
15
16
  return {
16
- traces: new OTLPTraceExporter({
17
- url: `${config.collectorUrl}/v1/traces`,
18
- compression: CompressionAlgorithm.GZIP,
19
- }),
17
+ spans: [
18
+ new SpanProcessorMap[config.collectorMode ?? "batch"](
19
+ new OTLPTraceExporter({
20
+ url: `${config.collectorUrl}/v1/traces`,
21
+ compression: CompressionAlgorithm.GZIP,
22
+ }),
23
+ ),
24
+ new EnrichSpanProcessor(config.spanAttributes),
25
+ ],
20
26
  metrics: new metrics.PeriodicExportingMetricReader({
21
27
  exporter: new OTLPMetricExporter({
22
28
  url: `${config.collectorUrl}/v1/metrics`,
package/lib/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- interface SDKConfig {
1
+ export interface NodeSDKConfig {
2
2
  /**
3
3
  * The opentelemetry collector entrypoint GRPC url.
4
4
  * If the collectoUrl is null or undefined, the instrumentation will not be activated.
@@ -32,9 +32,27 @@ interface SDKConfig {
32
32
  * @default []
33
33
  */
34
34
  ignoreUrls?: SamplerCondition[];
35
- }
36
35
 
37
- export interface NodeSDKConfig extends SDKConfig {
36
+ /**
37
+ * Object containing static properties or functions used to evaluate custom attributes for every logs and traces.
38
+ */
39
+ spanAttributes?: Record<
40
+ string,
41
+ SignalAttributeValue | (() => SignalAttributeValue)
42
+ >;
43
+
44
+ /**
45
+ * Object containing static properties used as resources attributes for the Node SDK initialization.
46
+ */
47
+ resourceAttributes?: Record<string, SignalAttributeValue>;
48
+
49
+ /**
50
+ * Faction value from 0 to 1, used by TraceIdRatioBasedSampler which it deterministically samples a percentage of traces that you pass in as a parameter.
51
+ *
52
+ * @default 1
53
+ */
54
+ traceRatio?: number;
55
+
38
56
  /**
39
57
  * Flag to enable or disable the tracing for node:fs module
40
58
  *
@@ -43,7 +61,7 @@ export interface NodeSDKConfig extends SDKConfig {
43
61
  enableFS?: boolean;
44
62
 
45
63
  /**
46
- * protocol used to send signals.
64
+ * Protocol used to send signals.
47
65
  *
48
66
  * @default grpc
49
67
  */
@@ -55,6 +73,8 @@ export interface SamplerCondition {
55
73
  url: string;
56
74
  }
57
75
 
76
+ export type SignalAttributeValue = string | number | boolean;
77
+
58
78
  export type SDKCollectorMode = "single" | "batch";
59
79
 
60
80
  export type SDKProtocol = "grpc" | "http" | "console";
@@ -1,13 +1,23 @@
1
- import { NodeSDK, resources } from "@opentelemetry/sdk-node";
2
- import type { NodeSDKConfig } from "./index.js";
3
- import type { Exporters } from "./options.js";
4
- import buildHttpExporters from "./http.js";
5
- import buildGrpcExporters from "./grpc.js";
6
- import buildConsoleExporters from "./console.js";
7
- import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
1
+ import {
2
+ diag,
3
+ DiagConsoleLogger,
4
+ DiagLogLevel,
5
+ Span,
6
+ } from "@opentelemetry/api";
8
7
  import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
9
8
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
9
+ import { NodeSDK, resources } from "@opentelemetry/sdk-node";
10
+ import {
11
+ AlwaysOffSampler,
12
+ ParentBasedSampler,
13
+ TraceIdRatioBasedSampler,
14
+ } from "@opentelemetry/sdk-trace-base";
10
15
  import packageJson from "../package.json" with { type: "json" };
16
+ import buildConsoleExporters from "./exporter/console.js";
17
+ import buildGrpcExporters from "./exporter/grpc.js";
18
+ import buildHttpExporters from "./exporter/http.js";
19
+ import type { NodeSDKConfig } from "./index.js";
20
+ import type { Exporters } from "./options.js";
11
21
  import { UrlSampler } from "./url-sampler.js";
12
22
 
13
23
  export default function buildNodeInstrumentation(
@@ -44,6 +54,19 @@ export default function buildNodeInstrumentation(
44
54
  exporter = buildGrpcExporters(config);
45
55
  }
46
56
 
57
+ const urlSampler = new UrlSampler(
58
+ config.ignoreUrls,
59
+ new TraceIdRatioBasedSampler(config.traceRatio ?? 1),
60
+ );
61
+
62
+ const mainSampler = new ParentBasedSampler({
63
+ root: urlSampler,
64
+ remoteParentSampled: urlSampler,
65
+ remoteParentNotSampled: new AlwaysOffSampler(),
66
+ localParentSampled: urlSampler,
67
+ localParentNotSampled: new AlwaysOffSampler(),
68
+ });
69
+
47
70
  try {
48
71
  diag.setLogger(
49
72
  new DiagConsoleLogger(),
@@ -56,18 +79,34 @@ export default function buildNodeInstrumentation(
56
79
  resource: new resources.Resource({
57
80
  "o11y.sdk.name": packageJson.name,
58
81
  "o11y.sdk.version": packageJson.version,
82
+ ...config.resourceAttributes,
59
83
  }),
84
+ spanProcessors: exporter.spans,
60
85
  serviceName: config.serviceName,
61
- traceExporter: exporter.traces,
62
86
  metricReader: exporter.metrics,
63
87
  logRecordProcessors: exporter.logs,
64
- sampler: new UrlSampler(config.ignoreUrls),
88
+ sampler: mainSampler,
65
89
  textMapPropagator: new W3CTraceContextPropagator(),
66
90
  instrumentations: [
67
91
  getNodeAutoInstrumentations({
68
92
  "@opentelemetry/instrumentation-fs": {
69
93
  enabled: config.enableFS ?? false,
70
94
  },
95
+ "@opentelemetry/instrumentation-pino": {
96
+ logHook: (
97
+ _span: Span,
98
+ record: Record<string, unknown>,
99
+ _level?: number,
100
+ ) => {
101
+ if (config.spanAttributes != undefined) {
102
+ for (const [key, value] of Object.entries(
103
+ config.spanAttributes,
104
+ )) {
105
+ record[key] = typeof value === "function" ? value() : value;
106
+ }
107
+ }
108
+ },
109
+ },
71
110
  }),
72
111
  ],
73
112
  });
@@ -87,8 +126,7 @@ function isUrl(url: string): boolean {
87
126
  try {
88
127
  new URL(url);
89
128
  return true;
90
- } catch (err) {
91
- console.error(err);
129
+ } catch (_) {
92
130
  return false;
93
131
  }
94
132
  }
package/lib/metrics.ts CHANGED
@@ -10,26 +10,30 @@ import {
10
10
  ObservableGauge,
11
11
  ObservableUpDownCounter,
12
12
  UpDownCounter,
13
+ Attributes,
13
14
  } from "@opentelemetry/api";
14
15
 
15
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
16
- declare const MetricsMap: {
17
- gauge: Gauge;
18
- histogram: Histogram;
19
- counter: Counter;
20
- updowncounter: UpDownCounter;
21
- "async-counter": ObservableCounter;
22
- "async-updowncounter": ObservableUpDownCounter;
23
- "async-gauge": ObservableGauge;
16
+ type MetricTypeMap<TAttributes extends Attributes> = {
17
+ counter: Counter<TAttributes>;
18
+ histogram: Histogram<TAttributes>;
19
+ gauge: Gauge<TAttributes>;
20
+ updowncounter: UpDownCounter<TAttributes>;
21
+ "async-counter": ObservableCounter<TAttributes>;
22
+ "async-updowncounter": ObservableUpDownCounter<TAttributes>;
23
+ "async-gauge": ObservableGauge<TAttributes>;
24
24
  };
25
25
 
26
- type MetricType = keyof typeof MetricsMap;
26
+ export type MetricType = keyof MetricTypeMap<Attributes>;
27
27
 
28
- const MetricsFactoryMap: {
29
- [K in MetricType]: (
28
+ const MetricsFactoryMap: Record<
29
+ MetricType,
30
+ (
30
31
  meter: Meter,
31
- ) => (name: string, options?: MetricOptions) => (typeof MetricsMap)[K];
32
- } = {
32
+ ) => (
33
+ name: string,
34
+ options?: MetricOptions,
35
+ ) => MetricTypeMap<Attributes>[MetricType]
36
+ > = {
33
37
  gauge: (meter: Meter) => meter.createGauge,
34
38
  histogram: (meter: Meter) => meter.createHistogram,
35
39
  counter: (meter: Meter) => meter.createCounter,
@@ -37,34 +41,35 @@ const MetricsFactoryMap: {
37
41
  "async-counter": (meter: Meter) => meter.createObservableCounter,
38
42
  "async-updowncounter": (meter: Meter) => meter.createObservableUpDownCounter,
39
43
  "async-gauge": (meter: Meter) => meter.createObservableGauge,
40
- };
44
+ } as const;
41
45
 
42
46
  export interface MetricsParams {
47
+ meterName: string;
43
48
  metricName: string;
44
- attributeName: string;
45
49
  options?: MetricOptions;
46
50
  }
47
51
 
48
- function getMeter({ metricName, attributeName }: MetricsParams) {
49
- let meter: Meter;
50
- if (!metricName || !attributeName) {
51
- console.error("Invaid metric configuration!");
52
- meter = createNoopMeter();
53
- } else {
54
- meter = metrics.getMeter(`custom_metric.${metricName}`);
52
+ function getMeter({ meterName }: MetricsParams) {
53
+ if (!meterName) {
54
+ console.error("Invalid metric name!");
55
+ return createNoopMeter();
55
56
  }
56
- return meter;
57
+
58
+ return metrics.getMeter(`custom_metric.${meterName}`);
57
59
  }
58
60
 
59
- export function getMetric<T extends MetricType>(
60
- type: T,
61
- p: MetricsParams,
62
- ): (typeof MetricsMap)[T] {
61
+ export function getMetric<
62
+ T extends MetricType,
63
+ TAttributes extends Attributes = Attributes,
64
+ >(type: T, p: MetricsParams): MetricTypeMap<TAttributes>[T] {
63
65
  const meter = getMeter(p);
64
66
 
65
67
  if (!MetricsFactoryMap[type]) {
66
68
  throw new Error(`Unsupported metric type: ${type}`);
67
69
  }
68
70
 
69
- return MetricsFactoryMap[type](meter).bind(meter)(p.attributeName, p.options);
71
+ return MetricsFactoryMap[type](meter).bind(meter)(
72
+ p.metricName,
73
+ p.options,
74
+ ) as MetricTypeMap<TAttributes>[T];
70
75
  }
package/lib/options.ts CHANGED
@@ -1,7 +1,9 @@
1
- import type { logs, tracing, metrics } from "@opentelemetry/sdk-node";
1
+ import { LogRecordProcessor } from "@opentelemetry/sdk-logs";
2
+ import type { metrics } from "@opentelemetry/sdk-node";
3
+ import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
2
4
 
3
5
  export type Exporters = {
4
- traces: tracing.SpanExporter;
6
+ spans: SpanProcessor[];
5
7
  metrics: metrics.MetricReader;
6
- logs: logs.LogRecordProcessor[];
8
+ logs: LogRecordProcessor[];
7
9
  };
@@ -0,0 +1,39 @@
1
+ import { Context } from "@opentelemetry/api";
2
+ import {
3
+ ReadableSpan,
4
+ Span,
5
+ SpanProcessor,
6
+ } from "@opentelemetry/sdk-trace-base";
7
+ import { SignalAttributeValue } from "../index.js";
8
+
9
+ export class EnrichSpanProcessor implements SpanProcessor {
10
+ private _spanAttributes:
11
+ | Record<string, SignalAttributeValue | (() => SignalAttributeValue)>
12
+ | undefined;
13
+
14
+ constructor(
15
+ spanAttributes?: Record<
16
+ string,
17
+ SignalAttributeValue | (() => SignalAttributeValue)
18
+ >,
19
+ ) {
20
+ this._spanAttributes = spanAttributes;
21
+ }
22
+
23
+ forceFlush(): Promise<void> {
24
+ return Promise.resolve();
25
+ }
26
+ onStart(span: Span, _context: Context): void {
27
+ if (this._spanAttributes != undefined) {
28
+ for (const [key, value] of Object.entries(this._spanAttributes)) {
29
+ span.setAttribute(key, typeof value === "function" ? value() : value);
30
+ }
31
+ }
32
+ }
33
+ onEnd(_span: ReadableSpan): void {
34
+ return;
35
+ }
36
+ shutdown(): Promise<void> {
37
+ return Promise.resolve();
38
+ }
39
+ }
package/lib/traces.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { trace } from "@opentelemetry/api";
2
+
3
+ export function getActiveSpan() {
4
+ return trace.getActiveSpan();
5
+ }
@@ -1,10 +1,4 @@
1
- import {
2
- Context,
3
- SpanKind,
4
- Link,
5
- Attributes,
6
- isValidTraceId,
7
- } from "@opentelemetry/api";
1
+ import { Attributes, Context, Link, SpanKind } from "@opentelemetry/api";
8
2
  import {
9
3
  Sampler,
10
4
  SamplingDecision,
@@ -14,9 +8,11 @@ import { SamplerCondition } from "./index.js";
14
8
 
15
9
  export class UrlSampler implements Sampler {
16
10
  private _samplerCondition: SamplerCondition[];
11
+ private _nextSampler: Sampler;
17
12
 
18
- constructor(samplerCondition: SamplerCondition[] = []) {
13
+ constructor(samplerCondition: SamplerCondition[] = [], nextSampler: Sampler) {
19
14
  this._samplerCondition = samplerCondition;
15
+ this._nextSampler = nextSampler;
20
16
  }
21
17
 
22
18
  shouldSample(
@@ -41,13 +37,16 @@ export class UrlSampler implements Sampler {
41
37
  }
42
38
  }
43
39
 
44
- return {
45
- decision: isValidTraceId(traceId)
46
- ? SamplingDecision.RECORD_AND_SAMPLED
47
- : SamplingDecision.NOT_RECORD,
48
- };
40
+ return this._nextSampler.shouldSample(
41
+ _context,
42
+ traceId,
43
+ _spanName,
44
+ _spanKind,
45
+ attributes,
46
+ _links,
47
+ );
49
48
  }
50
49
  toString(): string {
51
- throw "UrlSampler";
50
+ return "UrlSampler";
52
51
  }
53
52
  }
package/lib/utils.ts CHANGED
@@ -1,10 +1,22 @@
1
+ import {
2
+ BatchLogRecordProcessor,
3
+ SimpleLogRecordProcessor,
4
+ } from "@opentelemetry/sdk-logs";
1
5
  import { SDKCollectorMode } from "./index.js";
2
- import { logs } from "@opentelemetry/sdk-node";
6
+ import { tracing } from "@opentelemetry/sdk-node";
3
7
 
4
8
  export const LogRecordProcessorMap: Record<
5
9
  SDKCollectorMode,
6
- typeof logs.SimpleLogRecordProcessor | typeof logs.BatchLogRecordProcessor
10
+ typeof SimpleLogRecordProcessor | typeof BatchLogRecordProcessor
7
11
  > = {
8
- single: logs.SimpleLogRecordProcessor,
9
- batch: logs.BatchLogRecordProcessor,
12
+ single: SimpleLogRecordProcessor,
13
+ batch: BatchLogRecordProcessor,
14
+ };
15
+
16
+ export const SpanProcessorMap: Record<
17
+ SDKCollectorMode,
18
+ typeof tracing.SimpleSpanProcessor | typeof tracing.BatchSpanProcessor
19
+ > = {
20
+ single: tracing.SimpleSpanProcessor,
21
+ batch: tracing.BatchSpanProcessor,
10
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.1.0-beta.6",
3
+ "version": "0.1.0-beta.8",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -20,29 +20,33 @@
20
20
  "license": "ISC",
21
21
  "dependencies": {
22
22
  "@opentelemetry/api": "^1.9.0",
23
- "@opentelemetry/auto-instrumentations-node": "^0.56.0",
23
+ "@opentelemetry/auto-instrumentations-node": "^0.56.1",
24
24
  "@opentelemetry/core": "1.30.1",
25
- "@opentelemetry/exporter-logs-otlp-grpc": "^0.57.1",
26
- "@opentelemetry/exporter-logs-otlp-http": "^0.57.1",
27
- "@opentelemetry/exporter-metrics-otlp-grpc": "^0.57.1",
28
- "@opentelemetry/exporter-metrics-otlp-http": "^0.57.1",
29
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.1",
30
- "@opentelemetry/exporter-trace-otlp-http": "^0.57.1",
31
- "@opentelemetry/instrumentation": "^0.57.1",
32
- "@opentelemetry/otlp-exporter-base": "^0.57.1",
25
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.57.2",
26
+ "@opentelemetry/exporter-logs-otlp-http": "^0.57.2",
27
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.57.2",
28
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.57.2",
29
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.2",
30
+ "@opentelemetry/exporter-trace-otlp-http": "^0.57.2",
31
+ "@opentelemetry/instrumentation": "^0.57.2",
32
+ "@opentelemetry/otlp-exporter-base": "^0.57.2",
33
+ "@opentelemetry/sdk-logs": "^0.57.2",
33
34
  "@opentelemetry/sdk-metrics": "^1.30.1",
34
- "@opentelemetry/sdk-node": "^0.57.1",
35
+ "@opentelemetry/sdk-node": "^0.57.2",
35
36
  "@opentelemetry/sdk-trace-base": "^1.30.1"
36
37
  },
37
38
  "devDependencies": {
38
- "@types/node": "^22.12.0",
39
- "@vitest/coverage-v8": "^3.0.4",
40
- "tsx": "^4.19.2",
41
- "typescript": "^5.7.3",
42
- "vitest": "^3.0.4"
39
+ "@types/node": "^22.13.9",
40
+ "@vitest/coverage-v8": "^3.0.8",
41
+ "tsx": "^4.19.3",
42
+ "typescript": "^5.8.2",
43
+ "vitest": "^3.0.8"
43
44
  },
44
45
  "scripts": {
45
46
  "build": "rm -rf dist && tsc -p tsconfig.json",
46
- "test": "vitest"
47
+ "test": "vitest",
48
+ "test:unit": "vitest --project unit",
49
+ "test:integration": "pnpm --filter @ogcio/o11y run prepare:integration && vitest --project integration",
50
+ "test:integration:dryrun": "vitest --project integration"
47
51
  }
48
52
  }
@@ -0,0 +1,105 @@
1
+ import {
2
+ AttributeValue,
3
+ Context,
4
+ Exception,
5
+ Link,
6
+ Span,
7
+ SpanAttributes,
8
+ SpanAttributeValue,
9
+ SpanContext,
10
+ SpanStatus,
11
+ TimeInput,
12
+ } from "@opentelemetry/api";
13
+ import { describe, expect, it } from "vitest";
14
+ import { EnrichSpanProcessor } from "../lib/processor/enrich-span-processor.js";
15
+
16
+ class MockSpan implements Span {
17
+ public attributes: Record<string, AttributeValue> = {};
18
+
19
+ spanContext(): SpanContext {
20
+ throw new Error("Method not implemented.");
21
+ }
22
+ setAttribute(key: string, value: AttributeValue): this {
23
+ this.attributes[key] = value;
24
+ return this;
25
+ }
26
+ setAttributes(attributes: SpanAttributes): this {
27
+ throw new Error("Method not implemented.");
28
+ }
29
+ addEvent(
30
+ name: string,
31
+ attributesOrStartTime?: SpanAttributes | TimeInput,
32
+ startTime?: TimeInput,
33
+ ): this {
34
+ throw new Error("Method not implemented.");
35
+ }
36
+ addLink(link: Link): this {
37
+ throw new Error("Method not implemented.");
38
+ }
39
+ addLinks(links: Link[]): this {
40
+ throw new Error("Method not implemented.");
41
+ }
42
+ setStatus(status: SpanStatus): this {
43
+ throw new Error("Method not implemented.");
44
+ }
45
+ updateName(name: string): this {
46
+ throw new Error("Method not implemented.");
47
+ }
48
+ end(endTime?: TimeInput): void {
49
+ throw new Error("Method not implemented.");
50
+ }
51
+ isRecording(): boolean {
52
+ throw new Error("Method not implemented.");
53
+ }
54
+ recordException(exception: Exception, time?: TimeInput): void {
55
+ throw new Error("Method not implemented.");
56
+ }
57
+ }
58
+
59
+ describe("EnrichSpanProcessor", () => {
60
+ it("should set static attributes on span", () => {
61
+ const spanAttributes = {
62
+ key1: "value1",
63
+ key2: 123,
64
+ };
65
+ const processor = new EnrichSpanProcessor(spanAttributes);
66
+ const mockSpan = new MockSpan();
67
+ const mockContext = {} as Context;
68
+
69
+ processor.onStart(mockSpan, mockContext);
70
+
71
+ expect(mockSpan.attributes["key1"]).toBe("value1");
72
+ expect(mockSpan.attributes["key2"]).toBe(123);
73
+ });
74
+
75
+ it("should set dynamic attributes on span", () => {
76
+ const spanAttributes = {
77
+ dynamicKey: () => "dynamicValue",
78
+ };
79
+ const processor = new EnrichSpanProcessor(spanAttributes);
80
+ const mockSpan = new MockSpan();
81
+ const mockContext = {} as Context;
82
+
83
+ processor.onStart(mockSpan, mockContext);
84
+
85
+ expect(mockSpan.attributes["dynamicKey"]).toBe("dynamicValue");
86
+ });
87
+
88
+ it("should not set attributes if none are provided", () => {
89
+ const processor = new EnrichSpanProcessor();
90
+ const mockSpan = new MockSpan();
91
+ const mockContext = {} as Context;
92
+
93
+ processor.onStart(mockSpan, mockContext);
94
+
95
+ expect(mockSpan.attributes["key1"]).toBeUndefined();
96
+ });
97
+
98
+ it("default method, should maintain default behaviour", async () => {
99
+ const processor = new EnrichSpanProcessor();
100
+
101
+ expect(processor.onEnd(null!)).toBeUndefined();
102
+ expect(await processor.shutdown()).toBeUndefined();
103
+ expect(await processor.forceFlush()).toBeUndefined();
104
+ });
105
+ });
@@ -12,6 +12,15 @@ describe("instrumentNode", () => {
12
12
  serviceName: "custom-service",
13
13
  collectorUrl: "http://custom-collector.com",
14
14
  protocol: "grpc",
15
+ resourceAttributes: {
16
+ "team.infra.cluster": "dev-01",
17
+ "team.infra.pod": "01",
18
+ "team.service.type": "fastify",
19
+ },
20
+ spanAttributes: {
21
+ "signal.namespace": "example",
22
+ "signal.number": () => "callback",
23
+ },
15
24
  };
16
25
 
17
26
  const buildNodeInstrumentation = await import(
@@ -23,6 +23,27 @@ describe("instrumentation integration test", () => {
23
23
  ) {
24
24
  if (parsedLine["attributes"]["http.target"] === "/api/dummy") {
25
25
  dummy_traces_counter++;
26
+
27
+ // verify global sdk span resource
28
+ assert.equal(
29
+ parsedLine["resource_attributes"]["team.infra.pod"],
30
+ "01",
31
+ );
32
+ assert.equal(
33
+ parsedLine["resource_attributes"]["team.service.type"],
34
+ "fastify",
35
+ );
36
+
37
+ // verify global sdk span attributes
38
+ assert.equal(parsedLine["attributes"]["signal.namespace"], "example");
39
+
40
+ // verify runtime custom span inside dev application
41
+ assert.equal(parsedLine["attributes"]["business.info"], "dummy");
42
+ assert.equal(
43
+ parsedLine["attributes"]["business.request_type"],
44
+ "application/json",
45
+ );
46
+
26
47
  continue;
27
48
  }
28
49
  if (parsedLine["attributes"]["http.target"] === "/api/health") {