@fluojs/metrics 1.0.3 → 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
@@ -20,6 +20,10 @@ HTTP metric과 platform telemetry를 포함해 fluo 애플리케이션을 위한
20
20
  pnpm add @fluojs/metrics
21
21
  ```
22
22
 
23
+ ## 요구 사항
24
+
25
+ `@fluojs/metrics`는 Node.js 20 이상에서 실행됩니다. package manifest는 `engines.node >=20.0.0`을 선언합니다.
26
+
23
27
  ## 사용 시점
24
28
 
25
29
  - 애플리케이션이 Prometheus-compatible scraping을 위한 `/metrics` endpoint를 노출해야 할 때
@@ -40,17 +44,18 @@ class AppModule {}
40
44
 
41
45
  `MetricsModule.forRoot()`는 기본적으로 `GET /metrics`를 노출합니다. HTTP request instrumentation middleware를 설치하려면 `http: true` 또는 `http` option object를 전달하세요. HTTP 계측이 활성화되면 request total, error count, request duration을 기록합니다. 운영 환경에서는 scrape endpoint boundary를 명시적으로 다루세요. platform-level proxy가 준비될 때까지 `path: false`로 끄거나 dedicated endpoint middleware를 연결할 수 있습니다.
42
46
 
43
- Scrape endpoint는 active `prom-client` Registry output을 해당 Registry의 Prometheus content type으로 반환합니다. `MetricsModule.forRoot()`는 `registry` option을 전달하지 않는 한 격리된 Registry를 생성합니다. framework metric과 application-defined metric이 하나의 scrape surface를 의도적으로 공유해야 할 때만 shared `Registry`를 전달하세요.
47
+ Scrape endpoint는 active `prom-client` Registry output을 해당 Registry의 Prometheus content type으로 반환합니다. `MetricsModule.forRoot()`는 `registry` option을 전달하지 않는 한 application bootstrap마다 격리된 Registry를 생성합니다. 같은 dynamic module class를 다른 bootstrap에서 재사용해도 격리된 metric state는 새로 만들어집니다. framework metric과 application-defined metric이 하나의 scrape surface를 의도적으로 공유해야 할 때만 shared `Registry`를 전달하세요.
44
48
 
45
49
  ## 공개 책임
46
50
 
47
51
  | 표면 | 책임 | 경계 |
48
52
  | --- | --- | --- |
49
53
  | `MetricsModule.forRoot(...)` | Prometheus scrape endpoint, default metrics, optional HTTP instrumentation, platform telemetry, registry ownership을 wiring합니다. | `provider`는 현재 `'prometheus'`만 받습니다. `path: false`는 scrape route와 route-scoped endpoint middleware를 비활성화합니다. |
50
- | `MetricsService` | Active Registry 위에서 custom `Counter`, `Gauge`, `Histogram`을 만들기 위한 application-facing facade입니다. | 비즈니스/application metric은 package internals 대신 서비스를 사용하세요. |
51
- | `METER_PROVIDER` / `PrometheusMeterProvider` | Provider token이 필요한 first-party package integration용 low-level meter bridge입니다. | Application code는 package-level integration을 직접 조합하는 경우가 아니면 보통 token이 필요하지 않습니다. |
54
+ | `MetricsService` | Active Registry 위에서 custom `Counter`, `Gauge`, `Histogram`을 만드는 application-facing facade이며, 고급 Registry 공유를 위한 `getRegistry()`도 제공합니다. | 비즈니스/application metric은 collector helper를 사용하세요. `getRegistry()`는 active `prom-client` Registry를 `MetricsModule.forRoot({ registry })`로 직접 받을 수 없는 integration에 넘겨야 할 때만 사용하세요. |
55
+ | `Registry` | Shared-registry setup을 위한 `prom-client` `Registry` constructor re-export입니다. | 같은 Prometheus Registry 구현체이므로 중복 metric name은 Prometheus semantics에 따라 계속 실패합니다. |
56
+ | `METER_PROVIDER` / `PrometheusMeterProvider` / meter type | Provider token 또는 backend-neutral counter/gauge/histogram facade가 필요한 first-party package integration용 low-level meter bridge입니다. | Application code는 package-level integration을 직접 조합하는 경우가 아니면 보통 이 token이 필요하지 않습니다. 현재 bundled provider backend는 Prometheus뿐입니다. |
52
57
  | `middleware` | Framework HTTP metrics와 endpoint-scoped middleware 뒤의 module middleware chain에 참여하는 module-level middleware입니다. | Route-scoped가 아니므로 scrape route만 보호하려면 `endpointMiddleware`를 사용하세요. |
53
- | `endpointMiddleware` | 설정된 scrape endpoint에만 바인딩되는 class-based `@fluojs/http` middleware constructor입니다. | `path: false`일 때는 무시됩니다. 함수나 global middleware declaration은 이 option의 계약 밖입니다. |
58
+ | `endpointMiddleware` | 설정된 scrape endpoint에만 바인딩되는 class-based `@fluojs/http` middleware constructor입니다. | `path: false`일 때만 무시됩니다. `''`를 포함한 모든 문자열 `path`는 활성 endpoint path입니다. 함수나 global middleware declaration은 이 option의 계약 밖입니다. |
54
59
 
55
60
  ## 공통 패턴
56
61
 
@@ -103,9 +108,39 @@ MetricsModule.forRoot({
103
108
 
104
109
  `endpointMiddleware`는 class-based `@fluojs/http` middleware constructor를 받으며 metrics scrape endpoint에만 바인딩됩니다. middleware function이나 global middleware declaration은 이 option의 패키지 계약이 아닙니다. `middleware`는 module-level middleware로 남아 endpoint-scoped middleware 뒤의 module chain에서 실행되고, `endpointMiddleware`는 `path: false`로 scrape route를 비활성화하면 완전히 건너뜁니다. HTTP 계측이 활성화된 경우 endpoint middleware가 던진 실패도 내장 HTTP request/error collector에 기록됩니다.
105
110
 
111
+ ### Custom metric은 한 번 생성하고 재사용하기
112
+
113
+ `MetricsService.counter(...)`, `gauge(...)`, `histogram(...)`은 active Registry에 Prometheus collector를 생성합니다. 각 custom metric은 provider construction 또는 application startup 중 한 번만 만들고, business action이 발생할 때는 반환된 collector를 재사용하세요.
114
+
115
+ ```ts
116
+ import { Inject } from '@fluojs/core';
117
+ import { MetricsService } from '@fluojs/metrics';
118
+
119
+ @Inject(MetricsService)
120
+ class OrdersService {
121
+ private readonly ordersCreated: ReturnType<MetricsService['counter']>;
122
+
123
+ constructor(metrics: MetricsService) {
124
+ this.ordersCreated = metrics.counter({
125
+ name: 'orders_created_total',
126
+ help: 'Total orders created',
127
+ });
128
+ }
129
+
130
+ recordOrderCreated(): void {
131
+ this.ordersCreated.inc();
132
+ }
133
+ }
134
+ ```
135
+
136
+ 같은 이름으로 `MetricsService.counter(...)`를 다시 호출하면 collector를 다시 만들려고 하므로 Prometheus의 duplicate-name failure behavior를 따릅니다. 요청이나 command handler마다 새로 만들지 말고 collector를 저장해 재사용하세요.
137
+
138
+ `MetricsService.getRegistry()`는 module scrape endpoint, 내장 HTTP collector, platform telemetry, service를 통해 만든 custom collector가 함께 사용하는 동일한 active `prom-client` Registry를 반환합니다. Bootstrap을 직접 소유한다면 `MetricsModule.forRoot({ registry })`에 명시적 `registry`를 전달하는 방식을 우선하세요. `getRegistry()`는 DI로 `MetricsService`를 받은 advanced integration이 이미 활성화된 Registry에 third-party Prometheus collector를 등록해야 할 때 사용합니다.
139
+
106
140
  ### Framework metric과 app metric이 하나의 registry를 공유하기
107
141
 
108
142
  ```ts
