@fluojs/metrics 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -22,7 +22,7 @@ pnpm add @fluojs/metrics
22
22
 
23
23
  ## 요구 사항
24
24
 
25
- `@fluojs/metrics`는 Node.js 20 이상에서 실행됩니다. package manifest는 `engines.node >=20.0.0`을 선언합니다.
25
+ `@fluojs/metrics`는 패키지 자체의 지원 계약인 Node.js `>=24.0.0 <27`을 요구합니다.
26
26
 
27
27
  ## 사용 시점
28
28
 
@@ -44,14 +44,15 @@ class AppModule {}
44
44
 
45
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를 연결할 수 있습니다.
46
46
 
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`를 전달하세요.
47
+ Scrape endpoint는 active `prom-client` Registry output을 해당 Registry의 Prometheus content type으로 반환합니다. `MetricsModule.forRoot()`는 bootstrap이 `METRICS_REGISTRY`를 구성하거나 legacy `registry` option을 제공하지 않는 한 application bootstrap마다 격리된 Registry를 생성합니다. 같은 dynamic module class를 다른 bootstrap에서 재사용해도 격리된 metric state는 새로 만들어집니다. framework metric과 application-defined metric이 하나의 scrape surface를 의도적으로 공유해야 할 때만 bootstrap에서 shared `Registry`를 구성하세요.
48
48
 
49
49
  ## 공개 책임
50
50
 
51
51
  | 표면 | 책임 | 경계 |
52
52
  | --- | --- | --- |
53
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를 비활성화합니다. |
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에 넘겨야 할 때만 사용하세요. |
54
+ | `MetricsService` | Active Registry 위에서 custom `Counter`, `Gauge`, `Histogram`을 만드는 application-facing facade이며, 고급 Registry 공유를 위한 `getRegistry()`도 제공합니다. | `MetricsService`는 non-global service입니다. `MetricsModule.forRoot(...)` registration을 직접 import한 module 또는 `MetricsService`를 re-export하는 module을 import한 module에서 inject하세요. 관련 없는 sibling module에는 자동으로 제공되지 않습니다. 비즈니스/application metric은 collector helper를 사용하세요. `getRegistry()`는 active `prom-client` Registry를 bootstrap의 `METRICS_REGISTRY`로 직접 받을 수 없는 integration에 넘겨야 할 때만 사용하세요. |
55
+ | `METRICS_REGISTRY` | Shared `prom-client` Registry를 위한 bootstrap provider token입니다. | `bootstrapApplication()` provider가 module의 legacy `registry` option보다 우선하여 ownership을 가집니다. |
55
56
  | `Registry` | Shared-registry setup을 위한 `prom-client` `Registry` constructor re-export입니다. | 같은 Prometheus Registry 구현체이므로 중복 metric name은 Prometheus semantics에 따라 계속 실패합니다. |
56
57
  | `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뿐입니다. |
57
58
  | `middleware` | Framework HTTP metrics와 endpoint-scoped middleware 뒤의 module middleware chain에 참여하는 module-level middleware입니다. | Route-scoped가 아니므로 scrape route만 보호하려면 `endpointMiddleware`를 사용하세요. |
@@ -82,6 +83,21 @@ MetricsModule.forRoot({
82
83
  });
83
84
  ```
84
85
 
86
+ ### HTTP duration histogram bucket 구성
87
+
88
+ ```ts
89
+ MetricsModule.forRoot({
90
+ http: {
91
+ durationHistogramBuckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
92
+ },
93
+ });
94
+ ```
95
+
96
+ `durationHistogramBuckets`는 내장 HTTP request duration histogram의
97
+ `prom-client` 기본값을 대체합니다. 값의 단위는 초이며, alerting하려는
98
+ latency 범위에 맞게 정해야 합니다. 각 경계는 유한하고 엄격히 증가해야 하며,
99
+ 잘못된 구성은 setup 중 거부됩니다.
100
+
85
101
  ### 메트릭 엔드포인트 보호 또는 비활성화
86
102
 
87
103
  ```ts
