@lokalise/opentelemetry-fastify-bootstrap 3.0.0 → 4.0.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/README.md CHANGED
@@ -66,6 +66,8 @@ initOpenTelemetry()
66
66
  | `skippedPaths` | `string[]` | `['/health', '/metrics', '/']` | Paths to exclude from tracing |
67
67
  | `consoleSpans` | `boolean` | `false` | Enable console span exporter for debugging |
68
68
  | `spanProcessors` | `SpanProcessor[]` | `[]` | Additional span processors to register |
69
+ | `dbNamespaceBySystem` | `Record<string, string>` | `undefined` | Maps `db.system` values to the `db.namespace` to report for them. When set, the Datadog-bound trace exporter is wrapped so matching outbound DB spans carry `db.namespace` in the export payload, joining them to Datadog's existing inferred-service entity for the cluster. The shared span is left untouched. See [Joining a Datadog inferred-service entity](#joining-a-datadog-inferred-service-entity). |
70
+ | `skipStreamEndpoints` | `boolean` | `true` | Exclude streaming/SSE server spans from exported traces (detected via the `Accept: text/event-stream` request header), so their long keep-alive durations don't skew latency metrics/SLOs. See [Excluding streaming / SSE endpoints](#excluding-streaming--sse-endpoints). |
69
71
 
70
72
  ### Debugging with Console Spans
71
73
 
