@bleedingdev/modern-js-server-runtime-extensions 0.0.0-trusted-publisher-bootstrap

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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/cjs/contractGateAutopilot.js +162 -0
  4. package/dist/cjs/contractGateSnapshotStore.js +253 -0
  5. package/dist/cjs/env.js +58 -0
  6. package/dist/cjs/index.js +162 -0
  7. package/dist/cjs/mfCache.js +106 -0
  8. package/dist/cjs/moduleFederationCss.js +285 -0
  9. package/dist/cjs/runtimeFallbackSignal.js +311 -0
  10. package/dist/cjs/telemetry.js +373 -0
  11. package/dist/cjs/telemetryCore.js +819 -0
  12. package/dist/esm/contractGateAutopilot.mjs +124 -0
  13. package/dist/esm/contractGateSnapshotStore.mjs +190 -0
  14. package/dist/esm/env.mjs +17 -0
  15. package/dist/esm/index.mjs +6 -0
  16. package/dist/esm/mfCache.mjs +55 -0
  17. package/dist/esm/moduleFederationCss.mjs +225 -0
  18. package/dist/esm/runtimeFallbackSignal.mjs +222 -0
  19. package/dist/esm/telemetry.mjs +275 -0
  20. package/dist/esm/telemetryCore.mjs +759 -0
  21. package/dist/esm-node/contractGateAutopilot.mjs +125 -0
  22. package/dist/esm-node/contractGateSnapshotStore.mjs +192 -0
  23. package/dist/esm-node/env.mjs +18 -0
  24. package/dist/esm-node/index.mjs +7 -0
  25. package/dist/esm-node/mfCache.mjs +56 -0
  26. package/dist/esm-node/moduleFederationCss.mjs +226 -0
  27. package/dist/esm-node/runtimeFallbackSignal.mjs +223 -0
  28. package/dist/esm-node/telemetry.mjs +276 -0
  29. package/dist/esm-node/telemetryCore.mjs +760 -0
  30. package/dist/types/contractGateAutopilot.d.ts +35 -0
  31. package/dist/types/contractGateSnapshotStore.d.ts +57 -0
  32. package/dist/types/env.d.ts +40 -0
  33. package/dist/types/index.d.ts +6 -0
  34. package/dist/types/mfCache.d.ts +27 -0
  35. package/dist/types/moduleFederationCss.d.ts +87 -0
  36. package/dist/types/runtimeFallbackSignal.d.ts +94 -0
  37. package/dist/types/telemetry.d.ts +12 -0
  38. package/dist/types/telemetryCore.d.ts +257 -0
  39. package/package.json +69 -0
  40. package/rslib.config.mts +4 -0
  41. package/rstest.config.mts +7 -0
  42. package/src/contractGateAutopilot.ts +247 -0
  43. package/src/contractGateSnapshotStore.ts +420 -0
  44. package/src/env.ts +63 -0
  45. package/src/index.ts +84 -0
  46. package/src/mfCache.ts +119 -0
  47. package/src/moduleFederationCss.ts +473 -0
  48. package/src/runtimeFallbackSignal.ts +584 -0
  49. package/src/telemetry.ts +554 -0
  50. package/src/telemetryCore.ts +1332 -0
  51. package/tests/contractGateAutopilot.test.ts +203 -0
  52. package/tests/contractGateSnapshotStore.test.ts +223 -0
  53. package/tests/env.test.ts +73 -0
  54. package/tests/helpers.ts +19 -0
  55. package/tests/mfCache.test.ts +150 -0
  56. package/tests/moduleFederationCss.test.ts +392 -0
  57. package/tests/registration.test.ts +112 -0
  58. package/tests/telemetry.test.ts +360 -0
  59. package/tests/telemetryAutopilot.test.ts +993 -0
  60. package/tests/telemetryCanaryOrchestrator.test.ts +140 -0
  61. package/tests/telemetryLifecycle.test.ts +168 -0
  62. package/tests/telemetryTraceparent.test.ts +167 -0
  63. package/tests/tsconfig.json +11 -0
  64. package/tsconfig.json +10 -0
