@davidilie/telemetry-node 0.2.0 → 0.4.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,12 +47,24 @@ 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,
55
57
  processors, or exporters when environment-based defaults are insufficient.
58
+ Resource detectors default to an empty list, excluding automatic host/process
59
+ metadata and `OTEL_RESOURCE_ATTRIBUTES` / `OTEL_SERVICE_NAME` overrides. Set
60
+ `sdk.resourceDetectors` only for reviewed detectors: their output bypasses core
61
+ redaction and may override the configured resource identity.
62
+
63
+ Wrap a custom trace exporter in the exported `RedactingSpanExporter` to apply
64
+ core privacy bounds to completed automatic spans as well. It redacts copies
65
+ of span names, attributes, events, links, and status messages, leaving the
66
+ original spans and trace context intact. Resource and instrumentation-scope
67
+ metadata remain trusted configuration; configure those separately.
56
68
 
57
69
  ## Export configuration
58
70
 
@@ -60,7 +72,6 @@ The standalone SDK follows standard OpenTelemetry environment variables. For
60
72
  Alloy's private OTLP/HTTP receiver:
61
73
 
62
74
  ```sh
63
- OTEL_SERVICE_NAME=email-worker
64
75
  OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy.observability.svc.cluster.local:4318
65
76
  OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
66
77
  ```
@@ -165,10 +176,17 @@ collected into the same backend. Disable one route deliberately.
165
176
 
166
177
  ## Spans and context
167
178
 
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.
179
+ The Node adapter supplies active-span execution to the shared core `withSpan`.
180
+ The span is active during the operation, including asynchronous work, while
181
+ core applies context, consent, sampling, and shutdown checks. Use it when events/logs need the same trace.
170
182
  `client.startSpan()` creates a span but does not make it active.
171
183
 