@@ -110,7 +126,7 @@ MetricsModule.forRoot({
110
126
 
111
127
  ### Custom metric은 한 번 생성하고 재사용하기
112
128
 
113
- `MetricsService.counter(...)`, `gauge(...)`, `histogram(...)`은 active Registry에 Prometheus collector를 생성합니다. 각 custom metric은 provider construction 또는 application startup 중 한 번만 만들고, business action이 발생할 때는 반환된 collector를 재사용하세요.
129
+ `MetricsService.counter(...)`, `gauge(...)`, `histogram(...)`은 active Registry에 Prometheus collector를 생성합니다. `MetricsService`는 non-global service이므로, 이를 inject하는 provider 또는 controller는 `MetricsModule.forRoot(...)` registration을 직접 import하거나 `MetricsService`를 re-export하는 module을 import한 module에 속해야 합니다. 관련 없는 sibling module에는 자동으로 제공되지 않습니다. 각 custom metric은 provider construction 또는 application startup 중 한 번만 만들고, business action이 발생할 때는 반환된 collector를 재사용하세요.
114
130
 
115
131
  ```ts
116
132
  import { Inject } from '@fluojs/core';
@@ -135,30 +151,42 @@ class OrdersService {
135
151
 
136
152
  같은 이름으로 `MetricsService.counter(...)`를 다시 호출하면 collector를 다시 만들려고 하므로 Prometheus의 duplicate-name failure behavior를 따릅니다. 요청이나 command handler마다 새로 만들지 말고 collector를 저장해 재사용하세요.
137
153
 
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를 등록해야 할 때 사용합니다.
154
+ `MetricsService.getRegistry()`는 module scrape endpoint, 내장 HTTP collector, platform telemetry, service를 통해 만든 custom collector가 함께 사용하는 동일한 active `prom-client` Registry를 반환합니다. Bootstrap을 직접 소유한다면 public `METRICS_REGISTRY` token으로 명시적 shared registry 구성하세요. `getRegistry()`는 DI로 `MetricsService`를 받은 advanced integration이 이미 활성화된 Registry에 third-party Prometheus collector를 등록해야 할 때 사용합니다.
139
155
 
140
156
  ### Framework metric과 app metric이 하나의 registry를 공유하기
141
157
 
158
+ 애플리케이션이 `Counter`, `Registry` 같은 raw collector를 `prom-client`에서 직접
159
+ import하기로 했다면 애플리케이션 의존성에 `prom-client`를 추가하세요.
160
+
161
+ ```bash
162
+ pnpm add prom-client
163
+ ```
164
+
165
+ `@fluojs/metrics`는 내부적으로 `prom-client`를 사용하지만, 그 의존성만으로 애플리케이션에서
166
+ `prom-client`를 지원되는 transitive import로 사용할 수 있는 것은 아닙니다. 아래 setup은
167
+ `@fluojs/metrics`가 re-export하는 `Registry`를 사용하므로 해당 direct dependency가 필요하지 않습니다.
168
+
142
169
  ```ts
143
170
  import { Module } from '@fluojs/core';
144
- import { Counter, Registry } from 'prom-client';
145
- import { MetricsModule } from '@fluojs/metrics';
171
+ import { METRICS_REGISTRY, MetricsModule, Registry } from '@fluojs/metrics';
172
+ import { bootstrapApplication } from '@fluojs/runtime';
146
173
 
147
174
  const registry = new Registry();
148
175
 
149
- new Counter({
150
- name: 'orders_total',
151
- help: 'Total orders processed',
152
- registers: [registry],
153
- });
154
-
155
176
  @Module({
156
- imports: [MetricsModule.forRoot({ http: true, registry })],
177
+ imports: [MetricsModule.forRoot({ http: true })],
157
178
  })
158
179
  class AppModule {}
180
+
181
+ const app = await bootstrapApplication({
182
+ rootModule: AppModule,
183
+ providers: [{ provide: METRICS_REGISTRY, useValue: registry }],
184
+ });
159
185
  ```
160
186
 
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 규칙대로 계속 빠르게 실패합니다.
187
+ `Registry`는 `@fluojs/metrics`가 re-export하므로setup에는 `prom-client` 직접 dependency가 필요하지 않습니다. application collector는 위의 `MetricsService` pattern으로 생성하세요.
188
+
189
+ 여러 `MetricsModule` 인스턴스가 같은 Registry를 의도적으로 공유하는 경우, 내장 HTTP 메트릭은 framework ownership, label schema, effective HTTP instrumentation configuration이 모두 일치할 때만 기존 `http_requests_total`, `http_errors_total`, `http_request_duration_seconds` collector를 재사용합니다. Compatibility 검사는 `pathLabelMode`, 정확히 같은 `pathLabelNormalizer` 함수 참조, `unknownPathLabel` fallback 의미론, 순서가 있는 `durationHistogramBuckets` 값을 포함하므로 서로 다른 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
190
 
163
191
  ### 중복 메트릭 이름은 계속 빠르게 실패합니다
164
192
 
@@ -170,7 +198,7 @@ Prometheus 메트릭 이름은 하나의 Registry 안에서 고유해야 합니
170
198
 
171
199
  - `fluo_component_ready`: 준비 완료 시 1, 아닐 시 0.
172
200
  - `fluo_component_health`: 정상 상태 시 1, 아닐 시 0.
173
- - `fluo_metrics_registry_mode`: active registry mode를 `mode="isolated"` 또는 `mode="shared"` label과 gauge value `1`로 나타냅니다.
201
+ - `fluo_metrics_registry_mode`: `MetricsModule.forRoot()`가 Registry생성하면 `mode="isolated"`, bootstrap이 `METRICS_REGISTRY`를 제공하거나 legacy `registry` option을 제공하면 `mode="shared"` label과 gauge value `1`을 노출합니다. 이 label은 bootstrap 또는 module registration 중 선택한 실효 Registry ownership configuration을 나타내며 scrape 시점에 Registry 공유 여부를 추론하지 않습니다.
174
202
 
175
203
  이 데이터는 built-in `/metrics` controller와 `MetricsService.getRegistry().metrics()`를 사용하는 advanced custom scraper를 포함해 active Registry가 스크레이프될 때마다 `PLATFORM_SHELL`을 쿼리하여 갱신됩니다. 초기화 시 환경 라벨을 제공할 수 있습니다.
176
204
 
@@ -204,23 +232,26 @@ MetricsModule.forRoot({
204
232
  ## 공개 API
205
233
 
206
234
  - `MetricsModule.forRoot(options)`
235
+ - 의도적으로 공유하는 `Registry`를 위한 bootstrap 전용 token `METRICS_REGISTRY`
207
236
  - `MetricsService` 및 `counter(...)`, `gauge(...)`, `histogram(...)`, `getRegistry()`
208
237
  - `METER_PROVIDER` (Token)
209
238
  - `PrometheusMeterProvider`
210
239
  - Meter abstraction type: `MeterProvider`, `MeterCounter`, `MeterGauge`, `MeterHistogram`
211
240
  - `HttpMetricsMiddleware` 및 HTTP path-label 옵션 타입
212
241
  - `provider`(현재는 `'prometheus'`만 지원), module-level `middleware`, endpoint-scoped `endpointMiddleware`를 포함한 module option
213
- - `prom-client`의 `Registry`
242
+ - `prom-client`에서 re-export한 `Registry`
214
243
 
215
244
  ### 운영 기본값
216
245
 
217
246
  - `path`의 기본값은 `'/metrics'`입니다. `''`를 포함한 모든 문자열 path는 scrape endpoint를 노출하며, `path: false`로만 scrape endpoint를 완전히 비활성화할 수 있습니다.
218
- - `registry`를 생략하면 application bootstrap마다 fresh isolated Registry, `MetricsService`, meter provider, telemetry collector set을 소유합니다.
247
+ - bootstrap `METRICS_REGISTRY`와 legacy `registry` option을 모두 제공하지 않으면 application bootstrap마다 fresh isolated Registry, `MetricsService`, meter provider, telemetry collector set을 소유합니다.
248
+ - Bootstrap `METRICS_REGISTRY` provider는 legacy `registry` option보다 우선하며, 같은 token을 가진 관련 없는 module provider는 metrics ownership을 구성하지 않습니다.
219
249
  - scrape response는 active Registry의 Prometheus content type과 Registry contents를 사용합니다.
220
250
  - `defaultMetrics`의 기본값은 `true`이며, `defaultMetrics: false`로 해당 Registry의 Prometheus 기본 프로세스/Node.js collector를 끌 수 있습니다.
221
251
  - `endpointMiddleware`는 class-based route-scoped middleware를 스크레이프 엔드포인트에만 바인딩합니다. HTTP 계측이 활성화된 경우 endpoint middleware 실패는 내장 HTTP collector에 집계됩니다.
222
252
  - HTTP 메트릭은 `http: true` 또는 `http` 옵션 객체를 전달한 경우에만 설치되며, 설치된 뒤에는 기본적으로 템플릿 기반 경로 라벨 정규화를 사용합니다.
223
- - 내장 HTTP collector는 같은 Registry를 공유하는 모듈 인스턴스 사이에서 framework-owned이고 예상 label schema 및 일치하는 path-label configuration가진 경우에만 재사용됩니다. 플랫폼 텔레메트리 Gauge는 framework-owned이고 예상 label schema를 가진 경우에만 재사용되며, 커스텀 애플리케이션 메트릭 이름 충돌은 Prometheus의 중복 이름 실패 동작을 유지합니다.
253
+ - `http.durationHistogramBuckets`는 내장 HTTP request duration histogram bucket명시적인 단위 경계로 대체합니다.
254
+ - 내장 HTTP collector는 같은 Registry를 공유하는 모듈 인스턴스 사이에서 framework-owned이고 예상 label schema 및 일치하는 HTTP instrumentation configuration을 가진 경우에만 재사용됩니다. 플랫폼 텔레메트리 Gauge는 framework-owned이고 예상 label schema를 가진 경우에만 재사용되며, 커스텀 애플리케이션 메트릭 이름 충돌은 Prometheus의 중복 이름 실패 동작을 유지합니다.
224
255
  - Shared Registry 텔레메트리 refresh는 소유 metrics module이 하나라도 active인 동안 유지되고 마지막 module이 종료되면 원래 `metrics()` 함수를 복원합니다.
225
256
  - raw path 라벨은 `allowUnsafeRawPathLabelMode: true`를 명시한 bounded internal route에서만 사용해야 합니다.
226
257
  - 플랫폼 텔레메트리는 `PLATFORM_SHELL`이 실제로 누락된 경우에만 생략되며, 그 외 resolve 실패는 스크레이프를 실패시킵니다.
package/README.md CHANGED
@@ -22,7 +22,7 @@ pnpm add @fluojs/metrics
22
22
 
23
23
  ## Requirements
24
24
 
25
- `@fluojs/metrics` runs on Node.js 20 or newer; the package manifest declares `engines.node >=20.0.0`.
25
+ `@fluojs/metrics` requires Node.js `>=24.0.0 <27` as its package-owned support contract.
26
26
 
27
27
  ## When to Use
28
28
 
@@ -44,14 +44,15 @@ class AppModule {}
44
44
 
45
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.
46
46
 
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.
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 the bootstrap configures `METRICS_REGISTRY` or the legacy `registry` option is supplied; reusing the same dynamic module class for another bootstrap receives fresh isolated metrics state. Configure a shared `Registry` at bootstrap only when framework metrics and application-defined metrics intentionally share one scrape surface.
48
48
 
49
49
  ## Public Responsibilities
50
50
 
51
51
  | Surface | Responsibility | Boundary |
52
52
  | --- | --- | --- |
53
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. |
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. |
54
+ | `MetricsService` | Application-facing facade for custom `Counter`, `Gauge`, and `Histogram` metrics on the active registry, plus `getRegistry()` for deliberate advanced registry sharing. | `MetricsService` is non-global: inject it from a module that directly imports a `MetricsModule.forRoot(...)` registration or imports a module that re-exports `MetricsService`; unrelated sibling modules do not receive it automatically. 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 `METRICS_REGISTRY` at bootstrap. |
55
+ | `METRICS_REGISTRY` | Bootstrap provider token for a shared `prom-client` Registry. | A provider supplied to `bootstrapApplication()` takes ownership over the module's legacy `registry` option. |
55
56
  | `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
57
  | `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. |
57
58
  | `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. |
@@ -82,6 +83,21 @@ MetricsModule.forRoot({
82
83
  });
83
84
  ```
84
85
 
86
+ ### Configure HTTP duration histogram buckets
87
+
88
+ ```ts
89
+ MetricsModule.forRoot({
90
+ http: {
91
+ durationHistogramBuckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
92
+ },
93
+ });
94
+ ```
95
+
96
+ `durationHistogramBuckets` replaces the built-in HTTP request duration histogram's
97
+ `prom-client` defaults. Values are measured in seconds and must fit the latency
98
+ range you intend to alert on. Each boundary must be finite and strictly increasing;
99
+ invalid configuration is rejected during setup.
100
+
85
101
  ### Protect or disable the metrics endpoint
86
102
 
87
103
  ```ts
@@ -110,7 +126,7 @@ MetricsModule.forRoot({
110
126
 
111
127
  ### Create custom metrics once and reuse them
112
128
 
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.
129
+ `MetricsService.counter(...)`, `gauge(...)`, and `histogram(...)` create Prometheus collectors on the active registry. `MetricsService` is non-global: the provider or controller that injects it must belong to a module that directly imports a `MetricsModule.forRoot(...)` registration or imports a module that re-exports `MetricsService`; unrelated sibling modules do not receive it automatically. Create each custom metric once during provider construction or application startup, then reuse the returned collector when business actions occur.
114
130
 
115
131
  ```ts
116
132
  import { Inject } from '@fluojs/core';
@@ -135,30 +151,43 @@ class OrdersService {
135
151
 
136
152
  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
153
 
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.
154
+ `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. When you own the bootstrap, configure an explicit shared registry with the public `METRICS_REGISTRY` token. 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
155
 
140
156
  ### Share one registry for framework and app metrics
141
157
 
158
+ If your application chooses to import raw collectors such as `Counter` or
159
+ `Registry` directly from `prom-client`, add it to your application's dependencies:
160
+
161
+ ```bash
162
+ pnpm add prom-client
163
+ ```
164
+
165
+ `@fluojs/metrics` uses `prom-client` internally, but its dependency does not make
166
+ `prom-client` a supported transitive import for your application. The setup below
167
+ uses the `Registry` re-export from `@fluojs/metrics`, so it does not require that
168
+ direct dependency.
169
+
142
170
  ```ts
143
171
  import { Module } from '@fluojs/core';
144
- import { Counter, Registry } from 'prom-client';
145
- import { MetricsModule } from '@fluojs/metrics';
172
+ import { METRICS_REGISTRY, MetricsModule, Registry } from '@fluojs/metrics';
173
+ import { bootstrapApplication } from '@fluojs/runtime';
146
174
 
147
175
  const registry = new Registry();
148
176
 
149
- new Counter({
150
- name: 'orders_total',
151
- help: 'Total orders processed',
152
- registers: [registry],
153
- });
154
-
155
177
  @Module({
156
- imports: [MetricsModule.forRoot({ http: true, registry })],
178
+ imports: [MetricsModule.forRoot({ http: true })],
157
179
  })
158
180
  class AppModule {}
181
+
182
+ const app = await bootstrapApplication({
183
+ rootModule: AppModule,
184
+ providers: [{ provide: METRICS_REGISTRY, useValue: registry }],
185
+ });
159
186
  ```
160
187
 
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.
188
+ `Registry` is re-exported by `@fluojs/metrics`, so this setup needs no direct `prom-client` dependency. Create application collectors through the `MetricsService` pattern above.
189
+
190
+ 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 HTTP instrumentation configuration match. Compatibility includes `pathLabelMode`, the exact `pathLabelNormalizer` function reference, `unknownPathLabel` fallback semantics, and ordered `durationHistogramBuckets` values, 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
191
 
163
192
  ### Duplicate metric names still fail fast
164
193
 
@@ -170,7 +199,7 @@ The module emits fluo-specific gauges that mirror the platform shell and registe
170
199
 
171
200
  - `fluo_component_ready`: `1` when a component is ready, otherwise `0`.
172
201
  - `fluo_component_health`: `1` when a component is healthy, otherwise `0`.
173
- - `fluo_metrics_registry_mode`: gauge value `1` with a `mode="isolated"` or `mode="shared"` label for the active registry mode.
202
+ - `fluo_metrics_registry_mode`: gauge value `1` with `mode="isolated"` when `MetricsModule.forRoot()` creates its registry, or `mode="shared"` when bootstrap supplies `METRICS_REGISTRY` or the legacy `registry` option is supplied. The label reports the effective registry ownership configuration selected during bootstrap or module registration; it does not infer registry sharing at scrape time.
174
203
 
175
204
  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
205
 
@@ -204,23 +233,26 @@ MetricsModule.forRoot({
204
233
  ## Public API
205
234
 
206
235
  - `MetricsModule.forRoot(options)`
236
+ - `METRICS_REGISTRY`, the bootstrap-only token for an intentionally shared `Registry`
207
237
  - `MetricsService`, including `counter(...)`, `gauge(...)`, `histogram(...)`, and `getRegistry()`
208
238
  - `METER_PROVIDER`
209
239
  - `PrometheusMeterProvider`
210
240
  - Meter abstraction types: `MeterProvider`, `MeterCounter`, `MeterGauge`, and `MeterHistogram`
211
241
  - `HttpMetricsMiddleware` and HTTP path-label option types
212
242
  - Module options including `provider` (currently only `'prometheus'`), module-level `middleware`, and endpoint-scoped `endpointMiddleware`
213
- - `Registry` from `prom-client`
243
+ - `Registry`, re-exported from `prom-client`
214
244
 
215
245
  ### Operational defaults
216
246
 
217
247
  - `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.
248
+ - When neither bootstrap `METRICS_REGISTRY` nor the legacy `registry` option is supplied, each application bootstrap owns a fresh isolated registry, `MetricsService`, meter provider, and telemetry collector set.
249
+ - A bootstrap `METRICS_REGISTRY` provider takes precedence over the legacy `registry` option; an unrelated module provider with the same token does not configure metrics ownership.
219
250
  - The scrape response uses the active registry's Prometheus content type and registry contents.
220
251
  - `defaultMetrics` defaults to `true`, and `defaultMetrics: false` disables Prometheus default process and Node.js collectors for that registry.
221
252
  - `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
253
  - 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 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.
254
+ - `http.durationHistogramBuckets` replaces the built-in HTTP request duration histogram buckets with explicit second-based boundaries.
255
+ - 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 HTTP instrumentation 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
256
  - Shared Registry telemetry refresh remains installed while any owning metrics module is active and restores the original `metrics()` function after the last module closes.
225
257
  - Raw path labels require `allowUnsafeRawPathLabelMode: true` and should stay limited to bounded internal routes.
226
258
  - Platform telemetry is omitted only when `PLATFORM_SHELL` is genuinely missing; other resolution failures fail the scrape.
@@ -13,6 +13,8 @@ export interface HttpMetricsPathLabelContext {
13
13
  export type HttpMetricsPathLabelNormalizer = (context: HttpMetricsPathLabelContext) => string;
14
14
  /** Options that tune HTTP request metric label generation. */
15
15
  export interface HttpMetricsMiddlewareOptions {
16
+ /** Duration buckets in seconds for the built-in HTTP request histogram. */
17
+ durationHistogramBuckets?: readonly number[];
16
18
  pathLabelMode?: HttpMetricsPathLabelMode;
17
19
  pathLabelNormalizer?: HttpMetricsPathLabelNormalizer;
18
20
  unknownPathLabel?: string;
@@ -28,6 +30,16 @@ export declare class HttpMetricsMiddleware implements Middleware {
28
30
  private readonly pathLabelMode;
29
31
  private readonly pathLabelNormalizer?;
30
32
  private readonly unknownPathLabel;
33
+ /**
34
+ * Create the built-in HTTP request collectors in a Prometheus registry.
35
+ *
36
+ * @param registry Registry that owns or reuses the built-in HTTP collectors.
37
+ * @param options HTTP metric label and duration histogram configuration.
38
+ * @throws {Error} When raw path labels are configured without the explicit unsafe opt-in.
39
+ * @throws {Error} When duration histogram bucket boundaries are not finite and strictly increasing.
40
+ * @throws {Error} When an application-owned collector uses a built-in HTTP collector name.
41
+ * @throws {Error} When a reused framework collector has a different label schema or HTTP instrumentation configuration.
42
+ */
31
43
  constructor(registry: Registry, options?: HttpMetricsMiddlewareOptions);
32
44
  private resolvePathLabel;
33
45
  handle(context: MiddlewareContext, next: Next): Promise<void>;
@@ -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;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"}
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;AA6BhE,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,2EAA2E;IAC3E,wBAAwB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,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;IAE1C;;;;;;;;;OASG;gBACS,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"}
@@ -38,6 +38,17 @@ export class HttpMetricsMiddleware {
38
38
  pathLabelMode;
39
39
  pathLabelNormalizer;
40
40
  unknownPathLabel;
41
+
42
+ /**
43
+ * Create the built-in HTTP request collectors in a Prometheus registry.
44
+ *
45
+ * @param registry Registry that owns or reuses the built-in HTTP collectors.
46
+ * @param options HTTP metric label and duration histogram configuration.
47
+ * @throws {Error} When raw path labels are configured without the explicit unsafe opt-in.
48
+ * @throws {Error} When duration histogram bucket boundaries are not finite and strictly increasing.
49
+ * @throws {Error} When an application-owned collector uses a built-in HTTP collector name.
50
+ * @throws {Error} When a reused framework collector has a different label schema or HTTP instrumentation configuration.
51
+ */
41
52
  constructor(registry, options = {}) {
42
53
  const collectorConfiguration = resolveHttpMetricsCollectorConfiguration(options);
43
54
  this.pathLabelMode = collectorConfiguration.pathLabelMode;
@@ -150,7 +161,10 @@ function getOrCreateHttpHistogram(registry, config, collectorConfiguration) {
150
161
  const histogram = createPrometheusHistogram(registry, {
151
162
  help: config.help,
152
163
  labelNames: [...config.labelNames],
153
- name: config.name
164
+ name: config.name,
165
+ ...(collectorConfiguration.durationHistogramBuckets ? {
166
+ buckets: [...collectorConfiguration.durationHistogramBuckets]
167
+ } : {})
154
168
  });
155
169
  FRAMEWORK_HTTP_HISTOGRAMS.add(histogram);
156
170
  FRAMEWORK_HTTP_COLLECTOR_CONFIGURATION.set(histogram, collectorConfiguration);
@@ -167,7 +181,15 @@ function resolveHttpMetricsCollectorConfiguration(options) {
167
181
  if (options.pathLabelMode === 'raw' && options.allowUnsafeRawPathLabelMode !== true) {
168
182
  throw new Error('HttpMetricsMiddleware pathLabelMode "raw" is disabled by default. Pass allowUnsafeRawPathLabelMode: true only when you have bounded path cardinality.');
169
183
  }
184
+ let previousDurationHistogramBucket;
185
+ for (const durationHistogramBucket of options.durationHistogramBuckets ?? []) {
186
+ if (!Number.isFinite(durationHistogramBucket) || previousDurationHistogramBucket !== undefined && durationHistogramBucket <= previousDurationHistogramBucket) {
187
+ throw new Error('HttpMetricsMiddleware durationHistogramBuckets must contain finite, strictly increasing boundaries.');
188
+ }
189
+ previousDurationHistogramBucket = durationHistogramBucket;
190
+ }
170
191
  return {
192
+ durationHistogramBuckets: options.durationHistogramBuckets ? [...options.durationHistogramBuckets] : undefined,
171
193
  pathLabelMode: options.pathLabelMode ?? 'template',
172
194
  pathLabelNormalizer: options.pathLabelNormalizer,
173
195
  unknownPathLabel: options.unknownPathLabel ?? 'UNKNOWN'
@@ -176,18 +198,22 @@ function resolveHttpMetricsCollectorConfiguration(options) {
176
198
  function assertHttpMetricConfiguration(metric, metricName, expected) {
177
199
  const registered = FRAMEWORK_HTTP_COLLECTOR_CONFIGURATION.get(metric);
178
200
  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.`);
201
+ throw new Error(`Metric name "${metricName}" is already registered as a framework-owned HTTP collector without HTTP instrumentation configuration metadata. Built-in HTTP metrics require matching HTTP collector configuration before reuse.`);
180
202
  }
181
203
  if (hasSameHttpMetricConfiguration(registered, expected)) {
182
204
  return;
183
205
  }
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)}.`);
206
+ throw new Error(`Metric name "${metricName}" is already registered with framework HTTP collector configuration ${describeHttpMetricConfiguration(registered)}. Built-in HTTP metrics require matching HTTP collector configuration before reuse; received ${describeHttpMetricConfiguration(expected)}.`);
185
207
  }
186
208
  function hasSameHttpMetricConfiguration(left, right) {
187
- return left.pathLabelMode === right.pathLabelMode && left.pathLabelNormalizer === right.pathLabelNormalizer && left.unknownPathLabel === right.unknownPathLabel;
209
+ return hasSameDurationHistogramBuckets(left.durationHistogramBuckets, right.durationHistogramBuckets) && left.pathLabelMode === right.pathLabelMode && left.pathLabelNormalizer === right.pathLabelNormalizer && left.unknownPathLabel === right.unknownPathLabel;
210
+ }
211
+ function hasSameDurationHistogramBuckets(left, right) {
212
+ return left === right || left !== undefined && right !== undefined && left.length === right.length && left.every((bucket, index) => bucket === right[index]);
188
213
  }
189
214
  function describeHttpMetricConfiguration(configuration) {
190
- return `pathLabelMode="${configuration.pathLabelMode}", pathLabelNormalizer=${configuration.pathLabelNormalizer ? 'custom' : 'none'}, unknownPathLabel="${configuration.unknownPathLabel}"`;
215
+ const durationHistogramBuckets = configuration.durationHistogramBuckets ? `, durationHistogramBuckets=[${configuration.durationHistogramBuckets.join(',')}]` : '';
216
+ return `pathLabelMode="${configuration.pathLabelMode}", pathLabelNormalizer=${configuration.pathLabelNormalizer ? 'custom' : 'none'}, unknownPathLabel="${configuration.unknownPathLabel}"${durationHistogramBuckets}`;
191
217
  }
192
218
  function normalizePathToTemplate(path, params) {
193
219
  if (!path) {
@@ -1,9 +1,12 @@
1
+ import { type Token } from '@fluojs/core';
1
2
  import { type Middleware, type MiddlewareLike } from '@fluojs/http';
2
3
  import { type ModuleType } from '@fluojs/runtime';
3
4
  import { type Registry } from 'prom-client';
4
5
  import { type HttpMetricsPathLabelMode, type HttpMetricsPathLabelNormalizer } from './http-metrics-middleware.js';
5
6
  /** HTTP-specific metric labeling options exposed by `MetricsModule.forRoot(...)`. */
6
7
  export interface MetricsHttpOptions {
8
+ /** Duration buckets in seconds for the built-in HTTP request histogram. */
9
+ durationHistogramBuckets?: readonly number[];
7
10
  /** How request paths are converted into Prometheus label values. Defaults to route templates. */
8
11
  pathLabelMode?: HttpMetricsPathLabelMode;
9
12
  /** Custom path-label normalizer for bounded application-specific label values. */
@@ -36,13 +39,14 @@ export interface MetricsModuleOptions {
36
39
  /** Instance label value. Defaults to `local`. */
37
40
  instance?: string;
38
41
  };
39
- /** External Prometheus registry to share between built-in and custom metrics. */
42
+ /** Legacy shared-registry fallback. Prefer the `METRICS_REGISTRY` bootstrap provider. */
40
43
  registry?: Registry;
41
44
  }
45
+ /** Bootstrap provider token for a Registry shared by metrics module instances. */
46
+ export declare const METRICS_REGISTRY: Token<Registry>;
42
47
  /** Module entry point that exposes `/metrics` and optional HTTP/runtime telemetry. */
43
48
  export declare class MetricsModule {
44
49
  private static registeredRegistries;
45
- private static httpInstrumentationRegistrations;
46
50
  /**
47
51
  * Register framework metrics, optional HTTP middleware, and a scrape endpoint.
48
52
  *
@@ -50,11 +54,10 @@ export declare class MetricsModule {
50
54
  * ```ts
51
55
  * MetricsModule.forRoot({
52
56
  * http: { pathLabelMode: 'template' },
53
- * registry: new Registry(),
54
57
  * });
55
58
  * ```
56
59
  *
57
- * @param options Metrics endpoint, registry, HTTP middleware, and runtime telemetry configuration.
60
+ * @param options Metrics endpoint, HTTP middleware, and runtime telemetry configuration.
58
61
  * @returns A runtime module that exposes metrics through the configured path.
59
62
  */
60
63
  static forRoot(options?: MetricsModuleOptions): ModuleType;
@@ -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,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"}
1
+ {"version":3,"file":"metrics-module.d.ts","sourceRoot":"","sources":["../src/metrics-module.ts"],"names":[],"mappings":"AAGA,OAAO,EAAU,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AAElD,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;AAMtC,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,wBAAwB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,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,yFAAyF;IACzF,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,QAAQ,CAAuC,CAAC;AAErF,sFAAsF;AACtF,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAA2B;IAE9D;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,UAAU;IA+G9D,OAAO,CAAC,MAAM,CAAC,cAAc;CAyB9B"}
@@ -3,16 +3,19 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
3
3
  function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
4
4
  function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
5
5
  function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
6
+ import { readFileSync } from 'node:fs';
7
+ import { createRequire } from 'node:module';
6
8
  import { Inject } from '@fluojs/core';
7
9
  import { ContainerResolutionError } from '@fluojs/di';
8
- import { Controller, Get, forRoutes } from '@fluojs/http';
10
+ import { Controller, forRoutes, Get } from '@fluojs/http';
9
11
  import { defineModule, PLATFORM_SHELL } from '@fluojs/runtime';
10
- import { RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
12
+ import { BOOTSTRAP_PROVIDER_TOKENS, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
11
13
  import { collectDefaultMetrics, Gauge, Registry as PrometheusRegistry } from 'prom-client';
12
14
  import { HttpMetricsMiddleware } from './http-metrics-middleware.js';
13
- import { METER_PROVIDER } from './providers/meter-provider.js';
14
15
  import { MetricsService } from './metrics-service.js';
16
+ import { METER_PROVIDER } from './providers/meter-provider.js';
15
17
  import { PrometheusMeterProvider } from './providers/prometheus-meter-provider.js';
18
+ import { SerializedScrapeQueue } from './serialized-scrape-queue.js';
16
19
 
17
20
  /** HTTP-specific metric labeling options exposed by `MetricsModule.forRoot(...)`. */
18
21
 
@@ -20,10 +23,12 @@ import { PrometheusMeterProvider } from './providers/prometheus-meter-provider.j
20
23
  * Module options for exposing Prometheus metrics and runtime platform telemetry.
21
24
  */
22
25
 
26
+ /** Bootstrap provider token for a Registry shared by metrics module instances. */
27
+ export const METRICS_REGISTRY = Symbol.for('fluo.metrics.registry');
28
+
23
29
  /** Module entry point that exposes `/metrics` and optional HTTP/runtime telemetry. */
24
30
  export class MetricsModule {
25
31
  static registeredRegistries = new WeakSet();
26
- static httpInstrumentationRegistrations = new WeakMap();
27
32
 
28
33
  /**
29
34
  * Register framework metrics, optional HTTP middleware, and a scrape endpoint.
@@ -32,11 +37,10 @@ export class MetricsModule {
32
37
  * ```ts
33
38
  * MetricsModule.forRoot({
34
39
  * http: { pathLabelMode: 'template' },
35
- * registry: new Registry(),
36
40
  * });
37
41
  * ```
38
42
  *
39
- * @param options Metrics endpoint, registry, HTTP middleware, and runtime telemetry configuration.
43
+ * @param options Metrics endpoint, HTTP middleware, and runtime telemetry configuration.
40
44
  * @returns A runtime module that exposes metrics through the configured path.
41
45
  */
42
46
  static forRoot(options = {}) {
@@ -46,44 +50,33 @@ export class MetricsModule {
46
50
  }
47
51
  const httpOptions = resolveHttpOptions(options.http);
48
52
  const metricsPath = options.path === undefined ? '/metrics' : options.path;
49
- let registryToken = Symbol('MetricsModule.registry');
53
+ const registryToken = Symbol('MetricsModule.registry');
50
54
  const platformTelemetryToken = Symbol('MetricsModule.platformTelemetry');
51
- let httpMetricsMiddleware = httpOptions ? createHttpMetricsMiddleware(registryToken, httpOptions) : undefined;
55
+ const httpMetricsMiddleware = httpOptions ? createHttpMetricsMiddleware(registryToken, httpOptions) : undefined;
52
56
  const endpointMiddleware = typeof metricsPath === 'string' ? (options.endpointMiddleware ?? []).map(middlewareClass => forRoutes(middlewareClass, metricsPath)) : [];
53
57
  const middleware = [...endpointMiddleware, ...(options.middleware ?? [])];
54
58
  const registryProvider = {
55
59
  provide: registryToken,
56
- useFactory: () => MetricsModule.createRegistry(options)
60
+ inject: [RUNTIME_CONTAINER, BOOTSTRAP_PROVIDER_TOKENS],
61
+ useFactory: async (container, bootstrapProviderTokens) => {
62
+ const runtimeContainer = assertRuntimeContainer(container);
63
+ const configuredRegistry = assertBootstrapProviderTokens(bootstrapProviderTokens).has(METRICS_REGISTRY) ? assertPrometheusRegistry(await runtimeContainer.resolve(METRICS_REGISTRY)) : undefined;
64
+ return MetricsModule.createRegistry(options, configuredRegistry);
65
+ }
57
66
  };
58
67
  const imports = [];
59
- const validationProviders = [];
60
- let includeRuntimeRegistryProvider = httpMetricsMiddleware === undefined;
68
+ const includeRuntimeRegistryProvider = httpMetricsMiddleware === undefined;
61
69
  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
- }
70
+ class MetricsHttpInstrumentationModule {}
71
+ defineModule(MetricsHttpInstrumentationModule, {
72
+ exports: [registryToken],
73
+ global: true,
74
+ middleware: [httpMetricsMiddleware],
75
+ providers: [registryProvider, httpMetricsMiddleware]
76
+ });
77
+ imports.push(MetricsHttpInstrumentationModule);
85
78
  }
86
- const providers = [...(includeRuntimeRegistryProvider ? [registryProvider] : []), ...validationProviders, {
79
+ const providers = [...(includeRuntimeRegistryProvider ? [registryProvider] : []), {
87
80
  provide: MetricsService,
88
81
  inject: [registryToken, platformTelemetryToken],
89
82
  useFactory: registry => new MetricsService(assertPrometheusRegistry(registry))
@@ -93,8 +86,8 @@ export class MetricsModule {
93
86
  useFactory: registry => new PrometheusMeterProvider(assertPrometheusRegistry(registry))
94
87
  }, {
95
88
  provide: platformTelemetryToken,
96
- inject: [registryToken, RUNTIME_CONTAINER],
97
- useFactory: (registry, container) => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), assertRuntimeContainer(container), options.registry ? 'shared' : 'isolated', options.platformTelemetry)
89
+ inject: [registryToken, RUNTIME_CONTAINER, BOOTSTRAP_PROVIDER_TOKENS],
90
+ useFactory: (registry, container, bootstrapProviderTokens) => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), assertRuntimeContainer(container), resolveRegistryMode(options, assertBootstrapProviderTokens(bootstrapProviderTokens)), options.platformTelemetry)
98
91
  }];
99
92
  const controllers = [];
100
93
  if (typeof metricsPath === 'string') {
@@ -109,9 +102,9 @@ export class MetricsModule {
109
102
  } = _applyDecs(this, [Inject(registryToken, platformTelemetryToken), Controller('')], [[Get(metricsRoutePath), 2, "getMetrics"]]));
110
103
  }
111
104
  constructor(registry, platformTelemetry) {
105
+ _initProto(this);
112
106
  this.registry = registry;
113
107
  this.platformTelemetry = platformTelemetry;
114
- _initProto(this);
115
108
  }
116
109
  async getMetrics(_input, ctx) {
117
110
  ctx.response.setHeader('content-type', this.registry.contentType);
@@ -133,21 +126,47 @@ export class MetricsModule {
133
126
  });
134
127
  return MetricsRuntimeModule;
135
128
  }
136
- static createRegistry(options) {
137
- const registry = options.registry ?? new PrometheusRegistry();
129
+ static createRegistry(options, configuredRegistry) {
130
+ const registry = configuredRegistry ?? options.registry ?? new PrometheusRegistry();
138
131
  if (options.defaultMetrics !== false && !MetricsModule.registeredRegistries.has(registry)) {
132
+ assertNoDefaultMetricCollisions(registry);
133
+ const existingMetricNames = new Set(registry.getMetricsAsArray().map(metric => metric.name));
134
+ try {
135
+ collectDefaultMetrics({
136
+ register: registry
137
+ });
138
+ } catch (error) {
139
+ for (const metric of registry.getMetricsAsArray()) {
140
+ if (!existingMetricNames.has(metric.name)) {
141
+ registry.removeSingleMetric(metric.name);
142
+ }
143
+ }
144
+ throw error;
145
+ }
139
146
  MetricsModule.registeredRegistries.add(registry);
140
- collectDefaultMetrics({
141
- register: registry
142
- });
143
147
  }
144
148
  return registry;
145
149
  }
146
150
  }
147
151
  const PLATFORM_COMPONENT_LABELS = ['component_id', 'component_kind', 'operation', 'result', 'env', 'instance'];
148
152
  const REGISTRY_MODE_LABELS = ['mode'];
153
+ const require = createRequire(import.meta.url);
154
+ // prom-client exposes collector IDs publicly but not their metric names. This v15.1.3-only
155
+ // private metadata seam keeps collision preflight side-effect-free; the exact package pin and
156
+ // runtime-support regression must be updated together before changing prom-client.
157
+ const DEFAULT_METRIC_COLLECTORS = collectDefaultMetrics.metricsList.map(collectorName => {
158
+ const collector = require(`prom-client/lib/metrics/${collectorName}`);
159
+ if (!hasDefaultMetricNames(collector)) {
160
+ throw new Error(`prom-client default collector "${collectorName}" does not expose metricNames.`);
161
+ }
162
+ return {
163
+ collectorName,
164
+ metricNames: collector.metricNames
165
+ };
166
+ });
149
167
  const FRAMEWORK_PLATFORM_GAUGES = new WeakSet();
150
168
  const PLATFORM_TELEMETRY_REGISTRY_STATES = new WeakMap();
169
+ const HTTP_INSTRUMENTATION_OWNERS = new WeakMap();
151
170
  const HEALTH_STATUSES = ['healthy', 'unhealthy', 'degraded'];
152
171
  const READINESS_STATUSES = ['ready', 'not-ready', 'degraded'];
153
172
  const PLATFORM_SHELL_TOKEN_NAMES = new Set([String(PLATFORM_SHELL)]);
@@ -156,14 +175,18 @@ function createHttpMetricsMiddleware(registryToken, httpOptions) {
156
175
  let _MetricsHttpMiddlewar;
157
176
  class MetricsHttpMiddleware {
158
177
  static {
159
- [_MetricsHttpMiddlewar, _initClass2] = _applyDecs(this, [Inject(registryToken)], []).c;
178
+ [_MetricsHttpMiddlewar, _initClass2] = _applyDecs(this, [Inject(registryToken, RUNTIME_CONTAINER)], []).c;
160
179
  }
161
180
  delegate;
162
- constructor(registry) {
163
- this.delegate = new HttpMetricsMiddleware(registry, httpOptions);
181
+ constructor(registry, container) {
182
+ const httpMetricsMiddleware = new HttpMetricsMiddleware(registry, httpOptions);
183
+ const runtimeContainer = assertRuntimeContainer(container);
184
+ if (claimsHttpInstrumentationOwnership(runtimeContainer, registry)) {
185
+ this.delegate = httpMetricsMiddleware;
186
+ }
164
187
  }
165
188
  handle(context, next) {
166
- return this.delegate.handle(context, next);
189
+ return this.delegate ? this.delegate.handle(context, next) : next();
167
190
  }
168
191
  static {
169
192
  _initClass2();
@@ -171,12 +194,17 @@ function createHttpMetricsMiddleware(registryToken, httpOptions) {
171
194
  }
172
195
  return _MetricsHttpMiddlewar;
173
196
  }
174
- function createHttpCollectorValidationProvider(registryToken, httpOptions) {
175
- return {
176
- provide: Symbol('MetricsModule.httpCollectorValidation'),
177
- inject: [registryToken],
178
- useFactory: registry => new HttpMetricsMiddleware(assertPrometheusRegistry(registry), httpOptions)
179
- };
197
+ function claimsHttpInstrumentationOwnership(container, registry) {
198
+ let registryOwners = HTTP_INSTRUMENTATION_OWNERS.get(container);
199
+ if (!registryOwners) {
200
+ registryOwners = new WeakSet();
201
+ HTTP_INSTRUMENTATION_OWNERS.set(container, registryOwners);
202
+ }
203
+ if (registryOwners.has(registry)) {
204
+ return false;
205
+ }
206
+ registryOwners.add(registry);
207
+ return true;
180
208
  }
181
209
  function assertPrometheusRegistry(value) {
182
210
  if (!(value instanceof PrometheusRegistry)) {
@@ -184,12 +212,58 @@ function assertPrometheusRegistry(value) {
184
212
  }
185
213
  return value;
186
214
  }
215
+ function hasDefaultMetricNames(value) {
216
+ return typeof value === 'function' && 'metricNames' in value && Array.isArray(value.metricNames) && value.metricNames.every(metricName => typeof metricName === 'string');
217
+ }
218
+ function assertNoDefaultMetricCollisions(registry) {
219
+ for (const {
220
+ collectorName,
221
+ metricNames
222
+ } of DEFAULT_METRIC_COLLECTORS) {
223
+ if (!isDefaultCollectorActive(collectorName)) {
224
+ continue;
225
+ }
226
+ for (const metricName of metricNames) {
227
+ if (registry.getSingleMetric(metricName)) {
228
+ throw new Error(`A metric with the name ${metricName} has already been registered.`);
229
+ }
230
+ }
231
+ }
232
+ }
233
+ function isDefaultCollectorActive(collectorName) {
234
+ if (collectorName === 'processHandles') {
235
+ return typeof Reflect.get(process, '_getActiveHandles') === 'function';
236
+ }
237
+ if (collectorName === 'processRequests') {
238
+ return typeof Reflect.get(process, '_getActiveRequests') === 'function';
239
+ }
240
+ if (collectorName === 'processOpenFileDescriptors') {
241
+ return process.platform === 'linux';
242
+ }
243
+ if (collectorName === 'processMaxFileDescriptors') {
244
+ try {
245
+ return readFileSync('/proc/self/limits', 'utf8').split('\n').some(line => line.startsWith('Max open files'));
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+ return true;
251
+ }
187
252
  function assertRuntimeContainer(value) {
188
253
  if (!isRuntimeContainer(value)) {
189
254
  throw new Error('MetricsModule runtime container provider resolved an invalid container.');
190
255
  }
191
256
  return value;
192
257
  }
258
+ function assertBootstrapProviderTokens(value) {
259
+ if (!(value instanceof Set)) {
260
+ throw new Error('MetricsModule bootstrap provider token metadata resolved invalidly.');
261
+ }
262
+ return value;
263
+ }
264
+ function resolveRegistryMode(options, bootstrapProviderTokens) {
265
+ return options.registry || bootstrapProviderTokens.has(METRICS_REGISTRY) ? 'shared' : 'isolated';
266
+ }
193
267
  function isRuntimeContainer(value) {
194
268
  return typeof value === 'object' && value !== null && 'resolve' in value && typeof value.resolve === 'function';
195
269
  }
@@ -202,7 +276,7 @@ function getRuntimePlatformTelemetryRegistryState(registry) {
202
276
  lastHealthStatuses: new Map(),
203
277
  lastReadinessStatuses: new Map(),
204
278
  registrations: [],
205
- scrapeChain: Promise.resolve()
279
+ scrapeQueue: new SerializedScrapeQueue()
206
280
  };
207
281
  PLATFORM_TELEMETRY_REGISTRY_STATES.set(registry, state);
208
282
  return state;
@@ -238,6 +312,12 @@ function assertGaugeLabelSchema(gauge, config) {
238
312
  throw new Error(`Metric name "${config.name}" is already registered with labels [${registeredLabels}]. Built-in platform telemetry requires labels [${expectedLabels}].`);
239
313
  }
240
314
  }
315
+ function removeFrameworkGauge(registry, gauge, metricName) {
316
+ if (!FRAMEWORK_PLATFORM_GAUGES.has(gauge) || registry.getSingleMetric(metricName) !== gauge) {
317
+ return;
318
+ }
319
+ registry.removeSingleMetric(metricName);
320
+ }
241
321
  class RuntimePlatformTelemetry {
242
322
  readinessGauge;
243
323
  healthGauge;
@@ -270,7 +350,7 @@ class RuntimePlatformTelemetry {
270
350
  collectMetrics(registry) {
271
351
  return registry.metrics();
272
352
  }
273
- onModuleDestroy() {
353
+ async onModuleDestroy() {
274
354
  const registrationIndex = this.telemetryState.registrations.lastIndexOf(this);
275
355
  if (registrationIndex >= 0) {
276
356
  this.telemetryState.registrations.splice(registrationIndex, 1);
@@ -278,11 +358,20 @@ class RuntimePlatformTelemetry {
278
358
  if (this.telemetryState.registrations.length > 0) {
279
359
  return;
280
360
  }
281
- const originalMetrics = this.telemetryState.originalMetrics;
282
- if (originalMetrics) {
283
- this.registry.metrics = originalMetrics;
284
- this.telemetryState.originalMetrics = undefined;
285
- }
361
+ await this.telemetryState.scrapeQueue.drain(() => {
362
+ if (this.telemetryState.registrations.length > 0) {
363
+ return;
364
+ }
365
+ const originalMetrics = this.telemetryState.originalMetrics;
366
+ if (originalMetrics) {
367
+ this.clearPlatformTelemetry();
368
+ removeFrameworkGauge(this.registry, this.readinessGauge, 'fluo_component_ready');
369
+ removeFrameworkGauge(this.registry, this.healthGauge, 'fluo_component_health');
370
+ removeFrameworkGauge(this.registry, this.registryModeGauge, 'fluo_metrics_registry_mode');
371
+ this.registry.metrics = originalMetrics;
372
+ this.telemetryState.originalMetrics = undefined;
373
+ }
374
+ });
286
375
  }
287
376
  installRegistryRefresh() {
288
377
  this.telemetryState.registrations.push(this);
@@ -293,24 +382,28 @@ class RuntimePlatformTelemetry {
293
382
  const telemetryState = this.telemetryState;
294
383
  const originalMetrics = registry.metrics;
295
384
  telemetryState.originalMetrics = originalMetrics;
296
- registry.metrics = async () => {
297
- const activeRegistration = telemetryState.registrations.at(-1);
298
- await activeRegistration?.refresh();
299
- return await originalMetrics.call(registry);
385
+ registry.metrics = () => {
386
+ return telemetryState.scrapeQueue.enqueue(async () => {
387
+ const activeRegistration = telemetryState.registrations.at(-1);
388
+ if (!activeRegistration) {
389
+ return originalMetrics.call(registry);
390
+ }
391
+ return activeRegistration.collectScrape(() => originalMetrics.call(registry));
392
+ });
300
393
  };
301
394
  }
395
+ async collectScrape(render) {
396
+ await this.refresh();
397
+ return await render();
398
+ }
302
399
  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;
400
+ const platformShell = await this.resolvePlatformShell();
401
+ if (!platformShell) {
402
+ this.clearPlatformTelemetry();
403
+ return;
404
+ }
405
+ const snapshot = await platformShell.snapshot();
406
+ this.syncSnapshot(snapshot);
314
407
  }
315
408
  syncSnapshot(snapshot) {
316
409
  const env = this.labels?.env ?? 'unknown';
@@ -508,6 +601,7 @@ function resolveHttpOptions(http) {
508
601
  }
509
602
  return {
510
603
  allowUnsafeRawPathLabelMode: http.allowUnsafeRawPathLabelMode,
604
+ durationHistogramBuckets: http.durationHistogramBuckets,
511
605
  pathLabelMode: http.pathLabelMode,
512
606
  pathLabelNormalizer: http.pathLabelNormalizer,
513
607
  unknownPathLabel: http.unknownPathLabel
@@ -1,6 +1,11 @@
1
1
  import type { Registry, CounterConfiguration, GaugeConfiguration, HistogramConfiguration } from 'prom-client';
2
2
  /**
3
- * Small facade for creating custom Prometheus metrics on the module registry.
3
+ * Small facade for creating custom Prometheus metrics on its `MetricsModule` registry.
4
+ *
5
+ * @remarks
6
+ * `MetricsService` is non-global: a module can inject it when it directly imports a
7
+ * `MetricsModule.forRoot(...)` registration or imports a module that re-exports
8
+ * `MetricsService`; unrelated sibling modules do not receive it automatically.
4
9
  */
5
10
  export declare class MetricsService {
6
11
  private readonly registry;
@@ -1 +1 @@
1
- {"version":3,"file":"metrics-service.d.ts","sourceRoot":"","sources":["../src/metrics-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAIrB;;GAEG;AACH,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,QAAQ;IAE/C;;;;;OAKG;IACH,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAIlE;;;;;OAKG;IACH,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAI9D;;;;;OAKG;IACH,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC;IAItE;;;;OAIG;IACH,WAAW,IAAI,QAAQ;CAGxB"}
1
+ {"version":3,"file":"metrics-service.d.ts","sourceRoot":"","sources":["../src/metrics-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAIrB;;;;;;;GAOG;AACH,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,QAAQ;IAE/C;;;;;OAKG;IACH,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAIlE;;;;;OAKG;IACH,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAI9D;;;;;OAKG;IACH,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC;IAItE;;;;OAIG;IACH,WAAW,IAAI,QAAQ;CAGxB"}
@@ -1,7 +1,12 @@
1
1
  import { createPrometheusCounter, createPrometheusGauge, createPrometheusHistogram } from './providers/prometheus-metrics-factory.js';
2
2
 
3
3
  /**
4
- * Small facade for creating custom Prometheus metrics on the module registry.
4
+ * Small facade for creating custom Prometheus metrics on its `MetricsModule` registry.
5
+ *
6
+ * @remarks
7
+ * `MetricsService` is non-global: a module can inject it when it directly imports a
8
+ * `MetricsModule.forRoot(...)` registration or imports a module that re-exports
9
+ * `MetricsService`; unrelated sibling modules do not receive it automatically.
5
10
  */
6
11
  export class MetricsService {
7
12
  constructor(registry) {
@@ -0,0 +1,19 @@
1
+ /** Observable state for package-internal serialized scrape scheduling. */
2
+ export interface SerializedScrapeQueueState {
3
+ readonly isRunning: boolean;
4
+ readonly queued: number;
5
+ }
6
+ /**
7
+ * Serializes refresh-and-render scrape work while preserving individual results.
8
+ *
9
+ * Failed work does not poison later queue entries.
10
+ */
11
+ export declare class SerializedScrapeQueue {
12
+ private isRunning;
13
+ private queued;
14
+ private tail;
15
+ get state(): SerializedScrapeQueueState;
16
+ enqueue<T>(task: () => Promise<T>): Promise<T>;
17
+ drain(finalizer: () => void): Promise<void>;
18
+ }
19
+ //# sourceMappingURL=serialized-scrape-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialized-scrape-queue.d.ts","sourceRoot":"","sources":["../src/serialized-scrape-queue.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,IAAI,CAAoC;IAEhD,IAAI,KAAK,IAAI,0BAA0B,CAKtC;IAED,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAqBxC,KAAK,CAAC,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAalD"}
@@ -0,0 +1,43 @@
1
+ /** Observable state for package-internal serialized scrape scheduling. */
2
+
3
+ /**
4
+ * Serializes refresh-and-render scrape work while preserving individual results.
5
+ *
6
+ * Failed work does not poison later queue entries.
7
+ */
8
+ export class SerializedScrapeQueue {
9
+ isRunning = false;
10
+ queued = 0;
11
+ tail = Promise.resolve();
12
+ get state() {
13
+ return {
14
+ isRunning: this.isRunning,
15
+ queued: this.queued
16
+ };
17
+ }
18
+ enqueue(task) {
19
+ this.queued += 1;
20
+ const scrape = this.tail.then(async () => {
21
+ this.queued -= 1;
22
+ this.isRunning = true;
23
+ try {
24
+ return await task();
25
+ } finally {
26
+ this.isRunning = false;
27
+ }
28
+ });
29
+ this.tail = scrape.then(() => undefined, () => undefined);
30
+ return scrape;
31
+ }
32
+ async drain(finalizer) {
33
+ for (;;) {
34
+ const observedTail = this.tail;
35
+ await observedTail;
36
+ if (this.tail !== observedTail) {
37
+ continue;
38
+ }
39
+ finalizer();
40
+ return;
41
+ }
42
+ }
43
+ }
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "monitoring",
9
9
  "observability"
10
10
  ],
11
- "version": "2.0.0",
11
+ "version": "3.0.0",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -17,7 +17,7 @@
17
17
  "directory": "packages/metrics"
18
18
  },
19
19
  "engines": {
20
- "node": ">=20.0.0"
20
+ "node": ">=24.0.0 <27"
21
21
  },
22
22
  "publishConfig": {
23
23
  "access": "public"
@@ -35,15 +35,16 @@
35
35
  "dist"
36
36
  ],
37
37
  "dependencies": {
38
- "prom-client": "^15.1.3",
39
- "@fluojs/core": "^1.1.0",
40
- "@fluojs/di": "^2.0.0",
41
- "@fluojs/http": "^2.0.1",
42
- "@fluojs/runtime": "^2.0.1"
38
+ "prom-client": "15.1.3",
39
+ "@fluojs/core": "^2.0.0",
40
+ "@fluojs/di": "^3.0.0",
41
+ "@fluojs/http": "^3.0.0",
42
+ "@fluojs/runtime": "^3.0.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^22.0.0",
46
- "vitest": "^3.2.4"
46
+ "vitest": "^4.1.11",
47
+ "@fluojs/testing": "^3.0.0"
47
48
  },
48
49
  "scripts": {
49
50
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",