@provide-io/telemetry 0.2.2

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.
Files changed (122) hide show
  1. package/README.md +247 -0
  2. package/dist/backpressure.d.ts +19 -0
  3. package/dist/backpressure.d.ts.map +1 -0
  4. package/dist/backpressure.js +51 -0
  5. package/dist/cardinality.d.ts +15 -0
  6. package/dist/cardinality.d.ts.map +1 -0
  7. package/dist/cardinality.js +69 -0
  8. package/dist/classification.d.ts +29 -0
  9. package/dist/classification.d.ts.map +1 -0
  10. package/dist/classification.js +58 -0
  11. package/dist/config.d.ts +156 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +350 -0
  14. package/dist/consent.d.ts +11 -0
  15. package/dist/consent.d.ts.map +1 -0
  16. package/dist/consent.js +50 -0
  17. package/dist/context.d.ts +60 -0
  18. package/dist/context.d.ts.map +1 -0
  19. package/dist/context.js +127 -0
  20. package/dist/exceptions.d.ts +14 -0
  21. package/dist/exceptions.d.ts.map +1 -0
  22. package/dist/exceptions.js +21 -0
  23. package/dist/fingerprint.d.ts +5 -0
  24. package/dist/fingerprint.d.ts.map +1 -0
  25. package/dist/fingerprint.js +50 -0
  26. package/dist/hash.d.ts +8 -0
  27. package/dist/hash.d.ts.map +1 -0
  28. package/dist/hash.js +102 -0
  29. package/dist/health.d.ts +54 -0
  30. package/dist/health.d.ts.map +1 -0
  31. package/dist/health.js +102 -0
  32. package/dist/index.d.ts +52 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +59 -0
  35. package/dist/logger.d.ts +28 -0
  36. package/dist/logger.d.ts.map +1 -0
  37. package/dist/logger.js +254 -0
  38. package/dist/metrics.d.ts +78 -0
  39. package/dist/metrics.d.ts.map +1 -0
  40. package/dist/metrics.js +238 -0
  41. package/dist/otel-logs.d.ts +29 -0
  42. package/dist/otel-logs.d.ts.map +1 -0
  43. package/dist/otel-logs.js +127 -0
  44. package/dist/otel-noop.d.ts +13 -0
  45. package/dist/otel-noop.d.ts.map +1 -0
  46. package/dist/otel-noop.js +5 -0
  47. package/dist/otel.d.ts +20 -0
  48. package/dist/otel.d.ts.map +1 -0
  49. package/dist/otel.js +80 -0
  50. package/dist/pii.d.ts +43 -0
  51. package/dist/pii.d.ts.map +1 -0
  52. package/dist/pii.js +278 -0
  53. package/dist/pretty.d.ts +12 -0
  54. package/dist/pretty.d.ts.map +1 -0
  55. package/dist/pretty.js +85 -0
  56. package/dist/propagation.d.ts +52 -0
  57. package/dist/propagation.d.ts.map +1 -0
  58. package/dist/propagation.js +183 -0
  59. package/dist/react.d.ts +38 -0
  60. package/dist/react.d.ts.map +1 -0
  61. package/dist/react.js +72 -0
  62. package/dist/receipts.d.ts +26 -0
  63. package/dist/receipts.d.ts.map +1 -0
  64. package/dist/receipts.js +69 -0
  65. package/dist/resilience.d.ts +26 -0
  66. package/dist/resilience.d.ts.map +1 -0
  67. package/dist/resilience.js +183 -0
  68. package/dist/runtime.d.ts +33 -0
  69. package/dist/runtime.d.ts.map +1 -0
  70. package/dist/runtime.js +133 -0
  71. package/dist/sampling.d.ts +9 -0
  72. package/dist/sampling.d.ts.map +1 -0
  73. package/dist/sampling.js +53 -0
  74. package/dist/sanitize.d.ts +6 -0
  75. package/dist/sanitize.d.ts.map +1 -0
  76. package/dist/sanitize.js +7 -0
  77. package/dist/schema.d.ts +41 -0
  78. package/dist/schema.d.ts.map +1 -0
  79. package/dist/schema.js +109 -0
  80. package/dist/shutdown.d.ts +2 -0
  81. package/dist/shutdown.d.ts.map +1 -0
  82. package/dist/shutdown.js +15 -0
  83. package/dist/slo.d.ts +25 -0
  84. package/dist/slo.d.ts.map +1 -0
  85. package/dist/slo.js +115 -0
  86. package/dist/testing.d.ts +10 -0
  87. package/dist/testing.d.ts.map +1 -0
  88. package/dist/testing.js +51 -0
  89. package/dist/tracing.d.ts +51 -0
  90. package/dist/tracing.d.ts.map +1 -0
  91. package/dist/tracing.js +181 -0
  92. package/package.json +139 -0
  93. package/src/backpressure.ts +68 -0
  94. package/src/cardinality.ts +83 -0
  95. package/src/classification.ts +87 -0
  96. package/src/config.ts +589 -0
  97. package/src/consent.ts +61 -0
  98. package/src/context.ts +157 -0
  99. package/src/exceptions.ts +24 -0
  100. package/src/fingerprint.ts +53 -0
  101. package/src/hash.ts +118 -0
  102. package/src/health.ts +175 -0
  103. package/src/index.ts +183 -0
  104. package/src/logger.ts +287 -0
  105. package/src/metrics.ts +204 -0
  106. package/src/otel-logs.ts +161 -0
  107. package/src/otel-noop.ts +19 -0
  108. package/src/otel.ts +112 -0
  109. package/src/pii.ts +358 -0
  110. package/src/pretty.ts +93 -0
  111. package/src/propagation.ts +222 -0
  112. package/src/react.ts +98 -0
  113. package/src/receipts.ts +97 -0
  114. package/src/resilience.ts +220 -0
  115. package/src/runtime.ts +171 -0
  116. package/src/sampling.ts +68 -0
  117. package/src/sanitize.ts +8 -0
  118. package/src/schema.ts +135 -0
  119. package/src/shutdown.ts +18 -0
  120. package/src/slo.ts +156 -0
  121. package/src/testing.ts +56 -0
  122. package/src/tracing.ts +211 -0