184
+ The callback receives a safe span handle, including `traceContext()`.
185
+ `client.traceWebhook({ destination: "discord.sales" }, send)` tracks one
186
+ outgoing POST under the active parent. See the
187
+ [webhook guide](https://github.com/DavidIlie/davidapps-telemetry/blob/main/docs/webhooks.md)
188
+ for privacy, delivery outcomes, metrics, and Grafana Discord notifications.
189
+
172
190
  Both methods accept `{ kind: "internal" | "server" | "client" | "producer" |
173
191
  "consumer" }` after attributes. Manual span handles also expose
174
192
  `setStatus("unset" | "ok" | "error", message?)`.
@@ -211,9 +229,11 @@ context; `startSpan` does not.
211
229
  apply; set one layer to `1` if multiplied rates are not intentional.
212
230
  - State changes affect new work and do not erase an already exported or queued
213
231
  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.
232
+ - `flush()` drains core work, then calls the registered trace, log, and metric
233
+ providers' public `forceFlush()` methods when available. This flushes all
234
+ pending work on those providers, not only this client's records. Failures
235
+ are reported through `onError` without rejecting the client promise.
236
+ `shutdown()` on the `/register` client shuts down the `NodeSDK` and its processors.
217
237
  - Only a client returned by `/register` owns and shuts down the installed
218
238
  `NodeSDK`. A client created from the root entrypoint does not own the
219
239
  framework provider.
@@ -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,9 +1,74 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sampler = require("./sampler-CGBWEZod.cjs");
2
+ const require_sampler = require("./sampler-vVC2xXgy.cjs");
3
+ let _davidilie_telemetry_core = require("@davidilie/telemetry-core");
4
+ //#region src/exporter.ts
5
+ function redactSpan(span) {
6
+ const name = span.name.replace(/https?:\/\/[^\s<>"']+/gi, (value) => {
7
+ try {
8
+ return new URL(value).origin;
9
+ } catch {
10
+ return "[REDACTED_URL]";
11
+ }
12
+ });
13
+ return {
14
+ name: (0, _davidilie_telemetry_core.redactText)(name, 256),
15
+ kind: span.kind,
16
+ spanContext: () => span.spanContext(),
17
+ ...span.parentSpanContext ? { parentSpanContext: span.parentSpanContext } : {},
18
+ startTime: span.startTime,
19
+ endTime: span.endTime,
20
+ duration: span.duration,
21
+ ended: span.ended,
22
+ resource: span.resource,
23
+ instrumentationScope: span.instrumentationScope,
24
+ droppedAttributesCount: span.droppedAttributesCount,
25
+ droppedEventsCount: span.droppedEventsCount,
26
+ droppedLinksCount: span.droppedLinksCount,
27
+ attributes: require_sampler.toOtelAttributes((0, _davidilie_telemetry_core.sanitizeAttributes)(span.attributes)),
28
+ status: {
29
+ code: span.status.code,
30
+ ...span.status.message ? { message: (0, _davidilie_telemetry_core.redactText)(span.status.message) } : {}
31
+ },
32
+ events: span.events.map((event) => ({
33
+ ...event,
34
+ name: (0, _davidilie_telemetry_core.redactText)(event.name, 256),
35
+ attributes: require_sampler.toOtelAttributes((0, _davidilie_telemetry_core.sanitizeAttributes)(event.attributes))
36
+ })),
37
+ links: span.links.map((link) => ({
38
+ ...link,
39
+ attributes: require_sampler.toOtelAttributes((0, _davidilie_telemetry_core.sanitizeAttributes)(link.attributes))
40
+ }))
41
+ };
42
+ }
43
+ /** Apply core privacy bounds to completed spans without mutating another processor's data. */
44
+ var RedactingSpanExporter = class {
45
+ delegate;
46
+ constructor(delegate) {
47
+ this.delegate = delegate;
48
+ }
49
+ export(spans, done) {
50
+ let sanitized;
51
+ try {
52
+ sanitized = spans.map(redactSpan);
53
+ } catch {
54
+ done({ code: 1 });
55
+ return;
56
+ }
57
+ this.delegate.export(sanitized, done);
58
+ }
59
+ shutdown() {
60
+ return this.delegate.shutdown();
61
+ }
62
+ forceFlush() {
63
+ return this.delegate.forceFlush?.() ?? Promise.resolve();
64
+ }
65
+ };
66
+ //#endregion
3
67
  exports.DynamicTelemetrySampler = require_sampler.DynamicTelemetrySampler;
4
68
  exports.NodeTelemetryClient = require_sampler.NodeTelemetryClient;
5
69
  exports.OpenTelemetryAdapter = require_sampler.OpenTelemetryAdapter;
6
70
  exports.OpenTelemetrySpan = require_sampler.OpenTelemetrySpan;
71
+ exports.RedactingSpanExporter = RedactingSpanExporter;
7
72
  exports.createNodeTelemetry = require_sampler.createNodeTelemetry;
8
73
  exports.createOpenTelemetryAdapter = require_sampler.createOpenTelemetryAdapter;
9
74
  exports.currentTraceContext = require_sampler.currentTraceContext;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
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
- import { Sampler, SamplingResult } from "@opentelemetry/sdk-trace-base";
4
+ import { ReadableSpan, Sampler, SamplingResult, SpanExporter } from "@opentelemetry/sdk-trace-base";
5
5
  //#region src/attributes.d.ts
6
6
  declare function toOtelAttributeValue(value: AttributeValue$1): AttributeValue;
7
7
  declare function toOtelAttributes(attributes?: Attributes$1): Attributes;
@@ -31,4 +31,14 @@ declare class DynamicTelemetrySampler implements Sampler {
31
31
  toString(): string;
32
32
  }
33
33
  //#endregion
34
- export { DynamicTelemetrySampler, type MeasurementMode, NodeTelemetryClient, type NodeTelemetryConfig, OpenTelemetryAdapter, type OpenTelemetryAdapterConfig, OpenTelemetrySpan, type TelemetrySamplingState, createNodeTelemetry, createOpenTelemetryAdapter, currentTraceContext, recordException, startSpan, telemetryResourceAttributes, toOtelAttributeValue, toOtelAttributes, withSpan };
34
+ //#region src/exporter.d.ts
35
+ /** Apply core privacy bounds to completed spans without mutating another processor's data. */
36
+ declare class RedactingSpanExporter implements SpanExporter {
37
+ private readonly delegate;
38
+ constructor(delegate: SpanExporter);
39
+ export(spans: ReadableSpan[], done: Parameters<SpanExporter["export"]>[1]): void;
40
+ shutdown(): Promise<void>;
41
+ forceFlush(): Promise<void>;
42
+ }
43
+ //#endregion
44
+ export { DynamicTelemetrySampler, type MeasurementMode, NodeTelemetryClient, type NodeTelemetryConfig, OpenTelemetryAdapter, type OpenTelemetryAdapterConfig, OpenTelemetrySpan, RedactingSpanExporter, type TelemetrySamplingState, createNodeTelemetry, createOpenTelemetryAdapter, currentTraceContext, recordException, startSpan, telemetryResourceAttributes, toOtelAttributeValue, toOtelAttributes, withSpan };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
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
- import { Sampler, SamplingResult } from "@opentelemetry/sdk-trace-base";
4
+ import { ReadableSpan, Sampler, SamplingResult, SpanExporter } from "@opentelemetry/sdk-trace-base";
5
5
  //#region src/attributes.d.ts
6
6
  declare function toOtelAttributeValue(value: AttributeValue$1): AttributeValue;
7
7
  declare function toOtelAttributes(attributes?: Attributes$1): Attributes;
@@ -31,4 +31,14 @@ declare class DynamicTelemetrySampler implements Sampler {
31
31
  toString(): string;
32
32
  }
33
33
  //#endregion
34
- export { DynamicTelemetrySampler, type MeasurementMode, NodeTelemetryClient, type NodeTelemetryConfig, OpenTelemetryAdapter, type OpenTelemetryAdapterConfig, OpenTelemetrySpan, type TelemetrySamplingState, createNodeTelemetry, createOpenTelemetryAdapter, currentTraceContext, recordException, startSpan, telemetryResourceAttributes, toOtelAttributeValue, toOtelAttributes, withSpan };
34
+ //#region src/exporter.d.ts
35
+ /** Apply core privacy bounds to completed spans without mutating another processor's data. */
36
+ declare class RedactingSpanExporter implements SpanExporter {
37
+ private readonly delegate;
38
+ constructor(delegate: SpanExporter);
39
+ export(spans: ReadableSpan[], done: Parameters<SpanExporter["export"]>[1]): void;
40
+ shutdown(): Promise<void>;
41
+ forceFlush(): Promise<void>;
42
+ }
43
+ //#endregion
44
+ export { DynamicTelemetrySampler, type MeasurementMode, NodeTelemetryClient, type NodeTelemetryConfig, OpenTelemetryAdapter, type OpenTelemetryAdapterConfig, OpenTelemetrySpan, RedactingSpanExporter, type TelemetrySamplingState, createNodeTelemetry, createOpenTelemetryAdapter, currentTraceContext, recordException, startSpan, telemetryResourceAttributes, toOtelAttributeValue, toOtelAttributes, withSpan };
package/dist/index.mjs CHANGED
@@ -1,2 +1,66 @@
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";
2
- export { DynamicTelemetrySampler, NodeTelemetryClient, OpenTelemetryAdapter, OpenTelemetrySpan, createNodeTelemetry, createOpenTelemetryAdapter, currentTraceContext, recordException, startSpan, telemetryResourceAttributes, toOtelAttributeValue, toOtelAttributes, withSpan };
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-80dS1PU-.mjs";
2
+ import { redactText, sanitizeAttributes } from "@davidilie/telemetry-core";
3
+ //#region src/exporter.ts
4
+ function redactSpan(span) {
5
+ const name = span.name.replace(/https?:\/\/[^\s<>"']+/gi, (value) => {
6
+ try {
7
+ return new URL(value).origin;
8
+ } catch {
9
+ return "[REDACTED_URL]";
10
+ }
11
+ });
12
+ return {
13
+ name: redactText(name, 256),
14
+ kind: span.kind,
15
+ spanContext: () => span.spanContext(),
16
+ ...span.parentSpanContext ? { parentSpanContext: span.parentSpanContext } : {},
17
+ startTime: span.startTime,
18
+ endTime: span.endTime,
19
+ duration: span.duration,
20
+ ended: span.ended,
21
+ resource: span.resource,
22
+ instrumentationScope: span.instrumentationScope,
23
+ droppedAttributesCount: span.droppedAttributesCount,
24
+ droppedEventsCount: span.droppedEventsCount,
25
+ droppedLinksCount: span.droppedLinksCount,
26
+ attributes: toOtelAttributes(sanitizeAttributes(span.attributes)),
27
+ status: {
28
+ code: span.status.code,
29
+ ...span.status.message ? { message: redactText(span.status.message) } : {}
30
+ },
31
+ events: span.events.map((event) => ({
32
+ ...event,
33
+ name: redactText(event.name, 256),
34
+ attributes: toOtelAttributes(sanitizeAttributes(event.attributes))
35
+ })),
36
+ links: span.links.map((link) => ({
37
+ ...link,
38
+ attributes: toOtelAttributes(sanitizeAttributes(link.attributes))
39
+ }))
40
+ };
41
+ }
42
+ /** Apply core privacy bounds to completed spans without mutating another processor's data. */
43
+ var RedactingSpanExporter = class {
44
+ delegate;
45
+ constructor(delegate) {
46
+ this.delegate = delegate;
47
+ }
48
+ export(spans, done) {
49
+ let sanitized;
50
+ try {
51
+ sanitized = spans.map(redactSpan);
52
+ } catch {
53
+ done({ code: 1 });
54
+ return;
55
+ }
56
+ this.delegate.export(sanitized, done);
57
+ }
58
+ shutdown() {
59
+ return this.delegate.shutdown();
60
+ }
61
+ forceFlush() {
62
+ return this.delegate.forceFlush?.() ?? Promise.resolve();
63
+ }
64
+ };
65
+ //#endregion
66
+ export { DynamicTelemetrySampler, NodeTelemetryClient, OpenTelemetryAdapter, OpenTelemetrySpan, RedactingSpanExporter, 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-vVC2xXgy.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,
@@ -21,6 +24,7 @@ function registerNodeTelemetry(config) {
21
24
  });
22
25
  const sdk = new _opentelemetry_sdk_node.NodeSDK({
23
26
  ...config.sdk,
27
+ resourceDetectors: config.sdk?.resourceDetectors ?? [],
24
28
  sampler,
25
29
  resource: (0, _opentelemetry_resources.resourceFromAttributes)(require_sampler.telemetryResourceAttributes(resource)),
26
30
  serviceName: resource.serviceName
@@ -37,13 +41,15 @@ function registerNodeTelemetry(config) {
37
41
  },
38
42
  measurementMode: config.measurementMode ?? (config.sdk?.metricReaders?.length ? "metrics" : "spans"),
39
43
  shutdown: async () => {
44
+ const registration = sharedGlobal[REGISTRATION];
45
+ if (registration?.client === client) registration.stopped = true;
40
46
  await sdk.shutdown();
41
- if (sharedGlobal[REGISTRATION]?.client === client) delete sharedGlobal[REGISTRATION];
42
47
  }
43
48
  });
44
49
  sharedGlobal[REGISTRATION] = {
45
50
  client,
46
- sdk
51
+ sdk,
52
+ stopped: false
47
53
  };
48
54
  return client;
49
55
  }
