@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/index.ts ADDED
@@ -0,0 +1,183 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * @provide-io/telemetry — TypeScript structured logging + OTEL
6
+ *
7
+ * Feature parity with the Python provide.telemetry package.
8
+ *
9
+ * Quick start:
10
+ * import { setupTelemetry, getLogger, bindContext } from '@provide-io/telemetry';
11
+ *
12
+ * setupTelemetry({ serviceName: 'my-app', logLevel: 'debug' });
13
+ * const log = getLogger('api');
14
+ * log.info({ event: 'request_ok', method: 'GET', path: '/api/v1/users', status: 200 });
15
+ */
16
+
17
+ // Config + setup
18
+ export {
19
+ setupTelemetry,
20
+ applyConfigPolicies,
21
+ getConfig,
22
+ configFromEnv,
23
+ parseOtlpHeaders,
24
+ redactConfig,
25
+ version,
26
+ __version__,
27
+ } from './config';
28
+ export type { TelemetryConfig, RuntimeOverrides } from './config';
29
+
30
+ // Logger
31
+ export { getLogger, logger } from './logger';
32
+ export type { Logger } from './logger';
33
+
34
+ // Context binding (mirrors Python bind_context / unbind_context / clear_context)
35
+ export {
36
+ bindContext,
37
+ unbindContext,
38
+ clearContext,
39
+ getContext,
40
+ runWithContext,
41
+ bindSessionContext,
42
+ getSessionId,
43
+ clearSessionContext,
44
+ } from './context';
45
+
46
+ // Error fingerprinting (mirrors Python add_error_fingerprint processor)
47
+ export { computeErrorFingerprint } from './fingerprint';
48
+
49
+ // Pretty ANSI renderer (mirrors Python PrettyRenderer)
50
+ export { formatPretty, supportsColor } from './pretty';
51
+
52
+ // Metrics (mirrors Python counter / gauge / histogram)
53
+ export {
54
+ counter,
55
+ gauge,
56
+ histogram,
57
+ getMeter,
58
+ CounterInstrument,
59
+ GaugeInstrument,
60
+ HistogramInstrument,
61
+ } from './metrics';
62
+ export type { Counter, Histogram, Meter, MetricOptions, UpDownCounter } from './metrics';
63
+
64
+ // Tracing (mirrors Python @trace decorator)
65
+ export {
66
+ withTrace,
67
+ traceDecorator as trace,
68
+ getActiveTraceIds,
69
+ getTracer,
70
+ tracer,
71
+ setTraceContext,
72
+ getTraceContext,
73
+ } from './tracing';
74
+
75
+ // Optional OTEL SDK wiring (call after setupTelemetry to activate exporters)
76
+ export { registerOtelProviders } from './otel';
77
+
78
+ // PII sanitization utilities
79
+ export {
80
+ sanitize,
81
+ DEFAULT_SANITIZE_FIELDS,
82
+ sanitizePayload,
83
+ registerPiiRule,
84
+ getPiiRules,
85
+ replacePiiRules,
86
+ resetPiiRulesForTests,
87
+ } from './pii';
88
+ export type { MaskMode, PIIRule, SanitizePayloadOptions } from './pii';
89
+
90
+ // Exceptions
91
+ export { TelemetryError, ConfigurationError } from './exceptions';
92
+
93
+ // Health
94
+ export { getHealthSnapshot, setSetupError } from './health';
95
+ export type { HealthSnapshot } from './health';
96
+
97
+ // Backpressure
98
+ export { setQueuePolicy, getQueuePolicy, tryAcquire, release } from './backpressure';
99
+ export type { QueuePolicy, QueueTicket } from './backpressure';
100
+
101
+ // Cardinality
102
+ export {
103
+ OVERFLOW_VALUE,
104
+ registerCardinalityLimit,
105
+ getCardinalityLimits,
106
+ clearCardinalityLimits,
107
+ guardAttributes,
108
+ } from './cardinality';
109
+ export type { CardinalityLimit } from './cardinality';
110
+
111
+ // Sampling
112
+ export { setSamplingPolicy, getSamplingPolicy, shouldSample } from './sampling';
113
+ export type { SamplingPolicy } from './sampling';
114
+
115
+ // Resilience
116
+ export {
117
+ setExporterPolicy,
118
+ getExporterPolicy,
119
+ runWithResilience,
120
+ getCircuitState,
121
+ TelemetryTimeoutError,
122
+ } from './resilience';
123
+ export type { ExporterPolicy, CircuitState } from './resilience';
124
+
125
+ // Schema
126
+ export {
127
+ EventSchemaError,
128
+ event,
129
+ eventName,
130
+ validateEventName,
131
+ validateRequiredKeys,
132
+ } from './schema';
133
+ export type { EventRecord } from './schema';
134
+
135
+ // SLO
136
+ export { recordRedMetrics, recordUseMetrics, classifyError } from './slo';
137
+ export type { ErrorClassification } from './slo';
138
+
139
+ // Propagation
140
+ export {
141
+ extractW3cContext,
142
+ bindPropagationContext,
143
+ clearPropagationContext,
144
+ getActivePropagationContext,
145
+ } from './propagation';
146
+ export type { PropagationContext } from './propagation';
147
+
148
+ // Runtime reconfiguration
149
+ export {
150
+ getRuntimeConfig,
151
+ updateRuntimeConfig,
152
+ reloadRuntimeFromEnv,
153
+ reconfigureTelemetry,
154
+ } from './runtime';
155
+
156
+ // Test utilities
157
+ export { resetTelemetryState, resetTraceContext, telemetryTestPlugin } from './testing';
158
+
159
+ // Optional governance module — strippable: consent
160
+ export type { ConsentLevel } from './consent';
161
+ export {
162
+ setConsentLevel,
163
+ getConsentLevel,
164
+ shouldAllow,
165
+ loadConsentFromEnv,
166
+ resetConsentForTests,
167
+ } from './consent';
168
+
169
+ // Optional governance module — strippable
170
+ export type { DataClass, ClassificationRule, ClassificationPolicy } from './classification';
171
+ export {
172
+ registerClassificationRules,
173
+ setClassificationPolicy,
174
+ getClassificationPolicy,
175
+ resetClassificationForTests,
176
+ } from './classification';
177
+
178
+ // Optional governance module — strippable: receipts
179
+ export type { RedactionReceipt } from './receipts';
180
+ export { enableReceipts, getEmittedReceiptsForTests, resetReceiptsForTests } from './receipts';
181
+
182
+ // Shutdown
183
+ export { shutdownTelemetry } from './shutdown';
package/src/logger.ts ADDED
@@ -0,0 +1,287 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Structured logger — wraps pino with:
6
+ * - browser.write hook in actual browsers; custom stream in Node.js/Vitest
7
+ * - window.__pinoLogs capture for Playwright/devtools inspection
8
+ * - Automatic context binding (from bindContext())
9
+ * - Automatic OTEL trace_id/span_id injection
10
+ * - PII sanitization
11
+ * - msg fallback: if msg is empty, defaults to obj.event
12
+ *
13
+ * Mirrors Python provide.telemetry get_logger().
14
+ */
15
+
16
+ import pino from 'pino';
17
+ import { getConfig } from './config';
18
+ import { getContext } from './context';
19
+ import { computeErrorFingerprint } from './fingerprint';
20
+ import { formatPretty, supportsColor } from './pretty';
21
+ import { emitLogRecord } from './otel-logs';
22
+ import { sanitizePayload } from './pii';
23
+ import { sanitize } from './sanitize';
24
+ import { EventSchemaError, validateEventName, validateRequiredKeys } from './schema';
25
+ import { getActiveTraceIds } from './tracing';
26
+
27
+ /** Pino level number → console method name. */
28
+ const LEVEL_MAP: Record<number, string> = {
29
+ 10: 'trace',
30
+ 20: 'debug',
31
+ 30: 'log',
32
+ 40: 'warn',
33
+ 50: 'error',
34
+ 60: 'error',
35
+ };
36
+
37
+ /** Public Logger interface — consumers should type against this, not pino.Logger. */
38
+ export interface Logger {
39
+ trace(obj: Record<string, unknown>, msg?: string): void;
40
+ debug(obj: Record<string, unknown>, msg?: string): void;
41
+ info(obj: Record<string, unknown>, msg?: string): void;
42
+ warn(obj: Record<string, unknown>, msg?: string): void;
43
+ error(obj: Record<string, unknown>, msg?: string): void;
44
+ /** Create a child logger with additional bound fields. */
45
+ child(bindings: Record<string, unknown>): Logger;
46
+ }
47
+
48
+ // Pino root instance — lazily created so config is read after setupTelemetry().
49
+ let _root: pino.Logger | null = null;
50
+
51
+ /**
52
+ * Build the write hook that enriches, sanitizes, captures, and optionally
53
+ * emits each log record. Config is read dynamically on every invocation so
54
+ * that resetTelemetryState() + setupTelemetry() changes take effect without
55
+ * needing to rebuild the hook closure.
56
+ */
57
+ export function makeWriteHook() {
58
+ // pino's WriteFn signature uses `object`; we cast internally for safe property access.
59
+ return (obj: object): void => {
60
+ // Read config dynamically — avoids stale-capture bug after _resetConfig().
61
+ const cfg = getConfig();
62
+ const o = obj as Record<string, unknown>;
63
+
64
+ // Inject OTEL trace/span IDs if an active span exists.
65
+ const ids = getActiveTraceIds();
66
+ if (ids.trace_id) o['trace_id'] = ids.trace_id;
67
+ if (ids.span_id) o['span_id'] = ids.span_id;
68
+
69
+ // Merge module-level context bindings.
70
+ Object.assign(o, getContext());
71
+
72
+ // Ensure msg is always non-empty — pino sets msg='' when no string arg is passed.
73
+ if (!o['msg']) o['msg'] = o['event'] ?? '';
74
+
75
+ // Caller info injection — intentionally expensive (creates Error per call).
76
+ // Stryker disable all
77
+ if (cfg.logIncludeCaller) {
78
+ const err = new Error();
79
+ const stack = err.stack?.split('\n');
80
+ /* v8 ignore next -- stack is always defined in V8 */
81
+ if (stack) {
82
+ for (const frame of stack.slice(1)) {
83
+ if (
84
+ !frame.includes('logger.ts') &&
85
+ !frame.includes('node_modules') &&
86
+ !frame.includes('pino')
87
+ ) {
88
+ const match = frame.match(/\((.+):(\d+):\d+\)/) ?? frame.match(/at (.+):(\d+):\d+/);
89
+ /* v8 ignore next -- match always succeeds for V8 stack frames */
90
+ if (match) {
91
+ o['caller_file'] = match[1].replace(/^.*\//, ''); // basename only
92
+ o['caller_line'] = Number(match[2]);
93
+ }
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ }
99
+ // Stryker enable all
100
+
101
+ // Error fingerprinting — stable hash from error name + stack.
102
+ const errObj = o['err'] as Record<string, unknown> | undefined;
103
+ const excName = (o['exc_name'] ?? o['exception'] ?? errObj?.['type'] ?? errObj?.['name']) as
104
+ | string
105
+ | undefined;
106
+ if (excName) {
107
+ const stack = (errObj?.['stack'] ?? o['stack']) as string | undefined;
108
+ o['error_fingerprint'] = computeErrorFingerprint(String(excName), stack);
109
+ }
110
+
111
+ // PII sanitization: blocked keys + secret detection + custom PII rules.
112
+ if (cfg.logSanitize) {
113
+ sanitize(o, cfg.sanitizeFields);
114
+ sanitizePayload(o, [], { maxDepth: cfg.piiMaxDepth });
115
+ }
116
+
117
+ // Strip timestamp when configured off.
118
+ if (!cfg.logIncludeTimestamp) {
119
+ delete o['time'];
120
+ }
121
+
122
+ // Schema validation — drop records that violate strict schema rules.
123
+ /* v8 ignore next -- V8 cannot fully attribute all ?? branches in a single expression */
124
+ if (cfg.strictSchema) {
125
+ const event = String(o['event'] ?? o['msg'] ?? '');
126
+ if (event) {
127
+ try {
128
+ validateEventName(event);
129
+ } catch (e) {
130
+ if (e instanceof EventSchemaError) return;
131
+ throw e;
132
+ }
133
+ }
134
+ if (cfg.requiredLogKeys.length > 0) {
135
+ try {
136
+ validateRequiredKeys(o, cfg.requiredLogKeys);
137
+ } catch (e) {
138
+ if (e instanceof EventSchemaError) return;
139
+ throw e;
140
+ }
141
+ }
142
+ }
143
+
144
+ // Export to OTLP when a log provider is registered (noop otherwise).
145
+ emitLogRecord(o);
146
+
147
+ // Capture to window.__pinoLogs for Playwright and devtools inspection.
148
+ // Check is done inline (not at module load) so it works when loaded in Node.js
149
+ // test environments that later gain a jsdom window.
150
+ if (typeof window !== 'undefined' && cfg.captureToWindow) {
151
+ if (!('__pinoLogs' in window)) {
152
+ (window as unknown as Record<string, unknown>)['__pinoLogs'] = [];
153
+ }
154
+ (window as unknown as Record<string, unknown[]>)['__pinoLogs'].push(o);
155
+ }
156
+
157
+ // Emit to console only when explicitly enabled (opt-in).
158
+ if (cfg.consoleOutput) {
159
+ const method = LEVEL_MAP[o['level'] as number] ?? 'log';
160
+ if (cfg.logFormat === 'pretty') {
161
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
+ (console as any)[method](formatPretty(o, supportsColor()));
163
+ } else {
164
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
165
+ (console as any)[method](o);
166
+ }
167
+ }
168
+ };
169
+ }
170
+
171
+ function getRootLogger(): pino.Logger {
172
+ // Stryker disable next-line ConditionalExpression
173
+ if (_root) return _root;
174
+ const cfg = getConfig();
175
+ const hook = makeWriteHook();
176
+
177
+ // pino only invokes browser.write when process.version is absent (real browser).
178
+ // In Node.js / Vitest, we use a custom destination stream that forwards every
179
+ // serialised log line back through the write hook.
180
+ // Stryker disable all
181
+ const isNodeEnv = typeof process !== 'undefined' && typeof process.version === 'string';
182
+
183
+ /* c8 ignore else */
184
+ if (isNodeEnv) {
185
+ const stream = {
186
+ write(msg: string) {
187
+ try {
188
+ hook(JSON.parse(msg.trimEnd()) as object);
189
+ } catch {
190
+ // Ignore malformed lines (e.g. pino flush sentinels).
191
+ }
192
+ },
193
+ };
194
+ _root = pino(
195
+ {
196
+ base: { service: cfg.serviceName, env: cfg.environment, version: cfg.version },
197
+ level: cfg.logLevel,
198
+ },
199
+ stream as unknown as pino.DestinationStream,
200
+ );
201
+ } else {
202
+ /* c8 ignore next 8 */
203
+ _root = pino({
204
+ base: { service: cfg.serviceName, env: cfg.environment, version: cfg.version },
205
+ level: cfg.logLevel,
206
+ browser: {
207
+ write: hook,
208
+ },
209
+ });
210
+ }
211
+ // Stryker enable all
212
+ return _root;
213
+ }
214
+
215
+ function adaptPino(pinoLogger: pino.Logger): Logger {
216
+ // Stryker disable all
217
+ return {
218
+ trace: (obj, msg) => pinoLogger.trace(obj, msg ?? ''),
219
+ debug: (obj, msg) => pinoLogger.debug(obj, msg ?? ''),
220
+ info: (obj, msg) => pinoLogger.info(obj, msg ?? ''),
221
+ warn: (obj, msg) => pinoLogger.warn(obj, msg ?? ''),
222
+ error: (obj, msg) => pinoLogger.error(obj, msg ?? ''),
223
+ child: (bindings) => adaptPino(pinoLogger.child(bindings)),
224
+ };
225
+ // Stryker enable all
226
+ }
227
+
228
+ /**
229
+ * Find the longest-prefix match in logModuleLevels for the given logger name.
230
+ * Returns the matched level string, or undefined if no match.
231
+ * Mirrors Python _LevelFilter longest-prefix matching.
232
+ */
233
+ function findModuleLevel(name: string, moduleLevels: Record<string, string>): string | undefined {
234
+ let bestMatch: string | undefined;
235
+ let bestLen = -1;
236
+ for (const prefix of Object.keys(moduleLevels)) {
237
+ if (
238
+ (prefix === '' || name === prefix || name.startsWith(prefix + '.')) &&
239
+ prefix.length > bestLen
240
+ ) {
241
+ bestMatch = prefix;
242
+ bestLen = prefix.length;
243
+ }
244
+ }
245
+ return bestMatch !== undefined ? moduleLevels[bestMatch] : undefined;
246
+ }
247
+
248
+ /**
249
+ * Return a logger for the given name.
250
+ * Name appears as the `name` field in every log record.
251
+ * Mirrors Python: get_logger(name)
252
+ */
253
+ export function getLogger(name?: string): Logger {
254
+ const root = getRootLogger();
255
+ // Stryker disable next-line ObjectLiteral
256
+ const pinoLogger = name ? root.child({ name }) : root;
257
+ // Apply per-module level overrides (longest-prefix match).
258
+ if (name) {
259
+ const cfg = getConfig();
260
+ const moduleLevels = cfg.logModuleLevels;
261
+ if (Object.keys(moduleLevels).length > 0) {
262
+ const level = findModuleLevel(name, moduleLevels);
263
+ if (level) {
264
+ pinoLogger.level = level.toLowerCase();
265
+ }
266
+ }
267
+ }
268
+ return adaptPino(pinoLogger);
269
+ }
270
+
271
+ /** Reset the root logger (forces re-creation with current config on next call). */
272
+ // Stryker disable next-line BlockStatement
273
+ export function _resetRootLogger(): void {
274
+ _root = null;
275
+ }
276
+
277
+ /** Module-level lazy singleton logger. Mirrors Python: logger = get_logger('default'). */
278
+ // Stryker disable all
279
+ export const logger: Logger = {
280
+ trace: (obj, msg) => getLogger('default').trace(obj, msg),
281
+ debug: (obj, msg) => getLogger('default').debug(obj, msg),
282
+ info: (obj, msg) => getLogger('default').info(obj, msg),
283
+ warn: (obj, msg) => getLogger('default').warn(obj, msg),
284
+ error: (obj, msg) => getLogger('default').error(obj, msg),
285
+ child: (bindings) => getLogger('default').child(bindings),
286
+ };
287
+ // Stryker enable all
package/src/metrics.ts ADDED
@@ -0,0 +1,204 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Metric instruments — mirrors Python provide.telemetry counter/gauge/histogram.
6
+ *
7
+ * Backed by @opentelemetry/api which provides no-op instruments when no SDK is registered.
8
+ * Safe to call in any environment; instruments are always callable without setup.
9
+ *
10
+ * Wrapper classes gate every `.add()`/`.record()` call through sampling + backpressure,
11
+ * matching the Python fallback.py pattern.
12
+ */
13
+
14
+ import {
15
+ type Attributes,
16
+ type Counter,
17
+ type Histogram,
18
+ type Meter,
19
+ type UpDownCounter,
20
+ metrics,
21
+ } from '@opentelemetry/api';
22
+ import { shouldSample } from './sampling';
23
+ import { tryAcquire, release } from './backpressure';
24
+ import { getActiveTraceIds } from './tracing';
25
+ import { getConfig } from './config';
26
+
27
+ export type { Counter, Histogram, Meter, UpDownCounter };
28
+
29
+ // Stryker disable next-line StringLiteral: meter name not observable with no-op OTEL SDK in tests
30
+ const METER_NAME = '@provide-io/telemetry';
31
+
32
+ export interface MetricOptions {
33
+ description?: string;
34
+ unit?: string;
35
+ }
36
+
37
+ /**
38
+ * Wrapper around OTel Counter that gates add() through sampling + backpressure.
39
+ */
40
+ export class CounterInstrument {
41
+ readonly name: string;
42
+ private readonly _inner: Counter;
43
+ private _value = 0;
44
+
45
+ constructor(name: string, inner: Counter) {
46
+ this.name = name;
47
+ this._inner = inner;
48
+ }
49
+
50
+ /** Cumulative counter value (in-process; useful for testing and health checks). */
51
+ get value(): number {
52
+ return this._value;
53
+ }
54
+
55
+ add(value: number, attributes?: Attributes): void {
56
+ if (!getConfig().metricsEnabled) return;
57
+ if (!shouldSample('metrics', this.name)) return;
58
+ const ticket = tryAcquire('metrics');
59
+ if (!ticket) return;
60
+ try {
61
+ const ids = getActiveTraceIds();
62
+ const enriched =
63
+ ids.trace_id && ids.span_id
64
+ ? { ...attributes, trace_id: ids.trace_id, span_id: ids.span_id }
65
+ : attributes;
66
+ this._inner.add(value, enriched);
67
+ this._value += value;
68
+ } finally {
69
+ release(ticket);
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Wrapper around OTel UpDownCounter with set semantics.
76
+ * Gates add()/set() through sampling + backpressure.
77
+ */
78
+ export class GaugeInstrument {
79
+ readonly name: string;
80
+ private readonly _inner: UpDownCounter;
81
+ private readonly _values: Map<string, number> = new Map();
82
+ private _lastValue = 0;
83
+
84
+ constructor(name: string, inner: UpDownCounter) {
85
+ this.name = name;
86
+ this._inner = inner;
87
+ }
88
+
89
+ /** Most recent value set or accumulated via add() (in-process; useful for testing and health checks). */
90
+ get value(): number {
91
+ return this._lastValue;
92
+ }
93
+
94
+ add(value: number, attributes?: Attributes): void {
95
+ if (!getConfig().metricsEnabled) return;
96
+ if (!shouldSample('metrics', this.name)) return;
97
+ const ticket = tryAcquire('metrics');
98
+ if (!ticket) return;
99
+ try {
100
+ this._inner.add(value, attributes);
101
+ this._lastValue += value;
102
+ } finally {
103
+ release(ticket);
104
+ }
105
+ }
106
+
107
+ set(value: number, attributes?: Attributes): void {
108
+ if (!getConfig().metricsEnabled) return;
109
+ if (!shouldSample('metrics', this.name)) return;
110
+ const ticket = tryAcquire('metrics');
111
+ if (!ticket) return;
112
+ try {
113
+ // Stryker disable next-line StringLiteral: empty string fallback for no-attributes key — functionally equivalent to any constant
114
+ const key = attributes ? JSON.stringify(attributes) : '';
115
+ const prev = this._values.get(key) ?? 0;
116
+ const delta = value - prev;
117
+ this._values.set(key, value);
118
+ this._inner.add(delta, attributes);
119
+ this._lastValue = value;
120
+ } finally {
121
+ release(ticket);
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Wrapper around OTel Histogram that gates record() through sampling + backpressure.
128
+ */
129
+ export class HistogramInstrument {
130
+ readonly name: string;
131
+ private readonly _inner: Histogram;
132
+ private _count = 0;
133
+ private _total = 0;
134
+
135
+ constructor(name: string, inner: Histogram) {
136
+ this.name = name;
137
+ this._inner = inner;
138
+ }
139
+
140
+ /** Number of values recorded (in-process; useful for testing and health checks). */
141
+ get count(): number {
142
+ return this._count;
143
+ }
144
+
145
+ /** Sum of all recorded values (in-process; useful for testing and health checks). */
146
+ get total(): number {
147
+ return this._total;
148
+ }
149
+
150
+ record(value: number, attributes?: Attributes): void {
151
+ if (!getConfig().metricsEnabled) return;
152
+ if (!shouldSample('metrics', this.name)) return;
153
+ const ticket = tryAcquire('metrics');
154
+ if (!ticket) return;
155
+ try {
156
+ const ids = getActiveTraceIds();
157
+ const enriched =
158
+ ids.trace_id && ids.span_id
159
+ ? { ...attributes, trace_id: ids.trace_id, span_id: ids.span_id }
160
+ : attributes;
161
+ this._inner.record(value, enriched);
162
+ this._count += 1;
163
+ this._total += value;
164
+ } finally {
165
+ release(ticket);
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Create a monotonically increasing counter.
172
+ * Mirrors Python: counter(name, description, unit)
173
+ */
174
+ export function counter(name: string, options?: MetricOptions): CounterInstrument {
175
+ const inner = metrics.getMeter(METER_NAME).createCounter(name, options);
176
+ return new CounterInstrument(name, inner);
177
+ }
178
+
179
+ /**
180
+ * Create an up-down counter (gauge — can increase or decrease).
181
+ * Mirrors Python: gauge(name, description, unit)
182
+ */
183
+ export function gauge(name: string, options?: MetricOptions): GaugeInstrument {
184
+ const inner = metrics.getMeter(METER_NAME).createUpDownCounter(name, options);
185
+ return new GaugeInstrument(name, inner);
186
+ }
187
+
188
+ /**
189
+ * Create a histogram for recording distributions (latencies, sizes).
190
+ * Mirrors Python: histogram(name, description, unit)
191
+ */
192
+ export function histogram(name: string, options?: MetricOptions): HistogramInstrument {
193
+ const inner = metrics.getMeter(METER_NAME).createHistogram(name, options);
194
+ return new HistogramInstrument(name, inner);
195
+ }
196
+
197
+ /**
198
+ * Return an OTEL Meter from the global meter provider.
199
+ * Mirrors Python: get_meter(name)
200
+ */
201
+ export function getMeter(name?: string): Meter {
202
+ // Stryker disable next-line LogicalOperator: getMeter(undefined) behaves identically to getMeter(name) with no-op OTEL API
203
+ return metrics.getMeter(name ?? METER_NAME);
204
+ }