@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 +51 -13
- package/README.md +50 -12
- package/dist/http-metrics-middleware.d.ts.map +1 -1
- package/dist/http-metrics-middleware.js +40 -11
- package/dist/metrics-module.d.ts +2 -0
- package/dist/metrics-module.d.ts.map +1 -1
- package/dist/metrics-module.js +270 -82
- package/package.json +5 -4
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`을
|
|
51
|
-
| `
|
|
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`일
|
|
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
|
|
173
|
+
- `fluo_metrics_registry_mode`: active registry mode를 `mode="isolated"` 또는 `mode="shared"` label과 gauge value `1`로 나타냅니다.
|
|
139
174
|
|
|
140
|
-
이 데이터는
|
|
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
|
-
플랫폼 텔레메트리는
|
|
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`을 사용할 수 없게
|
|
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'
|
|
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
|
|
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;
|
|
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
|
|
51
|
-
| `
|
|
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`;
|
|
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
|
|
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
|
|
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;
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.
|
|
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 '/';
|
package/dist/metrics-module.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/metrics-module.js
CHANGED
|
@@ -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
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
88
|
+
inject: [registryToken, platformTelemetryToken],
|
|
89
|
+
useFactory: registry => new MetricsService(assertPrometheusRegistry(registry))
|
|
61
90
|
}, {
|
|
62
91
|
provide: METER_PROVIDER,
|
|
63
|
-
|
|
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(
|
|
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
|
|
105
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
this.
|
|
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
|
|
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:
|
|
194
|
-
env,
|
|
330
|
+
currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.health),
|
|
195
331
|
gauge: this.healthGauge,
|
|
196
|
-
|
|
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:
|
|
204
|
-
env,
|
|
338
|
+
currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.readiness),
|
|
205
339
|
gauge: this.readinessGauge,
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
240
|
-
|
|
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
|
|
258
|
-
|
|
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
|
|
271
|
-
|
|
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
|
|
276
|
-
|
|
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
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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(
|
|
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
|
|
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
|
|
303
|
-
|
|
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": "
|
|
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/
|
|
40
|
-
"@fluojs/
|
|
41
|
-
"@fluojs/
|
|
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",
|