@fluojs/metrics 1.0.4 → 2.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.
package/README.ko.md CHANGED
@@ -158,7 +158,7 @@ new Counter({
158
158
  class AppModule {}
159
159
  ```
160
160
 
161
- 여러 `MetricsModule` 인스턴스가 같은 Registry를 의도적으로 공유하는 경우, 내장 HTTP 메트릭은 기존 `http_requests_total`, `http_errors_total`, `http_request_duration_seconds` collector를 재사용합니다. 내장 플랫폼 텔레메트리 Gauge도 같은 ownership 규칙을 따릅니다. 모듈이 만든 `fluo_component_ready`, `fluo_component_health`, `fluo_metrics_registry_mode` Gauge는 framework ownership과 label schema가 일치할 때만 재사용합니다. 애플리케이션이 직접 등록한 중복 메트릭 이름은 Prometheus Registry 규칙대로 계속 빠르게 실패합니다.
161
+ 여러 `MetricsModule` 인스턴스가 같은 Registry를 의도적으로 공유하는 경우, 내장 HTTP 메트릭은 framework ownership, label schema, effective path-label configuration이 모두 일치할 때만 기존 `http_requests_total`, `http_errors_total`, `http_request_duration_seconds` collector를 재사용합니다. Path-label compatibility 검사는 `pathLabelMode`, 정확히 같은 `pathLabelNormalizer` 함수 참조, `unknownPathLabel` fallback 의미론을 포함하므로 서로 다른 HTTP series policy를 하나의 collector set에 섞는 module instance는 빠르게 실패합니다. 내장 플랫폼 텔레메트리 Gauge도 같은 ownership 규칙을 따릅니다. 모듈이 만든 `fluo_component_ready`, `fluo_component_health`, `fluo_metrics_registry_mode` Gauge는 framework ownership과 label schema가 일치할 때만 재사용합니다. 플랫폼 텔레메트리 상태는 재사용된 Registry별로 추적되므로, 이후 스크레이프는 이전 module instance가 남긴 stale component readiness/health series를 제거한 뒤 메트릭을 반환합니다. Registry scrape wrapper는 최신 active module registration을 사용하며 마지막 registration이 종료되면 Registry의 원래 `metrics()` 함수를 복원합니다. 애플리케이션이 직접 등록한 중복 메트릭 이름은 Prometheus Registry 규칙대로 계속 빠르게 실패합니다.
162
162
 
163
163
  ### 중복 메트릭 이름은 계속 빠르게 실패합니다
164
164
 
@@ -170,9 +170,9 @@ Prometheus 메트릭 이름은 하나의 Registry 안에서 고유해야 합니
170
170
 
171
171
  - `fluo_component_ready`: 준비 완료 시 1, 아닐 시 0.
172
172
  - `fluo_component_health`: 정상 상태 시 1, 아닐 시 0.
173
- - `fluo_metrics_registry_mode`: active registry mode `isolated`인지 `shared`인지 나타냅니다.
173
+ - `fluo_metrics_registry_mode`: active registry mode `mode="isolated"` 또는 `mode="shared"` label과 gauge value `1`로 나타냅니다.
174
174
 
175
- 이 데이터는 스크레이프 시점에 `PLATFORM_SHELL`을 쿼리하여 갱신됩니다. 초기화 시 환경 라벨을 제공할 수 있습니다.
175
+ 이 데이터는 built-in `/metrics` controller와 `MetricsService.getRegistry().metrics()`를 사용하는 advanced custom scraper를 포함해 active Registry가 스크레이프될 때마다 `PLATFORM_SHELL`을 쿼리하여 갱신됩니다. 초기화 시 환경 라벨을 제공할 수 있습니다.
176
176
 
177
177
  ```ts
178
178
  MetricsModule.forRoot({
@@ -185,10 +185,10 @@ MetricsModule.forRoot({
185
185
 
186
186
  ### 런타임 플랫폼 텔레메트리 스크레이프 계약
187
187
 
188
- 플랫폼 텔레메트리는 `/metrics` 스크레이프마다 `PLATFORM_SHELL`을 resolve하여 `fluo_component_ready`와 `fluo_component_health`를 갱신합니다.
188
+ 플랫폼 텔레메트리는 built-in `/metrics` controller 또는 `MetricsService.getRegistry()`를 사용하는 advanced custom scraper가 active Registry를 스크레이프할 때마다 `PLATFORM_SHELL`을 resolve하여 `fluo_component_ready`와 `fluo_component_health`를 갱신합니다.
189
189
 
190
190
  - `PLATFORM_SHELL` 등록 자체가 빠진 경우에는 스크레이프가 계속 성공하고 플랫폼 텔레메트리 시리즈만 생략됩니다.
191
- - 직전 성공 스크레이프에서 플랫폼 텔레메트리를 노출한 뒤 `PLATFORM_SHELL`을 사용할 수 없게 되면, stale `fluo_component_ready` 및 `fluo_component_health` 시리즈를 제거한 뒤 메트릭을 반환합니다.
191
+ - 직전 성공 스크레이프에서 플랫폼 텔레메트리를 노출한 뒤 `PLATFORM_SHELL`을 사용할 수 없게 되거나 이후 module instance가 재사용된 Registry를 다른 플랫폼 snapshot으로 갱신하면, stale `fluo_component_ready` 및 `fluo_component_health` 시리즈를 제거한 뒤 메트릭을 반환합니다.
192
192
  - 그 외의 `PLATFORM_SHELL` resolve 실패는 조용히 삼키지 않고 스크레이프 실패로 그대로 드러납니다.
193
193
 
194
194
  ### 기본 프로세스/Node 메트릭 비활성화
@@ -220,10 +220,11 @@ MetricsModule.forRoot({
220
220
  - `defaultMetrics`의 기본값은 `true`이며, `defaultMetrics: false`로 해당 Registry의 Prometheus 기본 프로세스/Node.js collector를 끌 수 있습니다.
221
221
  - `endpointMiddleware`는 class-based route-scoped middleware를 스크레이프 엔드포인트에만 바인딩합니다. HTTP 계측이 활성화된 경우 endpoint middleware 실패는 내장 HTTP collector에 집계됩니다.
222
222
  - HTTP 메트릭은 `http: true` 또는 `http` 옵션 객체를 전달한 경우에만 설치되며, 설치된 뒤에는 기본적으로 템플릿 기반 경로 라벨 정규화를 사용합니다.
223
- - 내장 HTTP collector와 플랫폼 텔레메트리 Gauge는 같은 Registry를 공유하는 모듈 인스턴스 사이에서 framework-owned이고 예상 label schema를 가진 경우에만 재사용되며, 커스텀 애플리케이션 메트릭 이름 충돌은 Prometheus의 중복 이름 실패 동작을 유지합니다.
223
+ - 내장 HTTP collector는 같은 Registry를 공유하는 모듈 인스턴스 사이에서 framework-owned이고 예상 label schema 및 일치하는 path-label configuration을 가진 경우에만 재사용됩니다. 플랫폼 텔레메트리 Gauge는 framework-owned이고 예상 label schema를 가진 경우에만 재사용되며, 커스텀 애플리케이션 메트릭 이름 충돌은 Prometheus의 중복 이름 실패 동작을 유지합니다.
224
+ - Shared Registry 텔레메트리 refresh는 소유 metrics module이 하나라도 active인 동안 유지되고 마지막 module이 종료되면 원래 `metrics()` 함수를 복원합니다.
224
225
  - raw path 라벨은 `allowUnsafeRawPathLabelMode: true`를 명시한 bounded internal route에서만 사용해야 합니다.
225
226
  - 플랫폼 텔레메트리는 `PLATFORM_SHELL`이 실제로 누락된 경우에만 생략되며, 그 외 resolve 실패는 스크레이프를 실패시킵니다.
226
- - 직전 성공 스크레이프에서 노출된 플랫폼 텔레메트리 시리즈는 `PLATFORM_SHELL`을 사용할 수 없게 된 스크레이프에서 제거됩니다.
227
+ - 직전 성공 스크레이프에서 노출된 플랫폼 텔레메트리 시리즈는 `PLATFORM_SHELL`을 사용할 수 없게 된 스크레이프 또는 이후 module instance가 재사용된 Registry를 다른 플랫폼 snapshot으로 갱신한 스크레이프에서 제거됩니다.
227
228
 
228
229
  ## 관련 패키지
229
230
 
package/README.md CHANGED
@@ -158,7 +158,7 @@ new Counter({
158
158
  class AppModule {}
159
159
  ```
160
160
 
161
- When multiple metrics module instances intentionally share the same registry, built-in HTTP metrics reuse the existing `http_requests_total`, `http_errors_total`, and `http_request_duration_seconds` collectors instead of registering duplicate framework metrics. Built-in platform telemetry gauges follow the same ownership rule: module-created `fluo_component_ready`, `fluo_component_health`, and `fluo_metrics_registry_mode` gauges are reused only when their framework ownership and label schema match. Application-defined duplicate names still fail fast.
161
+ When multiple metrics module instances intentionally share the same registry, built-in HTTP metrics reuse the existing `http_requests_total`, `http_errors_total`, and `http_request_duration_seconds` collectors instead of registering duplicate framework metrics only when their framework ownership, label schema, and effective path-label configuration match. The path-label compatibility check includes `pathLabelMode`, the exact `pathLabelNormalizer` function reference, and `unknownPathLabel` fallback semantics, so incompatible module instances fail fast instead of mixing different HTTP series policies into one collector set. Built-in platform telemetry gauges follow the same ownership rule: module-created `fluo_component_ready`, `fluo_component_health`, and `fluo_metrics_registry_mode` gauges are reused only when their framework ownership and label schema match. Platform telemetry state is tracked per reused registry, so a later scrape replaces stale module-owned component readiness and health series from an earlier module instance before metrics are returned. The registry scrape wrapper keeps using the latest active module registration and restores the Registry's original `metrics()` function after the last registration closes. Application-defined duplicate names still fail fast.
162
162
 
163
163
  ### Duplicate metric names still fail fast
164
164
 
@@ -170,9 +170,9 @@ The module emits fluo-specific gauges that mirror the platform shell and registe
170
170
 
171
171
  - `fluo_component_ready`: `1` when a component is ready, otherwise `0`.
172
172
  - `fluo_component_health`: `1` when a component is healthy, otherwise `0`.
173
- - `fluo_metrics_registry_mode`: `isolated` or `shared` for the active registry mode.
173
+ - `fluo_metrics_registry_mode`: gauge value `1` with a `mode="isolated"` or `mode="shared"` label for the active registry mode.
174
174
 
175
- The platform snapshot is refreshed during each scrape, and you can attach environment labels up front.
175
+ The platform snapshot is refreshed during each registry scrape, including advanced `MetricsService.getRegistry().metrics()` scrape paths, and you can attach environment labels up front.
176
176
 
177
177
  ```ts
178
178
  MetricsModule.forRoot({
@@ -185,7 +185,7 @@ MetricsModule.forRoot({
185
185
 
186
186
  ### Runtime platform telemetry scrape contract
187
187
 
188
- Platform telemetry refreshes `fluo_component_ready` and `fluo_component_health` on each `/metrics` scrape by resolving `PLATFORM_SHELL`.
188
+ Platform telemetry refreshes `fluo_component_ready` and `fluo_component_health` on each active registry scrape by resolving `PLATFORM_SHELL`, whether the scrape flows through the built-in `/metrics` controller or an advanced custom scraper using `MetricsService.getRegistry()`.
189
189
 
190
190
  - If `PLATFORM_SHELL` is not registered, the scrape still succeeds and omits the platform telemetry series.
191
191
  - If `PLATFORM_SHELL` becomes unavailable after the last successful scrape, stale `fluo_component_ready` and `fluo_component_health` series are removed before metrics are returned.
@@ -220,10 +220,11 @@ MetricsModule.forRoot({
220
220
  - `defaultMetrics` defaults to `true`, and `defaultMetrics: false` disables Prometheus default process and Node.js collectors for that registry.
221
221
  - `endpointMiddleware` binds class-based route-scoped middleware only to the scrape endpoint; with HTTP instrumentation enabled, endpoint middleware failures are counted by the built-in HTTP collectors.
222
222
  - HTTP metrics are installed only when `http: true` or an `http` options object is provided, and then default to template-normalized path labels.
223
- - Built-in HTTP collectors and platform telemetry gauges are reused when module instances share one registry only if they are framework-owned and have the expected label schema; custom application metric name collisions keep Prometheus' duplicate-name failure behavior.
223
+ - Built-in HTTP collectors are reused when module instances share one registry only if they are framework-owned, have the expected label schema, and use matching path-label configuration; platform telemetry gauges are reused only if they are framework-owned and have the expected label schema; custom application metric name collisions keep Prometheus' duplicate-name failure behavior.
224
+ - Shared Registry telemetry refresh remains installed while any owning metrics module is active and restores the original `metrics()` function after the last module closes.
224
225
  - Raw path labels require `allowUnsafeRawPathLabelMode: true` and should stay limited to bounded internal routes.
225
226
  - Platform telemetry is omitted only when `PLATFORM_SHELL` is genuinely missing; other resolution failures fail the scrape.
226
- - Stale platform telemetry series are removed when `PLATFORM_SHELL` becomes unavailable after the last successful scrape.
227
+ - Stale platform telemetry series are removed when `PLATFORM_SHELL` becomes unavailable after the last successful scrape or when a later module instance refreshes a reused registry with a different platform snapshot.
227
228
 
228
229
  ## Related Packages
229
230
 
@@ -1 +1 @@
1
- {"version":3,"file":"http-metrics-middleware.d.ts","sourceRoot":"","sources":["../src/http-metrics-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAC1F,OAAO,EAAsB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAqBhE,oEAAoE;AACpE,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG,UAAU,CAAC;AAE1D,wDAAwD;AACxD,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,gBAAgB,CAAC;CAC3B;AAED,2EAA2E;AAC3E,MAAM,MAAM,8BAA8B,GAAG,CAAC,OAAO,EAAE,2BAA2B,KAAK,MAAM,CAAC;AAE9F,8DAA8D;AAC9D,MAAM,WAAW,4BAA4B;IAC3C,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,mBAAmB,CAAC,EAAE,8BAA8B,CAAC;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAsBD;;GAEG;AACH,qBAAa,qBAAsB,YAAW,UAAU;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;IAChD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsB;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2B;IACzD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAiC;IACtE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;gBAE9B,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,4BAAiC;IA2B1E,OAAO,CAAC,gBAAgB;IAmBlB,MAAM,CAAC,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnE,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,oBAAoB;CAsB7B"}
1
+ {"version":3,"file":"http-metrics-middleware.d.ts","sourceRoot":"","sources":["../src/http-metrics-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAC1F,OAAO,EAAsB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AA4BhE,oEAAoE;AACpE,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG,UAAU,CAAC;AAE1D,wDAAwD;AACxD,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,gBAAgB,CAAC;CAC3B;AAED,2EAA2E;AAC3E,MAAM,MAAM,8BAA8B,GAAG,CAAC,OAAO,EAAE,2BAA2B,KAAK,MAAM,CAAC;AAE9F,8DAA8D;AAC9D,MAAM,WAAW,4BAA4B;IAC3C,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,mBAAmB,CAAC,EAAE,8BAA8B,CAAC;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAsBD;;GAEG;AACH,qBAAa,qBAAsB,YAAW,UAAU;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;IAChD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsB;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2B;IACzD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAiC;IACtE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;gBAE9B,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,4BAAiC;IAuB1E,OAAO,CAAC,gBAAgB;IAmBlB,MAAM,CAAC,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnE,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,oBAAoB;CAsB7B"}
@@ -2,6 +2,7 @@ import { Counter, Histogram } from 'prom-client';
2
2
  import { createPrometheusCounter, createPrometheusHistogram } from './providers/prometheus-metrics-factory.js';
3
3
  const FRAMEWORK_HTTP_COUNTERS = new WeakSet();
4
4
  const FRAMEWORK_HTTP_HISTOGRAMS = new WeakSet();
5
+ const FRAMEWORK_HTTP_COLLECTOR_CONFIGURATION = new WeakMap();
5
6
 
6
7
  /** Strategy used to label request paths in emitted HTTP metrics. */
7
8
 
@@ -38,27 +39,25 @@ export class HttpMetricsMiddleware {
38
39
  pathLabelNormalizer;
39
40
  unknownPathLabel;
40
41
  constructor(registry, options = {}) {
41
- if (options.pathLabelMode === 'raw' && options.allowUnsafeRawPathLabelMode !== true) {
42
- throw new Error('HttpMetricsMiddleware pathLabelMode "raw" is disabled by default. Pass allowUnsafeRawPathLabelMode: true only when you have bounded path cardinality.');
43
- }
44
- this.pathLabelMode = options.pathLabelMode ?? 'template';
45
- this.pathLabelNormalizer = options.pathLabelNormalizer;
46
- this.unknownPathLabel = options.unknownPathLabel ?? 'UNKNOWN';
42
+ const collectorConfiguration = resolveHttpMetricsCollectorConfiguration(options);
43
+ this.pathLabelMode = collectorConfiguration.pathLabelMode;
44
+ this.pathLabelNormalizer = collectorConfiguration.pathLabelNormalizer;
45
+ this.unknownPathLabel = collectorConfiguration.unknownPathLabel;
47
46
  this.requestsTotal = getOrCreateHttpCounter(registry, {
48
47
  help: 'Total number of HTTP requests',
49
48
  labelNames: ['method', 'path', 'status'],
50
49
  name: 'http_requests_total'
51
- });
50
+ }, collectorConfiguration);
52
51
  this.errorsTotal = getOrCreateHttpCounter(registry, {
53
52
  help: 'Total number of HTTP error responses (4xx/5xx)',
54
53
  labelNames: ['method', 'path', 'status'],
55
54
  name: 'http_errors_total'
56
- });
55
+ }, collectorConfiguration);
57
56
  this.requestDuration = getOrCreateHttpHistogram(registry, {
58
57
  help: 'HTTP request duration in seconds',
59
58
  labelNames: ['method', 'path', 'status'],
60
59
  name: 'http_request_duration_seconds'
61
- });
60
+ }, collectorConfiguration);
62
61
  }
63
62
  resolvePathLabel(request) {
64
63
  if (this.pathLabelNormalizer) {
@@ -119,13 +118,14 @@ export class HttpMetricsMiddleware {
119
118
  }
120
119
  }
121
120
  }
122
- function getOrCreateHttpCounter(registry, config) {
121
+ function getOrCreateHttpCounter(registry, config, collectorConfiguration) {
123
122
  const existing = registry.getSingleMetric(config.name);
124
123
  if (existing instanceof Counter) {
125
124
  if (!FRAMEWORK_HTTP_COUNTERS.has(existing)) {
126
125
  throw new Error(`Metric name "${config.name}" is already registered by the application. Built-in HTTP metrics require framework-owned collectors.`);
127
126
  }
128
127
  assertHttpMetricLabelSchema(existing, config);
128
+ assertHttpMetricConfiguration(existing, config.name, collectorConfiguration);
129
129
  return existing;
130
130
  }
131
131
  const counter = createPrometheusCounter(registry, {
@@ -134,15 +134,17 @@ function getOrCreateHttpCounter(registry, config) {
134
134
  name: config.name
135
135
  });
136
136
  FRAMEWORK_HTTP_COUNTERS.add(counter);
137
+ FRAMEWORK_HTTP_COLLECTOR_CONFIGURATION.set(counter, collectorConfiguration);
137
138
  return counter;
138
139
  }
139
- function getOrCreateHttpHistogram(registry, config) {
140
+ function getOrCreateHttpHistogram(registry, config, collectorConfiguration) {
140
141
  const existing = registry.getSingleMetric(config.name);
141
142
  if (existing instanceof Histogram) {
142
143
  if (!FRAMEWORK_HTTP_HISTOGRAMS.has(existing)) {
143
144
  throw new Error(`Metric name "${config.name}" is already registered by the application. Built-in HTTP metrics require framework-owned collectors.`);
144
145
  }
145
146
  assertHttpMetricLabelSchema(existing, config);
147
+ assertHttpMetricConfiguration(existing, config.name, collectorConfiguration);
146
148
  return existing;
147
149
  }
148
150
  const histogram = createPrometheusHistogram(registry, {
@@ -151,6 +153,7 @@ function getOrCreateHttpHistogram(registry, config) {
151
153
  name: config.name
152
154
  });
153
155
  FRAMEWORK_HTTP_HISTOGRAMS.add(histogram);
156
+ FRAMEWORK_HTTP_COLLECTOR_CONFIGURATION.set(histogram, collectorConfiguration);
154
157
  return histogram;
155
158
  }
156
159
  function assertHttpMetricLabelSchema(metric, config) {
@@ -160,6 +163,32 @@ function assertHttpMetricLabelSchema(metric, config) {
160
163
  throw new Error(`Metric name "${config.name}" is already registered with labels [${registeredLabels}]. Built-in HTTP metrics require labels [${expectedLabels}].`);
161
164
  }
162
165
  }
166
+ function resolveHttpMetricsCollectorConfiguration(options) {
167
+ if (options.pathLabelMode === 'raw' && options.allowUnsafeRawPathLabelMode !== true) {
168
+ throw new Error('HttpMetricsMiddleware pathLabelMode "raw" is disabled by default. Pass allowUnsafeRawPathLabelMode: true only when you have bounded path cardinality.');
169
+ }
170
+ return {
171
+ pathLabelMode: options.pathLabelMode ?? 'template',
172
+ pathLabelNormalizer: options.pathLabelNormalizer,
173
+ unknownPathLabel: options.unknownPathLabel ?? 'UNKNOWN'
174
+ };
175
+ }
176
+ function assertHttpMetricConfiguration(metric, metricName, expected) {
177
+ const registered = FRAMEWORK_HTTP_COLLECTOR_CONFIGURATION.get(metric);
178
+ if (!registered) {
179
+ throw new Error(`Metric name "${metricName}" is already registered as a framework-owned HTTP collector without path-label configuration metadata. Built-in HTTP metrics require matching path-label configuration before reuse.`);
180
+ }
181
+ if (hasSameHttpMetricConfiguration(registered, expected)) {
182
+ return;
183
+ }
184
+ throw new Error(`Metric name "${metricName}" is already registered with framework HTTP path-label configuration ${describeHttpMetricConfiguration(registered)}. Built-in HTTP metrics require matching path-label configuration before reuse; received ${describeHttpMetricConfiguration(expected)}.`);
185
+ }
186
+ function hasSameHttpMetricConfiguration(left, right) {
187
+ return left.pathLabelMode === right.pathLabelMode && left.pathLabelNormalizer === right.pathLabelNormalizer && left.unknownPathLabel === right.unknownPathLabel;
188
+ }
189
+ function describeHttpMetricConfiguration(configuration) {
190
+ return `pathLabelMode="${configuration.pathLabelMode}", pathLabelNormalizer=${configuration.pathLabelNormalizer ? 'custom' : 'none'}, unknownPathLabel="${configuration.unknownPathLabel}"`;
191
+ }
163
192
  function normalizePathToTemplate(path, params) {
164
193
  if (!path) {
165
194
  return '/';
@@ -42,6 +42,7 @@ export interface MetricsModuleOptions {
42
42
  /** Module entry point that exposes `/metrics` and optional HTTP/runtime telemetry. */
43
43
  export declare class MetricsModule {
44
44
  private static registeredRegistries;
45
+ private static httpInstrumentationRegistrations;
45
46
  /**
46
47
  * Register framework metrics, optional HTTP middleware, and a scrape endpoint.
47
48
  *
@@ -1 +1 @@
1
- {"version":3,"file":"metrics-module.d.ts","sourceRoot":"","sources":["../src/metrics-module.ts"],"names":[],"mappings":"AAEA,OAAO,EAA8B,KAAK,UAAU,EAAE,KAAK,cAAc,EAAuB,MAAM,cAAc,CAAC;AACrH,OAAO,EAAgB,KAAK,UAAU,EAA8C,MAAM,iBAAiB,CAAC;AAC5G,OAAO,EAAgE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE1G,OAAO,EAGL,KAAK,wBAAwB,EAC7B,KAAK,8BAA8B,EACpC,MAAM,8BAA8B,CAAC;AAKtC,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,iGAAiG;IACjG,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,kFAAkF;IAClF,mBAAmB,CAAC,EAAE,8BAA8B,CAAC;IACrD,+DAA+D;IAC/D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,kGAAkG;IAClG,IAAI,CAAC,EAAE,OAAO,GAAG,kBAAkB,CAAC;IACpC,yHAAyH;IACzH,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,sHAAsH;IACtH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mGAAmG;IACnG,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAC9B,uGAAuG;IACvG,kBAAkB,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,CAAC,CAAC;IAC/D,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE;QAClB,iEAAiE;QACjE,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,iDAAiD;QACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,iFAAiF;IACjF,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,sFAAsF;AACtF,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAA2B;IAE9D;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,UAAU;IAqF9D,OAAO,CAAC,MAAM,CAAC,cAAc;CAU9B"}
1
+ {"version":3,"file":"metrics-module.d.ts","sourceRoot":"","sources":["../src/metrics-module.ts"],"names":[],"mappings":"AAEA,OAAO,EAA8B,KAAK,UAAU,EAAE,KAAK,cAAc,EAAuB,MAAM,cAAc,CAAC;AACrH,OAAO,EAEL,KAAK,UAAU,EAIhB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAgE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE1G,OAAO,EAGL,KAAK,wBAAwB,EAC7B,KAAK,8BAA8B,EACpC,MAAM,8BAA8B,CAAC;AAKtC,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,iGAAiG;IACjG,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,kFAAkF;IAClF,mBAAmB,CAAC,EAAE,8BAA8B,CAAC;IACrD,+DAA+D;IAC/D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,kGAAkG;IAClG,IAAI,CAAC,EAAE,OAAO,GAAG,kBAAkB,CAAC;IACpC,yHAAyH;IACzH,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,sHAAsH;IACtH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mGAAmG;IACnG,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAC9B,uGAAuG;IACvG,kBAAkB,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,CAAC,CAAC;IAC/D,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE;QAClB,iEAAiE;QACjE,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,iDAAiD;QACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,iFAAiF;IACjF,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,sFAAsF;AACtF,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAA2B;IAC9D,OAAO,CAAC,MAAM,CAAC,gCAAgC,CAA4D;IAE3G;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,UAAU;IA6H9D,OAAO,CAAC,MAAM,CAAC,cAAc;CAU9B"}
@@ -7,6 +7,7 @@ import { Inject } from '@fluojs/core';
7
7
  import { ContainerResolutionError } from '@fluojs/di';
8
8
  import { Controller, Get, forRoutes } from '@fluojs/http';
9
9
  import { defineModule, PLATFORM_SHELL } from '@fluojs/runtime';
10
+ import { RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
10
11
  import { collectDefaultMetrics, Gauge, Registry as PrometheusRegistry } from 'prom-client';
11
12
  import { HttpMetricsMiddleware } from './http-metrics-middleware.js';
12
13
  import { METER_PROVIDER } from './providers/meter-provider.js';
@@ -22,6 +23,7 @@ import { PrometheusMeterProvider } from './providers/prometheus-meter-provider.j
22
23
  /** Module entry point that exposes `/metrics` and optional HTTP/runtime telemetry. */
23
24
  export class MetricsModule {
24
25
  static registeredRegistries = new WeakSet();
26
+ static httpInstrumentationRegistrations = new WeakMap();
25
27
 
26
28
  /**
27
29
  * Register framework metrics, optional HTTP middleware, and a scrape endpoint.
@@ -44,17 +46,46 @@ export class MetricsModule {
44
46
  }
45
47
  const httpOptions = resolveHttpOptions(options.http);
46
48
  const metricsPath = options.path === undefined ? '/metrics' : options.path;
47
- const registryToken = Symbol('MetricsModule.registry');
49
+ let registryToken = Symbol('MetricsModule.registry');
48
50
  const platformTelemetryToken = Symbol('MetricsModule.platformTelemetry');
49
- const httpMetricsMiddleware = httpOptions ? createHttpMetricsMiddleware(registryToken, httpOptions) : undefined;
51
+ let httpMetricsMiddleware = httpOptions ? createHttpMetricsMiddleware(registryToken, httpOptions) : undefined;
50
52
  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] : []), {
53
+ const middleware = [...endpointMiddleware, ...(options.middleware ?? [])];
54
+ const registryProvider = {
53
55
  provide: registryToken,
54
56
  useFactory: () => MetricsModule.createRegistry(options)
55
- }, {
57
+ };
58
+ const imports = [];
59
+ const validationProviders = [];
60
+ let includeRuntimeRegistryProvider = httpMetricsMiddleware === undefined;
61
+ if (httpOptions && httpMetricsMiddleware) {
62
+ const existingRegistration = options.registry ? MetricsModule.httpInstrumentationRegistrations.get(options.registry) : undefined;
63
+ if (existingRegistration) {
64
+ registryToken = existingRegistration.registryToken;
65
+ httpMetricsMiddleware = undefined;
66
+ validationProviders.push(createHttpCollectorValidationProvider(registryToken, httpOptions));
67
+ imports.push(existingRegistration.moduleType);
68
+ } else {
69
+ includeRuntimeRegistryProvider = false;
70
+ class MetricsHttpInstrumentationModule {}
71
+ defineModule(MetricsHttpInstrumentationModule, {
72
+ exports: [registryToken],
73
+ global: true,
74
+ middleware: [httpMetricsMiddleware],
75
+ providers: [registryProvider, httpMetricsMiddleware]
76
+ });
77
+ if (options.registry) {
78
+ MetricsModule.httpInstrumentationRegistrations.set(options.registry, {
79
+ moduleType: MetricsHttpInstrumentationModule,
80
+ registryToken
81
+ });
82
+ }
83
+ imports.push(MetricsHttpInstrumentationModule);
84
+ }
85
+ }
86
+ const providers = [...(includeRuntimeRegistryProvider ? [registryProvider] : []), ...validationProviders, {
56
87
  provide: MetricsService,
57
- inject: [registryToken],
88
+ inject: [registryToken, platformTelemetryToken],
58
89
  useFactory: registry => new MetricsService(assertPrometheusRegistry(registry))
59
90
  }, {
60
91
  provide: METER_PROVIDER,
@@ -62,8 +93,8 @@ export class MetricsModule {
62
93
  useFactory: registry => new PrometheusMeterProvider(assertPrometheusRegistry(registry))
63
94
  }, {
64
95
  provide: platformTelemetryToken,
65
- inject: [registryToken],
66
- useFactory: registry => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), options.registry ? 'shared' : 'isolated', options.platformTelemetry)
96
+ inject: [registryToken, RUNTIME_CONTAINER],
97
+ useFactory: (registry, container) => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), assertRuntimeContainer(container), options.registry ? 'shared' : 'isolated', options.platformTelemetry)
67
98
  }];
68
99
  const controllers = [];
69
100
  if (typeof metricsPath === 'string') {
@@ -84,7 +115,7 @@ export class MetricsModule {
84
115
  }
85
116
  async getMetrics(_input, ctx) {
86
117
  ctx.response.setHeader('content-type', this.registry.contentType);
87
- return this.platformTelemetry.collectMetrics(ctx, this.registry);
118
+ return this.platformTelemetry.collectMetrics(this.registry);
88
119
  }
89
120
  static {
90
121
  _initClass();
@@ -95,6 +126,8 @@ export class MetricsModule {
95
126
  class MetricsRuntimeModule {}
96
127
  defineModule(MetricsRuntimeModule, {
97
128
  controllers,
129
+ exports: [MetricsService, METER_PROVIDER],
130
+ imports,
98
131
  middleware,
99
132
  providers
100
133
  });
@@ -114,10 +147,10 @@ export class MetricsModule {
114
147
  const PLATFORM_COMPONENT_LABELS = ['component_id', 'component_kind', 'operation', 'result', 'env', 'instance'];
115
148
  const REGISTRY_MODE_LABELS = ['mode'];
116
149
  const FRAMEWORK_PLATFORM_GAUGES = new WeakSet();
150
+ const PLATFORM_TELEMETRY_REGISTRY_STATES = new WeakMap();
117
151
  const HEALTH_STATUSES = ['healthy', 'unhealthy', 'degraded'];
118
152
  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)]);
153
+ const PLATFORM_SHELL_TOKEN_NAMES = new Set([String(PLATFORM_SHELL)]);
121
154
  function createHttpMetricsMiddleware(registryToken, httpOptions) {
122
155
  let _initClass2;
123
156
  let _MetricsHttpMiddlewar;
@@ -138,12 +171,42 @@ function createHttpMetricsMiddleware(registryToken, httpOptions) {
138
171
  }
139
172
  return _MetricsHttpMiddlewar;
140
173
  }
174
+ function createHttpCollectorValidationProvider(registryToken, httpOptions) {
175
+ return {
176
+ provide: Symbol('MetricsModule.httpCollectorValidation'),
177
+ inject: [registryToken],
178
+ useFactory: registry => new HttpMetricsMiddleware(assertPrometheusRegistry(registry), httpOptions)
179
+ };
180
+ }
141
181
  function assertPrometheusRegistry(value) {
142
182
  if (!(value instanceof PrometheusRegistry)) {
143
183
  throw new Error('MetricsModule registry provider resolved an invalid Prometheus registry.');
144
184
  }
145
185
  return value;
146
186
  }
187
+ function assertRuntimeContainer(value) {
188
+ if (!isRuntimeContainer(value)) {
189
+ throw new Error('MetricsModule runtime container provider resolved an invalid container.');
190
+ }
191
+ return value;
192
+ }
193
+ function isRuntimeContainer(value) {
194
+ return typeof value === 'object' && value !== null && 'resolve' in value && typeof value.resolve === 'function';
195
+ }
196
+ function getRuntimePlatformTelemetryRegistryState(registry) {
197
+ const existing = PLATFORM_TELEMETRY_REGISTRY_STATES.get(registry);
198
+ if (existing) {
199
+ return existing;
200
+ }
201
+ const state = {
202
+ lastHealthStatuses: new Map(),
203
+ lastReadinessStatuses: new Map(),
204
+ registrations: [],
205
+ scrapeChain: Promise.resolve()
206
+ };
207
+ PLATFORM_TELEMETRY_REGISTRY_STATES.set(registry, state);
208
+ return state;
209
+ }
147
210
  function toReadinessValue(status) {
148
211
  return status === 'ready' ? 1 : 0;
149
212
  }
@@ -179,12 +242,13 @@ class RuntimePlatformTelemetry {
179
242
  readinessGauge;
180
243
  healthGauge;
181
244
  registryModeGauge;
182
- lastHealthStatuses = new Map();
183
- lastReadinessStatuses = new Map();
184
- scrapeChain = Promise.resolve();
185
- constructor(registry, registryMode, labels = {}) {
245
+ telemetryState;
246
+ constructor(registry, container, registryMode, labels = {}) {
247
+ this.registry = registry;
248
+ this.container = container;
186
249
  this.registryMode = registryMode;
187
250
  this.labels = labels;
251
+ this.telemetryState = getRuntimePlatformTelemetryRegistryState(registry);
188
252
  this.readinessGauge = getOrCreateGauge(registry, {
189
253
  help: 'Runtime platform component readiness from shared platform snapshot semantics.',
190
254
  labelNames: PLATFORM_COMPONENT_LABELS,
@@ -201,22 +265,54 @@ class RuntimePlatformTelemetry {
201
265
  name: 'fluo_metrics_registry_mode'
202
266
  });
203
267
  this.registryModeGauge.labels(this.registryMode).set(1);
268
+ this.installRegistryRefresh();
204
269
  }
205
- async collectMetrics(ctx, registry) {
206
- const collect = this.scrapeChain.then(async () => {
207
- await this.refresh(ctx);
208
- return registry.metrics();
209
- });
210
- this.scrapeChain = collect.then(() => undefined, () => undefined);
211
- return collect;
270
+ collectMetrics(registry) {
271
+ return registry.metrics();
212
272
  }
213
- async refresh(ctx) {
214
- const platformShell = await this.resolvePlatformShell(ctx);
215
- if (!platformShell) {
216
- this.clearPlatformTelemetry();
273
+ onModuleDestroy() {
274
+ const registrationIndex = this.telemetryState.registrations.lastIndexOf(this);
275
+ if (registrationIndex >= 0) {
276
+ this.telemetryState.registrations.splice(registrationIndex, 1);
277
+ }
278
+ if (this.telemetryState.registrations.length > 0) {
217
279
  return;
218
280
  }
219
- const snapshot = await platformShell.snapshot();
281
+ const originalMetrics = this.telemetryState.originalMetrics;
282
+ if (originalMetrics) {
283
+ this.registry.metrics = originalMetrics;
284
+ this.telemetryState.originalMetrics = undefined;
285
+ }
286
+ }
287
+ installRegistryRefresh() {
288
+ this.telemetryState.registrations.push(this);
289
+ if (this.telemetryState.originalMetrics) {
290
+ return;
291
+ }
292
+ const registry = this.registry;
293
+ const telemetryState = this.telemetryState;
294
+ const originalMetrics = registry.metrics;
295
+ telemetryState.originalMetrics = originalMetrics;
296
+ registry.metrics = async () => {
297
+ const activeRegistration = telemetryState.registrations.at(-1);
298
+ await activeRegistration?.refresh();
299
+ return await originalMetrics.call(registry);
300
+ };
301
+ }
302
+ async refresh() {
303
+ const collect = this.telemetryState.scrapeChain.then(async () => {
304
+ const platformShell = await this.resolvePlatformShell();
305
+ if (!platformShell) {
306
+ this.clearPlatformTelemetry();
307
+ return;
308
+ }
309
+ const snapshot = await platformShell.snapshot();
310
+ this.syncSnapshot(snapshot);
311
+ });
312
+ this.telemetryState.scrapeChain = collect.then(() => undefined, () => undefined);
313
+ await collect;
314
+ }
315
+ syncSnapshot(snapshot) {
220
316
  const env = this.labels?.env ?? 'unknown';
221
317
  const instance = this.labels?.instance ?? 'local';
222
318
  const components = [{
@@ -231,21 +327,17 @@ class RuntimePlatformTelemetry {
231
327
  readiness: component.readiness.status
232
328
  }))];
233
329
  this.syncGaugeStatuses({
234
- currentStatuses: new Map(components.map(component => [this.toComponentKey(component.id, component.kind), component.health])),
235
- env,
330
+ currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.health),
236
331
  gauge: this.healthGauge,
237
- instance,
238
- lastStatuses: this.lastHealthStatuses,
332
+ lastStatuses: this.telemetryState.lastHealthStatuses,
239
333
  operation: 'health',
240
334
  statuses: HEALTH_STATUSES,
241
335
  toMetricValue: toHealthValue
242
336
  });
243
337
  this.syncGaugeStatuses({
244
- currentStatuses: new Map(components.map(component => [this.toComponentKey(component.id, component.kind), component.readiness])),
245
- env,
338
+ currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.readiness),
246
339
  gauge: this.readinessGauge,
247
- instance,
248
- lastStatuses: this.lastReadinessStatuses,
340
+ lastStatuses: this.telemetryState.lastReadinessStatuses,
249
341
  operation: 'readiness',
250
342
  statuses: READINESS_STATUSES,
251
343
  toMetricValue: toReadinessValue
@@ -253,32 +345,30 @@ class RuntimePlatformTelemetry {
253
345
  }
254
346
  clearPlatformTelemetry() {
255
347
  this.clearGaugeStatuses({
256
- env: this.labels?.env ?? 'unknown',
257
348
  gauge: this.healthGauge,
258
- instance: this.labels?.instance ?? 'local',
259
- lastStatuses: this.lastHealthStatuses,
349
+ lastStatuses: this.telemetryState.lastHealthStatuses,
260
350
  operation: 'health',
261
351
  statuses: HEALTH_STATUSES
262
352
  });
263
353
  this.clearGaugeStatuses({
264
- env: this.labels?.env ?? 'unknown',
265
354
  gauge: this.readinessGauge,
266
- instance: this.labels?.instance ?? 'local',
267
- lastStatuses: this.lastReadinessStatuses,
355
+ lastStatuses: this.telemetryState.lastReadinessStatuses,
268
356
  operation: 'readiness',
269
357
  statuses: READINESS_STATUSES
270
358
  });
271
359
  }
272
360
  clearGaugeStatuses({
273
- env,
274
361
  gauge,
275
- instance,
276
362
  lastStatuses,
277
363
  operation,
278
364
  statuses
279
365
  }) {
280
- for (const componentKey of lastStatuses.keys()) {
281
- const [componentId, componentKind] = this.fromComponentKey(componentKey);
366
+ for (const {
367
+ componentId,
368
+ componentKind,
369
+ env,
370
+ instance
371
+ } of this.componentStatusEntries(lastStatuses)) {
282
372
  for (const status of statuses) {
283
373
  gauge.remove(componentId, componentKind, operation, status, env, instance);
284
374
  }
@@ -287,20 +377,23 @@ class RuntimePlatformTelemetry {
287
377
  }
288
378
  syncGaugeStatuses({
289
379
  currentStatuses,
290
- env,
291
380
  gauge,
292
- instance,
293
381
  lastStatuses,
294
382
  operation,
295
383
  statuses,
296
384
  toMetricValue
297
385
  }) {
298
- for (const [componentKey, previousStatus] of lastStatuses) {
299
- const nextStatus = currentStatuses.get(componentKey);
386
+ for (const {
387
+ componentId,
388
+ componentKind,
389
+ env,
390
+ instance,
391
+ status: previousStatus
392
+ } of this.componentStatusEntries(lastStatuses)) {
393
+ const nextStatus = this.getComponentStatus(currentStatuses, env, instance, componentId, componentKind);
300
394
  if (nextStatus === previousStatus) {
301
395
  continue;
302
396
  }
303
- const [componentId, componentKind] = this.fromComponentKey(componentKey);
304
397
  for (const status of statuses) {
305
398
  if (status !== previousStatus) {
306
399
  continue;
@@ -308,41 +401,100 @@ class RuntimePlatformTelemetry {
308
401
  gauge.remove(componentId, componentKind, operation, status, env, instance);
309
402
  }
310
403
  }
311
- for (const [componentKey, currentStatus] of currentStatuses) {
312
- const [componentId, componentKind] = this.fromComponentKey(componentKey);
404
+ for (const {
405
+ componentId,
406
+ componentKind,
407
+ env,
408
+ instance,
409
+ status: currentStatus
410
+ } of this.componentStatusEntries(currentStatuses)) {
313
411
  gauge.labels(componentId, componentKind, operation, currentStatus, env, instance).set(toMetricValue(currentStatus));
314
412
  }
315
413
  lastStatuses.clear();
316
- for (const [componentKey, currentStatus] of currentStatuses) {
317
- lastStatuses.set(componentKey, currentStatus);
414
+ for (const {
415
+ componentId,
416
+ componentKind,
417
+ env,
418
+ instance,
419
+ status: currentStatus
420
+ } of this.componentStatusEntries(currentStatuses)) {
421
+ this.setComponentStatus(lastStatuses, env, instance, componentId, componentKind, currentStatus);
318
422
  }
319
423
  }
320
- fromComponentKey(componentKey) {
321
- const separatorIndex = componentKey.indexOf('::');
322
- return [componentKey.slice(0, separatorIndex), componentKey.slice(separatorIndex + 2)];
424
+ toComponentStatusMap(components, env, instance, getStatus) {
425
+ const statuses = new Map();
426
+ for (const component of components) {
427
+ this.setComponentStatus(statuses, env, instance, component.id, component.kind, getStatus(component));
428
+ }
429
+ return statuses;
323
430
  }
324
- toComponentKey(componentId, componentKind) {
325
- return `${componentId}::${componentKind}`;
431
+ setComponentStatus(statuses, env, instance, componentId, componentKind, status) {
432
+ let statusByInstance = statuses.get(env);
433
+ if (!statusByInstance) {
434
+ statusByInstance = new Map();
435
+ statuses.set(env, statusByInstance);
436
+ }
437
+ let statusByComponent = statusByInstance.get(instance);
438
+ if (!statusByComponent) {
439
+ statusByComponent = new Map();
440
+ statusByInstance.set(instance, statusByComponent);
441
+ }
442
+ let statusByKind = statusByComponent.get(componentId);
443
+ if (!statusByKind) {
444
+ statusByKind = new Map();
445
+ statusByComponent.set(componentId, statusByKind);
446
+ }
447
+ statusByKind.set(componentKind, status);
448
+ }
449
+ getComponentStatus(statuses, env, instance, componentId, componentKind) {
450
+ return statuses.get(env)?.get(instance)?.get(componentId)?.get(componentKind);
451
+ }
452
+ *componentStatusEntries(statuses) {
453
+ for (const [env, statusByInstance] of statuses) {
454
+ for (const [instance, statusByComponent] of statusByInstance) {
455
+ for (const [componentId, statusByKind] of statusByComponent) {
456
+ for (const [componentKind, status] of statusByKind) {
457
+ yield {
458
+ componentId,
459
+ componentKind,
460
+ env,
461
+ instance,
462
+ status
463
+ };
464
+ }
465
+ }
466
+ }
467
+ }
326
468
  }
327
- async resolvePlatformShell(ctx) {
469
+ async resolvePlatformShell() {
470
+ const hasPlatformShell = hasContainerToken(this.container, PLATFORM_SHELL);
471
+ if (hasPlatformShell === false) {
472
+ return undefined;
473
+ }
328
474
  try {
329
- return await ctx.container.resolve(PLATFORM_SHELL);
475
+ return await this.container.resolve(PLATFORM_SHELL);
330
476
  } catch (error) {
331
- if (isMissingPlatformShellResolutionError(error)) {
477
+ if (hasPlatformShell !== true && isMissingPlatformShellResolutionError(error)) {
332
478
  return undefined;
333
479
  }
334
480
  throw error;
335
481
  }
336
482
  }
337
483
  }
484
+ function hasContainerToken(container, token) {
485
+ const has = container.has;
486
+ if (typeof has !== 'function') {
487
+ return undefined;
488
+ }
489
+ return has.call(container, token);
490
+ }
338
491
  function isMissingPlatformShellResolutionError(error) {
339
492
  if (!(error instanceof ContainerResolutionError)) {
340
493
  return false;
341
494
  }
342
495
  const containerError = error;
343
496
  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.';
497
+ return token !== undefined && PLATFORM_SHELL_TOKEN_NAMES.has(token);
346
498
  }
347
499
  function resolveHttpOptions(http) {
348
500
  if (!http) {
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "monitoring",
9
9
  "observability"
10
10
  ],
11
- "version": "1.0.4",
11
+ "version": "2.0.0",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -36,10 +36,10 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "prom-client": "^15.1.3",
39
- "@fluojs/core": "^1.0.3",
40
- "@fluojs/http": "^1.1.2",
41
- "@fluojs/di": "^1.1.0",
42
- "@fluojs/runtime": "^1.1.8"
39
+ "@fluojs/core": "^1.1.0",
40
+ "@fluojs/di": "^2.0.0",
41
+ "@fluojs/http": "^2.0.1",
42
+ "@fluojs/runtime": "^2.0.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^22.0.0",