@@ -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-80dS1PU-.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,
@@ -20,6 +23,7 @@ function registerNodeTelemetry(config) {
20
23
  });
21
24
  const sdk = new NodeSDK({
22
25
  ...config.sdk,
26
+ resourceDetectors: config.sdk?.resourceDetectors ?? [],
23
27
  sampler,
24
28
  resource: resourceFromAttributes(telemetryResourceAttributes(resource)),
25
29
  serviceName: resource.serviceName
@@ -36,13 +40,15 @@ function registerNodeTelemetry(config) {
36
40
  },
37
41
  measurementMode: config.measurementMode ?? (config.sdk?.metricReaders?.length ? "metrics" : "spans"),
38
42
  shutdown: async () => {
43
+ const registration = sharedGlobal[REGISTRATION];
44
+ if (registration?.client === client) registration.stopped = true;
39
45
  await sdk.shutdown();
40
- if (sharedGlobal[REGISTRATION]?.client === client) delete sharedGlobal[REGISTRATION];
41
46
  }
42
47
  });
43
48
  sharedGlobal[REGISTRATION] = {
44
49
  client,
45
- sdk
50
+ sdk,
51
+ stopped: false
46
52
  };
47
53
  return client;
48
54
  }
@@ -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,
@@ -46,13 +62,17 @@ const severityNumbers = {
46
62
  error: SeverityNumber.ERROR
47
63
  };
