@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 +43 -0
- package/dist/dbNamespaceSpanExporter.d.ts +63 -0
- package/dist/dbNamespaceSpanExporter.js +114 -0
- package/dist/dbNamespaceSpanExporter.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +73 -16
- package/dist/index.js.map +1 -1
- package/dist/streamSpanFilteringExporter.d.ts +35 -0
- package/dist/streamSpanFilteringExporter.js +48 -0
- package/dist/streamSpanFilteringExporter.js.map +1 -0
- package/package.json +10 -9
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 =
|
|
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
|
-
|
|
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":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAA;
|
|
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
|
+
"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.
|
|
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.
|
|
46
|
-
"@
|
|
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.
|
|
53
|
-
"@vitest/coverage-v8": "^4.1.
|
|
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": "
|
|
57
|
-
"vitest": "^4.1.
|
|
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",
|