@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/logger.js ADDED
@@ -0,0 +1,254 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Structured logger — wraps pino with:
5
+ * - browser.write hook in actual browsers; custom stream in Node.js/Vitest
6
+ * - window.__pinoLogs capture for Playwright/devtools inspection
7
+ * - Automatic context binding (from bindContext())
8
+ * - Automatic OTEL trace_id/span_id injection
9
+ * - PII sanitization
10
+ * - msg fallback: if msg is empty, defaults to obj.event
11
+ *
12
+ * Mirrors Python provide.telemetry get_logger().
13
+ */
14
+ import pino from 'pino';
15
+ import { getConfig } from './config';
16
+ import { getContext } from './context';
17
+ import { computeErrorFingerprint } from './fingerprint';
18
+ import { formatPretty, supportsColor } from './pretty';
19
+ import { emitLogRecord } from './otel-logs';
20
+ import { sanitizePayload } from './pii';
21
+ import { sanitize } from './sanitize';
22
+ import { EventSchemaError, validateEventName, validateRequiredKeys } from './schema';
23
+ import { getActiveTraceIds } from './tracing';
24
+ /** Pino level number → console method name. */
25
+ const LEVEL_MAP = {
26
+ 10: 'trace',
27
+ 20: 'debug',
28
+ 30: 'log',
29
+ 40: 'warn',
30
+ 50: 'error',
31
+ 60: 'error',
32
+ };
33
+ // Pino root instance — lazily created so config is read after setupTelemetry().
34
+ let _root = null;
35
+ /**
36
+ * Build the write hook that enriches, sanitizes, captures, and optionally
37
+ * emits each log record. Config is read dynamically on every invocation so
38
+ * that resetTelemetryState() + setupTelemetry() changes take effect without
39
+ * needing to rebuild the hook closure.
40
+ */
41
+ export function makeWriteHook() {
42
+ // pino's WriteFn signature uses `object`; we cast internally for safe property access.
43
+ return (obj) => {
44
+ // Read config dynamically — avoids stale-capture bug after _resetConfig().
45
+ const cfg = getConfig();
46
+ const o = obj;
47
+ // Inject OTEL trace/span IDs if an active span exists.
48
+ const ids = getActiveTraceIds();
49
+ if (ids.trace_id)
50
+ o['trace_id'] = ids.trace_id;
51
+ if (ids.span_id)
52
+ o['span_id'] = ids.span_id;
53
+ // Merge module-level context bindings.
54
+ Object.assign(o, getContext());
55
+ // Ensure msg is always non-empty — pino sets msg='' when no string arg is passed.
56
+ if (!o['msg'])
57
+ o['msg'] = o['event'] ?? '';
58
+ // Caller info injection — intentionally expensive (creates Error per call).
59
+ // Stryker disable all
60
+ if (cfg.logIncludeCaller) {
61
+ const err = new Error();
62
+ const stack = err.stack?.split('\n');
63
+ /* v8 ignore next -- stack is always defined in V8 */
64
+ if (stack) {
65
+ for (const frame of stack.slice(1)) {
66
+ if (!frame.includes('logger.ts') &&
67
+ !frame.includes('node_modules') &&
68
+ !frame.includes('pino')) {
69
+ const match = frame.match(/\((.+):(\d+):\d+\)/) ?? frame.match(/at (.+):(\d+):\d+/);
70
+ /* v8 ignore next -- match always succeeds for V8 stack frames */
71
+ if (match) {
72
+ o['caller_file'] = match[1].replace(/^.*\//, ''); // basename only
73
+ o['caller_line'] = Number(match[2]);
74
+ }
75
+ break;
76
+ }
77
+ }
78
+ }
79
+ }
80
+ // Stryker enable all
81
+ // Error fingerprinting — stable hash from error name + stack.
82
+ const errObj = o['err'];
83
+ const excName = (o['exc_name'] ?? o['exception'] ?? errObj?.['type'] ?? errObj?.['name']);
84
+ if (excName) {
85
+ const stack = (errObj?.['stack'] ?? o['stack']);
86
+ o['error_fingerprint'] = computeErrorFingerprint(String(excName), stack);
87
+ }
88
+ // PII sanitization: blocked keys + secret detection + custom PII rules.
89
+ if (cfg.logSanitize) {
90
+ sanitize(o, cfg.sanitizeFields);
91
+ sanitizePayload(o, [], { maxDepth: cfg.piiMaxDepth });
92
+ }
93
+ // Strip timestamp when configured off.
94
+ if (!cfg.logIncludeTimestamp) {
95
+ delete o['time'];
96
+ }
97
+ // Schema validation — drop records that violate strict schema rules.
98
+ /* v8 ignore next -- V8 cannot fully attribute all ?? branches in a single expression */
99
+ if (cfg.strictSchema) {
100
+ const event = String(o['event'] ?? o['msg'] ?? '');
101
+ if (event) {
102
+ try {
103
+ validateEventName(event);
104
+ }
105
+ catch (e) {
106
+ if (e instanceof EventSchemaError)
107
+ return;
108
+ throw e;
109
+ }
110
+ }
111
+ if (cfg.requiredLogKeys.length > 0) {
112
+ try {
113
+ validateRequiredKeys(o, cfg.requiredLogKeys);
114
+ }
115
+ catch (e) {
116
+ if (e instanceof EventSchemaError)
117
+ return;
118
+ throw e;
119
+ }
120
+ }
121
+ }
122
+ // Export to OTLP when a log provider is registered (noop otherwise).
123
+ emitLogRecord(o);
124
+ // Capture to window.__pinoLogs for Playwright and devtools inspection.
125
+ // Check is done inline (not at module load) so it works when loaded in Node.js
126
+ // test environments that later gain a jsdom window.
127
+ if (typeof window !== 'undefined' && cfg.captureToWindow) {
128
+ if (!('__pinoLogs' in window)) {
129
+ window['__pinoLogs'] = [];
130
+ }
131
+ window['__pinoLogs'].push(o);
132
+ }
133
+ // Emit to console only when explicitly enabled (opt-in).
134
+ if (cfg.consoleOutput) {
135
+ const method = LEVEL_MAP[o['level']] ?? 'log';
136
+ if (cfg.logFormat === 'pretty') {
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ console[method](formatPretty(o, supportsColor()));
139
+ }
140
+ else {
141
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
142
+ console[method](o);
143
+ }
144
+ }
145
+ };
146
+ }
147
+ function getRootLogger() {
148
+ // Stryker disable next-line ConditionalExpression
149
+ if (_root)
150
+ return _root;
151
+ const cfg = getConfig();
152
+ const hook = makeWriteHook();
153
+ // pino only invokes browser.write when process.version is absent (real browser).
154
+ // In Node.js / Vitest, we use a custom destination stream that forwards every
155
+ // serialised log line back through the write hook.
156
+ // Stryker disable all
157
+ const isNodeEnv = typeof process !== 'undefined' && typeof process.version === 'string';
158
+ /* c8 ignore else */
159
+ if (isNodeEnv) {
160
+ const stream = {
161
+ write(msg) {
162
+ try {
163
+ hook(JSON.parse(msg.trimEnd()));
164
+ }
165
+ catch {
166
+ // Ignore malformed lines (e.g. pino flush sentinels).
167
+ }
168
+ },
169
+ };
170
+ _root = pino({
171
+ base: { service: cfg.serviceName, env: cfg.environment, version: cfg.version },
172
+ level: cfg.logLevel,
173
+ }, stream);
174
+ }
175
+ else {
176
+ /* c8 ignore next 8 */
177
+ _root = pino({
178
+ base: { service: cfg.serviceName, env: cfg.environment, version: cfg.version },
179
+ level: cfg.logLevel,
180
+ browser: {
181
+ write: hook,
182
+ },
183
+ });
184
+ }
185
+ // Stryker enable all
186
+ return _root;
187
+ }
188
+ function adaptPino(pinoLogger) {
189
+ // Stryker disable all
190
+ return {
191
+ trace: (obj, msg) => pinoLogger.trace(obj, msg ?? ''),
192
+ debug: (obj, msg) => pinoLogger.debug(obj, msg ?? ''),
193
+ info: (obj, msg) => pinoLogger.info(obj, msg ?? ''),
194
+ warn: (obj, msg) => pinoLogger.warn(obj, msg ?? ''),
195
+ error: (obj, msg) => pinoLogger.error(obj, msg ?? ''),
196
+ child: (bindings) => adaptPino(pinoLogger.child(bindings)),
197
+ };
198
+ // Stryker enable all
199
+ }
200
+ /**
201
+ * Find the longest-prefix match in logModuleLevels for the given logger name.
202
+ * Returns the matched level string, or undefined if no match.
203
+ * Mirrors Python _LevelFilter longest-prefix matching.
204
+ */
205
+ function findModuleLevel(name, moduleLevels) {
206
+ let bestMatch;
207
+ let bestLen = -1;
208
+ for (const prefix of Object.keys(moduleLevels)) {
209
+ if ((prefix === '' || name === prefix || name.startsWith(prefix + '.')) &&
210
+ prefix.length > bestLen) {
211
+ bestMatch = prefix;
212
+ bestLen = prefix.length;
213
+ }
214
+ }
215
+ return bestMatch !== undefined ? moduleLevels[bestMatch] : undefined;
216
+ }
217
+ /**
218
+ * Return a logger for the given name.
219
+ * Name appears as the `name` field in every log record.
220
+ * Mirrors Python: get_logger(name)
221
+ */
222
+ export function getLogger(name) {
223
+ const root = getRootLogger();
224
+ // Stryker disable next-line ObjectLiteral
225
+ const pinoLogger = name ? root.child({ name }) : root;
226
+ // Apply per-module level overrides (longest-prefix match).
227
+ if (name) {
228
+ const cfg = getConfig();
229
+ const moduleLevels = cfg.logModuleLevels;
230
+ if (Object.keys(moduleLevels).length > 0) {
231
+ const level = findModuleLevel(name, moduleLevels);
232
+ if (level) {
233
+ pinoLogger.level = level.toLowerCase();
234
+ }
235
+ }
236
+ }
237
+ return adaptPino(pinoLogger);
238
+ }
239
+ /** Reset the root logger (forces re-creation with current config on next call). */
240
+ // Stryker disable next-line BlockStatement
241
+ export function _resetRootLogger() {
242
+ _root = null;
243
+ }
244
+ /** Module-level lazy singleton logger. Mirrors Python: logger = get_logger('default'). */
245
+ // Stryker disable all
246
+ export const logger = {
247
+ trace: (obj, msg) => getLogger('default').trace(obj, msg),
248
+ debug: (obj, msg) => getLogger('default').debug(obj, msg),
249
+ info: (obj, msg) => getLogger('default').info(obj, msg),
250
+ warn: (obj, msg) => getLogger('default').warn(obj, msg),
251
+ error: (obj, msg) => getLogger('default').error(obj, msg),
252
+ child: (bindings) => getLogger('default').child(bindings),
253
+ };
254
+ // Stryker enable all
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Metric instruments — mirrors Python provide.telemetry counter/gauge/histogram.
3
+ *
4
+ * Backed by @opentelemetry/api which provides no-op instruments when no SDK is registered.
5
+ * Safe to call in any environment; instruments are always callable without setup.
6
+ *
7
+ * Wrapper classes gate every `.add()`/`.record()` call through sampling + backpressure,
8
+ * matching the Python fallback.py pattern.
9
+ */
10
+ import { type Attributes, type Counter, type Histogram, type Meter, type UpDownCounter } from '@opentelemetry/api';
11
+ export type { Counter, Histogram, Meter, UpDownCounter };
12
+ export interface MetricOptions {
13
+ description?: string;
14
+ unit?: string;
15
+ }
16
+ /**
17
+ * Wrapper around OTel Counter that gates add() through sampling + backpressure.
18
+ */
19
+ export declare class CounterInstrument {
20
+ readonly name: string;
21
+ private readonly _inner;
22
+ private _value;
23
+ constructor(name: string, inner: Counter);
24
+ /** Cumulative counter value (in-process; useful for testing and health checks). */
25
+ get value(): number;
26
+ add(value: number, attributes?: Attributes): void;
27
+ }
28
+ /**
29
+ * Wrapper around OTel UpDownCounter with set semantics.
30
+ * Gates add()/set() through sampling + backpressure.
31
+ */
32
+ export declare class GaugeInstrument {
33
+ readonly name: string;
34
+ private readonly _inner;
35
+ private readonly _values;
36
+ private _lastValue;
37
+ constructor(name: string, inner: UpDownCounter);
38
+ /** Most recent value set or accumulated via add() (in-process; useful for testing and health checks). */
39
+ get value(): number;
40
+ add(value: number, attributes?: Attributes): void;
41
+ set(value: number, attributes?: Attributes): void;
42
+ }
43
+ /**
44
+ * Wrapper around OTel Histogram that gates record() through sampling + backpressure.
45
+ */
46
+ export declare class HistogramInstrument {
47
+ readonly name: string;
48
+ private readonly _inner;
49
+ private _count;
50
+ private _total;
51
+ constructor(name: string, inner: Histogram);
52
+ /** Number of values recorded (in-process; useful for testing and health checks). */
53
+ get count(): number;
54
+ /** Sum of all recorded values (in-process; useful for testing and health checks). */
55
+ get total(): number;
56
+ record(value: number, attributes?: Attributes): void;
57
+ }
58
+ /**
59
+ * Create a monotonically increasing counter.
60
+ * Mirrors Python: counter(name, description, unit)
61
+ */
62
+ export declare function counter(name: string, options?: MetricOptions): CounterInstrument;
63
+ /**
64
+ * Create an up-down counter (gauge — can increase or decrease).
65
+ * Mirrors Python: gauge(name, description, unit)
66
+ */
67
+ export declare function gauge(name: string, options?: MetricOptions): GaugeInstrument;
68
+ /**
69
+ * Create a histogram for recording distributions (latencies, sizes).
70
+ * Mirrors Python: histogram(name, description, unit)
71
+ */
72
+ export declare function histogram(name: string, options?: MetricOptions): HistogramInstrument;
73
+ /**
74
+ * Return an OTEL Meter from the global meter provider.
75
+ * Mirrors Python: get_meter(name)
76
+ */
77
+ export declare function getMeter(name?: string): Meter;
78
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,OAAO,EACL,KAAK,UAAU,EACf,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,aAAa,EAEnB,MAAM,oBAAoB,CAAC;AAM5B,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;AAKzD,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,MAAM,CAAK;gBAEP,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;IAKxC,mFAAmF;IACnF,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI;CAiBlD;AAED;;;GAGG;AACH,qBAAa,eAAe;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,OAAO,CAAC,UAAU,CAAK;gBAEX,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa;IAK9C,yGAAyG;IACzG,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI;IAajD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI;CAiBlD;AAED;;GAEG;AACH,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,MAAM,CAAK;gBAEP,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS;IAK1C,oFAAoF;IACpF,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,qFAAqF;IACrF,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI;CAkBrD;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,iBAAiB,CAGhF;AAED;;;GAGG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,eAAe,CAG5E;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,mBAAmB,CAGpF;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAG7C"}
@@ -0,0 +1,238 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Metric instruments — mirrors Python provide.telemetry counter/gauge/histogram.
5
+ *
6
+ * Backed by @opentelemetry/api which provides no-op instruments when no SDK is registered.
7
+ * Safe to call in any environment; instruments are always callable without setup.
8
+ *
9
+ * Wrapper classes gate every `.add()`/`.record()` call through sampling + backpressure,
10
+ * matching the Python fallback.py pattern.
11
+ */
12
+ import { metrics, } from '@opentelemetry/api';
13
+ import { shouldSample } from './sampling';
14
+ import { tryAcquire, release } from './backpressure';
15
+ import { getActiveTraceIds } from './tracing';
16
+ import { getConfig } from './config';
17
+ // Stryker disable next-line StringLiteral: meter name not observable with no-op OTEL SDK in tests
18
+ const METER_NAME = '@provide-io/telemetry';
19
+ /**
20
+ * Wrapper around OTel Counter that gates add() through sampling + backpressure.
21
+ */
22
+ export class CounterInstrument {
23
+ constructor(name, inner) {
24
+ Object.defineProperty(this, "name", {
25
+ enumerable: true,
26
+ configurable: true,
27
+ writable: true,
28
+ value: void 0
29
+ });
30
+ Object.defineProperty(this, "_inner", {
31
+ enumerable: true,
32
+ configurable: true,
33
+ writable: true,
34
+ value: void 0
35
+ });
36
+ Object.defineProperty(this, "_value", {
37
+ enumerable: true,
38
+ configurable: true,
39
+ writable: true,
40
+ value: 0
41
+ });
42
+ this.name = name;
43
+ this._inner = inner;
44
+ }
45
+ /** Cumulative counter value (in-process; useful for testing and health checks). */
46
+ get value() {
47
+ return this._value;
48
+ }
49
+ add(value, attributes) {
50
+ if (!getConfig().metricsEnabled)
51
+ return;
52
+ if (!shouldSample('metrics', this.name))
53
+ return;
54
+ const ticket = tryAcquire('metrics');
55
+ if (!ticket)
56
+ return;
57
+ try {
58
+ const ids = getActiveTraceIds();
59
+ const enriched = ids.trace_id && ids.span_id
60
+ ? { ...attributes, trace_id: ids.trace_id, span_id: ids.span_id }
61
+ : attributes;
62
+ this._inner.add(value, enriched);
63
+ this._value += value;
64
+ }
65
+ finally {
66
+ release(ticket);
67
+ }
68
+ }
69
+ }
70
+ /**
71
+ * Wrapper around OTel UpDownCounter with set semantics.
72
+ * Gates add()/set() through sampling + backpressure.
73
+ */
74
+ export class GaugeInstrument {
75
+ constructor(name, inner) {
76
+ Object.defineProperty(this, "name", {
77
+ enumerable: true,
78
+ configurable: true,
79
+ writable: true,
80
+ value: void 0
81
+ });
82
+ Object.defineProperty(this, "_inner", {
83
+ enumerable: true,
84
+ configurable: true,
85
+ writable: true,
86
+ value: void 0
87
+ });
88
+ Object.defineProperty(this, "_values", {
89
+ enumerable: true,
90
+ configurable: true,
91
+ writable: true,
92
+ value: new Map()
93
+ });
94
+ Object.defineProperty(this, "_lastValue", {
95
+ enumerable: true,
96
+ configurable: true,
97
+ writable: true,
98
+ value: 0
99
+ });
100
+ this.name = name;
101
+ this._inner = inner;
102
+ }
103
+ /** Most recent value set or accumulated via add() (in-process; useful for testing and health checks). */
104
+ get value() {
105
+ return this._lastValue;
106
+ }
107
+ add(value, attributes) {
108
+ if (!getConfig().metricsEnabled)
109
+ return;
110
+ if (!shouldSample('metrics', this.name))
111
+ return;
112
+ const ticket = tryAcquire('metrics');
113
+ if (!ticket)
114
+ return;
115
+ try {
116
+ this._inner.add(value, attributes);
117
+ this._lastValue += value;
118
+ }
119
+ finally {
120
+ release(ticket);
121
+ }
122
+ }
123
+ set(value, attributes) {
124
+ if (!getConfig().metricsEnabled)
125
+ return;
126
+ if (!shouldSample('metrics', this.name))
127
+ return;
128
+ const ticket = tryAcquire('metrics');
129
+ if (!ticket)
130
+ return;
131
+ try {
132
+ // Stryker disable next-line StringLiteral: empty string fallback for no-attributes key — functionally equivalent to any constant
133
+ const key = attributes ? JSON.stringify(attributes) : '';
134
+ const prev = this._values.get(key) ?? 0;
135
+ const delta = value - prev;
136
+ this._values.set(key, value);
137
+ this._inner.add(delta, attributes);
138
+ this._lastValue = value;
139
+ }
140
+ finally {
141
+ release(ticket);
142
+ }
143
+ }
144
+ }
145
+ /**
146
+ * Wrapper around OTel Histogram that gates record() through sampling + backpressure.
147
+ */
148
+ export class HistogramInstrument {
149
+ constructor(name, inner) {
150
+ Object.defineProperty(this, "name", {
151
+ enumerable: true,
152
+ configurable: true,
153
+ writable: true,
154
+ value: void 0
155
+ });
156
+ Object.defineProperty(this, "_inner", {
157
+ enumerable: true,
158
+ configurable: true,
159
+ writable: true,
160
+ value: void 0
161
+ });
162
+ Object.defineProperty(this, "_count", {
163
+ enumerable: true,
164
+ configurable: true,
165
+ writable: true,
166
+ value: 0
167
+ });
168
+ Object.defineProperty(this, "_total", {
169
+ enumerable: true,
170
+ configurable: true,
171
+ writable: true,
172
+ value: 0
173
+ });
174
+ this.name = name;
175
+ this._inner = inner;
176
+ }
177
+ /** Number of values recorded (in-process; useful for testing and health checks). */
178
+ get count() {
179
+ return this._count;
180
+ }
181
+ /** Sum of all recorded values (in-process; useful for testing and health checks). */
182
+ get total() {
183
+ return this._total;
184
+ }
185
+ record(value, attributes) {
186
+ if (!getConfig().metricsEnabled)
187
+ return;
188
+ if (!shouldSample('metrics', this.name))
189
+ return;
190
+ const ticket = tryAcquire('metrics');
191
+ if (!ticket)
192
+ return;
193
+ try {
194
+ const ids = getActiveTraceIds();
195
+ const enriched = ids.trace_id && ids.span_id
196
+ ? { ...attributes, trace_id: ids.trace_id, span_id: ids.span_id }
197
+ : attributes;
198
+ this._inner.record(value, enriched);
199
+ this._count += 1;
200
+ this._total += value;
201
+ }
202
+ finally {
203
+ release(ticket);
204
+ }
205
+ }
206
+ }
207
+ /**
208
+ * Create a monotonically increasing counter.
209
+ * Mirrors Python: counter(name, description, unit)
210
+ */
211
+ export function counter(name, options) {
212
+ const inner = metrics.getMeter(METER_NAME).createCounter(name, options);
213
+ return new CounterInstrument(name, inner);
214
+ }
215
+ /**
216
+ * Create an up-down counter (gauge — can increase or decrease).
217
+ * Mirrors Python: gauge(name, description, unit)
218
+ */
219
+ export function gauge(name, options) {
220
+ const inner = metrics.getMeter(METER_NAME).createUpDownCounter(name, options);
221
+ return new GaugeInstrument(name, inner);
222
+ }
223
+ /**
224
+ * Create a histogram for recording distributions (latencies, sizes).
225
+ * Mirrors Python: histogram(name, description, unit)
226
+ */
227
+ export function histogram(name, options) {
228
+ const inner = metrics.getMeter(METER_NAME).createHistogram(name, options);
229
+ return new HistogramInstrument(name, inner);
230
+ }
231
+ /**
232
+ * Return an OTEL Meter from the global meter provider.
233
+ * Mirrors Python: get_meter(name)
234
+ */
235
+ export function getMeter(name) {
236
+ // Stryker disable next-line LogicalOperator: getMeter(undefined) behaves identically to getMeter(name) with no-op OTEL API
237
+ return metrics.getMeter(name ?? METER_NAME);
238
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Optional OTEL SDK log wiring — activated when registerOtelProviders() runs.
3
+ *
4
+ * Peer deps required:
5
+ * @opentelemetry/sdk-logs — LoggerProvider, BatchLogRecordProcessor
6
+ * @opentelemetry/exporter-logs-otlp-http — OTLPLogExporter
7
+ * @opentelemetry/api-logs — logs global, SeverityNumber
8
+ *
9
+ * Mirrors Python provide.telemetry.logger.core OTLPLogExporter wiring.
10
+ */
11
+ import type { TelemetryConfig } from './config';
12
+ import type { ShutdownableProvider } from './runtime';
13
+ /**
14
+ * Construct an OTLPLogExporter + LoggerProvider and register it globally.
15
+ * Returns a ShutdownableProvider so the caller can flush/shutdown it.
16
+ * Throws if any peer dep is missing (caught by the caller in otel.ts).
17
+ */
18
+ export declare function setupOtelLogProvider(cfg: TelemetryConfig): Promise<ShutdownableProvider>;
19
+ /**
20
+ * Emit a pino log record to the OTel LoggerProvider.
21
+ * Called from makeWriteHook() on every log line after enrichment and sanitization.
22
+ * No-op when no provider is registered (graceful degradation).
23
+ */
24
+ export declare function emitLogRecord(o: Record<string, unknown>): void;
25
+ /** Exposed for tests and resetTelemetryState(). */
26
+ export declare function _resetOtelLogProviderForTests(): void;
27
+ /** Exposed for integration tests to inspect state. */
28
+ export declare function _getOtelLogProvider(): any;
29
+ //# sourceMappingURL=otel-logs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otel-logs.d.ts","sourceRoot":"","sources":["../src/otel-logs.ts"],"names":[],"mappings":"AAOA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AA2BtD;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAmC9F;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAsD9D;AAED,mDAAmD;AACnD,wBAAgB,6BAA6B,IAAI,IAAI,CAGpD;AAED,sDAAsD;AAEtD,wBAAgB,mBAAmB,IAAI,GAAG,CAEzC"}