@@ -81,6 +83,46 @@ initOpenTelemetry({
81
83
 
82
84
  When enabled, spans are printed to the console in addition to being sent to the OTLP exporter. This is useful for verifying that instrumentation is working correctly without needing to run a full observability stack.
83
85
 
86
+ ### Joining a Datadog inferred-service entity
87
+
88
+ Datadog's APM service catalog auto-creates inferred-service entities for downstream clusters from peer tags such as `peer.db.system` and `peer.db.name`, deriving `peer.db.name` from the OTel-canonical `db.namespace` (see the [peer-tags precedence list](https://docs.datadoghq.com/tracing/services/inferred_services/?tab=agentv7600#peer-tags) for the authoritative set). When no usable peer tag is present, Datadog falls back to `peer.hostname` — and downstream clusters reached by raw IP (e.g. an Elasticsearch cluster on EC2 addressed by IP) then surface as the synthetic `blocked-ip-address` service in the dependency map.
89
+
90
+ Some instrumentations don't set `db.namespace`. Notably the Node.js `@elastic/transport` (v8 Elasticsearch client) sets `db.system: elasticsearch` but never `db.namespace`, so outbound ES calls don't join the existing cluster entity.
91
+
92
+ Pass `dbNamespaceBySystem` to add `db.namespace` based on `db.system`:
93
+
94
+ ```ts
95
+ initOpenTelemetry({
96
+ dbNamespaceBySystem: { elasticsearch: 'lokalise' },
97
+ })
98
+ ```
99
+
100
+ For every span where `db.system: elasticsearch`, the exported payload gains:
101
+
102
+ - `db.namespace: lokalise` — the OTel-canonical, vendor-neutral attribute.
103
+
104
+ Datadog adds the `peer.*` prefix itself, deriving `peer.db.name` from `db.namespace` and `peer.db.system` from `db.system`, so we only set the short-form attribute and never write `peer.*` tags directly. See [Datadog peer tags](https://docs.datadoghq.com/tracing/services/inferred_services/?tab=agentv7600#peer-tags).
105
+
106
+ #### Why an exporter, not a span processor
107
+
108
+ A constant such as `lokalise` is a Datadog entity-keying heuristic, not the span's true OTel `db.namespace`. Rather than mutate the single `ReadableSpan` shared by every processor and exporter — which would make any other consumer (a different dashboard, OTel-native tooling) read `lokalise` as the real namespace — `dbNamespaceBySystem` wraps **only** the Datadog-bound exporter. The attribute is added to that exporter's own payload via a non-mutating view of the span; the original is never written to, so it can't be broken by a future SDK that freezes span attributes, and every other consumer sees the unmodified span. An existing non-empty `db.namespace` is never replaced.
109
+
110
+ `DbNamespaceSpanExporter` is exported from the package if you want to wrap an exporter directly.
111
+
112
+ ### Excluding streaming / SSE endpoints
113
+
114
+ An SSE (Server-Sent Events) or other long-lived streaming response keeps the HTTP request open for the whole lifetime of the stream, so the auto-instrumented server span's duration reflects the keep-alive window (often minutes) rather than the time-to-first-byte. Any latency metric or SLO derived from that span's duration is then skewed by those multi-minute values.
115
+
116
+ This filtering is enabled by default (`skipStreamEndpoints: true`), so those spans are dropped from the exported traces out of the box. Opt out by setting it to `false`:
117
+
118
+ ```ts
119
+ initOpenTelemetry({
120
+ skipStreamEndpoints: false,
121
+ })
122
+ ```
123
+
124
+ Streaming requests are detected by the `Accept: text/event-stream` request header — the header browser `EventSource` clients are required to send, and the same signal SSE content-negotiation keys on. Matching spans are tagged and dropped before export, generically for every streaming endpoint (no per-route list to maintain). The span still starts, so trace context still propagates to child spans, and it stays visible to console / user-supplied span processors — only the exported traces omit it.
125
+
84
126
  ### Adding Custom Span Processors
85
127
 
86
128
  You can integrate additional span processors to send telemetry data to multiple destinations or add custom processing logic.
@@ -107,6 +149,7 @@ initOpenTelemetry({
107
149
  - Configurable path filtering
108
150
  - Optional console span exporter for debugging
109
151
  - Support for custom span processors
152
+ - Optional `db.namespace` enrichment of the Datadog export payload to join inferred-service entities
110
153
  - Graceful shutdown support
111
154
 
112
155
  ## Graceful Shutdown
@@ -0,0 +1,63 @@
1
+ import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
2
+ export interface DbNamespaceSpanExporterOptions {
3
+ /**
4
+ * Maps an OTel `db.system` value to the `db.namespace` to add on matching
5
+ * spans in the export payload. Example: `{ elasticsearch: 'lokalise' }`. See
6
+ * {@link DbNamespaceSpanExporter} for why and how.
7
+ *
8
+ * Keys and values must be non-empty strings; the constructor throws on
9
+ * misconfigured entries so typos surface at startup instead of silently
10
+ * no-op'ing in production.
11
+ *
12
+ * @see https://opentelemetry.io/docs/specs/semconv/database/database-spans/ for well-known `db.system` values
13
+ */
14
+ dbNamespaceBySystem: Readonly<Record<string, string>>;
15
+ }
16
+ /**
17
+ * A `SpanExporter` decorator that adds `db.namespace` to outbound DB spans in
18
+ * the export payload — based on the OTel `db.system` value — then delegates to
19
+ * the wrapped exporter. It exists so Datadog's APM catalog joins those spans to
20
+ * an existing inferred-service entity for the cluster.
21
+ *
22
+ * Why (per Datadog inferred-services behavior as of 2026-06): a cluster reached
23
+ * by raw IP can't be classified from `peer.hostname` and gets bucketed into the
24
+ * synthetic `blocked-ip-address` service. Datadog instead keys entities on the
25
+ * `peer.db.*` tags, which it derives from short-form OTel attributes
26
+ * (`peer.db.name` from `db.namespace`, `peer.db.system` from `db.system`). The
27
+ * Node `@elastic/transport` v8 client sets `db.system` but not `db.namespace`;
28
+ * this fills that one gap. An existing non-empty `db.namespace` is never
29
+ * replaced.
30
+ *
31
+ * Why an exporter rather than a span processor: a constant like `lokalise` is a
32
+ * Datadog entity-keying heuristic, not the span's true OTel `db.namespace`. A
33
+ * mutating processor would write it into the one `ReadableSpan` shared by every
34
+ * processor and exporter, so any other consumer (a different dashboard, OTel-
35
+ * native tooling) would read it as the real namespace. An exporter is instead
36
+ * contractually allowed to shape its own export payload, so we wrap only the
37
+ * Datadog-bound exporter and leave the shared span untouched. This also sheds
38
+ * the fragility of mutating SDK-internal state: the original object is never
39
+ * written to (we return a `Proxy` overriding only `attributes`), so a future
40
+ * SDK that freezes span attributes cannot break this.
41
+ */
42
+ export declare class DbNamespaceSpanExporter implements SpanExporter {
43
+ private readonly delegate;
44
+ private readonly dbNamespaceBySystem;
45
+ private readonly loggedSystems;
46
+ constructor(delegate: SpanExporter, options: DbNamespaceSpanExporterOptions);
47
+ export(spans: ReadableSpan[], resultCallback: Parameters<SpanExporter['export']>[1]): void;
48
+ shutdown(): Promise<void>;
49
+ forceFlush(): Promise<void>;
50
+ /**
51
+ * Returns the span unchanged unless it is an unmapped/already-namespaced DB
52
+ * span; otherwise returns a non-mutating view with `db.namespace` added.
53
+ */
54
+ private withDbNamespace;
55
+ }
56
+ /**
57
+ * Validates a `db.system` -> `db.namespace` mapping and copies it into a `Map`
58
+ * so later mutation of the caller's object can't bypass validation and lookups
59
+ * can't hit `Object.prototype`. Throws on any empty key or non-empty-string
60
+ * value so typos fail at startup. Exported so the bootstrap can fail fast in
61
+ * every environment, not only when the exporter is constructed.
62
+ */
63
+ export declare function assertValidDbNamespaceBySystem(dbNamespaceBySystem: Readonly<Record<string, string>>): Map<string, string>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * A `SpanExporter` decorator that adds `db.namespace` to outbound DB spans in
3
+ * the export payload — based on the OTel `db.system` value — then delegates to
4
+ * the wrapped exporter. It exists so Datadog's APM catalog joins those spans to
5
+ * an existing inferred-service entity for the cluster.
6
+ *
7
+ * Why (per Datadog inferred-services behavior as of 2026-06): a cluster reached
8
+ * by raw IP can't be classified from `peer.hostname` and gets bucketed into the
9
+ * synthetic `blocked-ip-address` service. Datadog instead keys entities on the
10
+ * `peer.db.*` tags, which it derives from short-form OTel attributes
11
+ * (`peer.db.name` from `db.namespace`, `peer.db.system` from `db.system`). The
12
+ * Node `@elastic/transport` v8 client sets `db.system` but not `db.namespace`;
13
+ * this fills that one gap. An existing non-empty `db.namespace` is never
14
+ * replaced.
15
+ *
16
+ * Why an exporter rather than a span processor: a constant like `lokalise` is a
17
+ * Datadog entity-keying heuristic, not the span's true OTel `db.namespace`. A
18
+ * mutating processor would write it into the one `ReadableSpan` shared by every
19
+ * processor and exporter, so any other consumer (a different dashboard, OTel-
20
+ * native tooling) would read it as the real namespace. An exporter is instead
21
+ * contractually allowed to shape its own export payload, so we wrap only the
22
+ * Datadog-bound exporter and leave the shared span untouched. This also sheds
23
+ * the fragility of mutating SDK-internal state: the original object is never
24
+ * written to (we return a `Proxy` overriding only `attributes`), so a future
25
+ * SDK that freezes span attributes cannot break this.
26
+ */
27
+ export class DbNamespaceSpanExporter {
28
+ delegate;
29
+ dbNamespaceBySystem;
30
+ loggedSystems = new Set();
31
+ constructor(delegate, options) {
32
+ this.delegate = delegate;
33
+ this.dbNamespaceBySystem = assertValidDbNamespaceBySystem(options.dbNamespaceBySystem);
34
+ }
35
+ export(spans, resultCallback) {
36
+ this.delegate.export(spans.map((span) => this.withDbNamespace(span)), resultCallback);
37
+ }
38
+ shutdown() {
39
+ return this.delegate.shutdown();
40
+ }
41
+ forceFlush() {
42
+ return this.delegate.forceFlush?.() ?? Promise.resolve();
43
+ }
44
+ /**
45
+ * Returns the span unchanged unless it is an unmapped/already-namespaced DB
46
+ * span; otherwise returns a non-mutating view with `db.namespace` added.
47
+ */
48
+ withDbNamespace(span) {
49
+ const attrs = span.attributes;
50
+ const dbSystem = attrs['db.system'];
51
+ if (typeof dbSystem !== 'string' || dbSystem.length === 0)
52
+ return span;
53
+ const mapped = this.dbNamespaceBySystem.get(dbSystem);
54
+ if (!mapped)
55
+ return span;
56
+ const existing = attrs['db.namespace'];
57
+ if (typeof existing === 'string' && existing.length > 0)
58
+ return span;
59
+ // The source span is shared with every other processor/exporter, so we must
60
+ // not write to it. Return a Proxy that overrides only `attributes`; all
61
+ // other reads (spanContext(), resource, status, …) delegate to the original
62
+ // with `this` bound to it.
63
+ const attributes = { ...attrs, 'db.namespace': mapped };
64
+ const view = new Proxy(span, {
65
+ get(target, prop, _receiver) {
66
+ if (prop === 'attributes')
67
+ return attributes;
68
+ const value = Reflect.get(target, prop, target);
69
+ return typeof value === 'function' ? value.bind(target) : value;
70
+ },
71
+ });
72
+ if (!this.loggedSystems.has(dbSystem)) {
73
+ this.loggedSystems.add(dbSystem);
74
+ logEntry('info', '[OTEL] DbNamespaceSpanExporter: added db.namespace to first matching span', {
75
+ dbSystem,
76
+ dbNamespace: mapped,
77
+ });
78
+ }
79
+ return view;
80
+ }
81
+ }
82
+ /**
83
+ * Validates a `db.system` -> `db.namespace` mapping and copies it into a `Map`
84
+ * so later mutation of the caller's object can't bypass validation and lookups
85
+ * can't hit `Object.prototype`. Throws on any empty key or non-empty-string
86
+ * value so typos fail at startup. Exported so the bootstrap can fail fast in
87
+ * every environment, not only when the exporter is constructed.
88
+ */
89
+ export function assertValidDbNamespaceBySystem(dbNamespaceBySystem) {
90
+ const map = new Map();
91
+ for (const [system, value] of Object.entries(dbNamespaceBySystem)) {
92
+ if (system.length === 0 || typeof value !== 'string' || value.length === 0) {
93
+ throw new Error(`DbNamespaceSpanExporter: dbNamespaceBySystem[${JSON.stringify(system)}] must map a non-empty db.system key to a non-empty db.namespace string`);
94
+ }
95
+ map.set(system, value);
96
+ }
97
+ return map;
98
+ }
99
+ // Mirrors the JSON log shape used by `initOpenTelemetry` so all bootstrap
100
+ // output is greppable with the same parser. Kept local to avoid coupling this
101
+ // file to a logger module the package doesn't otherwise need.
102
+ function logEntry(level, msg, data) {
103
+ const entry = { level, time: Date.now(), msg, ...data };
104
+ const output = JSON.stringify(entry);
105
+ if (level === 'error') {
106
+ // biome-ignore lint/suspicious/noConsole: bootstrap logger
107
+ console.error(output);
108
+ }
109
+ else {
110
+ // biome-ignore lint/suspicious/noConsole: bootstrap logger
111
+ console.log(output);
112
+ }
113
+ }
114
+ //# sourceMappingURL=dbNamespaceSpanExporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dbNamespaceSpanExporter.js","sourceRoot":"","sources":["../src/dbNamespaceSpanExporter.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type SpanProcessor } from '@opentelemetry/sdk-trace-base';
2
+ export { DbNamespaceSpanExporter, type DbNamespaceSpanExporterOptions, } from './dbNamespaceSpanExporter.ts';
2
3
  export interface OpenTelemetryOptions {
3
4
  /**
4
5
  * Paths to exclude from tracing.
@@ -15,6 +16,38 @@ export interface OpenTelemetryOptions {
15
16
  * Additional span processors to register with the OpenTelemetry SDK.
16
17
  */
17
18
  spanProcessors?: SpanProcessor[];
19
+ /**
20
+ * When true, HTTP server spans for streaming / SSE responses are excluded
21
+ * from the exported traces — and therefore from any latency metric or SLO
22
+ * derived from their span duration. An SSE connection stays open for the
23
+ * whole stream lifetime, so its server span's duration reflects the keep-alive
24
+ * window (often minutes), not the time-to-first-byte, which otherwise skews
25
+ * those metrics.
26
+ *
27
+ * Streaming requests are detected by the `Accept: text/event-stream` request
28
+ * header (what browser `EventSource` clients are required to send, and the
29
+ * same signal SSE content-negotiation keys on); matching spans are tagged and
30
+ * dropped before export. The span still starts (so trace context propagates
31
+ * to child spans) and stays visible to console / user-supplied span
32
+ * processors.
33
+ *
34
+ * @default true
35
+ */
36
+ skipStreamEndpoints?: boolean;
37
+ /**
38
+ * Maps OTel `db.system` values to the `db.namespace` to report for them. When
39
+ * set, the Datadog-bound trace exporter is wrapped so matching outbound DB
40
+ * spans carry `db.namespace` in the export payload, joining them to Datadog's
41
+ * existing inferred-service entity for the cluster. Only the export payload is
42
+ * shaped — the shared span other processors/exporters see is left untouched.
43
+ * See {@link DbNamespaceSpanExporter} for the full mechanics.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * dbNamespaceBySystem: { elasticsearch: 'lokalise' }
48
+ * ```
49
+ */
50
+ dbNamespaceBySystem?: Readonly<Record<string, string>>;
18
51
  }
19
52
  /**
20
53
  * Initialize OpenTelemetry instrumentation.
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentation
3
3
  import { OTLPTraceExporter as OTLPTraceExporterGrpc } from '@opentelemetry/exporter-trace-otlp-grpc';
4
4
  import { NodeSDK } from '@opentelemetry/sdk-node';
5
5
  import { BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor, } from '@opentelemetry/sdk-trace-base';
6
+ import { assertValidDbNamespaceBySystem, DbNamespaceSpanExporter, } from './dbNamespaceSpanExporter.js';
7
+ import { STREAM_ENDPOINT_SPAN_ATTRIBUTE, StreamSpanFilteringExporter, } from './streamSpanFilteringExporter.js';
8
+ export { DbNamespaceSpanExporter, } from './dbNamespaceSpanExporter.js';
6
9
  function createLogEntry(level, msg, data) {
7
10
  return {
8
11
  level,
@@ -40,6 +43,68 @@ const logger = {
40
43
  debug: (msgOrData, msg) => log('debug', msgOrData, msg),
41
44
  };
42
45
  const DEFAULT_SKIPPED_PATHS = ['/health', '/metrics', '/'];
46
+ /**
47
+ * Marks the request's HTTP server span as a streaming/SSE endpoint when the
48
+ * client negotiated SSE via `Accept: text/event-stream`. Runs as the
49
+ * FastifyOtel `requestHook`, so the span already exists; the marker is later
50
+ * consumed by {@link StreamSpanFilteringExporter} to drop the span before it is
51
+ * exported. Browser `EventSource` clients are required to send this header, and
52
+ * it is the same signal SSE content-negotiation keys on.
53
+ */
54
+ const markStreamEndpointSpan = (span, request) => {
55
+ const accept = request.headers.accept;
56
+ // Media types are case-insensitive (RFC 7231 §3.1.1.1), so normalize before matching.
57
+ if (typeof accept === 'string' && accept.toLowerCase().includes('text/event-stream')) {
58
+ span.setAttribute(STREAM_ENDPOINT_SPAN_ATTRIBUTE, true);
59
+ }
60
+ };
61
+ /**
62
+ * Builds the Fastify OpenTelemetry instrumentation.
63
+ */
64
+ const createFastifyOtelInstrumentation = (skippedPaths, skipStreamEndpoints) => new FastifyOtelInstrumentation({
65
+ registerOnInitialization: true,
66
+ ignorePaths: (req) => {
67
+ if (!req.url)
68
+ return false;
69
+ // Extract path without query string, normalize empty to '/'
70
+ const path = req.url.split('?')[0] || '/';
71
+ return skippedPaths.includes(path);
72
+ },
73
+ requestHook: skipStreamEndpoints ? markStreamEndpointSpan : undefined,
74
+ });
75
+ /**
76
+ * Builds the OTLP trace exporter, optionally wrapped to add `db.namespace`
77
+ * and/or to drop streaming/SSE spans. Extracted from {@link initOpenTelemetry}
78
+ * to keep its cognitive complexity in check. Each wrapper shapes only its own
79
+ * export payload, leaving the shared span (seen by console / user span
80
+ * processors) untouched.
81
+ */
82
+ const buildTraceExporter = (exporterUrl, validatedDbNamespaceBySystem, skipStreamEndpoints) => {
83
+ // Default url is grpc://localhost:4317 (see OTLPTraceExporter docs).
84
+ const otlpExporter = new OTLPTraceExporterGrpc({ url: exporterUrl });
85
+ // db.namespace is added to the export payload, not the shared span, so other
86
+ // processors/exporters still see the unmodified span.
87
+ const withDbNamespace = validatedDbNamespaceBySystem
88
+ ? new DbNamespaceSpanExporter(otlpExporter, {
89
+ dbNamespaceBySystem: validatedDbNamespaceBySystem,
90
+ })
91
+ : otlpExporter;
92
+ // Drop SSE/streaming server spans so their multi-minute keep-alive durations
93
+ // don't pollute span-duration latency metrics/SLOs.
94
+ return skipStreamEndpoints ? new StreamSpanFilteringExporter(withDbNamespace) : withDbNamespace;
95
+ };
96
+ function resolveDbNamespaceBySystem(dbNamespaceBySystem) {
97
+ if (!dbNamespaceBySystem)
98
+ return undefined;
99
+ if (Object.keys(dbNamespaceBySystem).length === 0) {
100
+ logger.warn('[OTEL] dbNamespaceBySystem was provided but contains no entries; db.namespace enrichment will not be enabled');
101
+ return undefined;
102
+ }
103
+ // Validate eagerly so a misconfigured mapping throws in every environment
104
+ // (dev, CI), not only at production startup when the exporter is built.
105
+ assertValidDbNamespaceBySystem(dbNamespaceBySystem);
106
+ return dbNamespaceBySystem;
107
+ }
43
108
  let isInstrumentationRegistered = false;
44
109
  let sdk;
45
110
  /**
@@ -61,7 +126,7 @@ let sdk;
61
126
  * ```
62
127
  */
63
128
  export function initOpenTelemetry(options = {}) {
64
- const { skippedPaths = DEFAULT_SKIPPED_PATHS, consoleSpans = false, spanProcessors = [], } = options;
129
+ const { skippedPaths = DEFAULT_SKIPPED_PATHS, consoleSpans = false, spanProcessors = [], dbNamespaceBySystem, skipStreamEndpoints = true, } = options;
65
130
  logger.info('[OTEL] initOpenTelemetry called');
66
131
  const isOpenTelemetryEnabled = process.env.NODE_ENV !== 'test' && process.env.OTEL_ENABLED?.toLowerCase() === 'true';
67
132
  logger.info({
@@ -71,17 +136,18 @@ export function initOpenTelemetry(options = {}) {
71
136
  skippedPaths,
72
137
  consoleSpans,
73
138
  additionalSpanProcessorsCount: spanProcessors.length,
139
+ dbNamespaceSystemsConfigured: dbNamespaceBySystem ? Object.keys(dbNamespaceBySystem) : [],
140
+ skipStreamEndpoints,
74
141
  }, '[OTEL] Configuration');
142
+ // Validated outside the enabled gate so a misconfigured mapping throws in
143
+ // every environment (dev, CI) instead of only at production startup.
144
+ const validatedDbNamespaceBySystem = resolveDbNamespaceBySystem(dbNamespaceBySystem);
75
145
  if (isOpenTelemetryEnabled && !isInstrumentationRegistered) {
76
146
  logger.info('[OTEL] Initializing OpenTelemetry SDK...');
77
147
  // Configure the OTLP trace exporter
78
148
  const exporterUrl = process.env.OTEL_EXPORTER_URL || 'grpc://localhost:4317';
79
149
  logger.info({ exporterUrl }, '[OTEL] Configuring trace exporter');
80
- const traceExporter = new OTLPTraceExporterGrpc({
81
- // optional - url default value is http://localhost:4318/v1/traces (http)
82
- // or grpc://localhost:4317/opentelemetry.proto.collector.trace.v1.TraceService/Export (grpc)
83
- url: exporterUrl,
84
- });
150
+ const traceExporter = buildTraceExporter(exporterUrl, validatedDbNamespaceBySystem, skipStreamEndpoints);
85
151
  const allSpanProcessors = [
86
152
  new BatchSpanProcessor(traceExporter),
87
153
  ...spanProcessors,
@@ -95,16 +161,7 @@ export function initOpenTelemetry(options = {}) {
95
161
  spanProcessors: allSpanProcessors,
96
162
  instrumentations: [
97
163
  getNodeAutoInstrumentations(),
98
- new FastifyOtelInstrumentation({
99
- registerOnInitialization: true,
100
- ignorePaths: (req) => {
101
- if (!req.url)
102
- return false;
103
- // Extract path without query string, normalize empty to '/'
104
- const path = req.url.split('?')[0] || '/';
105
- return skippedPaths.includes(path);
106
- },
107
- }),
164
+ createFastifyOtelInstrumentation(skippedPaths, skipStreamEndpoints),
108
165
  ],
109
166
  });
110
167
  sdk.start();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
2
+ type ExportResult = Parameters<Parameters<SpanExporter['export']>[1]>[0];
3
+ /**
4
+ * Attribute marker set on the HTTP server span of a streaming/SSE response.
5
+ * Set by this package's `requestHook` (keyed on the `Accept: text/event-stream`
6
+ * request header); {@link StreamSpanFilteringExporter} drops every span
7
+ * carrying it before those spans are exported. Internal to the package — not
8
+ * part of the public API surface.
9
+ */
10
+ export declare const STREAM_ENDPOINT_SPAN_ATTRIBUTE = "stream.endpoint";
11
+ /**
12
+ * A `SpanExporter` decorator that drops streaming/SSE server spans from the
13
+ * export payload, then delegates the rest to the wrapped exporter.
14
+ *
15
+ * Why: an SSE/streaming response keeps the HTTP request open for the whole
16
+ * lifetime of the stream (e.g. a 5-minute keep-alive), so the auto-instrumented
17
+ * server span's duration is the stream lifetime, not the time-to-first-byte.
18
+ * Any latency metric or SLO derived from that span's duration is then wrecked
19
+ * by those multi-minute values. Dropping the span before it is exported removes
20
+ * it from such metrics at the source — generically for every streaming
21
+ * endpoint, with no per-route exclusion list to maintain.
22
+ *
23
+ * Why an exporter rather than a span processor or sampler: it layers onto a
24
+ * single exporter, so console / other processors and exporters still observe
25
+ * the stream spans (useful for local debugging). The shared span is never
26
+ * mutated.
27
+ */
28
+ export declare class StreamSpanFilteringExporter implements SpanExporter {
29
+ private readonly delegate;
30
+ constructor(delegate: SpanExporter);
31
+ export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
32
+ shutdown(): Promise<void>;
33
+ forceFlush(): Promise<void>;
34
+ }
35
+ export {};
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Attribute marker set on the HTTP server span of a streaming/SSE response.
3
+ * Set by this package's `requestHook` (keyed on the `Accept: text/event-stream`
4
+ * request header); {@link StreamSpanFilteringExporter} drops every span
5
+ * carrying it before those spans are exported. Internal to the package — not
6
+ * part of the public API surface.
7
+ */
8
+ export const STREAM_ENDPOINT_SPAN_ATTRIBUTE = 'stream.endpoint';
9
+ /**
10
+ * A `SpanExporter` decorator that drops streaming/SSE server spans from the
11
+ * export payload, then delegates the rest to the wrapped exporter.
12
+ *
13
+ * Why: an SSE/streaming response keeps the HTTP request open for the whole
14
+ * lifetime of the stream (e.g. a 5-minute keep-alive), so the auto-instrumented
15
+ * server span's duration is the stream lifetime, not the time-to-first-byte.
16
+ * Any latency metric or SLO derived from that span's duration is then wrecked
17
+ * by those multi-minute values. Dropping the span before it is exported removes
18
+ * it from such metrics at the source — generically for every streaming
19
+ * endpoint, with no per-route exclusion list to maintain.
20
+ *
21
+ * Why an exporter rather than a span processor or sampler: it layers onto a
22
+ * single exporter, so console / other processors and exporters still observe
23
+ * the stream spans (useful for local debugging). The shared span is never
24
+ * mutated.
25
+ */
26
+ export class StreamSpanFilteringExporter {
27
+ delegate;
28
+ constructor(delegate) {
29
+ this.delegate = delegate;
30
+ }
31
+ export(spans, resultCallback) {
32
+ const kept = spans.filter((span) => span.attributes[STREAM_ENDPOINT_SPAN_ATTRIBUTE] !== true);
33
+ // Empty batch: some OTLP transports treat it as a timeout/error, so
34
+ // short-circuit with success (0 === ExportResultCode.SUCCESS).
35
+ if (kept.length === 0) {
36
+ resultCallback({ code: 0 });
37
+ return;
38
+ }
39
+ this.delegate.export(kept, resultCallback);
40
+ }
41
+ shutdown() {
42
+ return this.delegate.shutdown();
43
+ }
44
+ forceFlush() {
45
+ return this.delegate.forceFlush?.() ?? Promise.resolve();
46
+ }
47
+ }
48
+ //# sourceMappingURL=streamSpanFilteringExporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streamSpanFilteringExporter.js","sourceRoot":"","sources":["../src/streamSpanFilteringExporter.ts"],"names":[],"mappings":"AAKA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,iBAAiB,CAAA;AAE/D;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,2BAA2B;IACrB,QAAQ,CAAc;IAEvC,YAAY,QAAsB;QAChC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;IAED,MAAM,CAAC,KAAqB,EAAE,cAA8C;QAC1E,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,8BAA8B,CAAC,KAAK,IAAI,CAAC,CAAA;QAC7F,oEAAoE;QACpE,+DAA+D;QAC/D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,cAAc,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;YAC3B,OAAM;QACR,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,cAAc,CAAC,CAAA;IAC5C,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAA;IACjC,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,IAAI,OAAO,CAAC,OAAO,EAAE,CAAA;IAC1D,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokalise/opentelemetry-fastify-bootstrap",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {},
36
36
  "peerDependencies": {
37
- "@fastify/otel": "0.18.1",
37
+ "@fastify/otel": "0.19.0",
38
38
  "@opentelemetry/auto-instrumentations-node": "^0.76.0",
39
39
  "@opentelemetry/exporter-trace-otlp-grpc": "^0.218.0",
40
40
  "@opentelemetry/sdk-node": "^0.218.0",
@@ -42,19 +42,20 @@
42
42
  "fastify": "^5.0.0"
43
43
  },
44
44
  "devDependencies": {
45
- "@biomejs/biome": "^2.3.7",
46
- "@lokalise/biome-config": "^3.1.0",
47
- "@lokalise/tsconfig": "^4.0.0",
45
+ "@biomejs/biome": "^2.5.0",
46
+ "@opentelemetry/api": "^1.9.1",
48
47
  "@opentelemetry/auto-instrumentations-node": "^0.76.0",
49
48
  "@opentelemetry/exporter-trace-otlp-grpc": "^0.218.0",
50
49
  "@opentelemetry/sdk-node": "^0.218.0",
51
50
  "@opentelemetry/sdk-trace-base": "^2.7.1",
52
- "@fastify/otel": "0.18.1",
53
- "@vitest/coverage-v8": "^4.1.7",
51
+ "@fastify/otel": "0.19.0",
52
+ "@vitest/coverage-v8": "^4.1.9",
54
53
  "fastify": "^5.7.5",
55
54
  "rimraf": "^6.1.2",
56
- "typescript": "6.0.3",
57
- "vitest": "^4.1.7"
55
+ "typescript": "7.0.2",
56
+ "vitest": "^4.1.9",
57
+ "@lokalise/biome-config": "^3.1.1",
58
+ "@lokalise/tsconfig": "^5.0.0"
58
59
  },
59
60
  "scripts": {
60
61
  "build": "rimraf dist && tsc --project tsconfig.build.json",