@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/config.ts ADDED
@@ -0,0 +1,589 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * TelemetryConfig — mirrors Python provide.telemetry TelemetryConfig.
6
+ *
7
+ * Env vars (same names as Python package):
8
+ * PROVIDE_TELEMETRY_SERVICE_NAME, PROVIDE_TELEMETRY_ENV (fallback: PROVIDE_ENV),
9
+ * PROVIDE_TELEMETRY_VERSION (fallback: PROVIDE_VERSION),
10
+ * PROVIDE_LOG_LEVEL, PROVIDE_LOG_FORMAT, PROVIDE_TRACE_ENABLED,
11
+ * PROVIDE_TELEMETRY_STRICT_SCHEMA,
12
+ * OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS
13
+ */
14
+
15
+ import { setSamplingPolicy } from './sampling';
16
+ import { setQueuePolicy } from './backpressure';
17
+ import { setExporterPolicy } from './resilience';
18
+ import { setSetupError } from './health';
19
+ import { ConfigurationError } from './exceptions';
20
+ export interface TelemetryConfig {
21
+ /** Service name injected into every log record. */
22
+ serviceName: string;
23
+ /** Deployment environment (e.g. "development", "production"). */
24
+ environment: string;
25
+ /** Application version injected into every log record. */
26
+ version: string;
27
+ /** Pino log level: trace | debug | info | warn | error. */
28
+ logLevel: string;
29
+ /** Output format: "json" (default) or "pretty". */
30
+ logFormat: 'json' | 'pretty';
31
+ /** Enable OTEL SDK registration on setupTelemetry(). */
32
+ otelEnabled: boolean;
33
+ /** OTLP export endpoint (e.g. "http://localhost:4318"). */
34
+ otlpEndpoint?: string;
35
+ /** OTLP headers as key=value pairs. */
36
+ otlpHeaders?: Record<string, string>;
37
+ /** Fields whose values are replaced with "[REDACTED]". */
38
+ sanitizeFields: string[];
39
+ /** Push every log object into window.__pinoLogs (browser only). */
40
+ captureToWindow: boolean;
41
+ /**
42
+ * Emit logs to browser console via console.debug/log/warn/error.
43
+ * Default false — use captureToWindow + window.__pinoLogs or OTEL export instead.
44
+ * Set true during local development for live devtools inspection.
45
+ */
46
+ consoleOutput: boolean;
47
+ /** Enforce strict event name validation (3-5 dot-separated segments). */
48
+ strictSchema: boolean;
49
+ /** Keys required on every log record when strictSchema is enabled. */
50
+ requiredLogKeys: string[];
51
+
52
+ // — Logging extras —
53
+ /** Include timestamp in log output. */
54
+ logIncludeTimestamp: boolean;
55
+ /** Include caller info in log output. */
56
+ logIncludeCaller: boolean;
57
+ /** Enable PII/secret sanitization in logs. */
58
+ logSanitize: boolean;
59
+ /** Attach code.filepath / code.lineno attributes to log records. */
60
+ logCodeAttributes: boolean;
61
+ /** Per-module log level overrides (e.g. {"provide.server": "DEBUG"}). */
62
+ logModuleLevels: Record<string, string>;
63
+
64
+ // — Tracing —
65
+ /** Trace sampling rate (0.0–1.0). */
66
+ traceSampleRate: number;
67
+
68
+ // — Metrics —
69
+ /** Enable metrics collection. */
70
+ metricsEnabled: boolean;
71
+
72
+ // — Per-signal sampling —
73
+ /** Probabilistic sampling rate for logs (0.0–1.0). */
74
+ samplingLogsRate: number;
75
+ /** Probabilistic sampling rate for traces (0.0–1.0). */
76
+ samplingTracesRate: number;
77
+ /** Probabilistic sampling rate for metrics (0.0–1.0). */
78
+ samplingMetricsRate: number;
79
+
80
+ // — Per-signal backpressure —
81
+ /** Max queue size for log export (0 = unbounded). */
82
+ backpressureLogsMaxsize: number;
83
+ /** Max queue size for trace export (0 = unbounded). */
84
+ backpressureTracesMaxsize: number;
85
+ /** Max queue size for metric export (0 = unbounded). */
86
+ backpressureMetricsMaxsize: number;
87
+
88
+ // — Per-signal exporter resilience —
89
+ /** Max retries for log export. */
90
+ exporterLogsRetries: number;
91
+ /** Backoff between log export retries (ms). */
92
+ exporterLogsBackoffMs: number;
93
+ /** Timeout for log export (ms). */
94
+ exporterLogsTimeoutMs: number;
95
+ /** If true, drop telemetry on export failure instead of crashing. */
96
+ exporterLogsFailOpen: boolean;
97
+ /** Max retries for trace export. */
98
+ exporterTracesRetries: number;
99
+ /** Backoff between trace export retries (ms). */
100
+ exporterTracesBackoffMs: number;
101
+ /** Timeout for trace export (ms). */
102
+ exporterTracesTimeoutMs: number;
103
+ /** If true, drop telemetry on export failure instead of crashing. */
104
+ exporterTracesFailOpen: boolean;
105
+ /** Max retries for metric export. */
106
+ exporterMetricsRetries: number;
107
+ /** Backoff between metric export retries (ms). */
108
+ exporterMetricsBackoffMs: number;
109
+ /** Timeout for metric export (ms). */
110
+ exporterMetricsTimeoutMs: number;
111
+ /** If true, drop telemetry on export failure instead of crashing. */
112
+ exporterMetricsFailOpen: boolean;
113
+
114
+ // — SLO —
115
+ /** Enable RED (Rate/Error/Duration) metrics. */
116
+ sloEnableRedMetrics: boolean;
117
+ /** Enable USE (Utilization/Saturation/Errors) metrics. */
118
+ sloEnableUseMetrics: boolean;
119
+
120
+ // — PII —
121
+ /** Maximum recursion depth for PII sanitization of nested objects. */
122
+ piiMaxDepth: number;
123
+
124
+ // — Security —
125
+ /** Max length for any single attribute value. */
126
+ securityMaxAttrValueLength: number;
127
+ /** Max number of attributes on a single span/log/metric point. */
128
+ securityMaxAttrCount: number;
129
+ }
130
+
131
+ /**
132
+ * Hot-reloadable config subset. Only fields that can be changed at runtime
133
+ * without restarting providers. All fields are optional.
134
+ */
135
+ export interface RuntimeOverrides {
136
+ // Sampling
137
+ samplingLogsRate?: number;
138
+ samplingTracesRate?: number;
139
+ samplingMetricsRate?: number;
140
+
141
+ // Backpressure
142
+ backpressureLogsMaxsize?: number;
143
+ backpressureTracesMaxsize?: number;
144
+ backpressureMetricsMaxsize?: number;
145
+
146
+ // Exporter resilience
147
+ exporterLogsRetries?: number;
148
+ exporterLogsBackoffMs?: number;
149
+ exporterLogsTimeoutMs?: number;
150
+ exporterLogsFailOpen?: boolean;
151
+ exporterTracesRetries?: number;
152
+ exporterTracesBackoffMs?: number;
153
+ exporterTracesTimeoutMs?: number;
154
+ exporterTracesFailOpen?: boolean;
155
+ exporterMetricsRetries?: number;
156
+ exporterMetricsBackoffMs?: number;
157
+ exporterMetricsTimeoutMs?: number;
158
+ exporterMetricsFailOpen?: boolean;
159
+
160
+ // Security
161
+ securityMaxAttrValueLength?: number;
162
+ securityMaxAttrCount?: number;
163
+
164
+ // SLO
165
+ sloEnableRedMetrics?: boolean;
166
+ sloEnableUseMetrics?: boolean;
167
+
168
+ // PII
169
+ piiMaxDepth?: number;
170
+ }
171
+
172
+ const DEFAULTS: TelemetryConfig = {
173
+ serviceName: 'provide-service',
174
+ environment: 'development',
175
+ version: 'unknown',
176
+ logLevel: 'info',
177
+ logFormat: 'json',
178
+ otelEnabled: false,
179
+ sanitizeFields: [],
180
+ captureToWindow: true,
181
+ consoleOutput: false,
182
+ strictSchema: false,
183
+ requiredLogKeys: [],
184
+ logIncludeTimestamp: true,
185
+ logIncludeCaller: true,
186
+ logSanitize: true,
187
+ logCodeAttributes: false,
188
+ logModuleLevels: {},
189
+ traceSampleRate: 1.0,
190
+ metricsEnabled: true,
191
+ samplingLogsRate: 1.0,
192
+ samplingTracesRate: 1.0,
193
+ samplingMetricsRate: 1.0,
194
+ backpressureLogsMaxsize: 0,
195
+ backpressureTracesMaxsize: 0,
196
+ backpressureMetricsMaxsize: 0,
197
+ exporterLogsRetries: 0,
198
+ exporterLogsBackoffMs: 0,
199
+ exporterLogsTimeoutMs: 10000,
200
+ exporterLogsFailOpen: true,
201
+ exporterTracesRetries: 0,
202
+ exporterTracesBackoffMs: 0,
203
+ exporterTracesTimeoutMs: 10000,
204
+ exporterTracesFailOpen: true,
205
+ exporterMetricsRetries: 0,
206
+ exporterMetricsBackoffMs: 0,
207
+ exporterMetricsTimeoutMs: 10000,
208
+ exporterMetricsFailOpen: true,
209
+ sloEnableRedMetrics: false,
210
+ sloEnableUseMetrics: false,
211
+ piiMaxDepth: 8,
212
+ securityMaxAttrValueLength: 1024,
213
+ securityMaxAttrCount: 64,
214
+ };
215
+
216
+ let _config: TelemetryConfig = { ...DEFAULTS };
217
+
218
+ /** Read a string from Node process.env. Silently returns undefined in non-Node environments. */
219
+ // Stryker disable BlockStatement
220
+ function nodeEnv(key: string): string | undefined {
221
+ try {
222
+ // process.env is not available in browser builds after tree-shaking,
223
+ // but some bundlers (esbuild, Vite) leave process.env.X inline replacements.
224
+ // Stryker disable next-line ConditionalExpression,StringLiteral: process is always defined in Node.js/test environments
225
+ return typeof process !== 'undefined' ? process.env[key] : undefined;
226
+ } catch {
227
+ return undefined;
228
+ }
229
+ }
230
+ // Stryker enable BlockStatement
231
+
232
+ /** Parse an env var as a number, falling back to `fallback` on missing or NaN. */
233
+ function envNumber(key: string, fallback: number): number {
234
+ const raw = nodeEnv(key);
235
+ // Stryker disable next-line ConditionalExpression: undefined check — removing returns NaN path which NaN guard catches identically
236
+ if (raw === undefined) return fallback;
237
+ const n = Number(raw);
238
+ return Number.isNaN(n) ? fallback : n;
239
+ }
240
+
241
+ function envBool(key: string, fallback: boolean): boolean {
242
+ const raw = nodeEnv(key);
243
+ if (raw === undefined || raw.trim() === '') return fallback;
244
+ switch (raw.trim().toLowerCase()) {
245
+ case '1':
246
+ case 'true':
247
+ case 'yes':
248
+ case 'on':
249
+ return true;
250
+ case '0':
251
+ case 'false':
252
+ case 'no':
253
+ case 'off':
254
+ return false;
255
+ default:
256
+ throw new ConfigurationError(
257
+ `invalid boolean for ${key}: ${JSON.stringify(raw)} (expected one of: 1,true,yes,on,0,false,no,off)`,
258
+ );
259
+ }
260
+ }
261
+
262
+ function envFloatInRange(key: string, fallback: number, min: number, max: number): number {
263
+ const value = envNumber(key, fallback);
264
+ if (!Number.isFinite(value) || value < min || value > max) {
265
+ throw new ConfigurationError(`${key} must be in [${min}, ${max}], got ${String(value)}`);
266
+ }
267
+ return value;
268
+ }
269
+
270
+ function envNonNegativeInt(key: string, fallback: number): number {
271
+ const value = envNumber(key, fallback);
272
+ if (!Number.isInteger(value) || value < 0) {
273
+ throw new ConfigurationError(`${key} must be a non-negative integer, got ${String(value)}`);
274
+ }
275
+ return value;
276
+ }
277
+
278
+ function envNonNegativeMsFromSeconds(key: string, fallbackMs: number): number {
279
+ const value = envSecondsToMs(key, fallbackMs);
280
+ if (!Number.isFinite(value) || value < 0) {
281
+ throw new ConfigurationError(`${key} must be >= 0, got ${String(value)}`);
282
+ }
283
+ return value;
284
+ }
285
+
286
+ /** Parse an env var expressed in seconds and return milliseconds. */
287
+ function envSecondsToMs(key: string, fallbackMs: number): number {
288
+ const raw = nodeEnv(key);
289
+ // Stryker disable next-line ConditionalExpression: same as envNumber — undefined falls through to NaN guard
290
+ if (raw === undefined) return fallbackMs;
291
+ const n = Number(raw);
292
+ return Number.isNaN(n) ? fallbackMs : n * 1000;
293
+ }
294
+
295
+ /** Parse a module_levels string like "mod1=DEBUG,mod2=WARN" into a Record. */
296
+ function parseModuleLevels(raw: string | undefined): Record<string, string> {
297
+ if (!raw) return {};
298
+ const result: Record<string, string> = {};
299
+ for (const pair of raw.split(',')) {
300
+ /* Stryker disable MethodExpression,StringLiteral,ConditionalExpression: trim + includes('=') guard — removing produces malformed but non-crashing output */
301
+ const trimmed = pair.trim();
302
+ if (!trimmed.includes('=')) continue;
303
+ /* Stryker restore MethodExpression,StringLiteral,ConditionalExpression */
304
+ const [mod, level] = trimmed.split('=', 2).map((s) => s.trim());
305
+ if (mod && level) result[mod] = level;
306
+ }
307
+ return result;
308
+ }
309
+
310
+ /**
311
+ * Build a TelemetryConfig from environment variables.
312
+ * Uses the same env var names as the Python package.
313
+ * Explicit values passed to setupTelemetry() override env vars.
314
+ */
315
+ export function configFromEnv(): TelemetryConfig {
316
+ const otelHeader = nodeEnv('OTEL_EXPORTER_OTLP_HEADERS');
317
+ const parsedHeaders = otelHeader ? parseOtlpHeaders(otelHeader) : undefined;
318
+
319
+ return {
320
+ serviceName: nodeEnv('PROVIDE_TELEMETRY_SERVICE_NAME') ?? DEFAULTS.serviceName,
321
+ environment: nodeEnv('PROVIDE_TELEMETRY_ENV') ?? nodeEnv('PROVIDE_ENV') ?? DEFAULTS.environment,
322
+ version: nodeEnv('PROVIDE_TELEMETRY_VERSION') ?? nodeEnv('PROVIDE_VERSION') ?? DEFAULTS.version,
323
+ logLevel: nodeEnv('PROVIDE_LOG_LEVEL')?.toLowerCase() ?? DEFAULTS.logLevel,
324
+ logFormat: (() => {
325
+ const fmt = nodeEnv('PROVIDE_LOG_FORMAT');
326
+ // Stryker disable next-line ConditionalExpression: 'json' is DEFAULTS.logFormat so removing its check returns the same default value
327
+ return fmt === 'json' || fmt === 'pretty' ? fmt : DEFAULTS.logFormat;
328
+ })(),
329
+ otelEnabled: envBool('PROVIDE_TRACE_ENABLED', DEFAULTS.otelEnabled),
330
+ otlpEndpoint: nodeEnv('OTEL_EXPORTER_OTLP_ENDPOINT'),
331
+ otlpHeaders: parsedHeaders,
332
+ sanitizeFields: DEFAULTS.sanitizeFields,
333
+ captureToWindow: true,
334
+ consoleOutput: false,
335
+ strictSchema: envBool('PROVIDE_TELEMETRY_STRICT_SCHEMA', DEFAULTS.strictSchema),
336
+ requiredLogKeys: (() => {
337
+ const raw = nodeEnv('PROVIDE_TELEMETRY_REQUIRED_KEYS');
338
+ return raw
339
+ ? raw
340
+ .split(',')
341
+ .map((s) => s.trim())
342
+ .filter(Boolean)
343
+ : [];
344
+ })(),
345
+
346
+ // Logging extras
347
+ logIncludeTimestamp: envBool('PROVIDE_LOG_INCLUDE_TIMESTAMP', DEFAULTS.logIncludeTimestamp),
348
+ logIncludeCaller: envBool('PROVIDE_LOG_INCLUDE_CALLER', DEFAULTS.logIncludeCaller),
349
+ logSanitize: envBool('PROVIDE_LOG_SANITIZE', DEFAULTS.logSanitize),
350
+ logCodeAttributes: envBool('PROVIDE_LOG_CODE_ATTRIBUTES', DEFAULTS.logCodeAttributes),
351
+ logModuleLevels: parseModuleLevels(nodeEnv('PROVIDE_LOG_MODULE_LEVELS')),
352
+
353
+ // Tracing
354
+ traceSampleRate: envFloatInRange('PROVIDE_TRACE_SAMPLE_RATE', DEFAULTS.traceSampleRate, 0, 1),
355
+
356
+ // Metrics
357
+ metricsEnabled: envBool('PROVIDE_METRICS_ENABLED', DEFAULTS.metricsEnabled),
358
+
359
+ // Per-signal sampling
360
+ samplingLogsRate: envFloatInRange(
361
+ 'PROVIDE_SAMPLING_LOGS_RATE',
362
+ DEFAULTS.samplingLogsRate,
363
+ 0,
364
+ 1,
365
+ ),
366
+ samplingTracesRate: envFloatInRange(
367
+ 'PROVIDE_SAMPLING_TRACES_RATE',
368
+ DEFAULTS.samplingTracesRate,
369
+ 0,
370
+ 1,
371
+ ),
372
+ samplingMetricsRate: envFloatInRange(
373
+ 'PROVIDE_SAMPLING_METRICS_RATE',
374
+ DEFAULTS.samplingMetricsRate,
375
+ 0,
376
+ 1,
377
+ ),
378
+
379
+ // Per-signal backpressure
380
+ backpressureLogsMaxsize: envNonNegativeInt(
381
+ 'PROVIDE_BACKPRESSURE_LOGS_MAXSIZE',
382
+ DEFAULTS.backpressureLogsMaxsize,
383
+ ),
384
+ backpressureTracesMaxsize: envNonNegativeInt(
385
+ 'PROVIDE_BACKPRESSURE_TRACES_MAXSIZE',
386
+ DEFAULTS.backpressureTracesMaxsize,
387
+ ),
388
+ backpressureMetricsMaxsize: envNonNegativeInt(
389
+ 'PROVIDE_BACKPRESSURE_METRICS_MAXSIZE',
390
+ DEFAULTS.backpressureMetricsMaxsize,
391
+ ),
392
+
393
+ // Per-signal exporter resilience
394
+ exporterLogsRetries: envNonNegativeInt(
395
+ 'PROVIDE_EXPORTER_LOGS_RETRIES',
396
+ DEFAULTS.exporterLogsRetries,
397
+ ),
398
+ exporterLogsBackoffMs: envNonNegativeMsFromSeconds(
399
+ 'PROVIDE_EXPORTER_LOGS_BACKOFF_SECONDS',
400
+ DEFAULTS.exporterLogsBackoffMs,
401
+ ),
402
+ exporterLogsTimeoutMs: envNonNegativeMsFromSeconds(
403
+ 'PROVIDE_EXPORTER_LOGS_TIMEOUT_SECONDS',
404
+ DEFAULTS.exporterLogsTimeoutMs,
405
+ ),
406
+ exporterLogsFailOpen: envBool('PROVIDE_EXPORTER_LOGS_FAIL_OPEN', DEFAULTS.exporterLogsFailOpen),
407
+ exporterTracesRetries: envNonNegativeInt(
408
+ 'PROVIDE_EXPORTER_TRACES_RETRIES',
409
+ DEFAULTS.exporterTracesRetries,
410
+ ),
411
+ exporterTracesBackoffMs: envNonNegativeMsFromSeconds(
412
+ 'PROVIDE_EXPORTER_TRACES_BACKOFF_SECONDS',
413
+ DEFAULTS.exporterTracesBackoffMs,
414
+ ),
415
+ exporterTracesTimeoutMs: envNonNegativeMsFromSeconds(
416
+ 'PROVIDE_EXPORTER_TRACES_TIMEOUT_SECONDS',
417
+ DEFAULTS.exporterTracesTimeoutMs,
418
+ ),
419
+ exporterTracesFailOpen: envBool(
420
+ 'PROVIDE_EXPORTER_TRACES_FAIL_OPEN',
421
+ DEFAULTS.exporterTracesFailOpen,
422
+ ),
423
+ exporterMetricsRetries: envNonNegativeInt(
424
+ 'PROVIDE_EXPORTER_METRICS_RETRIES',
425
+ DEFAULTS.exporterMetricsRetries,
426
+ ),
427
+ exporterMetricsBackoffMs: envNonNegativeMsFromSeconds(
428
+ 'PROVIDE_EXPORTER_METRICS_BACKOFF_SECONDS',
429
+ DEFAULTS.exporterMetricsBackoffMs,
430
+ ),
431
+ exporterMetricsTimeoutMs: envNonNegativeMsFromSeconds(
432
+ 'PROVIDE_EXPORTER_METRICS_TIMEOUT_SECONDS',
433
+ DEFAULTS.exporterMetricsTimeoutMs,
434
+ ),
435
+ exporterMetricsFailOpen: envBool(
436
+ 'PROVIDE_EXPORTER_METRICS_FAIL_OPEN',
437
+ DEFAULTS.exporterMetricsFailOpen,
438
+ ),
439
+
440
+ // SLO
441
+ sloEnableRedMetrics: envBool('PROVIDE_SLO_ENABLE_RED_METRICS', DEFAULTS.sloEnableRedMetrics),
442
+ sloEnableUseMetrics: envBool('PROVIDE_SLO_ENABLE_USE_METRICS', DEFAULTS.sloEnableUseMetrics),
443
+
444
+ // PII
445
+ piiMaxDepth: envNonNegativeInt('PROVIDE_LOG_PII_MAX_DEPTH', DEFAULTS.piiMaxDepth),
446
+
447
+ // Security
448
+ securityMaxAttrValueLength: envNonNegativeInt(
449
+ 'PROVIDE_SECURITY_MAX_ATTR_VALUE_LENGTH',
450
+ DEFAULTS.securityMaxAttrValueLength,
451
+ ),
452
+ securityMaxAttrCount: envNonNegativeInt(
453
+ 'PROVIDE_SECURITY_MAX_ATTR_COUNT',
454
+ DEFAULTS.securityMaxAttrCount,
455
+ ),
456
+ };
457
+ }
458
+
459
+ /** Return the active TelemetryConfig. */
460
+ export function getConfig(): TelemetryConfig {
461
+ return _config;
462
+ }
463
+
464
+ /**
465
+ * Apply parsed config fields to the runtime policy engines (sampling, backpressure, resilience).
466
+ * Mirrors Python provide.telemetry.runtime.apply_runtime_config.
467
+ */
468
+ export function applyConfigPolicies(cfg: TelemetryConfig): void {
469
+ // Sampling
470
+ setSamplingPolicy('logs', { defaultRate: cfg.samplingLogsRate });
471
+ setSamplingPolicy('traces', { defaultRate: cfg.samplingTracesRate });
472
+ setSamplingPolicy('metrics', { defaultRate: cfg.samplingMetricsRate });
473
+
474
+ // Backpressure
475
+ setQueuePolicy({
476
+ maxLogs: cfg.backpressureLogsMaxsize,
477
+ maxTraces: cfg.backpressureTracesMaxsize,
478
+ maxMetrics: cfg.backpressureMetricsMaxsize,
479
+ });
480
+
481
+ // Exporter resilience (per-signal)
482
+ setExporterPolicy('logs', {
483
+ retries: cfg.exporterLogsRetries,
484
+ backoffMs: cfg.exporterLogsBackoffMs,
485
+ timeoutMs: cfg.exporterLogsTimeoutMs,
486
+ failOpen: cfg.exporterLogsFailOpen,
487
+ });
488
+ setExporterPolicy('traces', {
489
+ retries: cfg.exporterTracesRetries,
490
+ backoffMs: cfg.exporterTracesBackoffMs,
491
+ timeoutMs: cfg.exporterTracesTimeoutMs,
492
+ failOpen: cfg.exporterTracesFailOpen,
493
+ });
494
+ setExporterPolicy('metrics', {
495
+ retries: cfg.exporterMetricsRetries,
496
+ backoffMs: cfg.exporterMetricsBackoffMs,
497
+ timeoutMs: cfg.exporterMetricsTimeoutMs,
498
+ failOpen: cfg.exporterMetricsFailOpen,
499
+ });
500
+ }
501
+
502
+ /**
503
+ * Configure telemetry. Call once at app startup.
504
+ * Merges explicit values over the current config (which may include env-derived values).
505
+ */
506
+ export function setupTelemetry(overrides?: Partial<TelemetryConfig>): void {
507
+ _config = { ...configFromEnv(), ...overrides };
508
+ try {
509
+ applyConfigPolicies(_config);
510
+ } catch (err: unknown) {
511
+ const message = err instanceof Error ? err.message : String(err);
512
+ setSetupError(message);
513
+ console.warn(`setupTelemetry: applyConfigPolicies failed: ${message}`);
514
+ }
515
+ }
516
+
517
+ /** Reset to defaults (used in tests). */
518
+ export function _resetConfig(): void {
519
+ _config = { ...DEFAULTS };
520
+ }
521
+
522
+ /**
523
+ * Parse OTLP-style header string "key=value,key2=value2" into a Record.
524
+ * Keys and values are URL-decoded. Malformed pairs (no '=') and empty keys are skipped.
525
+ * Values may contain '=' characters (only the first '=' splits key from value).
526
+ */
527
+ export function parseOtlpHeaders(raw: string): Record<string, string> {
528
+ const result: Record<string, string> = {};
529
+ // Stryker disable next-line ConditionalExpression: early return is an optimization — empty string splits to [""], idx<1 skips the only pair, returns {} identically
530
+ if (!raw) return result;
531
+ for (const pair of raw.split(',')) {
532
+ const idx = pair.indexOf('=');
533
+ if (idx < 1) continue; // no '=' or empty key
534
+ const rawKey = pair.slice(0, idx).trim();
535
+ const rawVal = pair.slice(idx + 1).trim();
536
+ try {
537
+ const key = decodeURIComponent(rawKey);
538
+ // Stryker disable next-line ConditionalExpression: defensive guard — unreachable because idx<1 check and trim() already exclude empty keys
539
+ /* v8 ignore next: defensive — idx<1 and trim() already exclude observable empty keys */
540
+ if (!key) continue;
541
+ const val = decodeURIComponent(rawVal);
542
+ result[key] = val;
543
+ } catch {
544
+ // Skip pairs with invalid URL encoding
545
+ continue;
546
+ }
547
+ }
548
+ return result;
549
+ }
550
+
551
+ /** Mask a single header value: show first 4 chars + **** if >= 8 chars, else ****. */
552
+ function maskHeaderValue(v: string): string {
553
+ return v.length < 8 ? '****' : v.slice(0, 4) + '****';
554
+ }
555
+
556
+ /** Mask the password component of a URL's userinfo, if present. */
557
+ function maskEndpointUrl(raw: string): string {
558
+ try {
559
+ const u = new URL(raw);
560
+ if (u.password) {
561
+ u.password = '****';
562
+ return u.toString();
563
+ }
564
+ } catch {
565
+ /* not a valid URL — return as-is */
566
+ }
567
+ return raw;
568
+ }
569
+
570
+ /**
571
+ * Return a copy of the config with OTLP secrets masked.
572
+ * Safe to log or serialize — never leaks header values or endpoint credentials.
573
+ */
574
+ export function redactConfig(config: TelemetryConfig): Record<string, unknown> {
575
+ const result: Record<string, unknown> = { ...config };
576
+ if (config.otlpHeaders && Object.keys(config.otlpHeaders).length > 0) {
577
+ result.otlpHeaders = Object.fromEntries(
578
+ Object.entries(config.otlpHeaders).map(([k, v]) => [k, maskHeaderValue(v)]),
579
+ );
580
+ }
581
+ if (config.otlpEndpoint) {
582
+ result.otlpEndpoint = maskEndpointUrl(config.otlpEndpoint);
583
+ }
584
+ return result;
585
+ }
586
+
587
+ /** Package version — mirrors Python __version__. */
588
+ export const version = '0.2.0';
589
+ export const __version__ = version;
package/src/consent.ts ADDED
@@ -0,0 +1,61 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Consent-aware telemetry collection — strippable governance module.
6
+ * When deleted, all signals pass through unchanged.
7
+ */
8
+
9
+ export type ConsentLevel = 'FULL' | 'FUNCTIONAL' | 'MINIMAL' | 'NONE';
10
+
11
+ const LOG_LEVEL_ORDER: Record<string, number> = {
12
+ TRACE: 0,
13
+ DEBUG: 1,
14
+ INFO: 2,
15
+ WARNING: 3,
16
+ WARN: 3,
17
+ ERROR: 4,
18
+ CRITICAL: 5,
19
+ };
20
+
21
+ let _consentLevel: ConsentLevel = 'FULL';
22
+
23
+ export function setConsentLevel(level: ConsentLevel): void {
24
+ _consentLevel = level;
25
+ }
26
+
27
+ export function getConsentLevel(): ConsentLevel {
28
+ return _consentLevel;
29
+ }
30
+
31
+ export function shouldAllow(signal: string, logLevel?: string): boolean {
32
+ const level = _consentLevel;
33
+ if (level === 'FULL') return true;
34
+ if (level === 'NONE') return false;
35
+ if (level === 'FUNCTIONAL') {
36
+ if (signal === 'logs') {
37
+ const order = LOG_LEVEL_ORDER[(logLevel ?? '').toUpperCase()] ?? 0;
38
+ return order >= LOG_LEVEL_ORDER['WARNING'];
39
+ }
40
+ if (signal === 'context') return false;
41
+ return true;
42
+ }
43
+ // MINIMAL
44
+ if (signal === 'logs') {
45
+ const order = LOG_LEVEL_ORDER[(logLevel ?? '').toUpperCase()] ?? 0;
46
+ return order >= LOG_LEVEL_ORDER['ERROR'];
47
+ }
48
+ return false;
49
+ }
50
+
51
+ export function loadConsentFromEnv(): void {
52
+ const raw = (process.env['PROVIDE_CONSENT_LEVEL'] ?? 'FULL').trim().toUpperCase() as ConsentLevel;
53
+ const valid: ConsentLevel[] = ['FULL', 'FUNCTIONAL', 'MINIMAL', 'NONE'];
54
+ if (valid.includes(raw)) {
55
+ _consentLevel = raw;
56
+ }
57
+ }
58
+
59
+ export function resetConsentForTests(): void {
60
+ _consentLevel = 'FULL';
61
+ }