package/dist/slo.js ADDED
@@ -0,0 +1,115 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * SLO-oriented telemetry helpers (RED/USE baseline).
5
+ * Mirrors Python provide.telemetry.slo.
6
+ */
7
+ import { counter, gauge, histogram, } from './metrics';
8
+ import { getConfig } from './config';
9
+ const _counters = new Map();
10
+ const _histograms = new Map();
11
+ const _gauges = new Map();
12
+ function _lazyCounter(name, description) {
13
+ let c = _counters.get(name);
14
+ if (!c) {
15
+ c = counter(name, { description });
16
+ _counters.set(name, c);
17
+ }
18
+ return c;
19
+ }
20
+ function _lazyHistogram(name, description, unit) {
21
+ let h = _histograms.get(name);
22
+ if (!h) {
23
+ h = histogram(name, { description, unit });
24
+ _histograms.set(name, h);
25
+ }
26
+ return h;
27
+ }
28
+ function _lazyGauge(name, description, unit) {
29
+ let g = _gauges.get(name);
30
+ if (!g) {
31
+ g = gauge(name, { description, unit });
32
+ _gauges.set(name, g);
33
+ }
34
+ return g;
35
+ }
36
+ export function recordRedMetrics(opts) {
37
+ if (!getConfig().sloEnableRedMetrics)
38
+ return;
39
+ const attrs = {
40
+ route: opts.route,
41
+ method: opts.method,
42
+ status_code: String(opts.statusCode),
43
+ };
44
+ _lazyCounter('http.requests.total', 'Total HTTP requests').add(1, attrs);
45
+ if (opts.statusCode >= 500) {
46
+ // Stryker disable next-line StringLiteral: error counter description is not tested
47
+ _lazyCounter('http.errors.total', 'Total HTTP errors').add(1, attrs);
48
+ }
49
+ _lazyHistogram('http.request.duration_ms', 'HTTP request latency', 'ms').record(opts.durationMs, attrs);
50
+ }
51
+ export function recordUseMetrics(opts) {
52
+ if (!getConfig().sloEnableUseMetrics)
53
+ return;
54
+ const g = _lazyGauge('resource.utilization', 'Resource utilization', opts.unit ?? '%');
55
+ g.set(opts.utilization, { resource: opts.resource });
56
+ }
57
+ export function classifyError(excName, statusCode) {
58
+ const isTimeout = statusCode === 0 || excName.toLowerCase().includes('timeout');
59
+ if (isTimeout) {
60
+ return {
61
+ errorType: 'timeout',
62
+ errorCode: statusCode,
63
+ errorName: excName,
64
+ category: 'timeout',
65
+ severity: 'info',
66
+ 'error.type': excName,
67
+ 'error.category': 'timeout',
68
+ 'error.severity': 'info',
69
+ 'http.status_code': String(statusCode),
70
+ };
71
+ }
72
+ if (statusCode >= 500) {
73
+ return {
74
+ errorType: 'server',
75
+ errorCode: statusCode,
76
+ errorName: excName,
77
+ category: 'server_error',
78
+ severity: 'critical',
79
+ 'error.type': excName,
80
+ 'error.category': 'server_error',
81
+ 'error.severity': 'critical',
82
+ 'http.status_code': String(statusCode),
83
+ };
84
+ }
85
+ if (statusCode >= 400) {
86
+ const sev = statusCode === 429 ? 'critical' : 'warning';
87
+ return {
88
+ errorType: 'client',
89
+ errorCode: statusCode,
90
+ errorName: excName,
91
+ category: 'client_error',
92
+ severity: sev,
93
+ 'error.type': excName,
94
+ 'error.category': 'client_error',
95
+ 'error.severity': sev,
96
+ 'http.status_code': String(statusCode),
97
+ };
98
+ }
99
+ return {
100
+ errorType: 'unknown',
101
+ errorCode: statusCode,
102
+ errorName: excName,
103
+ category: 'unknown',
104
+ severity: 'unknown',
105
+ 'error.type': excName,
106
+ 'error.category': 'unknown',
107
+ 'error.severity': 'unknown',
108
+ 'http.status_code': String(statusCode),
109
+ };
110
+ }
111
+ export function _resetSloForTests() {
112
+ _counters.clear();
113
+ _histograms.clear();
114
+ _gauges.clear();
115
+ }
@@ -0,0 +1,10 @@
1
+ /** Reset all telemetry state (config, context, PII rules, health, queues, sampling, resilience, SLO). */
2
+ export declare function resetTelemetryState(): void;
3
+ /** Clear manual trace context (traceId / spanId set via setTraceContext). */
4
+ export declare function resetTraceContext(): void;
5
+ /** Vitest plugin for automatic per-test telemetry isolation. */
6
+ export declare const telemetryTestPlugin: {
7
+ beforeEach(): void;
8
+ afterEach(): void;
9
+ };
10
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAuBA,yGAAyG;AACzG,wBAAgB,mBAAmB,IAAI,IAAI,CAc1C;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,gEAAgE;AAChE,eAAO,MAAM,mBAAmB;kBAChB,IAAI;iBAIL,IAAI;CAIlB,CAAC"}
@@ -0,0 +1,51 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Test utilities — reset all telemetry state between tests.
5
+ * Mirrors Python provide.telemetry.testing.
6
+ */
7
+ import { _resetConfig } from './config';
8
+ import { _resetContext } from './context';
9
+ import { _resetHealthForTests } from './health';
10
+ import { _resetBackpressureForTests } from './backpressure';
11
+ import { _resetCardinalityForTests } from './cardinality';
12
+ import { _resetSamplingForTests } from './sampling';
13
+ import { _resetResilienceForTests } from './resilience';
14
+ import { resetPiiRulesForTests } from './pii';
15
+ import { _resetSloForTests } from './slo';
16
+ import { _resetPropagationForTests } from './propagation';
17
+ import { _resetRootLogger } from './logger';
18
+ import { _resetOtelLogProviderForTests } from './otel-logs';
19
+ import { _resetTraceContext } from './tracing';
20
+ import { _resetRuntimeForTests } from './runtime';
21
+ /** Reset all telemetry state (config, context, PII rules, health, queues, sampling, resilience, SLO). */
22
+ export function resetTelemetryState() {
23
+ _resetConfig();
24
+ _resetContext();
25
+ _resetHealthForTests();
26
+ _resetBackpressureForTests();
27
+ _resetCardinalityForTests();
28
+ _resetSamplingForTests();
29
+ _resetResilienceForTests();
30
+ resetPiiRulesForTests();
31
+ _resetSloForTests();
32
+ _resetPropagationForTests();
33
+ _resetRootLogger();
34
+ _resetOtelLogProviderForTests();
35
+ _resetRuntimeForTests();
36
+ }
37
+ /** Clear manual trace context (traceId / spanId set via setTraceContext). */
38
+ export function resetTraceContext() {
39
+ _resetTraceContext();
40
+ }
41
+ /** Vitest plugin for automatic per-test telemetry isolation. */
42
+ export const telemetryTestPlugin = {
43
+ beforeEach() {
44
+ resetTelemetryState();
45
+ resetTraceContext();
46
+ },
47
+ afterEach() {
48
+ resetTelemetryState();
49
+ resetTraceContext();
50
+ },
51
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Tracing helpers — mirrors Python provide.telemetry @trace decorator and tracer access.
3
+ *
4
+ * Uses @opentelemetry/api which provides no-op implementations when no SDK is registered.
5
+ * withTrace() / @trace work safely without any OTEL setup; they just don't export spans.
6
+ */
7
+ import { type Tracer } from '@opentelemetry/api';
8
+ /**
9
+ * Manually inject trace/span IDs (e.g. from an incoming request header).
10
+ * Returned by getTraceContext(); cleared by _resetTraceContext().
11
+ */
12
+ export declare function setTraceContext(traceId: string, spanId: string): void;
13
+ /**
14
+ * Return the current trace context: manual injection first, then active OTEL span.
15
+ */
16
+ export declare function getTraceContext(): {
17
+ trace_id?: string;
18
+ span_id?: string;
19
+ };
20
+ /** Reset manually injected trace context (used in tests). */
21
+ export declare function _resetTraceContext(): void;
22
+ /** Return the tracer for the telemetry library. Noop when no SDK is registered. */
23
+ export declare function getTracer(): Tracer;
24
+ /** Module-level lazy singleton tracer — resolves provider at call time. */
25
+ export declare const tracer: Tracer;
26
+ /**
27
+ * Return trace_id and span_id from the currently active OTEL span.
28
+ * Returns an empty object when no span is active or OTEL is not configured.
29
+ */
30
+ export declare function getActiveTraceIds(): {
31
+ trace_id?: string;
32
+ span_id?: string;
33
+ };
34
+ /**
35
+ * Execute fn inside a new span named `name`.
36
+ * Works for both sync and async functions.
37
+ * Mirrors Python @trace decorator behaviour: records exceptions, sets ERROR status.
38
+ */
39
+ export declare function withTrace<T>(name: string, fn: () => T): T;
40
+ /**
41
+ * Method/function decorator that wraps the target in withTrace().
42
+ * Span name defaults to the decorated method name.
43
+ *
44
+ * Usage (requires experimentalDecorators: true):
45
+ * class Foo {
46
+ * @trace('my.operation')
47
+ * doWork() { ... }
48
+ * }
49
+ */
50
+ export declare function traceDecorator(name?: string): (_target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => PropertyDescriptor;
51
+ //# sourceMappingURL=tracing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../src/tracing.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,MAAM,EAKZ,MAAM,oBAAoB,CAAC;AAW5B;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAGrE;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAezE;AAED,6DAA6D;AAC7D,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC;AAED,mFAAmF;AACnF,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,2EAA2E;AAC3E,eAAO,MAAM,MAAM,EAAE,MAAqC,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAO3E;AAsED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CA0BzD;AAED;;;;;;;;;GASG;AAEH,wBAAgB,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,IAExC,SAAS,MAAM,EACf,aAAa,MAAM,GAAG,MAAM,EAC5B,YAAY,kBAAkB,KAC7B,kBAAkB,CAUtB"}
@@ -0,0 +1,181 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Tracing helpers — mirrors Python provide.telemetry @trace decorator and tracer access.
5
+ *
6
+ * Uses @opentelemetry/api which provides no-op implementations when no SDK is registered.
7
+ * withTrace() / @trace work safely without any OTEL setup; they just don't export spans.
8
+ */
9
+ import { SpanStatusCode, trace, context as otelContext, } from '@opentelemetry/api';
10
+ import { getActiveOtelContext } from './propagation';
11
+ import { randomHex } from './hash';
12
+ // Stryker disable next-line StringLiteral: tracer name is not observable without a real SDK
13
+ const TRACER_NAME = '@provide-io/telemetry';
14
+ // ── Manual trace context (injected without an active OTEL span) ───────────────
15
+ let _manualTraceId;
16
+ let _manualSpanId;
17
+ /**
18
+ * Manually inject trace/span IDs (e.g. from an incoming request header).
19
+ * Returned by getTraceContext(); cleared by _resetTraceContext().
20
+ */
21
+ export function setTraceContext(traceId, spanId) {
22
+ _manualTraceId = traceId;
23
+ _manualSpanId = spanId;
24
+ }
25
+ /**
26
+ * Return the current trace context: manual injection first, then active OTEL span.
27
+ */
28
+ export function getTraceContext() {
29
+ // Stryker disable next-line ConditionalExpression,LogicalOperator: setTraceContext always sets both; partial state not reachable via public API
30
+ if (_manualTraceId !== undefined || _manualSpanId !== undefined) {
31
+ return {
32
+ // Stryker disable next-line ConditionalExpression: _manualTraceId is always defined here (both set together)
33
+ ...(_manualTraceId !== undefined && { trace_id: _manualTraceId }),
34
+ // Stryker disable next-line ConditionalExpression: _manualSpanId is always defined here (both set together)
35
+ ...(_manualSpanId !== undefined && { span_id: _manualSpanId }),
36
+ };
37
+ }
38
+ const ids = getActiveTraceIds();
39
+ return {
40
+ ...(ids.trace_id !== undefined && { trace_id: ids.trace_id }),
41
+ ...(ids.span_id !== undefined && { span_id: ids.span_id }),
42
+ };
43
+ }
44
+ /** Reset manually injected trace context (used in tests). */
45
+ export function _resetTraceContext() {
46
+ _manualTraceId = undefined;
47
+ _manualSpanId = undefined;
48
+ }
49
+ /** Return the tracer for the telemetry library. Noop when no SDK is registered. */
50
+ export function getTracer() {
51
+ return trace.getTracer(TRACER_NAME);
52
+ }
53
+ /** Module-level lazy singleton tracer — resolves provider at call time. */
54
+ export const tracer = trace.getTracer(TRACER_NAME);
55
+ /**
56
+ * Return trace_id and span_id from the currently active OTEL span.
57
+ * Returns an empty object when no span is active or OTEL is not configured.
58
+ */
59
+ export function getActiveTraceIds() {
60
+ const span = trace.getActiveSpan();
61
+ if (!span)
62
+ return {};
63
+ const ctx = span.spanContext();
64
+ // OTEL no-op spans have all-zero IDs — treat as no active span.
65
+ if (ctx.traceId === '00000000000000000000000000000000')
66
+ return {};
67
+ return { trace_id: ctx.traceId, span_id: ctx.spanId };
68
+ }
69
+ const NOOP_TRACE_ID = '00000000000000000000000000000000';
70
+ /**
71
+ * Return true if the given span is a no-op (all-zero trace ID).
72
+ * Used to decide whether to inject synthetic random IDs.
73
+ */
74
+ function _isNoopSpan(span) {
75
+ return span.spanContext().traceId === NOOP_TRACE_ID;
76
+ }
77
+ /**
78
+ * Execute fn with random synthetic trace/span IDs set as manual context,
79
+ * then restore the previous manual context when done.
80
+ * Handles both sync and async results.
81
+ */
82
+ function _withSyntheticIds(fn) {
83
+ const prevTraceId = _manualTraceId;
84
+ const prevSpanId = _manualSpanId;
85
+ // Stryker disable next-line StringLiteral: random IDs are non-deterministic — exact value not observable in mutations
86
+ setTraceContext(randomHex(16), randomHex(8));
87
+ const result = fn();
88
+ if (result instanceof Promise) {
89
+ return result.then((value) => {
90
+ _manualTraceId = prevTraceId;
91
+ _manualSpanId = prevSpanId;
92
+ return value;
93
+ }, (err) => {
94
+ _manualTraceId = prevTraceId;
95
+ _manualSpanId = prevSpanId;
96
+ throw err;
97
+ });
98
+ }
99
+ _manualTraceId = prevTraceId;
100
+ _manualSpanId = prevSpanId;
101
+ return result;
102
+ }
103
+ /** Shared span handler for withTrace — records exceptions and sets ERROR status. */
104
+ function _spanHandler(fn, span) {
105
+ try {
106
+ const result = fn();
107
+ if (result instanceof Promise) {
108
+ return result.then((value) => {
109
+ span.end();
110
+ return value;
111
+ }, (err) => {
112
+ span.recordException(err instanceof Error ? err : new Error(String(err)));
113
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
114
+ span.end();
115
+ throw err;
116
+ });
117
+ }
118
+ span.end();
119
+ return result;
120
+ }
121
+ catch (err) {
122
+ span.recordException(err instanceof Error ? err : new Error(String(err)));
123
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
124
+ span.end();
125
+ throw err;
126
+ }
127
+ }
128
+ /**
129
+ * Execute fn inside a new span named `name`.
130
+ * Works for both sync and async functions.
131
+ * Mirrors Python @trace decorator behaviour: records exceptions, sets ERROR status.
132
+ */
133
+ export function withTrace(name, fn) {
134
+ const tracer = trace.getTracer(TRACER_NAME);
135
+ // If an OTel context was extracted from propagation headers, use it as parent.
136
+ try {
137
+ const activeCtx = getActiveOtelContext();
138
+ if (activeCtx) {
139
+ return otelContext.with(activeCtx, () => tracer.startActiveSpan(name, (span) => {
140
+ // Stryker disable next-line ConditionalExpression: noop detection is not observable without SDK — branch outcome equivalent under mutation
141
+ /* v8 ignore start: noop-span false branch + real-span return are unreachable without a registered OTel provider */
142
+ if (_isNoopSpan(span))
143
+ return _withSyntheticIds(() => _spanHandler(fn, span));
144
+ return _spanHandler(fn, span);
145
+ /* v8 ignore stop */
146
+ }));
147
+ }
148
+ }
149
+ catch {
150
+ // Graceful degradation — fall through to default behaviour.
151
+ }
152
+ return tracer.startActiveSpan(name, (span) => {
153
+ // Stryker disable next-line ConditionalExpression: noop detection is not observable without SDK — branch outcome equivalent under mutation
154
+ if (_isNoopSpan(span))
155
+ return _withSyntheticIds(() => _spanHandler(fn, span));
156
+ return _spanHandler(fn, span);
157
+ });
158
+ }
159
+ /**
160
+ * Method/function decorator that wraps the target in withTrace().
161
+ * Span name defaults to the decorated method name.
162
+ *
163
+ * Usage (requires experimentalDecorators: true):
164
+ * class Foo {
165
+ * @trace('my.operation')
166
+ * doWork() { ... }
167
+ * }
168
+ */
169
+ // Stryker disable BlockStatement: outer and inner decorator fns returning undefined leave descriptors unchanged — equivalent
170
+ export function traceDecorator(name) {
171
+ return function (_target, propertyKey, descriptor) {
172
+ // Stryker enable BlockStatement
173
+ // Stryker disable next-line LogicalOperator: span name not observable with no-op tracer
174
+ const spanName = name ?? String(propertyKey);
175
+ const original = descriptor.value;
176
+ descriptor.value = function (...args) {
177
+ return withTrace(spanName, () => original.apply(this, args));
178
+ };
179
+ return descriptor;
180
+ };
181
+ }
package/package.json ADDED
@@ -0,0 +1,139 @@
1
+ {
2
+ "name": "@provide-io/telemetry",
3
+ "version": "0.2.2",
4
+ "description": "Structured logging, OTEL traces/metrics for TypeScript frontends — parity with provide.telemetry Python package",
5
+ "license": "Apache-2.0",
6
+ "author": "provide.io llc",
7
+ "homepage": "https://github.com/provide-io/provide-telemetry#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/provide-io/provide-telemetry.git",
11
+ "directory": "typescript"
12
+ },
13
+ "keywords": [
14
+ "telemetry",
15
+ "observability",
16
+ "opentelemetry",
17
+ "otel",
18
+ "tracing",
19
+ "metrics",
20
+ "logging",
21
+ "structlog",
22
+ "pino",
23
+ "w3c-traceparent"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ },
35
+ "./otel": {
36
+ "types": "./dist/otel.d.ts",
37
+ "browser": "./dist/otel-noop.js",
38
+ "default": "./dist/otel.js"
39
+ },
40
+ "./react": {
41
+ "types": "./dist/react.d.ts",
42
+ "import": "./dist/react.js"
43
+ }
44
+ },
45
+ "main": "./dist/index.js",
46
+ "types": "./dist/index.d.ts",
47
+ "files": [
48
+ "dist",
49
+ "src"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsc",
53
+ "typecheck": "tsc --noEmit -p tsconfig.test.json",
54
+ "lint": "eslint src tests --max-warnings=0",
55
+ "lint:fix": "eslint src tests --fix",
56
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
57
+ "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest",
60
+ "test:coverage": "vitest run --coverage",
61
+ "test:mutation": "stryker run",
62
+ "test:memory": "npx tsx scripts/memory-audit.ts",
63
+ "perf": "tsx scripts/perf-smoke.ts",
64
+ "stress": "tsx scripts/stress-logging.ts && tsx scripts/stress-backpressure.ts && tsx scripts/stress-metrics.ts",
65
+ "bundle-size": "tsx scripts/bundle-size.ts",
66
+ "prepublishOnly": "npm run build && npm run test:coverage"
67
+ },
68
+ "dependencies": {
69
+ "@opentelemetry/api": "^1.9.1",
70
+ "pino": "^10.3.1"
71
+ },
72
+ "peerDependencies": {
73
+ "@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
74
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0",
75
+ "@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
76
+ "@opentelemetry/resources": "^2.6.1",
77
+ "@opentelemetry/sdk-logs": "^0.214.0",
78
+ "@opentelemetry/sdk-metrics": "^2.6.1",
79
+ "@opentelemetry/sdk-trace-base": "^2.6.1",
80
+ "react": ">=18"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "@opentelemetry/exporter-logs-otlp-http": {
84
+ "optional": true
85
+ },
86
+ "@opentelemetry/exporter-metrics-otlp-http": {
87
+ "optional": true
88
+ },
89
+ "@opentelemetry/exporter-trace-otlp-http": {
90
+ "optional": true
91
+ },
92
+ "@opentelemetry/resources": {
93
+ "optional": true
94
+ },
95
+ "@opentelemetry/sdk-logs": {
96
+ "optional": true
97
+ },
98
+ "@opentelemetry/sdk-metrics": {
99
+ "optional": true
100
+ },
101
+ "@opentelemetry/sdk-trace-base": {
102
+ "optional": true
103
+ },
104
+ "react": {
105
+ "optional": false
106
+ }
107
+ },
108
+ "devDependencies": {
109
+ "@eslint/js": "^10.0.1",
110
+ "@opentelemetry/context-async-hooks": "^2.6.1",
111
+ "@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
112
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0",
113
+ "@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
114
+ "@opentelemetry/resources": "^2.6.1",
115
+ "@opentelemetry/sdk-metrics": "^2.6.1",
116
+ "@opentelemetry/sdk-trace-base": "^2.6.1",
117
+ "@stryker-mutator/core": "^9.6.0",
118
+ "@stryker-mutator/vitest-runner": "^9.6.0",
119
+ "@testing-library/dom": "^10.4.1",
120
+ "@testing-library/react": "^16.0.0",
121
+ "@types/node": "^25.5.0",
122
+ "@types/react": "^18.0.0",
123
+ "@types/react-dom": "^18.3.7",
124
+ "@vitest/coverage-v8": "^4.1.2",
125
+ "eslint": "^10.1.0",
126
+ "eslint-config-prettier": "^10.0.0",
127
+ "fast-check": "^4.6.0",
128
+ "globals": "^17.4.0",
129
+ "happy-dom": "^20.8.8",
130
+ "prettier": "^3.0.0",
131
+ "react": "^18.0.0",
132
+ "react-dom": "^18.3.1",
133
+ "tsx": "^4.21.0",
134
+ "typescript": "^6.0.2",
135
+ "typescript-eslint": "^8.57.2",
136
+ "vite": "^8.0.3",
137
+ "vitest": "^4.1.2"
138
+ }
139
+ }
@@ -0,0 +1,68 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Bounded queue controls for telemetry signal paths.
6
+ * Mirrors Python provide.telemetry.backpressure.
7
+ */
8
+
9
+ export interface QueuePolicy {
10
+ maxLogs: number;
11
+ maxTraces: number;
12
+ maxMetrics: number;
13
+ }
14
+
15
+ export interface QueueTicket {
16
+ signal: 'logs' | 'traces' | 'metrics';
17
+ token: number;
18
+ }
19
+
20
+ const DEFAULT_POLICY: QueuePolicy = { maxLogs: 0, maxTraces: 0, maxMetrics: 0 };
21
+
22
+ let _policy: QueuePolicy = { ...DEFAULT_POLICY };
23
+ let _tokenCounter = 1;
24
+ const _acquired: Map<string, Set<number>> = new Map([
25
+ ['logs', new Set()],
26
+ ['traces', new Set()],
27
+ ['metrics', new Set()],
28
+ ]);
29
+
30
+ export function setQueuePolicy(policy: Partial<QueuePolicy>): void {
31
+ _policy = { ..._policy, ...policy };
32
+ }
33
+
34
+ export function getQueuePolicy(): QueuePolicy {
35
+ return { ..._policy };
36
+ }
37
+
38
+ function _maxFor(signal: QueueTicket['signal']): number {
39
+ if (signal === 'logs') return _policy.maxLogs;
40
+ if (signal === 'traces') return _policy.maxTraces;
41
+ return _policy.maxMetrics;
42
+ }
43
+
44
+ export function tryAcquire(signal: QueueTicket['signal']): QueueTicket | null {
45
+ const max = _maxFor(signal);
46
+ if (max <= 0) return { signal, token: 0 };
47
+ const set = _acquired.get(signal);
48
+ // Stryker disable next-line ConditionalExpression: defensive guard — _acquired is initialized with all three signal keys, so get() never returns undefined
49
+ /* v8 ignore next */
50
+ if (!set) return null;
51
+ if (set.size >= max) return null;
52
+ const token = _tokenCounter++;
53
+ set.add(token);
54
+ return { signal, token };
55
+ }
56
+
57
+ export function release(ticket: QueueTicket): void {
58
+ // Stryker disable next-line ConditionalExpression: token=0 is the unlimited-queue sentinel; delete(0) from set is always a no-op
59
+ if (ticket.token === 0) return;
60
+ // Stryker disable next-line OptionalChaining: signal is always a key in _acquired (initialized in the Map constructor)
61
+ _acquired.get(ticket.signal)?.delete(ticket.token);
62
+ }
63
+
64
+ export function _resetBackpressureForTests(): void {
65
+ _policy = { ...DEFAULT_POLICY };
66
+ _tokenCounter = 1;
67
+ for (const set of _acquired.values()) set.clear();
68
+ }