@@ -0,0 +1,760 @@
1
+ import "node:module";
2
+ class TelemetryStartupHealthError extends Error {
3
+ constructor(failedExporters){
4
+ super(`Telemetry startup health check failed for exporters: ${failedExporters.map((item)=>item.name).join(', ')}`), this.code = 'TELEMETRY_EXPORTER_STARTUP_HEALTH_FAILED';
5
+ this.name = 'TelemetryStartupHealthError';
6
+ this.failedExporters = failedExporters;
7
+ }
8
+ }
9
+ const DEFAULT_OTLP_ENDPOINT = 'http://127.0.0.1:4318/v1/logs';
10
+ const DEFAULT_VM_ENDPOINT = 'http://127.0.0.1:8428/api/v1/import/prometheus';
11
+ const DEFAULT_TIMEOUT_MS = 5000;
12
+ function clamp(value, min, max) {
13
+ return Math.max(min, Math.min(max, value));
14
+ }
15
+ function isRecord(value) {
16
+ return 'object' == typeof value && null !== value && !Array.isArray(value);
17
+ }
18
+ function redactObject(value, redactionKeys) {
19
+ if (!isRecord(value)) return;
20
+ const output = {};
21
+ for (const [key, nested] of Object.entries(value)){
22
+ if (redactionKeys.has(key)) {
23
+ output[key] = '[REDACTED]';
24
+ continue;
25
+ }
26
+ if (Array.isArray(nested)) {
27
+ output[key] = nested.map((item)=>{
28
+ if (isRecord(item)) return redactObject(item, redactionKeys);
29
+ return item;
30
+ });
31
+ continue;
32
+ }
33
+ if (isRecord(nested)) {
34
+ output[key] = redactObject(nested, redactionKeys);
35
+ continue;
36
+ }
37
+ output[key] = nested;
38
+ }
39
+ return output;
40
+ }
41
+ function normalizeLabels(labels) {
42
+ if (!labels) return;
43
+ const normalized = {};
44
+ for (const [key, value] of Object.entries(labels))if (null != value) normalized[key] = String(value);
45
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
46
+ }
47
+ function extractError(args) {
48
+ for (const arg of args)if (arg instanceof Error) return {
49
+ name: arg.name,
50
+ message: arg.message,
51
+ stack: arg.stack
52
+ };
53
+ }
54
+ function toTelemetryEnvelope(event, input) {
55
+ const base = {
56
+ timestamp: Date.now(),
57
+ service: input.service,
58
+ module: input.module,
59
+ environment: input.environment,
60
+ ...input.traceId ? {
61
+ traceId: input.traceId
62
+ } : {},
63
+ ...input.spanId ? {
64
+ spanId: input.spanId
65
+ } : {},
66
+ ...input.attributes ? {
67
+ attributes: input.attributes
68
+ } : {}
69
+ };
70
+ if ('log' === event.type) {
71
+ const payload = event.payload;
72
+ const args = payload.args || [];
73
+ const signalType = 'trace' === payload.level ? 'trace' : 'log';
74
+ return {
75
+ ...base,
76
+ signalType,
77
+ name: payload.message,
78
+ level: payload.level,
79
+ attributes: {
80
+ ...base.attributes || {},
81
+ args
82
+ },
83
+ error: extractError(args)
84
+ };
85
+ }
86
+ if ('timing' === event.type) return {
87
+ ...base,
88
+ signalType: 'metric',
89
+ name: event.payload.name,
90
+ value: event.payload.dur,
91
+ unit: 'ms',
92
+ tags: normalizeLabels(event.payload.tags),
93
+ attributes: {
94
+ ...base.attributes || {},
95
+ desc: event.payload.desc,
96
+ args: event.payload.args
97
+ }
98
+ };
99
+ return {
100
+ ...base,
101
+ signalType: 'metric',
102
+ name: event.payload.name,
103
+ value: 1,
104
+ unit: 'count',
105
+ tags: normalizeLabels(event.payload.tags),
106
+ attributes: {
107
+ ...base.attributes || {},
108
+ args: event.payload.args
109
+ }
110
+ };
111
+ }
112
+ async function postWithTimeout(options) {
113
+ const controller = new AbortController();
114
+ const timer = setTimeout(()=>controller.abort(), options.timeoutMs);
115
+ if ('function' == typeof timer.unref) timer.unref();
116
+ try {
117
+ const response = await fetch(options.endpoint, {
118
+ method: 'POST',
119
+ body: options.body,
120
+ headers: options.headers,
121
+ signal: controller.signal
122
+ });
123
+ if (!response.ok) throw new Error(`Telemetry exporter request failed: ${response.status} ${response.statusText}`);
124
+ } finally{
125
+ clearTimeout(timer);
126
+ }
127
+ }
128
+ function sanitizeMetricName(value) {
129
+ return value.replace(/[^a-zA-Z0-9_:]/g, '_').replace(/_+/g, '_');
130
+ }
131
+ function escapeLabelValue(value) {
132
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
133
+ }
134
+ function toPrometheusLine(envelope, metricPrefix) {
135
+ const metricName = sanitizeMetricName(`${metricPrefix}_${envelope.signalType}_${envelope.name}`);
136
+ const labels = {
137
+ service: envelope.service,
138
+ module: envelope.module,
139
+ environment: envelope.environment,
140
+ ...envelope.level ? {
141
+ level: envelope.level
142
+ } : {},
143
+ ...envelope.traceId ? {
144
+ trace_id: envelope.traceId
145
+ } : {},
146
+ ...envelope.spanId ? {
147
+ span_id: envelope.spanId
148
+ } : {},
149
+ ...envelope.tags || {}
150
+ };
151
+ const labelPairs = Object.entries(labels).sort(([a], [b])=>a.localeCompare(b)).map(([key, value])=>`${sanitizeMetricName(key)}="${escapeLabelValue(value)}"`);
152
+ const labelText = labelPairs.length > 0 ? `{${labelPairs.join(',')}}` : '';
153
+ const value = 'number' == typeof envelope.value && Number.isFinite(envelope.value) ? envelope.value : 1;
154
+ const timestampMs = envelope.timestamp;
155
+ return `${metricName}${labelText} ${value} ${timestampMs}`;
156
+ }
157
+ function createOtlpTelemetryExporter(options = {}) {
158
+ const endpoint = options.endpoint || DEFAULT_OTLP_ENDPOINT;
159
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
160
+ const headers = {
161
+ 'content-type': 'application/json',
162
+ ...options.headers || {}
163
+ };
164
+ return {
165
+ name: 'otlp',
166
+ async emit (batch) {
167
+ if (0 === batch.length) return;
168
+ const body = JSON.stringify({
169
+ resource: {
170
+ service: batch[0]?.service,
171
+ module: batch[0]?.module,
172
+ environment: batch[0]?.environment
173
+ },
174
+ emittedAt: Date.now(),
175
+ events: batch
176
+ });
177
+ await postWithTimeout({
178
+ endpoint,
179
+ body,
180
+ headers,
181
+ timeoutMs
182
+ });
183
+ }
184
+ };
185
+ }
186
+ function createVictoriaMetricsTelemetryExporter(options = {}) {
187
+ const endpoint = options.endpoint || DEFAULT_VM_ENDPOINT;
188
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
189
+ const metricPrefix = sanitizeMetricName(options.metricPrefix || 'modernjs');
190
+ const headers = {
191
+ 'content-type': 'text/plain; version=0.0.4',
192
+ ...options.headers || {}
193
+ };
194
+ return {
195
+ name: 'victoria-metrics',
196
+ async emit (batch) {
197
+ if (0 === batch.length) return;
198
+ const lines = batch.map((item)=>toPrometheusLine(item, metricPrefix));
199
+ await postWithTimeout({
200
+ endpoint,
201
+ body: `${lines.join('\n')}\n`,
202
+ headers,
203
+ timeoutMs
204
+ });
205
+ }
206
+ };
207
+ }
208
+ class TelemetryRegistry {
209
+ async register(exporter) {
210
+ this.exporters.push(exporter);
211
+ this.exporterHealth.set(exporter.name, {
212
+ name: exporter.name,
213
+ healthy: true,
214
+ failures: 0
215
+ });
216
+ if (exporter.init) try {
217
+ await exporter.init({
218
+ service: this.service,
219
+ module: this.module,
220
+ environment: this.environment
221
+ });
222
+ this.markExporterHealthy(exporter.name);
223
+ } catch (error) {
224
+ this.markExporterFailure(exporter.name, error);
225
+ throw error;
226
+ }
227
+ else this.markExporterHealthy(exporter.name);
228
+ }
229
+ getOrCreateExporterHealth(name) {
230
+ const existing = this.exporterHealth.get(name);
231
+ if (existing) return existing;
232
+ const next = {
233
+ name,
234
+ healthy: true,
235
+ failures: 0
236
+ };
237
+ this.exporterHealth.set(name, next);
238
+ return next;
239
+ }
240
+ markExporterHealthy(name) {
241
+ const status = this.getOrCreateExporterHealth(name);
242
+ status.healthy = true;
243
+ status.lastSuccessAt = Date.now();
244
+ status.lastError = void 0;
245
+ }
246
+ markExporterFailure(name, error) {
247
+ const status = this.getOrCreateExporterHealth(name);
248
+ status.healthy = false;
249
+ status.failures += 1;
250
+ status.lastFailureAt = Date.now();
251
+ status.lastError = error instanceof Error ? error.message : String(error);
252
+ }
253
+ maybeEmitSloAlert(type, value, threshold) {
254
+ if (!this.onSloAlert || value < threshold) return;
255
+ const now = Date.now();
256
+ const lastTimestamp = this.lastSloAlertAt.get(type) ?? 0;
257
+ if (now - lastTimestamp < this.alertCooldownMs) return;
258
+ this.lastSloAlertAt.set(type, now);
259
+ const queueDepth = this.queue.length;
260
+ try {
261
+ this.onSloAlert({
262
+ timestamp: now,
263
+ service: this.service,
264
+ module: this.module,
265
+ environment: this.environment,
266
+ type,
267
+ value,
268
+ threshold,
269
+ queueDepth,
270
+ queueCapacity: this.maxQueueSize,
271
+ queueUtilization: queueDepth / this.maxQueueSize,
272
+ totalDropped: this.totalDroppedCount
273
+ });
274
+ } catch (_error) {}
275
+ }
276
+ enqueue(envelope) {
277
+ if (this.samplingRate < 1 && Math.random() > this.samplingRate) return;
278
+ const redactedEnvelope = this.redactionKeys.size > 0 ? {
279
+ ...envelope,
280
+ attributes: redactObject(envelope.attributes, this.redactionKeys)
281
+ } : envelope;
282
+ if (this.queue.length >= this.maxQueueSize) {
283
+ this.queue.shift();
284
+ this.droppedCount += 1;
285
+ this.totalDroppedCount += 1;
286
+ this.maybeEmitSloAlert('queue.drop', this.totalDroppedCount, this.queueDroppedWarnThreshold);
287
+ }
288
+ this.queue.push(redactedEnvelope);
289
+ this.maybeEmitSloAlert('queue.utilization', this.queue.length / this.maxQueueSize, this.queueUtilizationWarnThreshold);
290
+ if (this.queue.length >= this.maxBatchSize) this.flush();
291
+ }
292
+ enqueueMetric(input) {
293
+ this.enqueue({
294
+ timestamp: Date.now(),
295
+ service: this.service,
296
+ module: this.module,
297
+ environment: this.environment,
298
+ signalType: 'metric',
299
+ name: input.name,
300
+ value: input.value,
301
+ unit: input.unit || 'count',
302
+ traceId: input.traceId,
303
+ spanId: input.spanId,
304
+ parentSpanId: input.parentSpanId,
305
+ tags: input.tags,
306
+ attributes: input.attributes
307
+ });
308
+ }
309
+ enqueueLog(input) {
310
+ this.enqueue({
311
+ timestamp: Date.now(),
312
+ service: this.service,
313
+ module: this.module,
314
+ environment: this.environment,
315
+ signalType: 'log',
316
+ name: input.name,
317
+ level: input.level,
318
+ traceId: input.traceId,
319
+ spanId: input.spanId,
320
+ parentSpanId: input.parentSpanId,
321
+ tags: input.tags,
322
+ attributes: input.attributes,
323
+ error: input.error
324
+ });
325
+ }
326
+ enqueueTrace(input) {
327
+ this.enqueue({
328
+ timestamp: Date.now(),
329
+ service: this.service,
330
+ module: this.module,
331
+ environment: this.environment,
332
+ signalType: 'trace',
333
+ name: input.name,
334
+ traceId: input.traceId,
335
+ spanId: input.spanId,
336
+ parentSpanId: input.parentSpanId,
337
+ tags: input.tags,
338
+ attributes: input.attributes
339
+ });
340
+ }
341
+ buildDroppedEnvelope(droppedCount) {
342
+ return {
343
+ timestamp: Date.now(),
344
+ service: this.service,
345
+ module: this.module,
346
+ environment: this.environment,
347
+ signalType: 'metric',
348
+ name: 'telemetry.queue.dropped',
349
+ value: droppedCount,
350
+ unit: 'count',
351
+ tags: {
352
+ reason: 'queue_backpressure'
353
+ }
354
+ };
355
+ }
356
+ buildQueueDepthEnvelope(queueDepth) {
357
+ return {
358
+ timestamp: Date.now(),
359
+ service: this.service,
360
+ module: this.module,
361
+ environment: this.environment,
362
+ signalType: 'metric',
363
+ name: 'telemetry.queue.depth',
364
+ value: queueDepth,
365
+ unit: 'count',
366
+ tags: {
367
+ capacity: String(this.maxQueueSize)
368
+ }
369
+ };
370
+ }
371
+ buildQueueUtilizationEnvelope(queueDepth) {
372
+ return {
373
+ timestamp: Date.now(),
374
+ service: this.service,
375
+ module: this.module,
376
+ environment: this.environment,
377
+ signalType: 'metric',
378
+ name: 'telemetry.queue.utilization',
379
+ value: queueDepth / this.maxQueueSize,
380
+ unit: 'ratio',
381
+ tags: {
382
+ capacity: String(this.maxQueueSize)
383
+ }
384
+ };
385
+ }
386
+ async emitBatch(batch) {
387
+ const results = await Promise.allSettled(this.exporters.map(async (exporter)=>{
388
+ await exporter.emit(batch);
389
+ return exporter.name;
390
+ }));
391
+ for (const [index, result] of results.entries()){
392
+ const exporterName = this.exporters[index]?.name || `exporter-${index}`;
393
+ if ('rejected' === result.status) {
394
+ this.markExporterFailure(exporterName, result.reason);
395
+ continue;
396
+ }
397
+ this.markExporterHealthy(exporterName);
398
+ }
399
+ }
400
+ buildStartupProbeEnvelope() {
401
+ return {
402
+ timestamp: Date.now(),
403
+ service: this.service,
404
+ module: this.module,
405
+ environment: this.environment,
406
+ signalType: 'log',
407
+ name: 'telemetry.exporter.startup_probe',
408
+ level: 'info',
409
+ tags: {
410
+ phase: 'startup'
411
+ },
412
+ attributes: {
413
+ source: 'TelemetryRegistry'
414
+ }
415
+ };
416
+ }
417
+ async startupHealthCheck(options) {
418
+ if (0 === this.exporters.length) return;
419
+ const probeBatch = [
420
+ this.buildStartupProbeEnvelope()
421
+ ];
422
+ const failedExporters = [];
423
+ await Promise.all(this.exporters.map(async (exporter)=>{
424
+ try {
425
+ await exporter.emit(probeBatch);
426
+ this.markExporterHealthy(exporter.name);
427
+ } catch (error) {
428
+ this.markExporterFailure(exporter.name, error);
429
+ const status = this.exporterHealth.get(exporter.name);
430
+ if (status) failedExporters.push({
431
+ ...status
432
+ });
433
+ }
434
+ }));
435
+ if ((options?.failLoud ?? true) && failedExporters.length > 0) throw new TelemetryStartupHealthError(failedExporters);
436
+ }
437
+ getExporterHealth() {
438
+ return Array.from(this.exporterHealth.values()).map((item)=>({
439
+ ...item
440
+ }));
441
+ }
442
+ getQueueStats() {
443
+ return {
444
+ depth: this.queue.length,
445
+ capacity: this.maxQueueSize,
446
+ utilization: this.queue.length / this.maxQueueSize,
447
+ pendingDropped: this.droppedCount,
448
+ totalDropped: this.totalDroppedCount
449
+ };
450
+ }
451
+ async flushInternal() {
452
+ const queueDepthBeforeFlush = this.queue.length;
453
+ if (queueDepthBeforeFlush > 0) {
454
+ this.queue.unshift(this.buildQueueUtilizationEnvelope(queueDepthBeforeFlush));
455
+ this.queue.unshift(this.buildQueueDepthEnvelope(queueDepthBeforeFlush));
456
+ }
457
+ if (this.droppedCount > 0) {
458
+ const droppedCount = this.droppedCount;
459
+ this.droppedCount = 0;
460
+ this.queue.unshift(this.buildDroppedEnvelope(droppedCount));
461
+ }
462
+ if (0 === this.queue.length) return;
463
+ if (0 === this.exporters.length) {
464
+ this.queue.length = 0;
465
+ return;
466
+ }
467
+ while(this.queue.length > 0){
468
+ const batch = this.queue.splice(0, this.maxBatchSize);
469
+ await this.emitBatch(batch);
470
+ }
471
+ await Promise.allSettled(this.exporters.map(async (exporter)=>{
472
+ if (exporter.flush) await exporter.flush();
473
+ }));
474
+ }
475
+ flush() {
476
+ if (this.flushing) return this.flushing;
477
+ this.flushing = this.flushInternal().finally(()=>{
478
+ this.flushing = null;
479
+ });
480
+ return this.flushing;
481
+ }
482
+ async shutdown() {
483
+ if (this.flushTimer) clearInterval(this.flushTimer);
484
+ await this.flush();
485
+ await Promise.allSettled(this.exporters.map(async (exporter)=>{
486
+ if (exporter.shutdown) await exporter.shutdown();
487
+ }));
488
+ }
489
+ constructor(options){
490
+ this.exporters = [];
491
+ this.queue = [];
492
+ this.droppedCount = 0;
493
+ this.totalDroppedCount = 0;
494
+ this.flushing = null;
495
+ this.exporterHealth = new Map();
496
+ this.lastSloAlertAt = new Map();
497
+ this.service = options.service;
498
+ this.module = options.module;
499
+ this.environment = options.environment;
500
+ this.samplingRate = clamp(options.samplingRate ?? 1, 0, 1);
501
+ this.maxBatchSize = Math.max(1, options.maxBatchSize ?? 50);
502
+ this.maxQueueSize = Math.max(1, options.maxQueueSize ?? 1000);
503
+ this.flushIntervalMs = Math.max(50, options.flushIntervalMs ?? 1000);
504
+ this.redactionKeys = new Set(options.redactionKeys || []);
505
+ this.queueUtilizationWarnThreshold = clamp(options.slo?.queueUtilizationWarnThreshold ?? 0.8, 0, 1);
506
+ this.queueDroppedWarnThreshold = Math.max(1, options.slo?.queueDroppedWarnThreshold ?? 1);
507
+ this.alertCooldownMs = Math.max(0, options.slo?.alertCooldownMs ?? 60000);
508
+ this.onSloAlert = options.slo?.onAlert;
509
+ this.flushTimer = setInterval(()=>{
510
+ this.flush();
511
+ }, this.flushIntervalMs);
512
+ if ('function' == typeof this.flushTimer.unref) this.flushTimer.unref();
513
+ }
514
+ }
515
+ class TelemetryCanaryOrchestrator {
516
+ setRequiredContractGates(gates) {
517
+ this.requiredContractGates = Array.from(new Set(gates.map((item)=>item.trim()).filter(Boolean)));
518
+ }
519
+ addRequiredContractGate(name) {
520
+ const normalizedName = name.trim();
521
+ if (!normalizedName) return;
522
+ if (!this.requiredContractGates.includes(normalizedName)) this.requiredContractGates.push(normalizedName);
523
+ }
524
+ setContractGate(name, passed, reason) {
525
+ this.contractGates.set(name, {
526
+ name,
527
+ passed,
528
+ reason,
529
+ updatedAt: Date.now()
530
+ });
531
+ }
532
+ setContractGates(gates) {
533
+ for (const [name, value] of Object.entries(gates)){
534
+ if ('boolean' == typeof value) {
535
+ this.setContractGate(name, value);
536
+ continue;
537
+ }
538
+ this.setContractGate(name, value.passed, value.reason);
539
+ }
540
+ }
541
+ resetToCanary() {
542
+ this.state = 'canary';
543
+ this.consecutiveHealthy = 0;
544
+ this.consecutiveFailures = 0;
545
+ }
546
+ collectFailures() {
547
+ const failures = [];
548
+ const queueStats = this.registry.getQueueStats();
549
+ const unhealthyExporterCount = this.registry.getExporterHealth().filter((item)=>!item.healthy).length;
550
+ if (queueStats.utilization > this.maxQueueUtilization) failures.push({
551
+ reason: 'queue_utilization',
552
+ threshold: this.maxQueueUtilization,
553
+ value: queueStats.utilization
554
+ });
555
+ if (queueStats.totalDropped > this.maxTotalDropped) failures.push({
556
+ reason: 'queue_dropped',
557
+ threshold: this.maxTotalDropped,
558
+ value: queueStats.totalDropped
559
+ });
560
+ if (unhealthyExporterCount > this.maxUnhealthyExporters) failures.push({
561
+ reason: 'unhealthy_exporter',
562
+ threshold: this.maxUnhealthyExporters,
563
+ value: unhealthyExporterCount
564
+ });
565
+ for (const gateName of this.requiredContractGates){
566
+ const gate = this.contractGates.get(gateName);
567
+ if (!gate) {
568
+ failures.push({
569
+ reason: 'contract_gate_missing',
570
+ gate: gateName,
571
+ message: `Contract gate "${gateName}" is missing`
572
+ });
573
+ continue;
574
+ }
575
+ if (!gate.passed) failures.push({
576
+ reason: 'contract_gate_failed',
577
+ gate: gateName,
578
+ message: gate.reason || `Contract gate "${gateName}" is not passing`
579
+ });
580
+ }
581
+ return {
582
+ failures,
583
+ queueStats,
584
+ unhealthyExporterCount
585
+ };
586
+ }
587
+ evaluate() {
588
+ const now = Date.now();
589
+ const { failures, queueStats, unhealthyExporterCount } = this.collectFailures();
590
+ let action = 'hold';
591
+ if (failures.length > 0) {
592
+ this.consecutiveHealthy = 0;
593
+ this.consecutiveFailures += 1;
594
+ if ('rolled_back' !== this.state && this.consecutiveFailures >= this.rollbackConsecutiveFailures) {
595
+ this.state = 'rolled_back';
596
+ action = 'rollback';
597
+ }
598
+ } else {
599
+ this.consecutiveFailures = 0;
600
+ this.consecutiveHealthy += 1;
601
+ if ('canary' === this.state && this.consecutiveHealthy >= this.minConsecutiveHealthyEvaluations) {
602
+ this.state = 'promoted';
603
+ action = 'promote';
604
+ }
605
+ }
606
+ const decision = {
607
+ timestamp: now,
608
+ action,
609
+ state: this.state,
610
+ consecutiveHealthy: this.consecutiveHealthy,
611
+ consecutiveFailures: this.consecutiveFailures,
612
+ failures,
613
+ queueStats,
614
+ unhealthyExporterCount,
615
+ contractGates: Array.from(this.contractGates.values()).map((item)=>({
616
+ ...item
617
+ }))
618
+ };
619
+ try {
620
+ this.onEvaluate?.(decision);
621
+ } catch (_error) {}
622
+ if ('promote' === action) try {
623
+ this.onPromote?.(decision);
624
+ } catch (_error) {}
625
+ if ('rollback' === action) try {
626
+ this.onRollback?.(decision);
627
+ } catch (_error) {}
628
+ return decision;
629
+ }
630
+ getStatusSnapshot() {
631
+ const now = Date.now();
632
+ const { failures, queueStats, unhealthyExporterCount } = this.collectFailures();
633
+ return {
634
+ timestamp: now,
635
+ state: this.state,
636
+ consecutiveHealthy: this.consecutiveHealthy,
637
+ consecutiveFailures: this.consecutiveFailures,
638
+ queueStats,
639
+ unhealthyExporterCount,
640
+ requiredContractGates: [
641
+ ...this.requiredContractGates
642
+ ],
643
+ contractGates: Array.from(this.contractGates.values()).map((item)=>({
644
+ ...item
645
+ })),
646
+ failurePreview: failures
647
+ };
648
+ }
649
+ start() {
650
+ if (this.evaluationTimer) return;
651
+ this.evaluationTimer = setInterval(()=>{
652
+ this.evaluate();
653
+ }, this.evaluationIntervalMs);
654
+ if ('function' == typeof this.evaluationTimer.unref) this.evaluationTimer.unref();
655
+ }
656
+ stop() {
657
+ if (this.evaluationTimer) {
658
+ clearInterval(this.evaluationTimer);
659
+ this.evaluationTimer = void 0;
660
+ }
661
+ }
662
+ constructor(options){
663
+ this.requiredContractGates = [];
664
+ this.contractGates = new Map();
665
+ this.state = 'canary';
666
+ this.consecutiveHealthy = 0;
667
+ this.consecutiveFailures = 0;
668
+ this.registry = options.registry;
669
+ this.evaluationIntervalMs = Math.max(250, options.evaluationIntervalMs ?? 15000);
670
+ this.minConsecutiveHealthyEvaluations = Math.max(1, options.minConsecutiveHealthyEvaluations ?? 3);
671
+ this.rollbackConsecutiveFailures = Math.max(1, options.rollbackConsecutiveFailures ?? 2);
672
+ this.maxQueueUtilization = clamp(options.maxQueueUtilization ?? 0.8, 0, 1);
673
+ this.maxTotalDropped = Math.max(0, options.maxTotalDropped ?? 0);
674
+ this.maxUnhealthyExporters = Math.max(0, options.maxUnhealthyExporters ?? 0);
675
+ this.setRequiredContractGates(options.requiredContractGates || []);
676
+ this.onEvaluate = options.onEvaluate;
677
+ this.onPromote = options.onPromote;
678
+ this.onRollback = options.onRollback;
679
+ }
680
+ }
681
+ function normalizeMetricsInput(prefixOrTags, tags) {
682
+ if ('string' == typeof prefixOrTags) return {
683
+ prefix: prefixOrTags,
684
+ tags: tags || {}
685
+ };
686
+ if (isRecord(prefixOrTags)) return {
687
+ prefix: void 0,
688
+ tags: prefixOrTags
689
+ };
690
+ return {
691
+ prefix: void 0,
692
+ tags: tags || {}
693
+ };
694
+ }
695
+ function normalizeMetricName(name, prefix) {
696
+ return prefix && prefix.length > 0 ? `${prefix}.${name}` : name;
697
+ }
698
+ function toTelemetryMetricTags(tags) {
699
+ const output = {};
700
+ for (const [key, value] of Object.entries(tags))if (null != value) output[key] = String(value);
701
+ return output;
702
+ }
703
+ function getTraceContext(tags) {
704
+ const traceId = 'string' == typeof tags.trace_id ? tags.trace_id : 'string' == typeof tags.traceId ? tags.traceId : void 0;
705
+ const spanId = 'string' == typeof tags.span_id ? tags.span_id : 'string' == typeof tags.spanId ? tags.spanId : void 0;
706
+ const parentSpanId = 'string' == typeof tags.parent_span_id ? tags.parent_span_id : 'string' == typeof tags.parentSpanId ? tags.parentSpanId : void 0;
707
+ return {
708
+ traceId,
709
+ spanId,
710
+ parentSpanId
711
+ };
712
+ }
713
+ const createTelemetryAwareMetrics = (baseMetrics, registry)=>{
714
+ const emitCounter = (name, value, prefixOrTags, tags)=>{
715
+ const normalized = normalizeMetricsInput(prefixOrTags, tags);
716
+ baseMetrics.emitCounter(name, value, normalized.prefix, normalized.tags);
717
+ try {
718
+ const metricName = normalizeMetricName(name, normalized.prefix);
719
+ const traceContext = getTraceContext(normalized.tags);
720
+ registry.enqueueMetric({
721
+ name: metricName,
722
+ value,
723
+ unit: 'count',
724
+ traceId: traceContext.traceId,
725
+ spanId: traceContext.spanId,
726
+ parentSpanId: traceContext.parentSpanId,
727
+ tags: toTelemetryMetricTags(normalized.tags),
728
+ attributes: normalized.tags
729
+ });
730
+ } catch (_error) {}
731
+ };
732
+ const emitTimer = (name, value, prefixOrTags, tags)=>{
733
+ const normalized = normalizeMetricsInput(prefixOrTags, tags);
734
+ baseMetrics.emitTimer(name, value, normalized.prefix, normalized.tags);
735
+ try {
736
+ const metricName = normalizeMetricName(name, normalized.prefix);
737
+ const traceContext = getTraceContext(normalized.tags);
738
+ registry.enqueueMetric({
739
+ name: metricName,
740
+ value,
741
+ unit: 'ms',
742
+ traceId: traceContext.traceId,
743
+ spanId: traceContext.spanId,
744
+ parentSpanId: traceContext.parentSpanId,
745
+ tags: toTelemetryMetricTags(normalized.tags),
746
+ attributes: normalized.tags
747
+ });
748
+ } catch (_error) {}
749
+ };
750
+ return {
751
+ ...baseMetrics,
752
+ emitCounter,
753
+ emitTimer
754
+ };
755
+ };
756
+ function maybeWarnLegacyOtlpEndpoint(endpoint) {
757
+ if (!endpoint || !endpoint.includes('/v1/metrics')) return;
758
+ console.warn(`[telemetry] OTLP endpoint "${endpoint}" looks like a metrics path. UltraModern telemetry exporter expects log-style envelopes (default: ${DEFAULT_OTLP_ENDPOINT}).`);
759
+ }
760
+ export { TelemetryCanaryOrchestrator, TelemetryRegistry, TelemetryStartupHealthError, createOtlpTelemetryExporter, createTelemetryAwareMetrics, createVictoriaMetricsTelemetryExporter, maybeWarnLegacyOtlpEndpoint, toTelemetryEnvelope };