@lokalise/opentelemetry-fastify-bootstrap 3.0.0 → 3.1.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 +28 -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 +15 -0
- package/dist/index.js +29 -2
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -66,6 +66,7 @@ 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). |
|
|
69
70
|
|
|
70
71
|
### Debugging with Console Spans
|
|
71
72
|
|
|
@@ -81,6 +82,32 @@ initOpenTelemetry({
|
|
|
81
82
|
|
|
82
83
|
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
84
|
|
|
85
|
+
### Joining a Datadog inferred-service entity
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
|
|
89
|
+
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.
|
|
90
|
+
|
|
91
|
+
Pass `dbNamespaceBySystem` to add `db.namespace` based on `db.system`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
initOpenTelemetry({
|
|
95
|
+
dbNamespaceBySystem: { elasticsearch: 'lokalise' },
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
For every span where `db.system: elasticsearch`, the exported payload gains:
|
|
100
|
+
|
|
101
|
+
- `db.namespace: lokalise` — the OTel-canonical, vendor-neutral attribute.
|
|
102
|
+
|
|
103
|
+
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).
|
|
104
|
+
|
|
105
|
+
#### Why an exporter, not a span processor
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
`DbNamespaceSpanExporter` is exported from the package if you want to wrap an exporter directly.
|
|
110
|
+
|
|
84
111
|
### Adding Custom Span Processors
|
|
85
112
|
|
|
86
113
|
You can integrate additional span processors to send telemetry data to multiple destinations or add custom processing logic.
|
|
@@ -107,6 +134,7 @@ initOpenTelemetry({
|
|
|
107
134
|
- Configurable path filtering
|
|
108
135
|
- Optional console span exporter for debugging
|
|
109
136
|
- Support for custom span processors
|
|
137
|
+
- Optional `db.namespace` enrichment of the Datadog export payload to join inferred-service entities
|
|
110
138
|
- Graceful shutdown support
|
|
111
139
|
|
|
112
140
|
## 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,20 @@ export interface OpenTelemetryOptions {
|
|
|
15
16
|
* Additional span processors to register with the OpenTelemetry SDK.
|
|
16
17
|
*/
|
|
17
18
|
spanProcessors?: SpanProcessor[];
|
|
19
|
+
/**
|
|
20
|
+
* Maps OTel `db.system` values to the `db.namespace` to report for them. When
|
|
21
|
+
* set, the Datadog-bound trace exporter is wrapped so matching outbound DB
|
|
22
|
+
* spans carry `db.namespace` in the export payload, joining them to Datadog's
|
|
23
|
+
* existing inferred-service entity for the cluster. Only the export payload is
|
|
24
|
+
* shaped — the shared span other processors/exporters see is left untouched.
|
|
25
|
+
* See {@link DbNamespaceSpanExporter} for the full mechanics.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* dbNamespaceBySystem: { elasticsearch: 'lokalise' }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
dbNamespaceBySystem?: Readonly<Record<string, string>>;
|
|
18
33
|
}
|
|
19
34
|
/**
|
|
20
35
|
* Initialize OpenTelemetry instrumentation.
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ 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
|
+
export { DbNamespaceSpanExporter, } from "./dbNamespaceSpanExporter.js";
|
|
6
8
|
function createLogEntry(level, msg, data) {
|
|
7
9
|
return {
|
|
8
10
|
level,
|
|
@@ -40,6 +42,18 @@ const logger = {
|
|
|
40
42
|
debug: (msgOrData, msg) => log('debug', msgOrData, msg),
|
|
41
43
|
};
|
|
42
44
|
const DEFAULT_SKIPPED_PATHS = ['/health', '/metrics', '/'];
|
|
45
|
+
function resolveDbNamespaceBySystem(dbNamespaceBySystem) {
|
|
46
|
+
if (!dbNamespaceBySystem)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (Object.keys(dbNamespaceBySystem).length === 0) {
|
|
49
|
+
logger.warn('[OTEL] dbNamespaceBySystem was provided but contains no entries; db.namespace enrichment will not be enabled');
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
// Validate eagerly so a misconfigured mapping throws in every environment
|
|
53
|
+
// (dev, CI), not only at production startup when the exporter is built.
|
|
54
|
+
assertValidDbNamespaceBySystem(dbNamespaceBySystem);
|
|
55
|
+
return dbNamespaceBySystem;
|
|
56
|
+
}
|
|
43
57
|
let isInstrumentationRegistered = false;
|
|
44
58
|
let sdk;
|
|
45
59
|
/**
|
|
@@ -61,7 +75,7 @@ let sdk;
|
|
|
61
75
|
* ```
|
|
62
76
|
*/
|
|
63
77
|
export function initOpenTelemetry(options = {}) {
|
|
64
|
-
const { skippedPaths = DEFAULT_SKIPPED_PATHS, consoleSpans = false, spanProcessors = [], } = options;
|
|
78
|
+
const { skippedPaths = DEFAULT_SKIPPED_PATHS, consoleSpans = false, spanProcessors = [], dbNamespaceBySystem, } = options;
|
|
65
79
|
logger.info('[OTEL] initOpenTelemetry called');
|
|
66
80
|
const isOpenTelemetryEnabled = process.env.NODE_ENV !== 'test' && process.env.OTEL_ENABLED?.toLowerCase() === 'true';
|
|
67
81
|
logger.info({
|
|
@@ -71,17 +85,30 @@ export function initOpenTelemetry(options = {}) {
|
|
|
71
85
|
skippedPaths,
|
|
72
86
|
consoleSpans,
|
|
73
87
|
additionalSpanProcessorsCount: spanProcessors.length,
|
|
88
|
+
dbNamespaceSystemsConfigured: dbNamespaceBySystem ? Object.keys(dbNamespaceBySystem) : [],
|
|
74
89
|
}, '[OTEL] Configuration');
|
|
90
|
+
// Validated outside the enabled gate so a misconfigured mapping throws in
|
|
91
|
+
// every environment (dev, CI) instead of only at production startup.
|
|
92
|
+
const validatedDbNamespaceBySystem = resolveDbNamespaceBySystem(dbNamespaceBySystem);
|
|
75
93
|
if (isOpenTelemetryEnabled && !isInstrumentationRegistered) {
|
|
76
94
|
logger.info('[OTEL] Initializing OpenTelemetry SDK...');
|
|
77
95
|
// Configure the OTLP trace exporter
|
|
78
96
|
const exporterUrl = process.env.OTEL_EXPORTER_URL || 'grpc://localhost:4317';
|
|
79
97
|
logger.info({ exporterUrl }, '[OTEL] Configuring trace exporter');
|
|
80
|
-
const
|
|
98
|
+
const otlpExporter = new OTLPTraceExporterGrpc({
|
|
81
99
|
// optional - url default value is http://localhost:4318/v1/traces (http)
|
|
82
100
|
// or grpc://localhost:4317/opentelemetry.proto.collector.trace.v1.TraceService/Export (grpc)
|
|
83
101
|
url: exporterUrl,
|
|
84
102
|
});
|
|
103
|
+
// Wrap ONLY the Datadog-bound exporter: db.namespace is added to its export
|
|
104
|
+
// payload, not to the shared span, so console/user processors and any other
|
|
105
|
+
// exporter still see the unmodified span. No ordering constraints — the
|
|
106
|
+
// transform happens entirely inside this exporter's own export().
|
|
107
|
+
const traceExporter = validatedDbNamespaceBySystem
|
|
108
|
+
? new DbNamespaceSpanExporter(otlpExporter, {
|
|
109
|
+
dbNamespaceBySystem: validatedDbNamespaceBySystem,
|
|
110
|
+
})
|
|
111
|
+
: otlpExporter;
|
|
85
112
|
const allSpanProcessors = [
|
|
86
113
|
new BatchSpanProcessor(traceExporter),
|
|
87
114
|
...spanProcessors,
|
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;AAC1D,OAAO,EAAE,2BAA2B,EAAE,MAAM,2CAA2C,CAAA;AACvF,OAAO,EAAE,iBAAiB,IAAI,qBAAqB,EAAE,MAAM,yCAAyC,CAAA;AACpG,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACjD,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,GAEpB,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lokalise/opentelemetry-fastify-bootstrap",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.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
55
|
"typescript": "6.0.3",
|
|
57
|
-
"vitest": "^4.1.
|
|
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",
|