@davidilie/telemetry-node 0.3.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
@@ -55,6 +55,16 @@ otherwise silently use a stopped provider. Do not register this package beside a
55
55
  The optional `sdk` property accepts `NodeSDKConfiguration` fields other than
56
56
  `resource` and `serviceName`. Use it for maintained instrumentations, readers,
57
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.
58
68
 
59
69
  ## Export configuration
60
70
 
@@ -62,7 +72,6 @@ The standalone SDK follows standard OpenTelemetry environment variables. For
62
72
  Alloy's private OTLP/HTTP receiver:
63
73
 
64
74
  ```sh
65
- OTEL_SERVICE_NAME=email-worker
66
75
  OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy.observability.svc.cluster.local:4318
67
76
  OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
68
77
  ```
package/dist/index.cjs CHANGED
@@ -1,9 +1,74 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sampler = require("./sampler-D5mi13Eh.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
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
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-B1PsF--M.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-D5mi13Eh.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");
@@ -24,6 +24,7 @@ function registerNodeTelemetry(config) {
24
24
  });
25
25
  const sdk = new _opentelemetry_sdk_node.NodeSDK({
26
26
  ...config.sdk,
27
+ resourceDetectors: config.sdk?.resourceDetectors ?? [],
27
28
  sampler,
28
29
  resource: (0, _opentelemetry_resources.resourceFromAttributes)(require_sampler.telemetryResourceAttributes(resource)),
29
30
  serviceName: resource.serviceName
package/dist/register.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { d as telemetryResourceAttributes, n as NodeTelemetryClient, t as DynamicTelemetrySampler } from "./sampler-B1PsF--M.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";
@@ -23,6 +23,7 @@ function registerNodeTelemetry(config) {
23
23
  });
24
24
  const sdk = new NodeSDK({
25
25
  ...config.sdk,
26
+ resourceDetectors: config.sdk?.resourceDetectors ?? [],
26
27
  sampler,
27
28
  resource: resourceFromAttributes(telemetryResourceAttributes(resource)),
28
29
  serviceName: resource.serviceName
@@ -62,13 +62,17 @@ const severityNumbers = {
62
62
  error: SeverityNumber.ERROR
63
63
  };
64
64
  function exceptionForOtel(error) {
65
- if (error instanceof Error) {
66
- const safe = new Error(redactText(error.message), { ...error.cause !== void 0 ? { cause: redactText(String(error.cause)) } : {} });
67
- safe.name = redactText(error.name, 256);
68
- if (error.stack) safe.stack = redactText(error.stack, 16384);
69
- 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";
70
75
  }
71
- return redactText(typeof error === "string" ? error : String(error));
72
76
  }
73
77
  function signalAttributes(signal) {
74
78
  return {
@@ -136,6 +140,7 @@ var OpenTelemetryAdapter = class {
136
140
  #logSampleRate;
137
141
  #shutdown;
138
142
  #histograms = /* @__PURE__ */ new Map();
143
+ #meterProvider;
139
144
  constructor(config) {
140
145
  this.#resource = sanitizeResource(config.resource);
141
146
  this.#instrumentationName = config.instrumentationName ?? this.#resource.serviceName;
@@ -225,11 +230,16 @@ var OpenTelemetryAdapter = class {
225
230
  }).end(new Date(signal.timestamp));
226
231
  if (this.#measurementMode === "spans") return;
227
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
+ }
228
238
  const key = `${signal.name}\u0000${signal.unit ?? ""}`;
229
239
  let histogram = this.#histograms.get(key);
230
240
  if (!histogram) {
231
241
  if (this.#histograms.size >= this.#maxMetricInstruments) return;
232
- 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 } : {} });
233
243
  this.#histograms.set(key, histogram);
234
244
  }
235
245
  const metricAttributes = Object.fromEntries(Object.entries(signal.attributes).filter(([name]) => this.#metricAttributeAllowlist.has(name)));
@@ -340,15 +350,21 @@ async function withSpan(name, operation, attributes = {}, options = {}) {
340
350
  });
341
351
  }
342
352
  function recordException(error, attributes = {}) {
343
- const activeSpan = trace.getActiveSpan();
344
- const span = activeSpan ?? trace.getTracer("@davidilie/telemetry-node").startSpan("exception");
345
- span.setAttributes(toOtelAttributes(sanitizeAttributes(attributes)));
346
- span.recordException(exceptionForOtel(error));
347
- span.setStatus({
348
- code: SpanStatusCode.ERROR,
349
- ...error instanceof Error ? { message: redactText(error.message) } : {}
350
- });
351
- 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
+ }
352
368
  }
353
369
  //#endregion
354
370
  //#region src/client.ts
@@ -62,13 +62,17 @@ const severityNumbers = {
62
62
  error: _opentelemetry_api_logs.SeverityNumber.ERROR
63
63
  };
64
64
  function exceptionForOtel(error) {
65
- if (error instanceof Error) {
66
- const safe = new Error((0, _davidilie_telemetry_core.redactText)(error.message), { ...error.cause !== void 0 ? { cause: (0, _davidilie_telemetry_core.redactText)(String(error.cause)) } : {} });
67
- safe.name = (0, _davidilie_telemetry_core.redactText)(error.name, 256);
68
- if (error.stack) safe.stack = (0, _davidilie_telemetry_core.redactText)(error.stack, 16384);
69
- 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";
70
75
  }
71
- return (0, _davidilie_telemetry_core.redactText)(typeof error === "string" ? error : String(error));
72
76
  }
73
77
  function signalAttributes(signal) {
74
78
  return {
@@ -136,6 +140,7 @@ var OpenTelemetryAdapter = class {
136
140
  #logSampleRate;
137
141
  #shutdown;
138
142
  #histograms = /* @__PURE__ */ new Map();
143
+ #meterProvider;
139
144
  constructor(config) {
140
145
  this.#resource = (0, _davidilie_telemetry_core.sanitizeResource)(config.resource);
141
146
  this.#instrumentationName = config.instrumentationName ?? this.#resource.serviceName;
@@ -225,11 +230,16 @@ var OpenTelemetryAdapter = class {
225
230
  }).end(new Date(signal.timestamp));
226
231
  if (this.#measurementMode === "spans") return;
227
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
+ }
228
238
  const key = `${signal.name}\u0000${signal.unit ?? ""}`;
229
239
  let histogram = this.#histograms.get(key);
230
240
  if (!histogram) {
231
241
  if (this.#histograms.size >= this.#maxMetricInstruments) return;
232
- 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 } : {} });
233
243
  this.#histograms.set(key, histogram);
234
244
  }
235
245
  const metricAttributes = Object.fromEntries(Object.entries(signal.attributes).filter(([name]) => this.#metricAttributeAllowlist.has(name)));
@@ -340,15 +350,21 @@ async function withSpan(name, operation, attributes = {}, options = {}) {
340
350
  });
341
351
  }
342
352
  function recordException(error, attributes = {}) {
343
- const activeSpan = _opentelemetry_api.trace.getActiveSpan();
344
- const span = activeSpan ?? _opentelemetry_api.trace.getTracer("@davidilie/telemetry-node").startSpan("exception");
345
- span.setAttributes(toOtelAttributes((0, _davidilie_telemetry_core.sanitizeAttributes)(attributes)));
346
- span.recordException(exceptionForOtel(error));
347
- span.setStatus({
348
- code: _opentelemetry_api.SpanStatusCode.ERROR,
349
- ...error instanceof Error ? { message: (0, _davidilie_telemetry_core.redactText)(error.message) } : {}
350
- });
351
- 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
+ }
352
368
  }
353
369
  //#endregion
354
370
  //#region src/client.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidilie/telemetry-node",
3
- "version": "0.3.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.3.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",