143
+ import { Module } from '@fluojs/core';
109
144
  import { Counter, Registry } from 'prom-client';
110
145
  import { MetricsModule } from '@fluojs/metrics';
111
146
 
@@ -123,7 +158,7 @@ new Counter({
123
158
  class AppModule {}
124
159
  ```
125
160
 
126
- 여러 `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 규칙대로 계속 빠르게 실패합니다.
127
162
 
128
163
  ### 중복 메트릭 이름은 계속 빠르게 실패합니다
129
164
 
@@ -135,9 +170,9 @@ Prometheus 메트릭 이름은 하나의 Registry 안에서 고유해야 합니
135
170
 
136
171
  - `fluo_component_ready`: 준비 완료 시 1, 아닐 시 0.
137
172
  - `fluo_component_health`: 정상 상태 시 1, 아닐 시 0.
138
- - `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`로 나타냅니다.
139
174
 
140
- 이 데이터는 스크레이프 시점에 `PLATFORM_SHELL`을 쿼리하여 갱신됩니다. 초기화 시 환경 라벨을 제공할 수 있습니다.
175
+ 이 데이터는 built-in `/metrics` controller와 `MetricsService.getRegistry().metrics()`를 사용하는 advanced custom scraper를 포함해 active Registry가 스크레이프될 때마다 `PLATFORM_SHELL`을 쿼리하여 갱신됩니다. 초기화 시 환경 라벨을 제공할 수 있습니다.
141
176
 
142
177
  ```ts
143
178
  MetricsModule.forRoot({
@@ -150,10 +185,10 @@ MetricsModule.forRoot({
150
185
 
151
186
  ### 런타임 플랫폼 텔레메트리 스크레이프 계약
152
187
 
153
- 플랫폼 텔레메트리는 `/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`를 갱신합니다.
154
189
 
155
190
  - `PLATFORM_SHELL` 등록 자체가 빠진 경우에는 스크레이프가 계속 성공하고 플랫폼 텔레메트리 시리즈만 생략됩니다.
156
- - 직전 성공 스크레이프에서 플랫폼 텔레메트리를 노출한 뒤 `PLATFORM_SHELL`을 사용할 수 없게 되면, stale `fluo_component_ready` 및 `fluo_component_health` 시리즈를 제거한 뒤 메트릭을 반환합니다.
191
+ - 직전 성공 스크레이프에서 플랫폼 텔레메트리를 노출한 뒤 `PLATFORM_SHELL`을 사용할 수 없게 되거나 이후 module instance가 재사용된 Registry를 다른 플랫폼 snapshot으로 갱신하면, stale `fluo_component_ready` 및 `fluo_component_health` 시리즈를 제거한 뒤 메트릭을 반환합니다.
157
192
  - 그 외의 `PLATFORM_SHELL` resolve 실패는 조용히 삼키지 않고 스크레이프 실패로 그대로 드러납니다.
158
193
 
159
194
  ### 기본 프로세스/Node 메트릭 비활성화
@@ -169,24 +204,27 @@ MetricsModule.forRoot({
169
204
  ## 공개 API
170
205
 
171
206
  - `MetricsModule.forRoot(options)`
172
- - `MetricsService`
207
+ - `MetricsService` 및 `counter(...)`, `gauge(...)`, `histogram(...)`, `getRegistry()`
173
208
  - `METER_PROVIDER` (Token)
174
209
  - `PrometheusMeterProvider`
210
+ - Meter abstraction type: `MeterProvider`, `MeterCounter`, `MeterGauge`, `MeterHistogram`
175
211
  - `HttpMetricsMiddleware` 및 HTTP path-label 옵션 타입
176
212
  - `provider`(현재는 `'prometheus'`만 지원), module-level `middleware`, endpoint-scoped `endpointMiddleware`를 포함한 module option
177
213
  - `prom-client`의 `Registry`
178
214
 
179
215
  ### 운영 기본값
180
216
 
181
- - `path`의 기본값은 `'/metrics'`이며, `path: false`로 스크레이프 엔드포인트를 완전히 비활성화할 수 있습니다.
217
+ - `path`의 기본값은 `'/metrics'`입니다. `''`를 포함한 모든 문자열 path는 scrape endpoint를 노출하며, `path: false`로만 scrape endpoint를 완전히 비활성화할 수 있습니다.
218
+ - `registry`를 생략하면 application bootstrap마다 fresh isolated Registry, `MetricsService`, meter provider, telemetry collector set을 소유합니다.
182
219
  - scrape response는 active Registry의 Prometheus content type과 Registry contents를 사용합니다.
183
220
  - `defaultMetrics`의 기본값은 `true`이며, `defaultMetrics: false`로 해당 Registry의 Prometheus 기본 프로세스/Node.js collector를 끌 수 있습니다.
184
221
  - `endpointMiddleware`는 class-based route-scoped middleware를 스크레이프 엔드포인트에만 바인딩합니다. HTTP 계측이 활성화된 경우 endpoint middleware 실패는 내장 HTTP collector에 집계됩니다.
185
222
  - HTTP 메트릭은 `http: true` 또는 `http` 옵션 객체를 전달한 경우에만 설치되며, 설치된 뒤에는 기본적으로 템플릿 기반 경로 라벨 정규화를 사용합니다.
186
- - 내장 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()` 함수를 복원합니다.
187
225
  - raw path 라벨은 `allowUnsafeRawPathLabelMode: true`를 명시한 bounded internal route에서만 사용해야 합니다.
188
226
  - 플랫폼 텔레메트리는 `PLATFORM_SHELL`이 실제로 누락된 경우에만 생략되며, 그 외 resolve 실패는 스크레이프를 실패시킵니다.
189
- - 직전 성공 스크레이프에서 노출된 플랫폼 텔레메트리 시리즈는 `PLATFORM_SHELL`을 사용할 수 없게 된 스크레이프에서 제거됩니다.
227
+ - 직전 성공 스크레이프에서 노출된 플랫폼 텔레메트리 시리즈는 `PLATFORM_SHELL`을 사용할 수 없게 된 스크레이프 또는 이후 module instance가 재사용된 Registry를 다른 플랫폼 snapshot으로 갱신한 스크레이프에서 제거됩니다.
190
228
 
191
229
  ## 관련 패키지
192
230
 
package/README.md CHANGED
@@ -20,6 +20,10 @@ Prometheus metrics exposure for fluo applications, including framework-aware HTT
20
20
  pnpm add @fluojs/metrics
21
21
  ```
22
22
 
23
+ ## Requirements
24
+
25
+ `@fluojs/metrics` runs on Node.js 20 or newer; the package manifest declares `engines.node >=20.0.0`.
26
+
23
27
  ## When to Use
24
28
 
25
29
  - when your app should expose a `/metrics` endpoint for Prometheus-compatible scraping
@@ -40,17 +44,18 @@ class AppModule {}
40
44
 
41
45
  `MetricsModule.forRoot()` exposes `GET /metrics` by default. Pass `http: true` (or an `http` options object) when you want the module to install HTTP request instrumentation middleware. When HTTP instrumentation is enabled, the module records request totals, error counts, and request duration. For production deployments, make the scrape endpoint boundary explicit: either disable it with `path: false` until a platform-level proxy is in place, or attach dedicated endpoint middleware.
42
46
 
43
- The scrape endpoint returns the active `prom-client` registry output with that registry's Prometheus content type. `MetricsModule.forRoot()` creates an isolated registry unless you pass a `registry` option; pass a shared `Registry` only when framework metrics and application-defined metrics intentionally share one scrape surface.
47
+ The scrape endpoint returns the active `prom-client` registry output with that registry's Prometheus content type. `MetricsModule.forRoot()` creates an isolated registry for each application bootstrap unless you pass a `registry` option; reusing the same dynamic module class for another bootstrap receives fresh isolated metrics state. Pass a shared `Registry` only when framework metrics and application-defined metrics intentionally share one scrape surface.
44
48
 
45
49
  ## Public Responsibilities
46
50
 
47
51
  | Surface | Responsibility | Boundary |
48
52
  | --- | --- | --- |
49
53
  | `MetricsModule.forRoot(...)` | Wires the Prometheus scrape endpoint, default metrics, optional HTTP instrumentation, platform telemetry, and registry ownership. | `provider` currently accepts only `'prometheus'`; `path: false` disables the scrape route and route-scoped endpoint middleware. |
50
- | `MetricsService` | Application-facing facade for custom `Counter`, `Gauge`, and `Histogram` metrics on the active registry. | Use this for business/application metrics instead of reaching into package internals. |
51
- | `METER_PROVIDER` / `PrometheusMeterProvider` | Low-level meter bridge for first-party package integrations that need a provider token. | Application code usually does not need this token unless it is composing package-level integrations. |
54
+ | `MetricsService` | Application-facing facade for custom `Counter`, `Gauge`, and `Histogram` metrics on the active registry, plus `getRegistry()` for deliberate advanced registry sharing. | Use collector helpers for business/application metrics. Use `getRegistry()` only when an integration must hand the active `prom-client` Registry to code that cannot receive `MetricsModule.forRoot({ registry })` directly. |
55
+ | `Registry` | Re-export of `prom-client`'s `Registry` constructor for shared-registry setups. | It is the same Prometheus registry implementation; duplicate metric names still fail according to Prometheus semantics. |
56
+ | `METER_PROVIDER` / `PrometheusMeterProvider` / meter types | Low-level meter bridge for first-party package integrations that need a provider token or backend-neutral counter/gauge/histogram facade. | Application code usually does not need this token unless it is composing package-level integrations; the only bundled provider backend today is Prometheus. |
52
57
  | `middleware` | Module-level middleware that participates in the module middleware chain after framework HTTP metrics and endpoint-scoped middleware. | It is not route-scoped; use `endpointMiddleware` when only the scrape route should be protected. |
53
- | `endpointMiddleware` | Class-based `@fluojs/http` middleware constructors bound only to the configured scrape endpoint. | Ignored when `path: false`; functions or global middleware declarations are outside this option's contract. |
58
+ | `endpointMiddleware` | Class-based `@fluojs/http` middleware constructors bound only to the configured scrape endpoint. | Ignored only when `path: false`; any string `path`, including `''`, remains an active endpoint path. Functions or global middleware declarations are outside this option's contract. |
54
59
 
55
60
  ## Common Patterns
56
61
 
@@ -103,9 +108,39 @@ MetricsModule.forRoot({
103
108
 
104
109
  `endpointMiddleware` accepts class-based `@fluojs/http` middleware constructors and binds them only to the metrics scrape endpoint. Middleware functions or global middleware declarations are not the package contract for this option. `middleware` remains module-level middleware and runs as part of the module chain after endpoint-scoped middleware, while `endpointMiddleware` is skipped entirely when `path: false` disables the scrape route. When HTTP instrumentation is enabled, failures thrown by endpoint middleware are recorded in the built-in HTTP request and error collectors.
105
110
 
111
+ ### Create custom metrics once and reuse them
112
+
113
+ `MetricsService.counter(...)`, `gauge(...)`, and `histogram(...)` create Prometheus collectors on the active registry. Create each custom metric once during provider construction or application startup, then reuse the returned collector when business actions occur.
114
+
115
+ ```ts
116
+ import { Inject } from '@fluojs/core';
117
+ import { MetricsService } from '@fluojs/metrics';
118
+
119
+ @Inject(MetricsService)
120
+ class OrdersService {
121
+ private readonly ordersCreated: ReturnType<MetricsService['counter']>;
122
+
123
+ constructor(metrics: MetricsService) {
124
+ this.ordersCreated = metrics.counter({
125
+ name: 'orders_created_total',
126
+ help: 'Total orders created',
127
+ });
128
+ }
129
+
130
+ recordOrderCreated(): void {
131
+ this.ordersCreated.inc();
132
+ }
133
+ }
134
+ ```
135
+
136
+ Calling `MetricsService.counter(...)` again with the same name recreates the collector and follows Prometheus' duplicate-name failure behavior. Store and reuse the collector instead of creating it inside each request or command handler.
137
+
138
+ `MetricsService.getRegistry()` returns the same active `prom-client` Registry used by the module scrape endpoint, built-in HTTP collectors, platform telemetry, and custom collectors created through the service. Prefer passing an explicit `registry` to `MetricsModule.forRoot({ registry })` when you own the bootstrap. Use `getRegistry()` for advanced integrations that receive `MetricsService` through DI and need to register a third-party Prometheus collector on the already active registry.
139
+
106
140
  ### Share one registry for framework and app metrics
107
141
 
108
142
  ```ts
143
+ import { Module } from '@fluojs/core';
109
144
  import { Counter, Registry } from 'prom-client';
110
145
  import { MetricsModule } from '@fluojs/metrics';
111
146
 
@@ -123,7 +158,7 @@ new Counter({
123
158
  class AppModule {}
124
159
  ```
125
160
 
126
- 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.
127
162
 
128
163
  ### Duplicate metric names still fail fast
129
164
 
@@ -135,9 +170,9 @@ The module emits fluo-specific gauges that mirror the platform shell and registe
135
170
 
136
171
  - `fluo_component_ready`: `1` when a component is ready, otherwise `0`.
137
172
  - `fluo_component_health`: `1` when a component is healthy, otherwise `0`.
138
- - `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.
139
174
 
140
- 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.
141
176
 
142
177
  ```ts
143
178
  MetricsModule.forRoot({
@@ -150,7 +185,7 @@ MetricsModule.forRoot({
150
185
 
151
186
  ### Runtime platform telemetry scrape contract
152
187
 
153
- 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()`.
154
189
 
155
190
  - If `PLATFORM_SHELL` is not registered, the scrape still succeeds and omits the platform telemetry series.
156
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.
@@ -169,24 +204,27 @@ MetricsModule.forRoot({
169
204
  ## Public API
170
205
 
171
206
  - `MetricsModule.forRoot(options)`
172
- - `MetricsService`
207
+ - `MetricsService`, including `counter(...)`, `gauge(...)`, `histogram(...)`, and `getRegistry()`
173
208
  - `METER_PROVIDER`
174
209
  - `PrometheusMeterProvider`
210
+ - Meter abstraction types: `MeterProvider`, `MeterCounter`, `MeterGauge`, and `MeterHistogram`
175
211
  - `HttpMetricsMiddleware` and HTTP path-label option types
176
212
  - Module options including `provider` (currently only `'prometheus'`), module-level `middleware`, and endpoint-scoped `endpointMiddleware`
177
213
  - `Registry` from `prom-client`
178
214
 
179
215
  ### Operational defaults
180
216
 
181
- - `path` defaults to `'/metrics'`, and `path: false` disables the scrape endpoint entirely.
217
+ - `path` defaults to `'/metrics'`, any string path including `''` exposes a scrape endpoint, and `path: false` disables the scrape endpoint entirely.
218
+ - When `registry` is omitted, each application bootstrap owns a fresh isolated registry, `MetricsService`, meter provider, and telemetry collector set.
182
219
  - The scrape response uses the active registry's Prometheus content type and registry contents.
183
220
  - `defaultMetrics` defaults to `true`, and `defaultMetrics: false` disables Prometheus default process and Node.js collectors for that registry.
184
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.
185
222
  - HTTP metrics are installed only when `http: true` or an `http` options object is provided, and then default to template-normalized path labels.
186
- - 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.
187
225
  - Raw path labels require `allowUnsafeRawPathLabelMode: true` and should stay limited to bounded internal routes.
188
226
  - Platform telemetry is omitted only when `PLATFORM_SHELL` is genuinely missing; other resolution failures fail the scrape.
189
- - 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.
190
228
 
191
229
  ## Related Packages
192
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
  *
@@ -57,5 +58,6 @@ export declare class MetricsModule {
57
58
  * @returns A runtime module that exposes metrics through the configured path.
58
59
  */
59
60
  static forRoot(options?: MetricsModuleOptions): ModuleType;
61
+ private static createRegistry;
60
62
  }
61
63
  //# sourceMappingURL=metrics-module.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics-module.d.ts","sourceRoot":"","sources":["../src/metrics-module.ts"],"names":[],"mappings":"AACA,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;CAqE/D"}
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"}
@@ -3,9 +3,11 @@ 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 { Inject } from '@fluojs/core';
6
7
  import { ContainerResolutionError } from '@fluojs/di';
7
8
  import { Controller, Get, forRoutes } from '@fluojs/http';
8
9
  import { defineModule, PLATFORM_SHELL } from '@fluojs/runtime';
10
+ import { RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
9
11
  import { collectDefaultMetrics, Gauge, Registry as PrometheusRegistry } from 'prom-client';
10
12
  import { HttpMetricsMiddleware } from './http-metrics-middleware.js';
11
13
  import { METER_PROVIDER } from './providers/meter-provider.js';
@@ -21,6 +23,7 @@ import { PrometheusMeterProvider } from './providers/prometheus-meter-provider.j
21
23
  /** Module entry point that exposes `/metrics` and optional HTTP/runtime telemetry. */
22
24
  export class MetricsModule {
23
25
  static registeredRegistries = new WeakSet();
26
+ static httpInstrumentationRegistrations = new WeakMap();
24
27
 
25
28
  /**
26
29
  * Register framework metrics, optional HTTP middleware, and a scrape endpoint.
@@ -43,24 +46,55 @@ export class MetricsModule {
43
46
  }
44
47
  const httpOptions = resolveHttpOptions(options.http);
45
48
  const metricsPath = options.path === undefined ? '/metrics' : options.path;
46
- const registry = options.registry ?? new PrometheusRegistry();
47
- const metricsService = new MetricsService(registry);
48
- const meterProvider = new PrometheusMeterProvider(registry);
49
- const platformTelemetry = new RuntimePlatformTelemetry(registry, options.registry ? 'shared' : 'isolated', options.platformTelemetry);
50
- if (options.defaultMetrics !== false && !MetricsModule.registeredRegistries.has(registry)) {
51
- MetricsModule.registeredRegistries.add(registry);
52
- collectDefaultMetrics({
53
- register: registry
54
- });
49
+ let registryToken = Symbol('MetricsModule.registry');
50
+ const platformTelemetryToken = Symbol('MetricsModule.platformTelemetry');
51
+ let httpMetricsMiddleware = httpOptions ? createHttpMetricsMiddleware(registryToken, httpOptions) : undefined;
52
+ const endpointMiddleware = typeof metricsPath === 'string' ? (options.endpointMiddleware ?? []).map(middlewareClass => forRoutes(middlewareClass, metricsPath)) : [];
53
+ const middleware = [...endpointMiddleware, ...(options.middleware ?? [])];
54
+ const registryProvider = {
55
+ provide: registryToken,
56
+ useFactory: () => MetricsModule.createRegistry(options)
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
+ }
55
85
  }
56
- const endpointMiddleware = metricsPath ? (options.endpointMiddleware ?? []).map(middlewareClass => forRoutes(middlewareClass, metricsPath)) : [];
57
- const middleware = [...(httpOptions ? [new HttpMetricsMiddleware(registry, httpOptions)] : []), ...endpointMiddleware, ...(options.middleware ?? [])];
58
- const providers = [{
86
+ const providers = [...(includeRuntimeRegistryProvider ? [registryProvider] : []), ...validationProviders, {
59
87
  provide: MetricsService,
60
- useValue: metricsService
88
+ inject: [registryToken, platformTelemetryToken],
89
+ useFactory: registry => new MetricsService(assertPrometheusRegistry(registry))
61
90
  }, {
62
91
  provide: METER_PROVIDER,
63
- useValue: meterProvider
92
+ inject: [registryToken],
93
+ useFactory: registry => new PrometheusMeterProvider(assertPrometheusRegistry(registry))
94
+ }, {
95
+ provide: platformTelemetryToken,
96
+ inject: [registryToken, RUNTIME_CONTAINER],
97
+ useFactory: (registry, container) => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), assertRuntimeContainer(container), options.registry ? 'shared' : 'isolated', options.platformTelemetry)
64
98
  }];
65
99
  const controllers = [];
66
100
  if (typeof metricsPath === 'string') {
@@ -72,14 +106,16 @@ export class MetricsModule {
72
106
  ({
73
107
  e: [_initProto],
74
108
  c: [_MetricsController, _initClass]
75
- } = _applyDecs(this, [Controller('')], [[Get(metricsRoutePath), 2, "getMetrics"]]));
109
+ } = _applyDecs(this, [Inject(registryToken, platformTelemetryToken), Controller('')], [[Get(metricsRoutePath), 2, "getMetrics"]]));
76
110
  }
77
- constructor() {
111
+ constructor(registry, platformTelemetry) {
112
+ this.registry = registry;
113
+ this.platformTelemetry = platformTelemetry;
78
114
  _initProto(this);
79
115
  }
80
116
  async getMetrics(_input, ctx) {
81
- ctx.response.setHeader('content-type', registry.contentType);
82
- return platformTelemetry.collectMetrics(ctx, registry);
117
+ ctx.response.setHeader('content-type', this.registry.contentType);
118
+ return this.platformTelemetry.collectMetrics(this.registry);
83
119
  }
84
120
  static {
85
121
  _initClass();
@@ -90,19 +126,87 @@ export class MetricsModule {
90
126
  class MetricsRuntimeModule {}
91
127
  defineModule(MetricsRuntimeModule, {
92
128
  controllers,
129
+ exports: [MetricsService, METER_PROVIDER],
130
+ imports,
93
131
  middleware,
94
132
  providers
95
133
  });
96
134
  return MetricsRuntimeModule;
97
135
  }
136
+ static createRegistry(options) {
137
+ const registry = options.registry ?? new PrometheusRegistry();
138
+ if (options.defaultMetrics !== false && !MetricsModule.registeredRegistries.has(registry)) {
139
+ MetricsModule.registeredRegistries.add(registry);
140
+ collectDefaultMetrics({
141
+ register: registry
142
+ });
143
+ }
144
+ return registry;
145
+ }
98
146
  }
99
147
  const PLATFORM_COMPONENT_LABELS = ['component_id', 'component_kind', 'operation', 'result', 'env', 'instance'];
100
148
  const REGISTRY_MODE_LABELS = ['mode'];
101
149
  const FRAMEWORK_PLATFORM_GAUGES = new WeakSet();
150
+ const PLATFORM_TELEMETRY_REGISTRY_STATES = new WeakMap();
102
151
  const HEALTH_STATUSES = ['healthy', 'unhealthy', 'degraded'];
103
152
  const READINESS_STATUSES = ['ready', 'not-ready', 'degraded'];
104
- const PLATFORM_SHELL_TOKEN_NAME = 'PLATFORM_SHELL';
105
- 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)]);
154
+ function createHttpMetricsMiddleware(registryToken, httpOptions) {
155
+ let _initClass2;
156
+ let _MetricsHttpMiddlewar;
157
+ class MetricsHttpMiddleware {
158
+ static {
159
+ [_MetricsHttpMiddlewar, _initClass2] = _applyDecs(this, [Inject(registryToken)], []).c;
160
+ }
161
+ delegate;
162
+ constructor(registry) {
163
+ this.delegate = new HttpMetricsMiddleware(registry, httpOptions);
164
+ }
165
+ handle(context, next) {
166
+ return this.delegate.handle(context, next);
167
+ }
168
+ static {
169
+ _initClass2();
170
+ }
171
+ }
172
+ return _MetricsHttpMiddlewar;
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
+ }
181
+ function assertPrometheusRegistry(value) {
182
+ if (!(value instanceof PrometheusRegistry)) {
183
+ throw new Error('MetricsModule registry provider resolved an invalid Prometheus registry.');
184
+ }
185
+ return value;
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
+ }
106
210
  function toReadinessValue(status) {
107
211
  return status === 'ready' ? 1 : 0;
108
212
  }
@@ -138,12 +242,13 @@ class RuntimePlatformTelemetry {
138
242
  readinessGauge;
139
243
  healthGauge;
140
244
  registryModeGauge;
141
- lastHealthStatuses = new Map();
142
- lastReadinessStatuses = new Map();
143
- scrapeChain = Promise.resolve();
144
- constructor(registry, registryMode, labels = {}) {
245
+ telemetryState;
246
+ constructor(registry, container, registryMode, labels = {}) {
247
+ this.registry = registry;
248
+ this.container = container;
145
249
  this.registryMode = registryMode;
146
250
  this.labels = labels;
251
+ this.telemetryState = getRuntimePlatformTelemetryRegistryState(registry);
147
252
  this.readinessGauge = getOrCreateGauge(registry, {
148
253
  help: 'Runtime platform component readiness from shared platform snapshot semantics.',
149
254
  labelNames: PLATFORM_COMPONENT_LABELS,
@@ -160,22 +265,54 @@ class RuntimePlatformTelemetry {
160
265
  name: 'fluo_metrics_registry_mode'
161
266
  });
162
267
  this.registryModeGauge.labels(this.registryMode).set(1);
268
+ this.installRegistryRefresh();
163
269
  }
164
- async collectMetrics(ctx, registry) {
165
- const collect = this.scrapeChain.then(async () => {
166
- await this.refresh(ctx);
167
- return registry.metrics();
168
- });
169
- this.scrapeChain = collect.then(() => undefined, () => undefined);
170
- return collect;
270
+ collectMetrics(registry) {
271
+ return registry.metrics();
171
272
  }
172
- async refresh(ctx) {
173
- const platformShell = await this.resolvePlatformShell(ctx);
174
- if (!platformShell) {
175
- 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) {
176
279
  return;
177
280
  }
178
- 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) {
179
316
  const env = this.labels?.env ?? 'unknown';
180
317
  const instance = this.labels?.instance ?? 'local';
181
318
  const components = [{
@@ -190,21 +327,17 @@ class RuntimePlatformTelemetry {
190
327
  readiness: component.readiness.status
191
328
  }))];
192
329
  this.syncGaugeStatuses({
193
- currentStatuses: new Map(components.map(component => [this.toComponentKey(component.id, component.kind), component.health])),
194
- env,
330
+ currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.health),
195
331
  gauge: this.healthGauge,
196
- instance,
197
- lastStatuses: this.lastHealthStatuses,
332
+ lastStatuses: this.telemetryState.lastHealthStatuses,
198
333
  operation: 'health',
199
334
  statuses: HEALTH_STATUSES,
200
335
  toMetricValue: toHealthValue
201
336
  });
202
337
  this.syncGaugeStatuses({
203
- currentStatuses: new Map(components.map(component => [this.toComponentKey(component.id, component.kind), component.readiness])),
204
- env,
338
+ currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.readiness),
205
339
  gauge: this.readinessGauge,
206
- instance,
207
- lastStatuses: this.lastReadinessStatuses,
340
+ lastStatuses: this.telemetryState.lastReadinessStatuses,
208
341
  operation: 'readiness',
209
342
  statuses: READINESS_STATUSES,
210
343
  toMetricValue: toReadinessValue
@@ -212,32 +345,30 @@ class RuntimePlatformTelemetry {
212
345
  }
213
346
  clearPlatformTelemetry() {
214
347
  this.clearGaugeStatuses({
215
- env: this.labels?.env ?? 'unknown',
216
348
  gauge: this.healthGauge,
217
- instance: this.labels?.instance ?? 'local',
218
- lastStatuses: this.lastHealthStatuses,
349
+ lastStatuses: this.telemetryState.lastHealthStatuses,
219
350
  operation: 'health',
220
351
  statuses: HEALTH_STATUSES
221
352
  });
222
353
  this.clearGaugeStatuses({
223
- env: this.labels?.env ?? 'unknown',
224
354
  gauge: this.readinessGauge,
225
- instance: this.labels?.instance ?? 'local',
226
- lastStatuses: this.lastReadinessStatuses,
355
+ lastStatuses: this.telemetryState.lastReadinessStatuses,
227
356
  operation: 'readiness',
228
357
  statuses: READINESS_STATUSES
229
358
  });
230
359
  }
231
360
  clearGaugeStatuses({
232
- env,
233
361
  gauge,
234
- instance,
235
362
  lastStatuses,
236
363
  operation,
237
364
  statuses
238
365
  }) {
239
- for (const componentKey of lastStatuses.keys()) {
240
- const [componentId, componentKind] = this.fromComponentKey(componentKey);
366
+ for (const {
367
+ componentId,
368
+ componentKind,
369
+ env,
370
+ instance
371
+ } of this.componentStatusEntries(lastStatuses)) {
241
372
  for (const status of statuses) {
242
373
  gauge.remove(componentId, componentKind, operation, status, env, instance);
243
374
  }
@@ -246,20 +377,23 @@ class RuntimePlatformTelemetry {
246
377
  }
247
378
  syncGaugeStatuses({
248
379
  currentStatuses,
249
- env,
250
380
  gauge,
251
- instance,
252
381
  lastStatuses,
253
382
  operation,
254
383
  statuses,
255
384
  toMetricValue
256
385
  }) {
257
- for (const [componentKey, previousStatus] of lastStatuses) {
258
- 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);
259
394
  if (nextStatus === previousStatus) {
260
395
  continue;
261
396
  }
262
- const [componentId, componentKind] = this.fromComponentKey(componentKey);
263
397
  for (const status of statuses) {
264
398
  if (status !== previousStatus) {
265
399
  continue;
@@ -267,49 +401,100 @@ class RuntimePlatformTelemetry {
267
401
  gauge.remove(componentId, componentKind, operation, status, env, instance);
268
402
  }
269
403
  }
270
- for (const [componentKey, currentStatus] of currentStatuses) {
271
- 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)) {
272
411
  gauge.labels(componentId, componentKind, operation, currentStatus, env, instance).set(toMetricValue(currentStatus));
273
412
  }
274
413
  lastStatuses.clear();
275
- for (const [componentKey, currentStatus] of currentStatuses) {
276
- 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);
277
422
  }
278
423
  }
279
- fromComponentKey(componentKey) {
280
- const separatorIndex = componentKey.indexOf('::');
281
- 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;
282
430
  }
283
- toComponentKey(componentId, componentKind) {
284
- 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
+ }
285
468
  }
286
- async resolvePlatformShell(ctx) {
469
+ async resolvePlatformShell() {
470
+ const hasPlatformShell = hasContainerToken(this.container, PLATFORM_SHELL);
471
+ if (hasPlatformShell === false) {
472
+ return undefined;
473
+ }
287
474
  try {
288
- return await ctx.container.resolve(PLATFORM_SHELL);
475
+ return await this.container.resolve(PLATFORM_SHELL);
289
476
  } catch (error) {
290
- if (isMissingPlatformShellResolutionError(error)) {
477
+ if (hasPlatformShell !== true && isMissingPlatformShellResolutionError(error)) {
291
478
  return undefined;
292
479
  }
293
480
  throw error;
294
481
  }
295
482
  }
296
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
+ }
297
491
  function isMissingPlatformShellResolutionError(error) {
298
492
  if (!(error instanceof ContainerResolutionError)) {
299
493
  return false;
300
494
  }
301
495
  const containerError = error;
302
- const message = String(containerError.message ?? '');
303
- const token = typeof containerError.meta?.['token'] === 'string' ? containerError.meta['token'] : undefined;
304
- if (token && PLATFORM_SHELL_TOKEN_NAMES.has(token)) {
305
- return message.startsWith(`No provider registered for token ${token}.`);
306
- }
307
- for (const tokenName of PLATFORM_SHELL_TOKEN_NAMES) {
308
- if (message.startsWith(`No provider registered for token ${tokenName}.`)) {
309
- return true;
310
- }
311
- }
312
- return false;
496
+ const token = typeof containerError.meta?.token === 'string' ? containerError.meta.token : undefined;
497
+ return token !== undefined && PLATFORM_SHELL_TOKEN_NAMES.has(token);
313
498
  }
314
499
  function resolveHttpOptions(http) {
315
500
  if (!http) {
@@ -318,6 +503,9 @@ function resolveHttpOptions(http) {
318
503
  if (http === true) {
319
504
  return {};
320
505
  }
506
+ if (http.pathLabelMode === 'raw' && http.allowUnsafeRawPathLabelMode !== true) {
507
+ throw new Error('HttpMetricsMiddleware pathLabelMode "raw" is disabled by default. Pass allowUnsafeRawPathLabelMode: true only when you have bounded path cardinality.');
508
+ }
321
509
  return {
322
510
  allowUnsafeRawPathLabelMode: http.allowUnsafeRawPathLabelMode,
323
511
  pathLabelMode: http.pathLabelMode,
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "monitoring",
9
9
  "observability"
10
10
  ],
11
- "version": "1.0.3",
11
+ "version": "2.0.0",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -36,9 +36,10 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "prom-client": "^15.1.3",
39
- "@fluojs/di": "^1.0.3",
40
- "@fluojs/http": "^1.1.0",
41
- "@fluojs/runtime": "^1.1.1"
39
+ "@fluojs/core": "^1.1.0",
40
+ "@fluojs/di": "^2.0.0",
41
+ "@fluojs/http": "^2.0.1",
42
+ "@fluojs/runtime": "^2.0.1"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@types/node": "^22.0.0",