@vercube/telemetry 1.3.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.
@@ -0,0 +1,197 @@
1
+ import { ROOT_CONTEXT, TraceFlags, createTraceState, isSpanContextValid, trace } from "@opentelemetry/api";
2
+ //#region src/Common/Propagation.ts
3
+ /** Header carrying the sampled parent span, per W3C Trace Context. */
4
+ const TRACEPARENT_HEADER = "traceparent";
5
+ /** Header carrying vendor-specific trace state, per W3C Trace Context. */
6
+ const TRACESTATE_HEADER = "tracestate";
7
+ /**
8
+ * `version-traceid-spanid-flags`, with anything after the flags tolerated so
9
+ * that a future version of the spec still parses as version `00` would.
10
+ */
11
+ const TRACEPARENT_REGEX = /^([\da-f]{2})-([\da-f]{32})-([\da-f]{16})-([\da-f]{2})(-.*)?$/;
12
+ /** All-zero ids are explicitly invalid. */
13
+ const INVALID_TRACE_ID = "0".repeat(32);
14
+ const INVALID_SPAN_ID = "0".repeat(16);
15
+ /**
16
+ * W3C Trace Context propagator.
17
+ *
18
+ * The reference implementation lives in `@opentelemetry/core`, which is an SDK
19
+ * package. Propagation has to work with nothing but `@opentelemetry/api`
20
+ * installed - a service that only forwards trace context should not need an
21
+ * exporter - so the format is implemented here instead. It is two headers and a
22
+ * fixed-width string.
23
+ */
24
+ var W3CTraceContextPropagator = class {
25
+ /**
26
+ * Writes the active span context into the carrier.
27
+ *
28
+ * @param context - The context to read the active span from
29
+ * @param carrier - The carrier to write into
30
+ * @param setter - Writer for the carrier
31
+ */
32
+ inject(context, carrier, setter) {
33
+ const spanContext = trace.getSpanContext(context);
34
+ if (!spanContext || !isSpanContextValid(spanContext)) return;
35
+ const flags = (spanContext.traceFlags ?? TraceFlags.NONE).toString(16).padStart(2, "0");
36
+ setter.set(carrier, TRACEPARENT_HEADER, `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`);
37
+ const state = spanContext.traceState?.serialize();
38
+ if (state) setter.set(carrier, TRACESTATE_HEADER, state);
39
+ }
40
+ /**
41
+ * Reads a remote span context out of the carrier.
42
+ *
43
+ * @param context - The context to extend
44
+ * @param carrier - The carrier to read from
45
+ * @param getter - Reader for the carrier
46
+ * @returns The context, with the remote span context attached when the headers were valid
47
+ */
48
+ extract(context, carrier, getter) {
49
+ const header = firstValue(getter.get(carrier, TRACEPARENT_HEADER));
50
+ if (header === void 0) return context;
51
+ const match = TRACEPARENT_REGEX.exec(header.trim());
52
+ if (!match) return context;
53
+ const [, version, traceId, spanId, flags] = match;
54
+ if (version === "ff" || traceId === INVALID_TRACE_ID || spanId === INVALID_SPAN_ID) return context;
55
+ const state = firstValue(getter.get(carrier, TRACESTATE_HEADER));
56
+ return trace.setSpanContext(context, {
57
+ traceId,
58
+ spanId,
59
+ traceFlags: Number.parseInt(flags, 16) & TraceFlags.SAMPLED,
60
+ isRemote: true,
61
+ traceState: state ? createTraceState(state) : void 0
62
+ });
63
+ }
64
+ /**
65
+ * The header names this propagator manages.
66
+ *
67
+ * @returns The list of header names
68
+ */
69
+ fields() {
70
+ return [TRACEPARENT_HEADER, TRACESTATE_HEADER];
71
+ }
72
+ };
73
+ /**
74
+ * Reads headers off a `Headers` object or a plain record.
75
+ */
76
+ const headersGetter = {
77
+ keys(carrier) {
78
+ if (carrier instanceof Headers) return [...carrier.keys()];
79
+ return carrier ? Object.keys(carrier) : [];
80
+ },
81
+ get(carrier, key) {
82
+ if (carrier instanceof Headers) return carrier.get(key) ?? void 0;
83
+ const value = carrier?.[key];
84
+ return typeof value === "string" ? value : void 0;
85
+ }
86
+ };
87
+ /**
88
+ * Writes headers to a `Headers` object or a plain record.
89
+ */
90
+ const headersSetter = { set(carrier, key, value) {
91
+ if (carrier instanceof Headers) {
92
+ carrier.set(key, value);
93
+ return;
94
+ }
95
+ if (carrier && typeof carrier === "object") carrier[key] = value;
96
+ } };
97
+ /**
98
+ * Normalizes the `string | string[] | undefined` a getter may return.
99
+ *
100
+ * @param value - The raw getter result
101
+ * @returns The first value, or undefined
102
+ */
103
+ function firstValue(value) {
104
+ if (Array.isArray(value)) return value[0];
105
+ return value ?? void 0;
106
+ }
107
+ //#endregion
108
+ //#region src/Context/VercubeContextManager.ts
109
+ /**
110
+ * OpenTelemetry context manager backed by Vercube's request context.
111
+ *
112
+ * OpenTelemetry ships `AsyncLocalStorageContextManager`, which allocates its
113
+ * own `AsyncLocalStorage`. Vercube already opens one frame per request for
114
+ * {@link RequestContext}, and a second async-context frame costs a measurable
115
+ * slice of throughput on a framework whose fast path is otherwise
116
+ * allocation-free. This manager stores the OpenTelemetry context inside the
117
+ * frame that already exists.
118
+ *
119
+ * Nested frames share the request's root frame, so values written with
120
+ * `RequestContext.set()` from inside a span are still visible after it ends.
121
+ */
122
+ var VercubeContextManager = class {
123
+ /** The request context this manager stores OpenTelemetry contexts in. */
124
+ fRequestContext;
125
+ /** Whether the manager is currently active. */
126
+ fEnabled = false;
127
+ /**
128
+ * @param requestContext - The application's request context service
129
+ */
130
+ constructor(requestContext) {
131
+ this.fRequestContext = requestContext;
132
+ }
133
+ /**
134
+ * The context active in the current async frame.
135
+ *
136
+ * @returns The active context, or the root context outside any frame
137
+ */
138
+ active() {
139
+ if (!this.fEnabled) return ROOT_CONTEXT;
140
+ return this.fRequestContext.getOtelContext() ?? ROOT_CONTEXT;
141
+ }
142
+ /**
143
+ * Runs `fn` with `context` active.
144
+ *
145
+ * The result is passed through unchanged, so a synchronous callback stays
146
+ * synchronous - the request fast path depends on it.
147
+ *
148
+ * @param context - The context to activate
149
+ * @param fn - The function to run
150
+ * @param thisArg - `this` for the call
151
+ * @param args - Arguments for the call
152
+ * @returns Whatever `fn` returned
153
+ */
154
+ with(context, fn, thisArg, ...args) {
155
+ if (!this.fEnabled) return fn.call(thisArg, ...args);
156
+ return this.fRequestContext.runWithOtelContext(context, () => fn.call(thisArg, ...args));
157
+ }
158
+ /**
159
+ * Binds a target to a context.
160
+ *
161
+ * Only functions are bound. OpenTelemetry's own manager additionally rebinds
162
+ * every listener of an `EventEmitter`; nothing in Vercube relies on that, and
163
+ * doing it would mean reaching into Node's emitter internals.
164
+ *
165
+ * @param context - The context to bind to
166
+ * @param target - The value to bind
167
+ * @returns The bound value, or the value unchanged when it is not a function
168
+ */
169
+ bind(context, target) {
170
+ if (typeof target !== "function") return target;
171
+ const fn = target;
172
+ const activate = (run) => this.with(context, run);
173
+ return function bound(...args) {
174
+ return activate(() => fn.apply(this, args));
175
+ };
176
+ }
177
+ /**
178
+ * Activates the manager.
179
+ *
180
+ * @returns This manager
181
+ */
182
+ enable() {
183
+ this.fEnabled = true;
184
+ return this;
185
+ }
186
+ /**
187
+ * Deactivates the manager. Everything falls back to the root context.
188
+ *
189
+ * @returns This manager
190
+ */
191
+ disable() {
192
+ this.fEnabled = false;
193
+ return this;
194
+ }
195
+ };
196
+ //#endregion
197
+ export { headersSetter as i, W3CTraceContextPropagator as n, headersGetter as r, VercubeContextManager as t };
@@ -0,0 +1,125 @@
1
+ import { Context, Meter, Span, SpanOptions, Tracer } from "@opentelemetry/api";
2
+ import { App, BasePlugin, ConfigTypes, TelemetryTypes } from "@vercube/core";
3
+ //#region src/Common/Telemetry.d.ts
4
+ /**
5
+ * Dependency-injection token and public API for telemetry.
6
+ *
7
+ * Injected the same way as `Logger`:
8
+ *
9
+ * ```ts
10
+ * class InvoiceService {
11
+ * @Inject(Telemetry)
12
+ * private gTelemetry!: Telemetry;
13
+ *
14
+ * public async refund(id: string) {
15
+ * return this.gTelemetry.span('invoice.refund', (span) => {
16
+ * span.setAttribute('invoice.id', id);
17
+ * return this.doRefund(id);
18
+ * });
19
+ * }
20
+ * }
21
+ * ```
22
+ *
23
+ * The token is only bound when telemetry is enabled, so inject it with
24
+ * `@InjectOptional` from code that must also run without it.
25
+ */
26
+ export declare abstract class Telemetry {
27
+ /** Tracer for the application's own spans. */
28
+ abstract get tracer(): Tracer;
29
+ /** Meter for the application's own instruments. */
30
+ abstract get meter(): Meter;
31
+ /**
32
+ * Runs `fn` inside a new span that ends when the work settles.
33
+ *
34
+ * The return value is passed through unchanged, so wrapping synchronous code
35
+ * does not make it asynchronous.
36
+ *
37
+ * @param name - Span name
38
+ * @param fn - The work to trace
39
+ * @param options - Span kind, attributes and links
40
+ * @returns Whatever `fn` returned
41
+ */
42
+ abstract span<T>(name: string, fn: (span: Span) => T, options?: SpanOptions): T;
43
+ /** The span currently active on this async execution path, if any. */
44
+ abstract activeSpan(): Span | undefined;
45
+ /** Trace id of the active span, in lowercase hex. */
46
+ abstract get traceId(): string | undefined;
47
+ /** Span id of the active span, in lowercase hex. */
48
+ abstract get spanId(): string | undefined;
49
+ /**
50
+ * Writes W3C trace context headers for the active span into a carrier, so a
51
+ * downstream service continues the same trace.
52
+ *
53
+ * @param carrier - `Headers` or a plain header record to write into
54
+ */
55
+ abstract inject(carrier: Headers | Record<string, string>): void;
56
+ /**
57
+ * Reads W3C trace context headers into a context usable as a span parent.
58
+ *
59
+ * @param headers - Incoming headers
60
+ * @returns A context carrying the remote parent
61
+ */
62
+ abstract extract(headers: Headers | Record<string, string>): Context;
63
+ /**
64
+ * Registers work to run on {@link Telemetry.flush}.
65
+ *
66
+ * @param flush - Flushes whatever the caller has buffered
67
+ */
68
+ abstract onFlush(flush: () => Promise<void>): void;
69
+ /**
70
+ * Pushes out everything buffered by batching drains and exporters.
71
+ *
72
+ * Call it before a process that may be frozen or killed goes away - a
73
+ * serverless invocation, a graceful shutdown - or telemetry produced at the
74
+ * very end of its life never leaves.
75
+ */
76
+ abstract flush(): Promise<void>;
77
+ }
78
+ //#endregion
79
+ //#region src/Plugins/TelemetryPlugin.d.ts
80
+ /**
81
+ * Activates OpenTelemetry instrumentation for the application.
82
+ *
83
+ * Register it in `vercube.config.ts`:
84
+ *
85
+ * ```ts
86
+ * export default defineConfig({
87
+ * telemetry: true,
88
+ * plugins: [TelemetryPlugin],
89
+ * });
90
+ * ```
91
+ *
92
+ * The plugin only wires the OpenTelemetry **API**: it registers a context
93
+ * manager and a W3C propagator, binds the {@link Telemetry} token and installs
94
+ * the hooks core calls into. Producing actual spans additionally requires a
95
+ * `TracerProvider`, which either the application registers through the standard
96
+ * OpenTelemetry SDK, `@vercube/telemetry/sdk`, or `@vercube/devtools`.
97
+ *
98
+ * Options given at registration time win over the `telemetry` field of the
99
+ * application config.
100
+ */
101
+ export declare class TelemetryPlugin extends BasePlugin<TelemetryTypes.Options> {
102
+ /** @inheritdoc */
103
+ name: string;
104
+ /**
105
+ * Starts watching container construction.
106
+ *
107
+ * This runs while the config is still being loaded, which is the only phase
108
+ * early enough: by the time `use()` runs the container has already built a
109
+ * good part of the application. Registering the plugin through
110
+ * `defineConfig({ plugins })` rather than `app.addPlugin()` is therefore what
111
+ * makes bootstrap spans complete.
112
+ *
113
+ * @param config - The merged configuration
114
+ * @param options - Options overriding the `telemetry` config field
115
+ */
116
+ configure(config: ConfigTypes.Config, options?: TelemetryTypes.Options): void;
117
+ /**
118
+ * Installs telemetry into the running application.
119
+ *
120
+ * @param app - The application
121
+ * @param options - Options overriding the `telemetry` config field
122
+ */
123
+ use(app: App, options?: TelemetryTypes.Options): void | Promise<void>;
124
+ }
125
+ //#endregion