48
64
  function exceptionForOtel(error) {
49
- if (error instanceof Error) {
50
- const safe = new Error(redactText(error.message), { ...error.cause !== void 0 ? { cause: redactText(String(error.cause)) } : {} });
51
- safe.name = redactText(error.name, 256);
52
- if (error.stack) safe.stack = redactText(error.stack, 16384);
53
- return safe;
65
+ try {
66
+ if (error instanceof Error) {
67
+ const safe = new Error(redactText(error.message), { ...error.cause !== void 0 ? { cause: redactText(String(error.cause)) } : {} });
68
+ safe.name = redactText(error.name, 256);
69
+ if (error.stack) safe.stack = redactText(error.stack, 16384);
70
+ return safe;
71
+ }
72
+ return redactText(typeof error === "string" ? error : String(error));
73
+ } catch {
74
+ return "Unserializable exception";
54
75
  }
55
- return redactText(typeof error === "string" ? error : String(error));
56
76
  }
57
77
  function signalAttributes(signal) {
58
78
  return {
@@ -100,6 +120,14 @@ var OpenTelemetrySpan = class {
100
120
  end() {
101
121
  this.#span.end();
102
122
  }
123
+ traceContext() {
124
+ const value = this.#span.spanContext();
125
+ return isSpanContextValid(value) ? {
126
+ traceId: value.traceId,
127
+ spanId: value.spanId,
128
+ traceFlags: value.traceFlags
129
+ } : void 0;
130
+ }
103
131
  };
104
132
  var OpenTelemetryAdapter = class {
105
133
  #instrumentationName;
@@ -112,6 +140,7 @@ var OpenTelemetryAdapter = class {
112
140
  #logSampleRate;
113
141
  #shutdown;
114
142
  #histograms = /* @__PURE__ */ new Map();
143
+ #meterProvider;
115
144
  constructor(config) {
116
145
  this.#resource = sanitizeResource(config.resource);
117
146
  this.#instrumentationName = config.instrumentationName ?? this.#resource.serviceName;
@@ -129,7 +158,7 @@ var OpenTelemetryAdapter = class {
129
158
  switch (signal.type) {
130
159
  case "event": {
131
160
  const activeSpan = trace.getActiveSpan();
132
- if (activeSpan) {
161
+ if (activeSpan?.isRecording()) {
133
162
  activeSpan.addEvent(signal.name, attributes, new Date(signal.timestamp));
134
163
  return;
135
164
  }
@@ -144,22 +173,23 @@ var OpenTelemetryAdapter = class {
144
173
  }
145
174
  case "exception": {
146
175
  const activeSpan = trace.getActiveSpan();
147
- const span = activeSpan ?? this.#tracer().startSpan("exception", {
176
+ const recordingSpan = activeSpan?.isRecording() ? activeSpan : void 0;
177
+ const span = recordingSpan ?? this.#tracer().startSpan("exception", {
148
178
  kind: SpanKind.INTERNAL,
149
179
  attributes,
150
180
  startTime: new Date(signal.timestamp)
151
181
  });
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
- });
182
+ span.addEvent("exception", {
183
+ ...attributes,
184
+ "exception.type": signal.exception.name,
185
+ "exception.message": signal.exception.message,
186
+ ...signal.exception.stack ? { "exception.stacktrace": signal.exception.stack } : {}
187
+ }, new Date(signal.timestamp));
158
188
  span.setStatus({
159
189
  code: SpanStatusCode.ERROR,
160
190
  message: signal.exception.message
161
191
  });
162
- if (!activeSpan) span.end(new Date(signal.timestamp));
192
+ if (!recordingSpan) span.end(new Date(signal.timestamp));
163
193
  return;
164
194
  }
165
195
  case "log":
@@ -200,11 +230,16 @@ var OpenTelemetryAdapter = class {
200
230
  }).end(new Date(signal.timestamp));
201
231
  if (this.#measurementMode === "spans") return;
202
232
  if (!VALID_INSTRUMENT_NAME.test(signal.name)) return;
233
+ const meterProvider = metrics.getMeterProvider();
234
+ if (meterProvider !== this.#meterProvider) {
235
+ this.#histograms.clear();
236
+ this.#meterProvider = meterProvider;
237
+ }
203
238
  const key = `${signal.name}\u0000${signal.unit ?? ""}`;
204
239
  let histogram = this.#histograms.get(key);
205
240
  if (!histogram) {
206
241
  if (this.#histograms.size >= this.#maxMetricInstruments) return;
207
- histogram = metrics.getMeter(this.#instrumentationName, this.#instrumentationVersion).createHistogram(signal.name, { ...signal.unit ? { unit: signal.unit } : {} });
242
+ histogram = meterProvider.getMeter(this.#instrumentationName, this.#instrumentationVersion).createHistogram(signal.name, { ...signal.unit ? { unit: signal.unit } : {} });
208
243
  this.#histograms.set(key, histogram);
209
244
  }
210
245
  const metricAttributes = Object.fromEntries(Object.entries(signal.attributes).filter(([name]) => this.#metricAttributeAllowlist.has(name)));
@@ -239,7 +274,7 @@ var OpenTelemetryAdapter = class {
239
274
  }, async (span) => {
240
275
  operationRan = true;
241
276
  try {
242
- operationResult = await operation();
277
+ operationResult = await operation(new OpenTelemetrySpan(span));
243
278
  operationSucceeded = true;
244
279
  } catch (error) {
245
280
  operationError = error;
@@ -261,7 +296,10 @@ var OpenTelemetryAdapter = class {
261
296
  }
262
297
  if (operationSucceeded) return operationResult;
263
298
  if (operationRan) throw operationError;
264
- return operation();
299
+ throw new Error("OpenTelemetry tracer did not invoke the span operation");
300
+ }
301
+ flush() {
302
+ return flushOpenTelemetryProviders();
265
303
  }
266
304
  async shutdown() {
267
305
  await this.#shutdown?.();
@@ -275,7 +313,7 @@ function createOpenTelemetryAdapter(config) {
275
313
  }
276
314
  function currentTraceContext() {
277
315
  const spanContext = trace.getSpan(context.active())?.spanContext();
278
- if (!spanContext) return void 0;
316
+ if (!spanContext || !isSpanContextValid(spanContext)) return void 0;
279
317
  return {
280
318
  traceId: spanContext.traceId,
281
319
  spanId: spanContext.spanId,
@@ -312,22 +350,25 @@ async function withSpan(name, operation, attributes = {}, options = {}) {
312
350
  });
313
351
  }
314
352
  function recordException(error, attributes = {}) {
315
- const activeSpan = trace.getActiveSpan();
316
- const span = activeSpan ?? trace.getTracer("@davidilie/telemetry-node").startSpan("exception");
317
- span.setAttributes(toOtelAttributes(sanitizeAttributes(attributes)));
318
- span.recordException(exceptionForOtel(error));
319
- span.setStatus({
320
- code: SpanStatusCode.ERROR,
321
- ...error instanceof Error ? { message: redactText(error.message) } : {}
322
- });
323
- if (!activeSpan) span.end();
353
+ let ownedSpan;
354
+ try {
355
+ const span = trace.getActiveSpan() ?? (ownedSpan = trace.getTracer("@davidilie/telemetry-node").startSpan("exception"));
356
+ const exception = exceptionForOtel(error);
357
+ span.setAttributes(toOtelAttributes(sanitizeAttributes(attributes)));
358
+ span.recordException(exception);
359
+ span.setStatus({
360
+ code: SpanStatusCode.ERROR,
361
+ message: typeof exception === "string" ? exception : exception.message
362
+ });
363
+ } catch {} finally {
364
+ try {
365
+ ownedSpan?.end();
366
+ } catch {}
367
+ }
324
368
  }
325
369
  //#endregion
326
370
  //#region src/client.ts
327
371
  var NodeTelemetryClient = class extends TelemetryClient {
328
- #adapter;
329
- #sampleRate;
330
- #onError;
331
372
  #onCollectionStateChange;
332
373
  #enabled;
333
374
  #consent;
@@ -353,9 +394,6 @@ var NodeTelemetryClient = class extends TelemetryClient {
353
394
  ...config.debug !== void 0 ? { debug: config.debug } : {},
354
395
  ...config.onError ? { onError: config.onError } : {}
355
396
  });
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
397
  this.#onCollectionStateChange = config.onCollectionStateChange;
360
398
  this.#enabled = config.enabled ?? true;
361
399
  this.#consent = config.consent ?? "granted";
@@ -376,26 +414,6 @@ var NodeTelemetryClient = class extends TelemetryClient {
376
414
  consent: this.#consent
377
415
  });
378
416
  }
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
417
  };
400
418
  function createNodeTelemetry(config) {
401
419
  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,
@@ -46,13 +62,17 @@ const severityNumbers = {
46
62
  error: _opentelemetry_api_logs.SeverityNumber.ERROR
47
63
  };
48
64
  function exceptionForOtel(error) {
49
- if (error instanceof Error) {
50
- const safe = new Error((0, _davidilie_telemetry_core.redactText)(error.message), { ...error.cause !== void 0 ? { cause: (0, _davidilie_telemetry_core.redactText)(String(error.cause)) } : {} });
51
- safe.name = (0, _davidilie_telemetry_core.redactText)(error.name, 256);
52
- if (error.stack) safe.stack = (0, _davidilie_telemetry_core.redactText)(error.stack, 16384);
53
- return safe;
65
+ try {
66
+ if (error instanceof Error) {
67
+ const safe = new Error((0, _davidilie_telemetry_core.redactText)(error.message), { ...error.cause !== void 0 ? { cause: (0, _davidilie_telemetry_core.redactText)(String(error.cause)) } : {} });
68
+ safe.name = (0, _davidilie_telemetry_core.redactText)(error.name, 256);
69
+ if (error.stack) safe.stack = (0, _davidilie_telemetry_core.redactText)(error.stack, 16384);
70
+ return safe;
71
+ }
72
+ return (0, _davidilie_telemetry_core.redactText)(typeof error === "string" ? error : String(error));
73
+ } catch {
74
+ return "Unserializable exception";
54
75
  }
55
- return (0, _davidilie_telemetry_core.redactText)(typeof error === "string" ? error : String(error));
56
76
  }
57
77
  function signalAttributes(signal) {
58
78
  return {
@@ -100,6 +120,14 @@ var OpenTelemetrySpan = class {
100
120
  end() {
101
121
  this.#span.end();
102
122
  }
123
+ traceContext() {
124
+ const value = this.#span.spanContext();
125
+ return (0, _opentelemetry_api.isSpanContextValid)(value) ? {
126
+ traceId: value.traceId,
127
+ spanId: value.spanId,
128
+ traceFlags: value.traceFlags
129
+ } : void 0;
130
+ }
103
131
  };
104
132
  var OpenTelemetryAdapter = class {
105
133
  #instrumentationName;
@@ -112,6 +140,7 @@ var OpenTelemetryAdapter = class {
112
140
  #logSampleRate;
113
141
  #shutdown;
114
142
  #histograms = /* @__PURE__ */ new Map();
143
+ #meterProvider;
115
144
  constructor(config) {
116
145
  this.#resource = (0, _davidilie_telemetry_core.sanitizeResource)(config.resource);
117
146
  this.#instrumentationName = config.instrumentationName ?? this.#resource.serviceName;
@@ -129,7 +158,7 @@ var OpenTelemetryAdapter = class {
129
158
  switch (signal.type) {
130
159
  case "event": {
131
160
  const activeSpan = _opentelemetry_api.trace.getActiveSpan();
132
- if (activeSpan) {
161
+ if (activeSpan?.isRecording()) {
133
162
  activeSpan.addEvent(signal.name, attributes, new Date(signal.timestamp));
134
163
  return;
135
164
  }
@@ -144,22 +173,23 @@ var OpenTelemetryAdapter = class {
144
173
  }
145
174
  case "exception": {
146
175
  const activeSpan = _opentelemetry_api.trace.getActiveSpan();
147
- const span = activeSpan ?? this.#tracer().startSpan("exception", {
176
+ const recordingSpan = activeSpan?.isRecording() ? activeSpan : void 0;
177
+ const span = recordingSpan ?? this.#tracer().startSpan("exception", {
148
178
  kind: _opentelemetry_api.SpanKind.INTERNAL,
149
179
  attributes,
150
180
  startTime: new Date(signal.timestamp)
151
181
  });
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
- });
182
+ span.addEvent("exception", {
183
+ ...attributes,
184
+ "exception.type": signal.exception.name,
185
+ "exception.message": signal.exception.message,
186
+ ...signal.exception.stack ? { "exception.stacktrace": signal.exception.stack } : {}
187
+ }, new Date(signal.timestamp));
158
188
  span.setStatus({
159
189
  code: _opentelemetry_api.SpanStatusCode.ERROR,
160
190
  message: signal.exception.message
161
191
  });
162
- if (!activeSpan) span.end(new Date(signal.timestamp));
192
+ if (!recordingSpan) span.end(new Date(signal.timestamp));
163
193
  return;
164
194
  }
165
195
  case "log":
@@ -200,11 +230,16 @@ var OpenTelemetryAdapter = class {
200
230
  }).end(new Date(signal.timestamp));
201
231
  if (this.#measurementMode === "spans") return;
202
232
  if (!VALID_INSTRUMENT_NAME.test(signal.name)) return;
233
+ const meterProvider = _opentelemetry_api.metrics.getMeterProvider();
234
+ if (meterProvider !== this.#meterProvider) {
235
+ this.#histograms.clear();
236
+ this.#meterProvider = meterProvider;
237
+ }
203
238
  const key = `${signal.name}\u0000${signal.unit ?? ""}`;
204
239
  let histogram = this.#histograms.get(key);
205
240
  if (!histogram) {
206
241
  if (this.#histograms.size >= this.#maxMetricInstruments) return;
207
- histogram = _opentelemetry_api.metrics.getMeter(this.#instrumentationName, this.#instrumentationVersion).createHistogram(signal.name, { ...signal.unit ? { unit: signal.unit } : {} });
242
+ histogram = meterProvider.getMeter(this.#instrumentationName, this.#instrumentationVersion).createHistogram(signal.name, { ...signal.unit ? { unit: signal.unit } : {} });
208
243
  this.#histograms.set(key, histogram);
209
244
  }
210
245
  const metricAttributes = Object.fromEntries(Object.entries(signal.attributes).filter(([name]) => this.#metricAttributeAllowlist.has(name)));
@@ -239,7 +274,7 @@ var OpenTelemetryAdapter = class {
239
274
  }, async (span) => {
240
275
  operationRan = true;
241
276
  try {
242
- operationResult = await operation();
277
+ operationResult = await operation(new OpenTelemetrySpan(span));
243
278
  operationSucceeded = true;
244
279
  } catch (error) {
245
280
  operationError = error;
@@ -261,7 +296,10 @@ var OpenTelemetryAdapter = class {
261
296
  }
262
297
  if (operationSucceeded) return operationResult;
263
298
  if (operationRan) throw operationError;
264
- return operation();
299
+ throw new Error("OpenTelemetry tracer did not invoke the span operation");
300
+ }
301
+ flush() {
302
+ return flushOpenTelemetryProviders();
265
303
  }
266
304
  async shutdown() {
267
305
  await this.#shutdown?.();
@@ -275,7 +313,7 @@ function createOpenTelemetryAdapter(config) {
275
313
  }
276
314
  function currentTraceContext() {
277
315
  const spanContext = _opentelemetry_api.trace.getSpan(_opentelemetry_api.context.active())?.spanContext();
278
- if (!spanContext) return void 0;
316
+ if (!spanContext || !(0, _opentelemetry_api.isSpanContextValid)(spanContext)) return void 0;
279
317
  return {
280
318
  traceId: spanContext.traceId,
281
319
  spanId: spanContext.spanId,
@@ -312,22 +350,25 @@ async function withSpan(name, operation, attributes = {}, options = {}) {
312
350
  });
313
351
  }
314
352
  function recordException(error, attributes = {}) {
315
- const activeSpan = _opentelemetry_api.trace.getActiveSpan();
316
- const span = activeSpan ?? _opentelemetry_api.trace.getTracer("@davidilie/telemetry-node").startSpan("exception");
317
- span.setAttributes(toOtelAttributes((0, _davidilie_telemetry_core.sanitizeAttributes)(attributes)));
318
- span.recordException(exceptionForOtel(error));
319
- span.setStatus({
320
- code: _opentelemetry_api.SpanStatusCode.ERROR,
321
- ...error instanceof Error ? { message: (0, _davidilie_telemetry_core.redactText)(error.message) } : {}
322
- });
323
- if (!activeSpan) span.end();
353
+ let ownedSpan;
354
+ try {
355
+ const span = _opentelemetry_api.trace.getActiveSpan() ?? (ownedSpan = _opentelemetry_api.trace.getTracer("@davidilie/telemetry-node").startSpan("exception"));
356
+ const exception = exceptionForOtel(error);
357
+ span.setAttributes(toOtelAttributes((0, _davidilie_telemetry_core.sanitizeAttributes)(attributes)));
358
+ span.recordException(exception);
359
+ span.setStatus({
360
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
361
+ message: typeof exception === "string" ? exception : exception.message
362
+ });
363
+ } catch {} finally {
364
+ try {
365
+ ownedSpan?.end();
366
+ } catch {}
367
+ }
324
368
  }
325
369
  //#endregion
326
370
  //#region src/client.ts
327
371
  var NodeTelemetryClient = class extends _davidilie_telemetry_core.TelemetryClient {
328
- #adapter;
329
- #sampleRate;
330
- #onError;
331
372
  #onCollectionStateChange;
332
373
  #enabled;
333
374
  #consent;
@@ -353,9 +394,6 @@ var NodeTelemetryClient = class extends _davidilie_telemetry_core.TelemetryClien
353
394
  ...config.debug !== void 0 ? { debug: config.debug } : {},
354
395
  ...config.onError ? { onError: config.onError } : {}
355
396
  });
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
397
  this.#onCollectionStateChange = config.onCollectionStateChange;
360
398
  this.#enabled = config.enabled ?? true;
361
399
  this.#consent = config.consent ?? "granted";
@@ -376,26 +414,6 @@ var NodeTelemetryClient = class extends _davidilie_telemetry_core.TelemetryClien
376
414
  consent: this.#consent
377
415
  });
378
416
  }
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
417
  };
400
418
  function createNodeTelemetry(config) {
401
419
  return new NodeTelemetryClient(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidilie/telemetry-node",
3
- "version": "0.2.0",
3
+ "version": "0.4.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.2.0",
68
+ "@davidilie/telemetry-core": "0.4.0",
69
69
  "@opentelemetry/api-logs": "0.221.0",
70
70
  "@opentelemetry/resources": "2.10.0",
71
71
  "@opentelemetry/sdk-node": "0.221.0",