@fluojs/metrics 1.0.4 → 3.0.0

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.
@@ -3,15 +3,19 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
3
3
  function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
4
4
  function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
5
5
  function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
6
+ import { readFileSync } from 'node:fs';
7
+ import { createRequire } from 'node:module';
6
8
  import { Inject } from '@fluojs/core';
7
9
  import { ContainerResolutionError } from '@fluojs/di';
8
- import { Controller, Get, forRoutes } from '@fluojs/http';
10
+ import { Controller, forRoutes, Get } from '@fluojs/http';
9
11
  import { defineModule, PLATFORM_SHELL } from '@fluojs/runtime';
12
+ import { BOOTSTRAP_PROVIDER_TOKENS, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
10
13
  import { collectDefaultMetrics, Gauge, Registry as PrometheusRegistry } from 'prom-client';
11
14
  import { HttpMetricsMiddleware } from './http-metrics-middleware.js';
12
- import { METER_PROVIDER } from './providers/meter-provider.js';
13
15
  import { MetricsService } from './metrics-service.js';
16
+ import { METER_PROVIDER } from './providers/meter-provider.js';
14
17
  import { PrometheusMeterProvider } from './providers/prometheus-meter-provider.js';
18
+ import { SerializedScrapeQueue } from './serialized-scrape-queue.js';
15
19
 
16
20
  /** HTTP-specific metric labeling options exposed by `MetricsModule.forRoot(...)`. */
17
21
 
@@ -19,6 +23,9 @@ import { PrometheusMeterProvider } from './providers/prometheus-meter-provider.j
19
23
  * Module options for exposing Prometheus metrics and runtime platform telemetry.
20
24
  */
21
25
 
26
+ /** Bootstrap provider token for a Registry shared by metrics module instances. */
27
+ export const METRICS_REGISTRY = Symbol.for('fluo.metrics.registry');
28
+
22
29
  /** Module entry point that exposes `/metrics` and optional HTTP/runtime telemetry. */
23
30
  export class MetricsModule {
24
31
  static registeredRegistries = new WeakSet();
@@ -30,11 +37,10 @@ export class MetricsModule {
30
37
  * ```ts
31
38
  * MetricsModule.forRoot({
32
39
  * http: { pathLabelMode: 'template' },
33
- * registry: new Registry(),
34
40
  * });
35
41
  * ```
36
42
  *
37
- * @param options Metrics endpoint, registry, HTTP middleware, and runtime telemetry configuration.
43
+ * @param options Metrics endpoint, HTTP middleware, and runtime telemetry configuration.
38
44
  * @returns A runtime module that exposes metrics through the configured path.
39
45
  */
40
46
  static forRoot(options = {}) {
@@ -48,13 +54,31 @@ export class MetricsModule {
48
54
  const platformTelemetryToken = Symbol('MetricsModule.platformTelemetry');
49
55
  const httpMetricsMiddleware = httpOptions ? createHttpMetricsMiddleware(registryToken, httpOptions) : undefined;
50
56
  const endpointMiddleware = typeof metricsPath === 'string' ? (options.endpointMiddleware ?? []).map(middlewareClass => forRoutes(middlewareClass, metricsPath)) : [];
51
- const middleware = [...(httpMetricsMiddleware ? [httpMetricsMiddleware] : []), ...endpointMiddleware, ...(options.middleware ?? [])];
52
- const providers = [...(httpMetricsMiddleware ? [httpMetricsMiddleware] : []), {
57
+ const middleware = [...endpointMiddleware, ...(options.middleware ?? [])];
58
+ const registryProvider = {
53
59
  provide: registryToken,
54
- useFactory: () => MetricsModule.createRegistry(options)
55
- }, {
60
+ inject: [RUNTIME_CONTAINER, BOOTSTRAP_PROVIDER_TOKENS],
61
+ useFactory: async (container, bootstrapProviderTokens) => {
62
+ const runtimeContainer = assertRuntimeContainer(container);
63
+ const configuredRegistry = assertBootstrapProviderTokens(bootstrapProviderTokens).has(METRICS_REGISTRY) ? assertPrometheusRegistry(await runtimeContainer.resolve(METRICS_REGISTRY)) : undefined;
64
+ return MetricsModule.createRegistry(options, configuredRegistry);
65
+ }
66
+ };
67
+ const imports = [];
68
+ const includeRuntimeRegistryProvider = httpMetricsMiddleware === undefined;
69
+ if (httpOptions && httpMetricsMiddleware) {
70
+ class MetricsHttpInstrumentationModule {}
71
+ defineModule(MetricsHttpInstrumentationModule, {
72
+ exports: [registryToken],
73
+ global: true,
74
+ middleware: [httpMetricsMiddleware],
75
+ providers: [registryProvider, httpMetricsMiddleware]
76
+ });
77
+ imports.push(MetricsHttpInstrumentationModule);
78
+ }
79
+ const providers = [...(includeRuntimeRegistryProvider ? [registryProvider] : []), {
56
80
  provide: MetricsService,
57
- inject: [registryToken],
81
+ inject: [registryToken, platformTelemetryToken],
58
82
  useFactory: registry => new MetricsService(assertPrometheusRegistry(registry))
59
83
  }, {
60
84
  provide: METER_PROVIDER,
@@ -62,8 +86,8 @@ export class MetricsModule {
62
86
  useFactory: registry => new PrometheusMeterProvider(assertPrometheusRegistry(registry))
63
87
  }, {
64
88
  provide: platformTelemetryToken,
65
- inject: [registryToken],
66
- useFactory: registry => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), options.registry ? 'shared' : 'isolated', options.platformTelemetry)
89
+ inject: [registryToken, RUNTIME_CONTAINER, BOOTSTRAP_PROVIDER_TOKENS],
90
+ useFactory: (registry, container, bootstrapProviderTokens) => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), assertRuntimeContainer(container), resolveRegistryMode(options, assertBootstrapProviderTokens(bootstrapProviderTokens)), options.platformTelemetry)
67
91
  }];
68
92
  const controllers = [];
69
93
  if (typeof metricsPath === 'string') {
@@ -78,13 +102,13 @@ export class MetricsModule {
78
102
  } = _applyDecs(this, [Inject(registryToken, platformTelemetryToken), Controller('')], [[Get(metricsRoutePath), 2, "getMetrics"]]));
79
103
  }
80
104
  constructor(registry, platformTelemetry) {
105
+ _initProto(this);
81
106
  this.registry = registry;
82
107
  this.platformTelemetry = platformTelemetry;
83
- _initProto(this);
84
108
  }
85
109
  async getMetrics(_input, ctx) {
86
110
  ctx.response.setHeader('content-type', this.registry.contentType);
87
- return this.platformTelemetry.collectMetrics(ctx, this.registry);
111
+ return this.platformTelemetry.collectMetrics(this.registry);
88
112
  }
89
113
  static {
90
114
  _initClass();
@@ -95,42 +119,74 @@ export class MetricsModule {
95
119
  class MetricsRuntimeModule {}
96
120
  defineModule(MetricsRuntimeModule, {
97
121
  controllers,
122
+ exports: [MetricsService, METER_PROVIDER],
123
+ imports,
98
124
  middleware,
99
125
  providers
100
126
  });
101
127
  return MetricsRuntimeModule;
102
128
  }
103
- static createRegistry(options) {
104
- const registry = options.registry ?? new PrometheusRegistry();
129
+ static createRegistry(options, configuredRegistry) {
130
+ const registry = configuredRegistry ?? options.registry ?? new PrometheusRegistry();
105
131
  if (options.defaultMetrics !== false && !MetricsModule.registeredRegistries.has(registry)) {
132
+ assertNoDefaultMetricCollisions(registry);
133
+ const existingMetricNames = new Set(registry.getMetricsAsArray().map(metric => metric.name));
134
+ try {
135
+ collectDefaultMetrics({
136
+ register: registry
137
+ });
138
+ } catch (error) {
139
+ for (const metric of registry.getMetricsAsArray()) {
140
+ if (!existingMetricNames.has(metric.name)) {
141
+ registry.removeSingleMetric(metric.name);
142
+ }
143
+ }
144
+ throw error;
145
+ }
106
146
  MetricsModule.registeredRegistries.add(registry);
107
- collectDefaultMetrics({
108
- register: registry
109
- });
110
147
  }
111
148
  return registry;
112
149
  }
113
150
  }
114
151
  const PLATFORM_COMPONENT_LABELS = ['component_id', 'component_kind', 'operation', 'result', 'env', 'instance'];
115
152
  const REGISTRY_MODE_LABELS = ['mode'];
153
+ const require = createRequire(import.meta.url);
154
+ // prom-client exposes collector IDs publicly but not their metric names. This v15.1.3-only
155
+ // private metadata seam keeps collision preflight side-effect-free; the exact package pin and
156
+ // runtime-support regression must be updated together before changing prom-client.
157
+ const DEFAULT_METRIC_COLLECTORS = collectDefaultMetrics.metricsList.map(collectorName => {
158
+ const collector = require(`prom-client/lib/metrics/${collectorName}`);
159
+ if (!hasDefaultMetricNames(collector)) {
160
+ throw new Error(`prom-client default collector "${collectorName}" does not expose metricNames.`);
161
+ }
162
+ return {
163
+ collectorName,
164
+ metricNames: collector.metricNames
165
+ };
166
+ });
116
167
  const FRAMEWORK_PLATFORM_GAUGES = new WeakSet();
168
+ const PLATFORM_TELEMETRY_REGISTRY_STATES = new WeakMap();
169
+ const HTTP_INSTRUMENTATION_OWNERS = new WeakMap();
117
170
  const HEALTH_STATUSES = ['healthy', 'unhealthy', 'degraded'];
118
171
  const READINESS_STATUSES = ['ready', 'not-ready', 'degraded'];
119
- const PLATFORM_SHELL_TOKEN_NAME = 'PLATFORM_SHELL';
120
- const PLATFORM_SHELL_TOKEN_NAMES = new Set([PLATFORM_SHELL_TOKEN_NAME, String(PLATFORM_SHELL)]);
172
+ const PLATFORM_SHELL_TOKEN_NAMES = new Set([String(PLATFORM_SHELL)]);
121
173
  function createHttpMetricsMiddleware(registryToken, httpOptions) {
122
174
  let _initClass2;
123
175
  let _MetricsHttpMiddlewar;
124
176
  class MetricsHttpMiddleware {
125
177
  static {
126
- [_MetricsHttpMiddlewar, _initClass2] = _applyDecs(this, [Inject(registryToken)], []).c;
178
+ [_MetricsHttpMiddlewar, _initClass2] = _applyDecs(this, [Inject(registryToken, RUNTIME_CONTAINER)], []).c;
127
179
  }
128
180
  delegate;
129
- constructor(registry) {
130
- this.delegate = new HttpMetricsMiddleware(registry, httpOptions);
181
+ constructor(registry, container) {
182
+ const httpMetricsMiddleware = new HttpMetricsMiddleware(registry, httpOptions);
183
+ const runtimeContainer = assertRuntimeContainer(container);
184
+ if (claimsHttpInstrumentationOwnership(runtimeContainer, registry)) {
185
+ this.delegate = httpMetricsMiddleware;
186
+ }
131
187
  }
132
188
  handle(context, next) {
133
- return this.delegate.handle(context, next);
189
+ return this.delegate ? this.delegate.handle(context, next) : next();
134
190
  }
135
191
  static {
136
192
  _initClass2();
@@ -138,12 +194,93 @@ function createHttpMetricsMiddleware(registryToken, httpOptions) {
138
194
  }
139
195
  return _MetricsHttpMiddlewar;
140
196
  }
197
+ function claimsHttpInstrumentationOwnership(container, registry) {
198
+ let registryOwners = HTTP_INSTRUMENTATION_OWNERS.get(container);
199
+ if (!registryOwners) {
200
+ registryOwners = new WeakSet();
201
+ HTTP_INSTRUMENTATION_OWNERS.set(container, registryOwners);
202
+ }
203
+ if (registryOwners.has(registry)) {
204
+ return false;
205
+ }
206
+ registryOwners.add(registry);
207
+ return true;
208
+ }
141
209
  function assertPrometheusRegistry(value) {
142
210
  if (!(value instanceof PrometheusRegistry)) {
143
211
  throw new Error('MetricsModule registry provider resolved an invalid Prometheus registry.');
144
212
  }
145
213
  return value;
146
214
  }
215
+ function hasDefaultMetricNames(value) {
216
+ return typeof value === 'function' && 'metricNames' in value && Array.isArray(value.metricNames) && value.metricNames.every(metricName => typeof metricName === 'string');
217
+ }
218
+ function assertNoDefaultMetricCollisions(registry) {
219
+ for (const {
220
+ collectorName,
221
+ metricNames
222
+ } of DEFAULT_METRIC_COLLECTORS) {
223
+ if (!isDefaultCollectorActive(collectorName)) {
224
+ continue;
225
+ }
226
+ for (const metricName of metricNames) {
227
+ if (registry.getSingleMetric(metricName)) {
228
+ throw new Error(`A metric with the name ${metricName} has already been registered.`);
229
+ }
230
+ }
231
+ }
232
+ }
233
+ function isDefaultCollectorActive(collectorName) {
234
+ if (collectorName === 'processHandles') {
235
+ return typeof Reflect.get(process, '_getActiveHandles') === 'function';
236
+ }
237
+ if (collectorName === 'processRequests') {
238
+ return typeof Reflect.get(process, '_getActiveRequests') === 'function';
239
+ }
240
+ if (collectorName === 'processOpenFileDescriptors') {
241
+ return process.platform === 'linux';
242
+ }
243
+ if (collectorName === 'processMaxFileDescriptors') {
244
+ try {
245
+ return readFileSync('/proc/self/limits', 'utf8').split('\n').some(line => line.startsWith('Max open files'));
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+ return true;
251
+ }
252
+ function assertRuntimeContainer(value) {
253
+ if (!isRuntimeContainer(value)) {
254
+ throw new Error('MetricsModule runtime container provider resolved an invalid container.');
255
+ }
256
+ return value;
257
+ }
258
+ function assertBootstrapProviderTokens(value) {
259
+ if (!(value instanceof Set)) {
260
+ throw new Error('MetricsModule bootstrap provider token metadata resolved invalidly.');
261
+ }
262
+ return value;
263
+ }
264
+ function resolveRegistryMode(options, bootstrapProviderTokens) {
265
+ return options.registry || bootstrapProviderTokens.has(METRICS_REGISTRY) ? 'shared' : 'isolated';
266
+ }
267
+ function isRuntimeContainer(value) {
268
+ return typeof value === 'object' && value !== null && 'resolve' in value && typeof value.resolve === 'function';
269
+ }
270
+ function getRuntimePlatformTelemetryRegistryState(registry) {
271
+ const existing = PLATFORM_TELEMETRY_REGISTRY_STATES.get(registry);
272
+ if (existing) {
273
+ return existing;
274
+ }
275
+ const state = {
276
+ lastHealthStatuses: new Map(),
277
+ lastReadinessStatuses: new Map(),
278
+ registrations: [],
279
+ scrapeQueue: new SerializedScrapeQueue()
280
+ };
281
+ PLATFORM_TELEMETRY_REGISTRY_STATES.set(registry, state);
282
+ return state;
283
+ }
147
284
  function toReadinessValue(status) {
148
285
  return status === 'ready' ? 1 : 0;
149
286
  }
@@ -175,16 +312,23 @@ function assertGaugeLabelSchema(gauge, config) {
175
312
  throw new Error(`Metric name "${config.name}" is already registered with labels [${registeredLabels}]. Built-in platform telemetry requires labels [${expectedLabels}].`);
176
313
  }
177
314
  }
315
+ function removeFrameworkGauge(registry, gauge, metricName) {
316
+ if (!FRAMEWORK_PLATFORM_GAUGES.has(gauge) || registry.getSingleMetric(metricName) !== gauge) {
317
+ return;
318
+ }
319
+ registry.removeSingleMetric(metricName);
320
+ }
178
321
  class RuntimePlatformTelemetry {
179
322
  readinessGauge;
180
323
  healthGauge;
181
324
  registryModeGauge;
182
- lastHealthStatuses = new Map();
183
- lastReadinessStatuses = new Map();
184
- scrapeChain = Promise.resolve();
185
- constructor(registry, registryMode, labels = {}) {
325
+ telemetryState;
326
+ constructor(registry, container, registryMode, labels = {}) {
327
+ this.registry = registry;
328
+ this.container = container;
186
329
  this.registryMode = registryMode;
187
330
  this.labels = labels;
331
+ this.telemetryState = getRuntimePlatformTelemetryRegistryState(registry);
188
332
  this.readinessGauge = getOrCreateGauge(registry, {
189
333
  help: 'Runtime platform component readiness from shared platform snapshot semantics.',
190
334
  labelNames: PLATFORM_COMPONENT_LABELS,
@@ -201,22 +345,67 @@ class RuntimePlatformTelemetry {
201
345
  name: 'fluo_metrics_registry_mode'
202
346
  });
203
347
  this.registryModeGauge.labels(this.registryMode).set(1);
348
+ this.installRegistryRefresh();
204
349
  }
205
- async collectMetrics(ctx, registry) {
206
- const collect = this.scrapeChain.then(async () => {
207
- await this.refresh(ctx);
208
- return registry.metrics();
350
+ collectMetrics(registry) {
351
+ return registry.metrics();
352
+ }
353
+ async onModuleDestroy() {
354
+ const registrationIndex = this.telemetryState.registrations.lastIndexOf(this);
355
+ if (registrationIndex >= 0) {
356
+ this.telemetryState.registrations.splice(registrationIndex, 1);
357
+ }
358
+ if (this.telemetryState.registrations.length > 0) {
359
+ return;
360
+ }
361
+ await this.telemetryState.scrapeQueue.drain(() => {
362
+ if (this.telemetryState.registrations.length > 0) {
363
+ return;
364
+ }
365
+ const originalMetrics = this.telemetryState.originalMetrics;
366
+ if (originalMetrics) {
367
+ this.clearPlatformTelemetry();
368
+ removeFrameworkGauge(this.registry, this.readinessGauge, 'fluo_component_ready');
369
+ removeFrameworkGauge(this.registry, this.healthGauge, 'fluo_component_health');
370
+ removeFrameworkGauge(this.registry, this.registryModeGauge, 'fluo_metrics_registry_mode');
371
+ this.registry.metrics = originalMetrics;
372
+ this.telemetryState.originalMetrics = undefined;
373
+ }
209
374
  });
210
- this.scrapeChain = collect.then(() => undefined, () => undefined);
211
- return collect;
212
375
  }
213
- async refresh(ctx) {
214
- const platformShell = await this.resolvePlatformShell(ctx);
376
+ installRegistryRefresh() {
377
+ this.telemetryState.registrations.push(this);
378
+ if (this.telemetryState.originalMetrics) {
379
+ return;
380
+ }
381
+ const registry = this.registry;
382
+ const telemetryState = this.telemetryState;
383
+ const originalMetrics = registry.metrics;
384
+ telemetryState.originalMetrics = originalMetrics;
385
+ registry.metrics = () => {
386
+ return telemetryState.scrapeQueue.enqueue(async () => {
387
+ const activeRegistration = telemetryState.registrations.at(-1);
388
+ if (!activeRegistration) {
389
+ return originalMetrics.call(registry);
390
+ }
391
+ return activeRegistration.collectScrape(() => originalMetrics.call(registry));
392
+ });
393
+ };
394
+ }
395
+ async collectScrape(render) {
396
+ await this.refresh();
397
+ return await render();
398
+ }
399
+ async refresh() {
400
+ const platformShell = await this.resolvePlatformShell();
215
401
  if (!platformShell) {
216
402
  this.clearPlatformTelemetry();
217
403
  return;
218
404
  }
219
405
  const snapshot = await platformShell.snapshot();
406
+ this.syncSnapshot(snapshot);
407
+ }
408
+ syncSnapshot(snapshot) {
220
409
  const env = this.labels?.env ?? 'unknown';
221
410
  const instance = this.labels?.instance ?? 'local';
222
411
  const components = [{
@@ -231,21 +420,17 @@ class RuntimePlatformTelemetry {
231
420
  readiness: component.readiness.status
232
421
  }))];
233
422
  this.syncGaugeStatuses({
234
- currentStatuses: new Map(components.map(component => [this.toComponentKey(component.id, component.kind), component.health])),
235
- env,
423
+ currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.health),
236
424
  gauge: this.healthGauge,
237
- instance,
238
- lastStatuses: this.lastHealthStatuses,
425
+ lastStatuses: this.telemetryState.lastHealthStatuses,
239
426
  operation: 'health',
240
427
  statuses: HEALTH_STATUSES,
241
428
  toMetricValue: toHealthValue
242
429
  });
243
430
  this.syncGaugeStatuses({
244
- currentStatuses: new Map(components.map(component => [this.toComponentKey(component.id, component.kind), component.readiness])),
245
- env,
431
+ currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.readiness),
246
432
  gauge: this.readinessGauge,
247
- instance,
248
- lastStatuses: this.lastReadinessStatuses,
433
+ lastStatuses: this.telemetryState.lastReadinessStatuses,
249
434
  operation: 'readiness',
250
435
  statuses: READINESS_STATUSES,
251
436
  toMetricValue: toReadinessValue
@@ -253,32 +438,30 @@ class RuntimePlatformTelemetry {
253
438
  }
254
439
  clearPlatformTelemetry() {
255
440
  this.clearGaugeStatuses({
256
- env: this.labels?.env ?? 'unknown',
257
441
  gauge: this.healthGauge,
258
- instance: this.labels?.instance ?? 'local',
259
- lastStatuses: this.lastHealthStatuses,
442
+ lastStatuses: this.telemetryState.lastHealthStatuses,
260
443
  operation: 'health',
261
444
  statuses: HEALTH_STATUSES
262
445
  });
263
446
  this.clearGaugeStatuses({
264
- env: this.labels?.env ?? 'unknown',
265
447
  gauge: this.readinessGauge,
266
- instance: this.labels?.instance ?? 'local',
267
- lastStatuses: this.lastReadinessStatuses,
448
+ lastStatuses: this.telemetryState.lastReadinessStatuses,
268
449
  operation: 'readiness',
269
450
  statuses: READINESS_STATUSES
270
451
  });
271
452
  }
272
453
  clearGaugeStatuses({
273
- env,
274
454
  gauge,
275
- instance,
276
455
  lastStatuses,
277
456
  operation,
278
457
  statuses
279
458
  }) {
280
- for (const componentKey of lastStatuses.keys()) {
281
- const [componentId, componentKind] = this.fromComponentKey(componentKey);
459
+ for (const {
460
+ componentId,
461
+ componentKind,
462
+ env,
463
+ instance
464
+ } of this.componentStatusEntries(lastStatuses)) {
282
465
  for (const status of statuses) {
283
466
  gauge.remove(componentId, componentKind, operation, status, env, instance);
284
467
  }
@@ -287,20 +470,23 @@ class RuntimePlatformTelemetry {
287
470
  }
288
471
  syncGaugeStatuses({
289
472
  currentStatuses,
290
- env,
291
473
  gauge,
292
- instance,
293
474
  lastStatuses,
294
475
  operation,
295
476
  statuses,
296
477
  toMetricValue
297
478
  }) {
298
- for (const [componentKey, previousStatus] of lastStatuses) {
299
- const nextStatus = currentStatuses.get(componentKey);
479
+ for (const {
480
+ componentId,
481
+ componentKind,
482
+ env,
483
+ instance,
484
+ status: previousStatus
485
+ } of this.componentStatusEntries(lastStatuses)) {
486
+ const nextStatus = this.getComponentStatus(currentStatuses, env, instance, componentId, componentKind);
300
487
  if (nextStatus === previousStatus) {
301
488
  continue;
302
489
  }
303
- const [componentId, componentKind] = this.fromComponentKey(componentKey);
304
490
  for (const status of statuses) {
305
491
  if (status !== previousStatus) {
306
492
  continue;
@@ -308,41 +494,100 @@ class RuntimePlatformTelemetry {
308
494
  gauge.remove(componentId, componentKind, operation, status, env, instance);
309
495
  }
310
496
  }
311
- for (const [componentKey, currentStatus] of currentStatuses) {
312
- const [componentId, componentKind] = this.fromComponentKey(componentKey);
497
+ for (const {
498
+ componentId,
499
+ componentKind,
500
+ env,
501
+ instance,
502
+ status: currentStatus
503
+ } of this.componentStatusEntries(currentStatuses)) {
313
504
  gauge.labels(componentId, componentKind, operation, currentStatus, env, instance).set(toMetricValue(currentStatus));
314
505
  }
315
506
  lastStatuses.clear();
316
- for (const [componentKey, currentStatus] of currentStatuses) {
317
- lastStatuses.set(componentKey, currentStatus);
507
+ for (const {
508
+ componentId,
509
+ componentKind,
510
+ env,
511
+ instance,
512
+ status: currentStatus
513
+ } of this.componentStatusEntries(currentStatuses)) {
514
+ this.setComponentStatus(lastStatuses, env, instance, componentId, componentKind, currentStatus);
318
515
  }
319
516
  }
320
- fromComponentKey(componentKey) {
321
- const separatorIndex = componentKey.indexOf('::');
322
- return [componentKey.slice(0, separatorIndex), componentKey.slice(separatorIndex + 2)];
517
+ toComponentStatusMap(components, env, instance, getStatus) {
518
+ const statuses = new Map();
519
+ for (const component of components) {
520
+ this.setComponentStatus(statuses, env, instance, component.id, component.kind, getStatus(component));
521
+ }
522
+ return statuses;
323
523
  }
324
- toComponentKey(componentId, componentKind) {
325
- return `${componentId}::${componentKind}`;
524
+ setComponentStatus(statuses, env, instance, componentId, componentKind, status) {
525
+ let statusByInstance = statuses.get(env);
526
+ if (!statusByInstance) {
527
+ statusByInstance = new Map();
528
+ statuses.set(env, statusByInstance);
529
+ }
530
+ let statusByComponent = statusByInstance.get(instance);
531
+ if (!statusByComponent) {
532
+ statusByComponent = new Map();
533
+ statusByInstance.set(instance, statusByComponent);
534
+ }
535
+ let statusByKind = statusByComponent.get(componentId);
536
+ if (!statusByKind) {
537
+ statusByKind = new Map();
538
+ statusByComponent.set(componentId, statusByKind);
539
+ }
540
+ statusByKind.set(componentKind, status);
541
+ }
542
+ getComponentStatus(statuses, env, instance, componentId, componentKind) {
543
+ return statuses.get(env)?.get(instance)?.get(componentId)?.get(componentKind);
544
+ }
545
+ *componentStatusEntries(statuses) {
546
+ for (const [env, statusByInstance] of statuses) {
547
+ for (const [instance, statusByComponent] of statusByInstance) {
548
+ for (const [componentId, statusByKind] of statusByComponent) {
549
+ for (const [componentKind, status] of statusByKind) {
550
+ yield {
551
+ componentId,
552
+ componentKind,
553
+ env,
554
+ instance,
555
+ status
556
+ };
557
+ }
558
+ }
559
+ }
560
+ }
326
561
  }
327
- async resolvePlatformShell(ctx) {
562
+ async resolvePlatformShell() {
563
+ const hasPlatformShell = hasContainerToken(this.container, PLATFORM_SHELL);
564
+ if (hasPlatformShell === false) {
565
+ return undefined;
566
+ }
328
567
  try {
329
- return await ctx.container.resolve(PLATFORM_SHELL);
568
+ return await this.container.resolve(PLATFORM_SHELL);
330
569
  } catch (error) {
331
- if (isMissingPlatformShellResolutionError(error)) {
570
+ if (hasPlatformShell !== true && isMissingPlatformShellResolutionError(error)) {
332
571
  return undefined;
333
572
  }
334
573
  throw error;
335
574
  }
336
575
  }
337
576
  }
577
+ function hasContainerToken(container, token) {
578
+ const has = container.has;
579
+ if (typeof has !== 'function') {
580
+ return undefined;
581
+ }
582
+ return has.call(container, token);
583
+ }
338
584
  function isMissingPlatformShellResolutionError(error) {
339
585
  if (!(error instanceof ContainerResolutionError)) {
340
586
  return false;
341
587
  }
342
588
  const containerError = error;
343
589
  const token = typeof containerError.meta?.token === 'string' ? containerError.meta.token : undefined;
344
- const hint = typeof containerError.meta?.hint === 'string' ? containerError.meta.hint : undefined;
345
- return token !== undefined && PLATFORM_SHELL_TOKEN_NAMES.has(token) && hint === 'Ensure the provider is registered in a module\'s providers array, or that the module exporting it is imported by the consuming module.';
590
+ return token !== undefined && PLATFORM_SHELL_TOKEN_NAMES.has(token);
346
591
  }
347
592
  function resolveHttpOptions(http) {
348
593
  if (!http) {
@@ -356,6 +601,7 @@ function resolveHttpOptions(http) {
356
601
  }
357
602
  return {
358
603
  allowUnsafeRawPathLabelMode: http.allowUnsafeRawPathLabelMode,
604
+ durationHistogramBuckets: http.durationHistogramBuckets,
359
605
  pathLabelMode: http.pathLabelMode,
360
606
  pathLabelNormalizer: http.pathLabelNormalizer,
361
607
  unknownPathLabel: http.unknownPathLabel
@@ -1,6 +1,11 @@
1
1
  import type { Registry, CounterConfiguration, GaugeConfiguration, HistogramConfiguration } from 'prom-client';
2
2
  /**
3
- * Small facade for creating custom Prometheus metrics on the module registry.
3
+ * Small facade for creating custom Prometheus metrics on its `MetricsModule` registry.
4
+ *
5
+ * @remarks
6
+ * `MetricsService` is non-global: a module can inject it when it directly imports a
7
+ * `MetricsModule.forRoot(...)` registration or imports a module that re-exports
8
+ * `MetricsService`; unrelated sibling modules do not receive it automatically.
4
9
  */
5
10
  export declare class MetricsService {
6
11
  private readonly registry;
@@ -1 +1 @@
1
- {"version":3,"file":"metrics-service.d.ts","sourceRoot":"","sources":["../src/metrics-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAIrB;;GAEG;AACH,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,QAAQ;IAE/C;;;;;OAKG;IACH,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAIlE;;;;;OAKG;IACH,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAI9D;;;;;OAKG;IACH,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC;IAItE;;;;OAIG;IACH,WAAW,IAAI,QAAQ;CAGxB"}
1
+ {"version":3,"file":"metrics-service.d.ts","sourceRoot":"","sources":["../src/metrics-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAIrB;;;;;;;GAOG;AACH,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,QAAQ;IAE/C;;;;;OAKG;IACH,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAIlE;;;;;OAKG;IACH,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAI9D;;;;;OAKG;IACH,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC;IAItE;;;;OAIG;IACH,WAAW,IAAI,QAAQ;CAGxB"}
@@ -1,7 +1,12 @@
1
1
  import { createPrometheusCounter, createPrometheusGauge, createPrometheusHistogram } from './providers/prometheus-metrics-factory.js';
2
2
 
3
3
  /**
4
- * Small facade for creating custom Prometheus metrics on the module registry.
4
+ * Small facade for creating custom Prometheus metrics on its `MetricsModule` registry.
5
+ *
6
+ * @remarks
7
+ * `MetricsService` is non-global: a module can inject it when it directly imports a
8
+ * `MetricsModule.forRoot(...)` registration or imports a module that re-exports
9
+ * `MetricsService`; unrelated sibling modules do not receive it automatically.
5
10
  */
6
11
  export class MetricsService {
7
12
  constructor(registry) {
@@ -0,0 +1,19 @@
1
+ /** Observable state for package-internal serialized scrape scheduling. */
2
+ export interface SerializedScrapeQueueState {
3
+ readonly isRunning: boolean;
4
+ readonly queued: number;
5
+ }
6
+ /**
7
+ * Serializes refresh-and-render scrape work while preserving individual results.
8
+ *
9
+ * Failed work does not poison later queue entries.
10
+ */
11
+ export declare class SerializedScrapeQueue {
12
+ private isRunning;
13
+ private queued;
14
+ private tail;
15
+ get state(): SerializedScrapeQueueState;
16
+ enqueue<T>(task: () => Promise<T>): Promise<T>;
17
+ drain(finalizer: () => void): Promise<void>;
18
+ }
19
+ //# sourceMappingURL=serialized-scrape-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialized-scrape-queue.d.ts","sourceRoot":"","sources":["../src/serialized-scrape-queue.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,IAAI,CAAoC;IAEhD,IAAI,KAAK,IAAI,0BAA0B,CAKtC;IAED,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAqBxC,KAAK,CAAC,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAalD"}