@davidilie/telemetry-node 0.1.3 → 0.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.
package/README.md CHANGED
@@ -47,8 +47,10 @@ process.once("SIGTERM", async () => {
47
47
  ```
48
48
 
49
49
  `registerNodeTelemetry` installs one `NodeSDK` per JavaScript global. Repeated
50
- calls return the first client and ignore later configuration until that client
51
- successfully shuts down. Do not register this package beside another provider.
50
+ calls return the first client and ignore later configuration. Registration is
51
+ terminal after shutdown: restart the process to register again. OTel retains
52
+ global provider references after shutdown, so another registration would
53
+ otherwise silently use a stopped provider. Do not register this package beside another provider.
52
54
 
53
55
  The optional `sdk` property accepts `NodeSDKConfiguration` fields other than
54
56
  `resource` and `serviceName`. Use it for maintained instrumentations, readers,
@@ -165,10 +167,17 @@ collected into the same backend. Disable one route deliberately.
165
167
 
166
168
  ## Spans and context
167
169
 
168
- The `NodeTelemetryClient` overrides the generic core `withSpan` so the new span
169
- is active during the operation. Use it when events/logs need the same trace.
170
+ The Node adapter supplies active-span execution to the shared core `withSpan`.
171
+ The span is active during the operation, including asynchronous work, while
172
+ core applies context, consent, sampling, and shutdown checks. Use it when events/logs need the same trace.
170
173
  `client.startSpan()` creates a span but does not make it active.
171
174
 
175
+ The callback receives a safe span handle, including `traceContext()`.
176
+ `client.traceWebhook({ destination: "discord.sales" }, send)` tracks one
177
+ outgoing POST under the active parent. See the
178
+ [webhook guide](https://github.com/DavidIlie/davidapps-telemetry/blob/main/docs/webhooks.md)
179
+ for privacy, delivery outcomes, metrics, and Grafana Discord notifications.
180
+
172
181
  Both methods accept `{ kind: "internal" | "server" | "client" | "producer" |
173
182
  "consumer" }` after attributes. Manual span handles also expose
174
183
  `setStatus("unset" | "ok" | "error", message?)`.
@@ -211,9 +220,11 @@ context; `startSpan` does not.
211
220
  apply; set one layer to `1` if multiplied rates are not intentional.
212
221
  - State changes affect new work and do not erase an already exported or queued
213
222
  provider batch.
214
- - `flush()` drains core work, but the Node adapter does not own a provider
215
- force-flush method. `shutdown()` on the `/register` client shuts down the
216
- `NodeSDK` and its processors.
223
+ - `flush()` drains core work, then calls the registered trace, log, and metric
224
+ providers' public `forceFlush()` methods when available. This flushes all
225
+ pending work on those providers, not only this client's records. Failures
226
+ are reported through `onError` without rejecting the client promise.
227
+ `shutdown()` on the `/register` client shuts down the `NodeSDK` and its processors.
217
228
  - Only a client returned by `/register` owns and shuts down the installed
218
229
  `NodeSDK`. A client created from the root entrypoint does not own the
219
230
  framework provider.
@@ -7,5 +7,6 @@ DavidApps Telemetry depends on permissively licensed open-source projects and us
7
7
  - Vercel Analytics and Vercel OTEL — MIT License — Copyright Vercel, Inc.
8
8
  - PostHog JavaScript SDKs — Apache-2.0/MIT — API reference only; no source is vendored.
9
9
  - Honeycomb OpenTelemetry React Native — MIT License — Copyright Honeycomb.io.
10
+ - Stripe Node.js SDK — MIT License — Copyright Ask Bjørn Hansen and Stripe, Inc.; API reference only.
10
11
 
11
- Reference license texts are retained in `licenses/` so future adaptations cannot accidentally lose their attribution obligations. The current source tree does not vendor code from PostHog, Vercel, Honeycomb, or the OpenTelemetry demo.
12
+ Reference license texts are retained in `licenses/` so future adaptations cannot accidentally lose their attribution obligations. The current source tree does not vendor code from PostHog, Vercel, Honeycomb, Stripe, or the OpenTelemetry demo.
package/UPSTREAM.yml CHANGED
@@ -54,3 +54,8 @@ sources:
54
54
  license: MIT
55
55
  commit: 07d144459177a58a347e18be762cf8f04e63cb1d
56
56
  use: api-reference
57
+ - name: stripe-node
58
+ repository: https://github.com/stripe/stripe-node
59
+ license: MIT
60
+ commit: 8adba1d35fdb76ba4104e5d7ae1ac999649c7061
61
+ use: api-reference
@@ -9,6 +9,7 @@ declare class OpenTelemetrySpan implements TelemetrySpan {
9
9
  recordException(error: unknown, attributes?: Attributes$1): void;
10
10
  setStatus(status: TelemetrySpanStatus, message?: string): this;
11
11
  end(): void;
12
+ traceContext(): TraceContext | undefined;
12
13
  }
13
14
  interface OpenTelemetryAdapterConfig {
14
15
  resource: TelemetryResource;
@@ -31,7 +32,8 @@ declare class OpenTelemetryAdapter implements TelemetryAdapter {
31
32
  send(signal: TelemetrySignal): void;
32
33
  startSpan(name: string, attributes?: Attributes$1, options?: TelemetrySpanOptions): TelemetrySpan;
33
34
  currentTraceContext(): TraceContext | undefined;
34
- withActiveSpan<T>(name: string, operation: () => T | Promise<T>, attributes?: Attributes$1, options?: TelemetrySpanOptions): Promise<T>;
35
+ withActiveSpan<T>(name: string, operation: (span: TelemetrySpan) => T | Promise<T>, attributes?: Attributes$1, options?: TelemetrySpanOptions): Promise<T>;
36
+ flush(): Promise<void>;
35
37
  shutdown(): Promise<void>;
36
38
  }
37
39
  declare function createOpenTelemetryAdapter(config: OpenTelemetryAdapterConfig): OpenTelemetryAdapter;
@@ -78,7 +80,6 @@ declare class NodeTelemetryClient extends TelemetryClient {
78
80
  constructor(config: NodeTelemetryInternalConfig);
79
81
  setEnabled(enabled: boolean): void;
80
82
  setConsent(consent: "granted" | "denied" | "pending"): void;
81
- withSpan<T>(name: string, operation: () => T | Promise<T>, attributes?: Attributes$1, options?: TelemetrySpanOptions): Promise<T>;
82
83
  }
83
84
  declare function createNodeTelemetry(config: NodeTelemetryConfig): NodeTelemetryClient;
84
85
  //#endregion
@@ -9,6 +9,7 @@ declare class OpenTelemetrySpan implements TelemetrySpan {
9
9
  recordException(error: unknown, attributes?: Attributes$1): void;
10
10
  setStatus(status: TelemetrySpanStatus, message?: string): this;
11
11
  end(): void;
12
+ traceContext(): TraceContext | undefined;
12
13
  }
13
14
  interface OpenTelemetryAdapterConfig {
14
15
  resource: TelemetryResource;
@@ -31,7 +32,8 @@ declare class OpenTelemetryAdapter implements TelemetryAdapter {
31
32
  send(signal: TelemetrySignal): void;
32
33
  startSpan(name: string, attributes?: Attributes$1, options?: TelemetrySpanOptions): TelemetrySpan;
33
34
  currentTraceContext(): TraceContext | undefined;
34
- withActiveSpan<T>(name: string, operation: () => T | Promise<T>, attributes?: Attributes$1, options?: TelemetrySpanOptions): Promise<T>;
35
+ withActiveSpan<T>(name: string, operation: (span: TelemetrySpan) => T | Promise<T>, attributes?: Attributes$1, options?: TelemetrySpanOptions): Promise<T>;
36
+ flush(): Promise<void>;
35
37
  shutdown(): Promise<void>;
36
38
  }
37
39
  declare function createOpenTelemetryAdapter(config: OpenTelemetryAdapterConfig): OpenTelemetryAdapter;
@@ -78,7 +80,6 @@ declare class NodeTelemetryClient extends TelemetryClient {
78
80
  constructor(config: NodeTelemetryInternalConfig);
79
81
  setEnabled(enabled: boolean): void;
80
82
  setConsent(consent: "granted" | "denied" | "pending"): void;
81
- withSpan<T>(name: string, operation: () => T | Promise<T>, attributes?: Attributes$1, options?: TelemetrySpanOptions): Promise<T>;
82
83
  }
83
84
  declare function createNodeTelemetry(config: NodeTelemetryConfig): NodeTelemetryClient;
84
85
  //#endregion
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sampler = require("./sampler-CGBWEZod.cjs");
2
+ const require_sampler = require("./sampler-D5mi13Eh.cjs");
3
3
  exports.DynamicTelemetrySampler = require_sampler.DynamicTelemetrySampler;
4
4
  exports.NodeTelemetryClient = require_sampler.NodeTelemetryClient;
5
5
  exports.OpenTelemetryAdapter = require_sampler.OpenTelemetryAdapter;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as OpenTelemetryAdapter, c as createOpenTelemetryAdapter, d as startSpan, f as withSpan, i as MeasurementMode, l as currentTraceContext, n as NodeTelemetryConfig, o as OpenTelemetryAdapterConfig, r as createNodeTelemetry, s as OpenTelemetrySpan, t as NodeTelemetryClient, u as recordException } from "./client-BzoFCwmQ.cjs";
1
+ import { a as OpenTelemetryAdapter, c as createOpenTelemetryAdapter, d as startSpan, f as withSpan, i as MeasurementMode, l as currentTraceContext, n as NodeTelemetryConfig, o as OpenTelemetryAdapterConfig, r as createNodeTelemetry, s as OpenTelemetrySpan, t as NodeTelemetryClient, u as recordException } from "./client-DYrSJX10.cjs";
2
2
  import { AttributeValue, Attributes, Context, Link, SpanKind } from "@opentelemetry/api";
3
3
  import { AttributeValue as AttributeValue$1, Attributes as Attributes$1, TelemetryResource } from "@davidilie/telemetry-core";
4
4
  import { Sampler, SamplingResult } from "@opentelemetry/sdk-trace-base";
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as OpenTelemetryAdapter, c as createOpenTelemetryAdapter, d as startSpan, f as withSpan, i as MeasurementMode, l as currentTraceContext, n as NodeTelemetryConfig, o as OpenTelemetryAdapterConfig, r as createNodeTelemetry, s as OpenTelemetrySpan, t as NodeTelemetryClient, u as recordException } from "./client-BzoFCwmQ.mjs";
1
+ import { a as OpenTelemetryAdapter, c as createOpenTelemetryAdapter, d as startSpan, f as withSpan, i as MeasurementMode, l as currentTraceContext, n as NodeTelemetryConfig, o as OpenTelemetryAdapterConfig, r as createNodeTelemetry, s as OpenTelemetrySpan, t as NodeTelemetryClient, u as recordException } from "./client-DYrSJX10.mjs";
2
2
  import { AttributeValue, Attributes, Context, Link, SpanKind } from "@opentelemetry/api";
3
3
  import { AttributeValue as AttributeValue$1, Attributes as Attributes$1, TelemetryResource } from "@davidilie/telemetry-core";
4
4
  import { Sampler, SamplingResult } from "@opentelemetry/sdk-trace-base";
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as OpenTelemetrySpan, c as recordException, d as telemetryResourceAttributes, f as toOtelAttributeValue, i as OpenTelemetryAdapter, l as startSpan, n as NodeTelemetryClient, o as createOpenTelemetryAdapter, p as toOtelAttributes, r as createNodeTelemetry, s as currentTraceContext, t as DynamicTelemetrySampler, u as withSpan } from "./sampler-ND4YGh_7.mjs";
1
+ import { a as OpenTelemetrySpan, c as recordException, d as telemetryResourceAttributes, f as toOtelAttributeValue, i as OpenTelemetryAdapter, l as startSpan, n as NodeTelemetryClient, o as createOpenTelemetryAdapter, p as toOtelAttributes, r as createNodeTelemetry, s as currentTraceContext, t as DynamicTelemetrySampler, u as withSpan } from "./sampler-B1PsF--M.mjs";
2
2
  export { DynamicTelemetrySampler, NodeTelemetryClient, OpenTelemetryAdapter, OpenTelemetrySpan, createNodeTelemetry, createOpenTelemetryAdapter, currentTraceContext, recordException, startSpan, telemetryResourceAttributes, toOtelAttributeValue, toOtelAttributes, withSpan };
package/dist/register.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sampler = require("./sampler-CGBWEZod.cjs");
2
+ const require_sampler = require("./sampler-D5mi13Eh.cjs");
3
3
  let _davidilie_telemetry_core = require("@davidilie/telemetry-core");
4
4
  let _opentelemetry_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
5
5
  let _opentelemetry_resources = require("@opentelemetry/resources");
@@ -13,7 +13,10 @@ function traceSampleRate(config) {
13
13
  function registerNodeTelemetry(config) {
14
14
  const sharedGlobal = globalThis;
15
15
  const existing = sharedGlobal[REGISTRATION];
16
- if (existing) return existing.client;
16
+ if (existing) {
17
+ if (existing.stopped) throw new Error("Node telemetry registration is terminal after shutdown; restart the process to register again");
18
+ return existing.client;
19
+ }
17
20
  const resource = (0, _davidilie_telemetry_core.sanitizeResource)(config.resource);
18
21
  const sampler = new require_sampler.DynamicTelemetrySampler(config.sdk?.sampler ?? new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.TraceIdRatioBasedSampler(traceSampleRate(config)) }), {
19
22
  enabled: config.enabled ?? true,
@@ -37,13 +40,15 @@ function registerNodeTelemetry(config) {
37
40
  },
38
41
  measurementMode: config.measurementMode ?? (config.sdk?.metricReaders?.length ? "metrics" : "spans"),
39
42
  shutdown: async () => {
43
+ const registration = sharedGlobal[REGISTRATION];
44
+ if (registration?.client === client) registration.stopped = true;
40
45
  await sdk.shutdown();
41
- if (sharedGlobal[REGISTRATION]?.client === client) delete sharedGlobal[REGISTRATION];
42
46
  }
43
47
  });
44
48
  sharedGlobal[REGISTRATION] = {
45
49
  client,
46
- sdk
50
+ sdk,
51
+ stopped: false
47
52
  };
48
53
  return client;
49
54
  }
@@ -1,4 +1,4 @@
1
- import { n as NodeTelemetryConfig, t as NodeTelemetryClient } from "./client-BzoFCwmQ.cjs";
1
+ import { n as NodeTelemetryConfig, t as NodeTelemetryClient } from "./client-DYrSJX10.cjs";
2
2
  import { NodeSDKConfiguration, NodeSDKConfiguration as NodeSDKConfiguration$1 } from "@opentelemetry/sdk-node";
3
3
  //#region src/register.d.ts
4
4
  interface RegisterNodeTelemetryConfig extends NodeTelemetryConfig {
@@ -1,4 +1,4 @@
1
- import { n as NodeTelemetryConfig, t as NodeTelemetryClient } from "./client-BzoFCwmQ.mjs";
1
+ import { n as NodeTelemetryConfig, t as NodeTelemetryClient } from "./client-DYrSJX10.mjs";
2
2
  import { NodeSDKConfiguration, NodeSDKConfiguration as NodeSDKConfiguration$1 } from "@opentelemetry/sdk-node";
3
3
  //#region src/register.d.ts
4
4
  interface RegisterNodeTelemetryConfig extends NodeTelemetryConfig {
package/dist/register.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { d as telemetryResourceAttributes, n as NodeTelemetryClient, t as DynamicTelemetrySampler } from "./sampler-ND4YGh_7.mjs";
1
+ import { d as telemetryResourceAttributes, n as NodeTelemetryClient, t as DynamicTelemetrySampler } from "./sampler-B1PsF--M.mjs";
2
2
  import { sanitizeResource } from "@davidilie/telemetry-core";
3
3
  import { ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
4
4
  import { resourceFromAttributes } from "@opentelemetry/resources";
@@ -12,7 +12,10 @@ function traceSampleRate(config) {
12
12
  function registerNodeTelemetry(config) {
13
13
  const sharedGlobal = globalThis;
14
14
  const existing = sharedGlobal[REGISTRATION];
15
- if (existing) return existing.client;
15
+ if (existing) {
16
+ if (existing.stopped) throw new Error("Node telemetry registration is terminal after shutdown; restart the process to register again");
17
+ return existing.client;
18
+ }
16
19
  const resource = sanitizeResource(config.resource);
17
20
  const sampler = new DynamicTelemetrySampler(config.sdk?.sampler ?? new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(traceSampleRate(config)) }), {
18
21
  enabled: config.enabled ?? true,
@@ -36,13 +39,15 @@ function registerNodeTelemetry(config) {
36
39
  },
37
40
  measurementMode: config.measurementMode ?? (config.sdk?.metricReaders?.length ? "metrics" : "spans"),
38
41
  shutdown: async () => {
42
+ const registration = sharedGlobal[REGISTRATION];
43
+ if (registration?.client === client) registration.stopped = true;
39
44
  await sdk.shutdown();
40
- if (sharedGlobal[REGISTRATION]?.client === client) delete sharedGlobal[REGISTRATION];
41
45
  }
42
46
  });
43
47
  sharedGlobal[REGISTRATION] = {
44
48
  client,
45
- sdk
49
+ sdk,
50
+ stopped: false
46
51
  };
47
52
  return client;
48
53
  }
@@ -1,4 +1,4 @@
1
- import { SpanKind, SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
1
+ import { SpanKind, SpanStatusCode, context, isSpanContextValid, metrics, trace } from "@opentelemetry/api";
2
2
  import { SeverityNumber, logs } from "@opentelemetry/api-logs";
3
3
  import { TelemetryClient, redactText, sanitizeAttributes, sanitizeResource, sanitizeSignal } from "@davidilie/telemetry-core";
4
4
  import { SamplingDecision } from "@opentelemetry/sdk-trace-base";
@@ -38,6 +38,22 @@ function telemetryResourceAttributes(resource) {
38
38
  };
39
39
  }
40
40
  //#endregion
41
+ //#region src/flush.ts
42
+ /** Flush registered SDK providers using their public lifecycle methods. */
43
+ async function flushOpenTelemetryProviders() {
44
+ let tracerProvider = trace.getTracerProvider();
45
+ if ("getDelegate" in tracerProvider && typeof tracerProvider.getDelegate === "function") tracerProvider = tracerProvider.getDelegate();
46
+ const providers = [
47
+ tracerProvider,
48
+ logs.getLoggerProvider(),
49
+ metrics.getMeterProvider()
50
+ ];
51
+ const errors = (await Promise.allSettled(providers.map(async (provider) => {
52
+ if ("forceFlush" in provider && typeof provider.forceFlush === "function") await provider.forceFlush();
53
+ }))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
54
+ if (errors.length > 0) throw new AggregateError(errors, "OpenTelemetry provider flush failed");
55
+ }
56
+ //#endregion
41
57
  //#region src/adapter.ts
42
58
  const severityNumbers = {
43
59
  debug: SeverityNumber.DEBUG,
@@ -100,6 +116,14 @@ var OpenTelemetrySpan = class {
100
116
  end() {
101
117
  this.#span.end();
102
118
  }
119
+ traceContext() {
120
+ const value = this.#span.spanContext();
121
+ return isSpanContextValid(value) ? {
122
+ traceId: value.traceId,
123
+ spanId: value.spanId,
124
+ traceFlags: value.traceFlags
125
+ } : void 0;
126
+ }
103
127
  };
104
128
  var OpenTelemetryAdapter = class {
105
129
  #instrumentationName;
@@ -129,7 +153,7 @@ var OpenTelemetryAdapter = class {
129
153
  switch (signal.type) {
130
154
  case "event": {
131
155
  const activeSpan = trace.getActiveSpan();
132
- if (activeSpan) {
156
+ if (activeSpan?.isRecording()) {
133
157
  activeSpan.addEvent(signal.name, attributes, new Date(signal.timestamp));
134
158
  return;
135
159
  }
@@ -144,22 +168,23 @@ var OpenTelemetryAdapter = class {
144
168
  }
145
169
  case "exception": {
146
170
  const activeSpan = trace.getActiveSpan();
147
- const span = activeSpan ?? this.#tracer().startSpan("exception", {
171
+ const recordingSpan = activeSpan?.isRecording() ? activeSpan : void 0;
172
+ const span = recordingSpan ?? this.#tracer().startSpan("exception", {
148
173
  kind: SpanKind.INTERNAL,
149
174
  attributes,
150
175
  startTime: new Date(signal.timestamp)
151
176
  });
152
- span.setAttributes(attributes);
153
- span.recordException({
154
- name: signal.exception.name,
155
- message: signal.exception.message,
156
- ...signal.exception.stack ? { stack: signal.exception.stack } : {}
157
- });
177
+ span.addEvent("exception", {
178
+ ...attributes,
179
+ "exception.type": signal.exception.name,
180
+ "exception.message": signal.exception.message,
181
+ ...signal.exception.stack ? { "exception.stacktrace": signal.exception.stack } : {}
182
+ }, new Date(signal.timestamp));
158
183
  span.setStatus({
159
184
  code: SpanStatusCode.ERROR,
160
185
  message: signal.exception.message
161
186
  });
162
- if (!activeSpan) span.end(new Date(signal.timestamp));
187
+ if (!recordingSpan) span.end(new Date(signal.timestamp));
163
188
  return;
164
189
  }
165
190
  case "log":
@@ -239,7 +264,7 @@ var OpenTelemetryAdapter = class {
239
264
  }, async (span) => {
240
265
  operationRan = true;
241
266
  try {
242
- operationResult = await operation();
267
+ operationResult = await operation(new OpenTelemetrySpan(span));
243
268
  operationSucceeded = true;
244
269
  } catch (error) {
245
270
  operationError = error;
@@ -261,7 +286,10 @@ var OpenTelemetryAdapter = class {
261
286
  }
262
287
  if (operationSucceeded) return operationResult;
263
288
  if (operationRan) throw operationError;
264
- return operation();
289
+ throw new Error("OpenTelemetry tracer did not invoke the span operation");
290
+ }
291
+ flush() {
292
+ return flushOpenTelemetryProviders();
265
293
  }
266
294
  async shutdown() {
267
295
  await this.#shutdown?.();
@@ -275,7 +303,7 @@ function createOpenTelemetryAdapter(config) {
275
303
  }
276
304
  function currentTraceContext() {
277
305
  const spanContext = trace.getSpan(context.active())?.spanContext();
278
- if (!spanContext) return void 0;
306
+ if (!spanContext || !isSpanContextValid(spanContext)) return void 0;
279
307
  return {
280
308
  traceId: spanContext.traceId,
281
309
  spanId: spanContext.spanId,
@@ -325,9 +353,6 @@ function recordException(error, attributes = {}) {
325
353
  //#endregion
326
354
  //#region src/client.ts
327
355
  var NodeTelemetryClient = class extends TelemetryClient {
328
- #adapter;
329
- #sampleRate;
330
- #onError;
331
356
  #onCollectionStateChange;
332
357
  #enabled;
333
358
  #consent;
@@ -353,9 +378,6 @@ var NodeTelemetryClient = class extends TelemetryClient {
353
378
  ...config.debug !== void 0 ? { debug: config.debug } : {},
354
379
  ...config.onError ? { onError: config.onError } : {}
355
380
  });
356
- this.#adapter = adapter;
357
- this.#sampleRate = config.providerManagedSampling ? 1 : Number.isFinite(config.sampleRate) ? Math.min(1, Math.max(0, config.sampleRate ?? 1)) : 1;
358
- this.#onError = config.onError;
359
381
  this.#onCollectionStateChange = config.onCollectionStateChange;
360
382
  this.#enabled = config.enabled ?? true;
361
383
  this.#consent = config.consent ?? "granted";
@@ -376,26 +398,6 @@ var NodeTelemetryClient = class extends TelemetryClient {
376
398
  consent: this.#consent
377
399
  });
378
400
  }
379
- async withSpan(name, operation, attributes = {}, options = {}) {
380
- if (!this.#enabled || this.#consent !== "granted" || Math.random() >= this.#sampleRate) return operation();
381
- let operationStarted = false;
382
- try {
383
- return await this.#adapter.withActiveSpan(redactText(name, 256), () => {
384
- operationStarted = true;
385
- return operation();
386
- }, sanitizeAttributes(attributes), options);
387
- } catch (error) {
388
- if (operationStarted) throw error;
389
- this.#reportAdapterError(error, { operation: "startSpan" });
390
- return operation();
391
- }
392
- }
393
- #reportAdapterError(error, context) {
394
- if (!this.#onError) return;
395
- try {
396
- Promise.resolve(this.#onError(error, context)).catch(() => void 0);
397
- } catch {}
398
- }
399
401
  };
400
402
  function createNodeTelemetry(config) {
401
403
  return new NodeTelemetryClient(config);
@@ -38,6 +38,22 @@ function telemetryResourceAttributes(resource) {
38
38
  };
39
39
  }
40
40
  //#endregion
41
+ //#region src/flush.ts
42
+ /** Flush registered SDK providers using their public lifecycle methods. */
43
+ async function flushOpenTelemetryProviders() {
44
+ let tracerProvider = _opentelemetry_api.trace.getTracerProvider();
45
+ if ("getDelegate" in tracerProvider && typeof tracerProvider.getDelegate === "function") tracerProvider = tracerProvider.getDelegate();
46
+ const providers = [
47
+ tracerProvider,
48
+ _opentelemetry_api_logs.logs.getLoggerProvider(),
49
+ _opentelemetry_api.metrics.getMeterProvider()
50
+ ];
51
+ const errors = (await Promise.allSettled(providers.map(async (provider) => {
52
+ if ("forceFlush" in provider && typeof provider.forceFlush === "function") await provider.forceFlush();
53
+ }))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
54
+ if (errors.length > 0) throw new AggregateError(errors, "OpenTelemetry provider flush failed");
55
+ }
56
+ //#endregion
41
57
  //#region src/adapter.ts
42
58
  const severityNumbers = {
43
59
  debug: _opentelemetry_api_logs.SeverityNumber.DEBUG,
@@ -100,6 +116,14 @@ var OpenTelemetrySpan = class {
100
116
  end() {
101
117
  this.#span.end();
102
118
  }
119
+ traceContext() {
120
+ const value = this.#span.spanContext();
121
+ return (0, _opentelemetry_api.isSpanContextValid)(value) ? {
122
+ traceId: value.traceId,
123
+ spanId: value.spanId,
124
+ traceFlags: value.traceFlags
125
+ } : void 0;
126
+ }
103
127
  };
104
128
  var OpenTelemetryAdapter = class {
105
129
  #instrumentationName;
@@ -129,7 +153,7 @@ var OpenTelemetryAdapter = class {
129
153
  switch (signal.type) {
130
154
  case "event": {
131
155
  const activeSpan = _opentelemetry_api.trace.getActiveSpan();
132
- if (activeSpan) {
156
+ if (activeSpan?.isRecording()) {
133
157
  activeSpan.addEvent(signal.name, attributes, new Date(signal.timestamp));
134
158
  return;
135
159
  }
@@ -144,22 +168,23 @@ var OpenTelemetryAdapter = class {
144
168
  }
145
169
  case "exception": {
146
170
  const activeSpan = _opentelemetry_api.trace.getActiveSpan();
147
- const span = activeSpan ?? this.#tracer().startSpan("exception", {
171
+ const recordingSpan = activeSpan?.isRecording() ? activeSpan : void 0;
172
+ const span = recordingSpan ?? this.#tracer().startSpan("exception", {
148
173
  kind: _opentelemetry_api.SpanKind.INTERNAL,
149
174
  attributes,
150
175
  startTime: new Date(signal.timestamp)
151
176
  });
152
- span.setAttributes(attributes);
153
- span.recordException({
154
- name: signal.exception.name,
155
- message: signal.exception.message,
156
- ...signal.exception.stack ? { stack: signal.exception.stack } : {}
157
- });
177
+ span.addEvent("exception", {
178
+ ...attributes,
179
+ "exception.type": signal.exception.name,
180
+ "exception.message": signal.exception.message,
181
+ ...signal.exception.stack ? { "exception.stacktrace": signal.exception.stack } : {}
182
+ }, new Date(signal.timestamp));
158
183
  span.setStatus({
159
184
  code: _opentelemetry_api.SpanStatusCode.ERROR,
160
185
  message: signal.exception.message
161
186
  });
162
- if (!activeSpan) span.end(new Date(signal.timestamp));
187
+ if (!recordingSpan) span.end(new Date(signal.timestamp));
163
188
  return;
164
189
  }
165
190
  case "log":
@@ -239,7 +264,7 @@ var OpenTelemetryAdapter = class {
239
264
  }, async (span) => {
240
265
  operationRan = true;
241
266
  try {
242
- operationResult = await operation();
267
+ operationResult = await operation(new OpenTelemetrySpan(span));
243
268
  operationSucceeded = true;
244
269
  } catch (error) {
245
270
  operationError = error;
@@ -261,7 +286,10 @@ var OpenTelemetryAdapter = class {
261
286
  }
262
287
  if (operationSucceeded) return operationResult;
263
288
  if (operationRan) throw operationError;
264
- return operation();
289
+ throw new Error("OpenTelemetry tracer did not invoke the span operation");
290
+ }
291
+ flush() {
292
+ return flushOpenTelemetryProviders();
265
293
  }
266
294
  async shutdown() {
267
295
  await this.#shutdown?.();
@@ -275,7 +303,7 @@ function createOpenTelemetryAdapter(config) {
275
303
  }
276
304
  function currentTraceContext() {
277
305
  const spanContext = _opentelemetry_api.trace.getSpan(_opentelemetry_api.context.active())?.spanContext();
278
- if (!spanContext) return void 0;
306
+ if (!spanContext || !(0, _opentelemetry_api.isSpanContextValid)(spanContext)) return void 0;
279
307
  return {
280
308
  traceId: spanContext.traceId,
281
309
  spanId: spanContext.spanId,
@@ -325,9 +353,6 @@ function recordException(error, attributes = {}) {
325
353
  //#endregion
326
354
  //#region src/client.ts
327
355
  var NodeTelemetryClient = class extends _davidilie_telemetry_core.TelemetryClient {
328
- #adapter;
329
- #sampleRate;
330
- #onError;
331
356
  #onCollectionStateChange;
332
357
  #enabled;
333
358
  #consent;
@@ -353,9 +378,6 @@ var NodeTelemetryClient = class extends _davidilie_telemetry_core.TelemetryClien
353
378
  ...config.debug !== void 0 ? { debug: config.debug } : {},
354
379
  ...config.onError ? { onError: config.onError } : {}
355
380
  });
356
- this.#adapter = adapter;
357
- this.#sampleRate = config.providerManagedSampling ? 1 : Number.isFinite(config.sampleRate) ? Math.min(1, Math.max(0, config.sampleRate ?? 1)) : 1;
358
- this.#onError = config.onError;
359
381
  this.#onCollectionStateChange = config.onCollectionStateChange;
360
382
  this.#enabled = config.enabled ?? true;
361
383
  this.#consent = config.consent ?? "granted";
@@ -376,26 +398,6 @@ var NodeTelemetryClient = class extends _davidilie_telemetry_core.TelemetryClien
376
398
  consent: this.#consent
377
399
  });
378
400
  }
379
- async withSpan(name, operation, attributes = {}, options = {}) {
380
- if (!this.#enabled || this.#consent !== "granted" || Math.random() >= this.#sampleRate) return operation();
381
- let operationStarted = false;
382
- try {
383
- return await this.#adapter.withActiveSpan((0, _davidilie_telemetry_core.redactText)(name, 256), () => {
384
- operationStarted = true;
385
- return operation();
386
- }, (0, _davidilie_telemetry_core.sanitizeAttributes)(attributes), options);
387
- } catch (error) {
388
- if (operationStarted) throw error;
389
- this.#reportAdapterError(error, { operation: "startSpan" });
390
- return operation();
391
- }
392
- }
393
- #reportAdapterError(error, context) {
394
- if (!this.#onError) return;
395
- try {
396
- Promise.resolve(this.#onError(error, context)).catch(() => void 0);
397
- } catch {}
398
- }
399
401
  };
400
402
  function createNodeTelemetry(config) {
401
403
  return new NodeTelemetryClient(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidilie/telemetry-node",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "OpenTelemetry-backed telemetry for Node.js services.",
5
5
  "keywords": [
6
6
  "analytics",
@@ -65,7 +65,7 @@
65
65
  "clean": "rm -rf dist coverage"
66
66
  },
67
67
  "dependencies": {
68
- "@davidilie/telemetry-core": "0.1.3",
68
+ "@davidilie/telemetry-core": "0.3.0",
69
69
  "@opentelemetry/api-logs": "0.221.0",
70
70
  "@opentelemetry/resources": "2.10.0",
71
71
  "@opentelemetry/sdk-node": "0.221.0",