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