@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/src/slo.ts ADDED
@@ -0,0 +1,156 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * SLO-oriented telemetry helpers (RED/USE baseline).
6
+ * Mirrors Python provide.telemetry.slo.
7
+ */
8
+
9
+ import {
10
+ type CounterInstrument,
11
+ type GaugeInstrument,
12
+ type HistogramInstrument,
13
+ counter,
14
+ gauge,
15
+ histogram,
16
+ } from './metrics';
17
+ import { getConfig } from './config';
18
+
19
+ const _counters = new Map<string, CounterInstrument>();
20
+ const _histograms = new Map<string, HistogramInstrument>();
21
+ const _gauges = new Map<string, GaugeInstrument>();
22
+
23
+ function _lazyCounter(name: string, description: string): CounterInstrument {
24
+ let c = _counters.get(name);
25
+ if (!c) {
26
+ c = counter(name, { description });
27
+ _counters.set(name, c);
28
+ }
29
+ return c;
30
+ }
31
+
32
+ function _lazyHistogram(name: string, description: string, unit: string): HistogramInstrument {
33
+ let h = _histograms.get(name);
34
+ if (!h) {
35
+ h = histogram(name, { description, unit });
36
+ _histograms.set(name, h);
37
+ }
38
+ return h;
39
+ }
40
+
41
+ function _lazyGauge(name: string, description: string, unit: string): GaugeInstrument {
42
+ let g = _gauges.get(name);
43
+ if (!g) {
44
+ g = gauge(name, { description, unit });
45
+ _gauges.set(name, g);
46
+ }
47
+ return g;
48
+ }
49
+
50
+ export function recordRedMetrics(opts: {
51
+ route: string;
52
+ method: string;
53
+ statusCode: number;
54
+ durationMs: number;
55
+ }): void {
56
+ if (!getConfig().sloEnableRedMetrics) return;
57
+ const attrs = {
58
+ route: opts.route,
59
+ method: opts.method,
60
+ status_code: String(opts.statusCode),
61
+ };
62
+ _lazyCounter('http.requests.total', 'Total HTTP requests').add(1, attrs);
63
+ if (opts.statusCode >= 500) {
64
+ // Stryker disable next-line StringLiteral: error counter description is not tested
65
+ _lazyCounter('http.errors.total', 'Total HTTP errors').add(1, attrs);
66
+ }
67
+ _lazyHistogram('http.request.duration_ms', 'HTTP request latency', 'ms').record(
68
+ opts.durationMs,
69
+ attrs,
70
+ );
71
+ }
72
+
73
+ export function recordUseMetrics(opts: {
74
+ resource: string;
75
+ utilization: number;
76
+ unit?: string;
77
+ }): void {
78
+ if (!getConfig().sloEnableUseMetrics) return;
79
+ const g = _lazyGauge('resource.utilization', 'Resource utilization', opts.unit ?? '%');
80
+ g.set(opts.utilization, { resource: opts.resource });
81
+ }
82
+
83
+ export interface ErrorClassification {
84
+ errorType: 'server' | 'client' | 'timeout' | 'unknown';
85
+ errorCode: number;
86
+ errorName: string;
87
+ category: 'server_error' | 'client_error' | 'timeout' | 'unknown';
88
+ severity: 'critical' | 'warning' | 'info' | 'unknown';
89
+ // OTel-aligned keys for cross-language parity with Go/Python
90
+ 'error.type': string;
91
+ 'error.category': string;
92
+ 'error.severity': string;
93
+ 'http.status_code': string;
94
+ }
95
+
96
+ export function classifyError(excName: string, statusCode: number): ErrorClassification {
97
+ const isTimeout = statusCode === 0 || excName.toLowerCase().includes('timeout');
98
+
99
+ if (isTimeout) {
100
+ return {
101
+ errorType: 'timeout',
102
+ errorCode: statusCode,
103
+ errorName: excName,
104
+ category: 'timeout',
105
+ severity: 'info',
106
+ 'error.type': excName,
107
+ 'error.category': 'timeout',
108
+ 'error.severity': 'info',
109
+ 'http.status_code': String(statusCode),
110
+ };
111
+ }
112
+ if (statusCode >= 500) {
113
+ return {
114
+ errorType: 'server',
115
+ errorCode: statusCode,
116
+ errorName: excName,
117
+ category: 'server_error',
118
+ severity: 'critical',
119
+ 'error.type': excName,
120
+ 'error.category': 'server_error',
121
+ 'error.severity': 'critical',
122
+ 'http.status_code': String(statusCode),
123
+ };
124
+ }
125
+ if (statusCode >= 400) {
126
+ const sev = statusCode === 429 ? 'critical' : 'warning';
127
+ return {
128
+ errorType: 'client',
129
+ errorCode: statusCode,
130
+ errorName: excName,
131
+ category: 'client_error',
132
+ severity: sev as 'critical' | 'warning',
133
+ 'error.type': excName,
134
+ 'error.category': 'client_error',
135
+ 'error.severity': sev,
136
+ 'http.status_code': String(statusCode),
137
+ };
138
+ }
139
+ return {
140
+ errorType: 'unknown',
141
+ errorCode: statusCode,
142
+ errorName: excName,
143
+ category: 'unknown',
144
+ severity: 'unknown',
145
+ 'error.type': excName,
146
+ 'error.category': 'unknown',
147
+ 'error.severity': 'unknown',
148
+ 'http.status_code': String(statusCode),
149
+ };
150
+ }
151
+
152
+ export function _resetSloForTests(): void {
153
+ _counters.clear();
154
+ _histograms.clear();
155
+ _gauges.clear();
156
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,56 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Test utilities — reset all telemetry state between tests.
6
+ * Mirrors Python provide.telemetry.testing.
7
+ */
8
+
9
+ import { _resetConfig } from './config';
10
+ import { _resetContext } from './context';
11
+ import { _resetHealthForTests } from './health';
12
+ import { _resetBackpressureForTests } from './backpressure';
13
+ import { _resetCardinalityForTests } from './cardinality';
14
+ import { _resetSamplingForTests } from './sampling';
15
+ import { _resetResilienceForTests } from './resilience';
16
+ import { resetPiiRulesForTests } from './pii';
17
+ import { _resetSloForTests } from './slo';
18
+ import { _resetPropagationForTests } from './propagation';
19
+ import { _resetRootLogger } from './logger';
20
+ import { _resetOtelLogProviderForTests } from './otel-logs';
21
+ import { _resetTraceContext } from './tracing';
22
+ import { _resetRuntimeForTests } from './runtime';
23
+
24
+ /** Reset all telemetry state (config, context, PII rules, health, queues, sampling, resilience, SLO). */
25
+ export function resetTelemetryState(): void {
26
+ _resetConfig();
27
+ _resetContext();
28
+ _resetHealthForTests();
29
+ _resetBackpressureForTests();
30
+ _resetCardinalityForTests();
31
+ _resetSamplingForTests();
32
+ _resetResilienceForTests();
33
+ resetPiiRulesForTests();
34
+ _resetSloForTests();
35
+ _resetPropagationForTests();
36
+ _resetRootLogger();
37
+ _resetOtelLogProviderForTests();
38
+ _resetRuntimeForTests();
39
+ }
40
+
41
+ /** Clear manual trace context (traceId / spanId set via setTraceContext). */
42
+ export function resetTraceContext(): void {
43
+ _resetTraceContext();
44
+ }
45
+
46
+ /** Vitest plugin for automatic per-test telemetry isolation. */
47
+ export const telemetryTestPlugin = {
48
+ beforeEach(): void {
49
+ resetTelemetryState();
50
+ resetTraceContext();
51
+ },
52
+ afterEach(): void {
53
+ resetTelemetryState();
54
+ resetTraceContext();
55
+ },
56
+ };
package/src/tracing.ts ADDED
@@ -0,0 +1,211 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Tracing helpers — mirrors Python provide.telemetry @trace decorator and tracer access.
6
+ *
7
+ * Uses @opentelemetry/api which provides no-op implementations when no SDK is registered.
8
+ * withTrace() / @trace work safely without any OTEL setup; they just don't export spans.
9
+ */
10
+
11
+ import {
12
+ type Tracer,
13
+ type Span,
14
+ SpanStatusCode,
15
+ trace,
16
+ context as otelContext,
17
+ } from '@opentelemetry/api';
18
+ import { getActiveOtelContext } from './propagation';
19
+ import { randomHex } from './hash';
20
+
21
+ // Stryker disable next-line StringLiteral: tracer name is not observable without a real SDK
22
+ const TRACER_NAME = '@provide-io/telemetry';
23
+
24
+ // ── Manual trace context (injected without an active OTEL span) ───────────────
25
+ let _manualTraceId: string | undefined;
26
+ let _manualSpanId: string | undefined;
27
+
28
+ /**
29
+ * Manually inject trace/span IDs (e.g. from an incoming request header).
30
+ * Returned by getTraceContext(); cleared by _resetTraceContext().
31
+ */
32
+ export function setTraceContext(traceId: string, spanId: string): void {
33
+ _manualTraceId = traceId;
34
+ _manualSpanId = spanId;
35
+ }
36
+
37
+ /**
38
+ * Return the current trace context: manual injection first, then active OTEL span.
39
+ */
40
+ export function getTraceContext(): { trace_id?: string; span_id?: string } {
41
+ // Stryker disable next-line ConditionalExpression,LogicalOperator: setTraceContext always sets both; partial state not reachable via public API
42
+ if (_manualTraceId !== undefined || _manualSpanId !== undefined) {
43
+ return {
44
+ // Stryker disable next-line ConditionalExpression: _manualTraceId is always defined here (both set together)
45
+ ...(_manualTraceId !== undefined && { trace_id: _manualTraceId }),
46
+ // Stryker disable next-line ConditionalExpression: _manualSpanId is always defined here (both set together)
47
+ ...(_manualSpanId !== undefined && { span_id: _manualSpanId }),
48
+ };
49
+ }
50
+ const ids = getActiveTraceIds();
51
+ return {
52
+ ...(ids.trace_id !== undefined && { trace_id: ids.trace_id }),
53
+ ...(ids.span_id !== undefined && { span_id: ids.span_id }),
54
+ };
55
+ }
56
+
57
+ /** Reset manually injected trace context (used in tests). */
58
+ export function _resetTraceContext(): void {
59
+ _manualTraceId = undefined;
60
+ _manualSpanId = undefined;
61
+ }
62
+
63
+ /** Return the tracer for the telemetry library. Noop when no SDK is registered. */
64
+ export function getTracer(): Tracer {
65
+ return trace.getTracer(TRACER_NAME);
66
+ }
67
+
68
+ /** Module-level lazy singleton tracer — resolves provider at call time. */
69
+ export const tracer: Tracer = trace.getTracer(TRACER_NAME);
70
+
71
+ /**
72
+ * Return trace_id and span_id from the currently active OTEL span.
73
+ * Returns an empty object when no span is active or OTEL is not configured.
74
+ */
75
+ export function getActiveTraceIds(): { trace_id?: string; span_id?: string } {
76
+ const span = trace.getActiveSpan();
77
+ if (!span) return {};
78
+ const ctx = span.spanContext();
79
+ // OTEL no-op spans have all-zero IDs — treat as no active span.
80
+ if (ctx.traceId === '00000000000000000000000000000000') return {};
81
+ return { trace_id: ctx.traceId, span_id: ctx.spanId };
82
+ }
83
+
84
+ const NOOP_TRACE_ID = '00000000000000000000000000000000';
85
+
86
+ /**
87
+ * Return true if the given span is a no-op (all-zero trace ID).
88
+ * Used to decide whether to inject synthetic random IDs.
89
+ */
90
+ function _isNoopSpan(span: Span): boolean {
91
+ return span.spanContext().traceId === NOOP_TRACE_ID;
92
+ }
93
+
94
+ /**
95
+ * Execute fn with random synthetic trace/span IDs set as manual context,
96
+ * then restore the previous manual context when done.
97
+ * Handles both sync and async results.
98
+ */
99
+ function _withSyntheticIds<T>(fn: () => T): T {
100
+ const prevTraceId = _manualTraceId;
101
+ const prevSpanId = _manualSpanId;
102
+ // Stryker disable next-line StringLiteral: random IDs are non-deterministic — exact value not observable in mutations
103
+ setTraceContext(randomHex(16), randomHex(8));
104
+ const result = fn();
105
+ if (result instanceof Promise) {
106
+ return result.then(
107
+ (value) => {
108
+ _manualTraceId = prevTraceId;
109
+ _manualSpanId = prevSpanId;
110
+ return value;
111
+ },
112
+ (err: unknown) => {
113
+ _manualTraceId = prevTraceId;
114
+ _manualSpanId = prevSpanId;
115
+ throw err;
116
+ },
117
+ ) as T;
118
+ }
119
+ _manualTraceId = prevTraceId;
120
+ _manualSpanId = prevSpanId;
121
+ return result;
122
+ }
123
+
124
+ /** Shared span handler for withTrace — records exceptions and sets ERROR status. */
125
+ function _spanHandler<T>(fn: () => T, span: Span): T {
126
+ try {
127
+ const result = fn();
128
+ if (result instanceof Promise) {
129
+ return result.then(
130
+ (value) => {
131
+ span.end();
132
+ return value;
133
+ },
134
+ (err: unknown) => {
135
+ span.recordException(err instanceof Error ? err : new Error(String(err)));
136
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
137
+ span.end();
138
+ throw err;
139
+ },
140
+ ) as T;
141
+ }
142
+ span.end();
143
+ return result;
144
+ } catch (err) {
145
+ span.recordException(err instanceof Error ? err : new Error(String(err)));
146
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
147
+ span.end();
148
+ throw err;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Execute fn inside a new span named `name`.
154
+ * Works for both sync and async functions.
155
+ * Mirrors Python @trace decorator behaviour: records exceptions, sets ERROR status.
156
+ */
157
+ export function withTrace<T>(name: string, fn: () => T): T {
158
+ const tracer = trace.getTracer(TRACER_NAME);
159
+
160
+ // If an OTel context was extracted from propagation headers, use it as parent.
161
+ try {
162
+ const activeCtx = getActiveOtelContext();
163
+ if (activeCtx) {
164
+ return otelContext.with(activeCtx as ReturnType<typeof otelContext.active>, () =>
165
+ tracer.startActiveSpan(name, (span: Span) => {
166
+ // Stryker disable next-line ConditionalExpression: noop detection is not observable without SDK — branch outcome equivalent under mutation
167
+ /* v8 ignore start: noop-span false branch + real-span return are unreachable without a registered OTel provider */
168
+ if (_isNoopSpan(span)) return _withSyntheticIds(() => _spanHandler(fn, span));
169
+ return _spanHandler(fn, span);
170
+ /* v8 ignore stop */
171
+ }),
172
+ );
173
+ }
174
+ } catch {
175
+ // Graceful degradation — fall through to default behaviour.
176
+ }
177
+
178
+ return tracer.startActiveSpan(name, (span: Span) => {
179
+ // Stryker disable next-line ConditionalExpression: noop detection is not observable without SDK — branch outcome equivalent under mutation
180
+ if (_isNoopSpan(span)) return _withSyntheticIds(() => _spanHandler(fn, span));
181
+ return _spanHandler(fn, span);
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Method/function decorator that wraps the target in withTrace().
187
+ * Span name defaults to the decorated method name.
188
+ *
189
+ * Usage (requires experimentalDecorators: true):
190
+ * class Foo {
191
+ * @trace('my.operation')
192
+ * doWork() { ... }
193
+ * }
194
+ */
195
+ // Stryker disable BlockStatement: outer and inner decorator fns returning undefined leave descriptors unchanged — equivalent
196
+ export function traceDecorator(name?: string) {
197
+ return function (
198
+ _target: object,
199
+ propertyKey: string | symbol,
200
+ descriptor: PropertyDescriptor,
201
+ ): PropertyDescriptor {
202
+ // Stryker enable BlockStatement
203
+ // Stryker disable next-line LogicalOperator: span name not observable with no-op tracer
204
+ const spanName = name ?? String(propertyKey);
205
+ const original = descriptor.value as (...args: unknown[]) => unknown;
206
+ descriptor.value = function (this: unknown, ...args: unknown[]) {
207
+ return withTrace(spanName, () => original.apply(this, args));
208
+ };
209
+ return descriptor;
210
+ };
211
+ }