@fluojs/metrics 1.0.0-beta.2 → 1.0.0-beta.4

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
4
 
5
- fluo 애플리케이션을 위한 Prometheus 메트릭 노출 모듈입니다. `MetricsModule`을 마운트하여 Node.js 기본 메트릭과 선택적인 저지수(low-cardinality) HTTP 요청 모니터링 기능이 포함된 `/metrics` 엔드포인트를 제공합니다.
5
+ HTTP metric과 platform telemetry를 포함해 fluo 애플리케이션을 위한 Prometheus metric을 노출합니다.
6
6
 
7
7
  ## 목차
8
8
 
@@ -22,50 +22,54 @@ pnpm add @fluojs/metrics
22
22
 
23
23
  ## 사용 시점
24
24
 
25
- - Prometheus VictoriaMetrics 수집기에 애플리케이션 시스템 메트릭을 내보내야 때.
26
- - 수동 계측 없이 자동화된 HTTP 요청 지연 시간 횟수 메트릭을 원할 때.
27
- - 메트릭 데이터를 fluo 런타임 헬스체크 준비 상태와 동기화하고 싶을 때.
25
+ - 애플리케이션이 Prometheus-compatible scraping을 위한 `/metrics` endpoint를 노출해야
26
+ - 손으로 middleware를 작성하지 않고 HTTP latency와 request count를 계측해야
27
+ - application telemetry를 fluo readinesshealth state와 맞춰야
28
28
 
29
29
  ## 빠른 시작
30
30
 
31
- 루트 모듈에 `MetricsModule.forRoot()`를 추가하여 기본 `/metrics` 엔드포인트를 활성화합니다.
32
-
33
- ```typescript
34
- import { Module } from '@fluojs/core';
31
+ ```ts
35
32
  import { MetricsModule } from '@fluojs/metrics';
33
+ import { Module } from '@fluojs/core';
36
34
 
37
35
  @Module({
38
- imports: [
39
- MetricsModule.forRoot(),
40
- ],
36
+ imports: [MetricsModule.forRoot({ http: true })],
41
37
  })
42
38
  class AppModule {}
43
-
44
- // GET /metrics → Prometheus 텍스트 형식
45
39
  ```
46
40
 
47
- `MetricsModule.forRoot()`는 기본적으로 `GET /metrics`를 노출합니다. 운영 환경에서는 경계를 명시적으로 다루세요. 플랫폼 프록시/네트워크 제어를 붙이기 전까지 `path: false`로 비활성화하거나, 전용 endpoint middleware를 연결하는 방식을 권장합니다.
41
+ `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를 연결할 있습니다.
48
42
 
49
- ## 공통 패턴
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`를 전달하세요.
50
44
 
51
- ### HTTP 라벨 정규화
45
+ ## 공통 패턴
52
46
 
53
- `MetricsModule`은 HTTP 메트릭을 수집하는 미들웨어를 포함합니다. 기본적으로 경로 라벨을 템플릿 형태(예: `/users/123` → `/users/:id`)로 정규화하여, 라벨 카디널리티(cardinality) 폭발을 방지합니다.
47
+ ### HTTP path label 정규화
54
48
 
55
- ```typescript
49
+ ```ts
56
50
  MetricsModule.forRoot({
57
51
  http: {
58
- pathLabelMode: 'template', // 기본 동작
52
+ pathLabelMode: 'template',
59
53
  unknownPathLabel: 'UNKNOWN',
60
54
  },
61
- })
55
+ });
62
56
  ```
63
57
 
64
- `pathLabelMode: 'raw'`는 이제 안전하지 않은 명시적 opt-in으로 취급됩니다. 경로 공간이 유한하다는 것을 보장할 수 있을 때만 `allowUnsafeRawPathLabelMode: true`와 함께 사용하세요.
58
+ `pathLabelMode: 'raw'`는 안전하지 않은 opt-in입니다. 경로 공간이 유한하다는 것을 증명할 수 있을 때만 `allowUnsafeRawPathLabelMode: true`와 함께 사용하세요.
59
+
60
+ ### Custom path label 정규화
61
+
62
+ ```ts
63
+ MetricsModule.forRoot({
64
+ http: {
65
+ pathLabelNormalizer: ({ path }) => (path.startsWith('/api/v1') ? '/api/v1/:resource' : path),
66
+ },
67
+ });
68
+ ```
65
69
 
66
70
  ### 메트릭 엔드포인트 보호 또는 비활성화
67
71
 
68
- ```typescript
72
+ ```ts
69
73
  import { ForbiddenException, type MiddlewareContext, type Next } from '@fluojs/http';
70
74
 
71
75
  class MetricsTokenMiddleware {
@@ -87,57 +91,51 @@ MetricsModule.forRoot({
87
91
  });
88
92
  ```
89
93
 
90
- 특수한 경로 매핑이 필요한 경우 커스텀 normalizer를 제공할 있습니다.
94
+ `endpointMiddleware`는 class-based `@fluojs/http` middleware constructor를 받으며 metrics scrape endpoint에만 바인딩됩니다. middleware function이나 global middleware declaration은 이 option의 패키지 계약이 아닙니다.
91
95
 
92
- ```typescript
93
- MetricsModule.forRoot({
94
- http: {
95
- pathLabelNormalizer: ({ path }) => (path.startsWith('/api/v1') ? '/api/v1/:resource' : path),
96
- },
97
- })
98
- ```
99
-
100
- ### 공유 Registry (권장)
101
-
102
- 커스텀 애플리케이션 메트릭을 프레임워크가 제공하는 메트릭과 하나의 `/metrics` 엔드포인트에서 통합하려면, 공유 `Registry` 인스턴스를 전달하세요.
96
+ ### Framework metric과 app metric이 하나의 registry를 공유하기
103
97
 
104
- ```typescript
105
- import { Registry, Counter } from 'prom-client';
106
- import { MetricsModule, MetricsService } from '@fluojs/metrics';
98
+ ```ts
99
+ import { Counter, Registry } from 'prom-client';
100
+ import { MetricsModule } from '@fluojs/metrics';
107
101
 
108
- const sharedRegistry = new Registry();
102
+ const registry = new Registry();
109
103
 
110
- // 커스텀 메트릭 등록
111
- const ordersTotal = new Counter({
104
+ new Counter({
112
105
  name: 'orders_total',
113
- help: '처리된 주문 수',
114
- registers: [sharedRegistry],
106
+ help: 'Total orders processed',
107
+ registers: [registry],
115
108
  });
116
109
 
117
110
  @Module({
118
- imports: [
119
- MetricsModule.forRoot({ registry: sharedRegistry }),
120
- ],
111
+ imports: [MetricsModule.forRoot({ http: true, registry })],
121
112
  })
122
113
  class AppModule {}
123
114
  ```
124
115
 
116
+ 여러 `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 규칙대로 계속 빠르게 실패합니다.
117
+
118
+ ### 중복 메트릭 이름은 계속 빠르게 실패합니다
119
+
120
+ Prometheus 메트릭 이름은 하나의 Registry 안에서 고유해야 합니다. 공유 Registry 모드는 애플리케이션 메트릭의 중복 이름을 조용히 덮어쓰지 않고 이 동작을 유지합니다. 애플리케이션이 내장 HTTP collector 또는 플랫폼 텔레메트리 Gauge 이름을 미리 등록한 경우, `MetricsModule.forRoot()`는 app-owned collector를 재사용하지 않고 충돌을 거부합니다.
121
+
125
122
  ### 런타임 플랫폼 텔레메트리
126
123
 
127
124
  이 모듈은 플랫폼 셸 및 등록된 컴포넌트의 내부 상태를 반영하는 fluo 전용 Gauge를 자동으로 생성합니다.
128
125
 
129
126
  - `fluo_component_ready`: 준비 완료 시 1, 아닐 시 0.
130
127
  - `fluo_component_health`: 정상 상태 시 1, 아닐 시 0.
128
+ - `fluo_metrics_registry_mode`: active registry mode가 `isolated`인지 `shared`인지 나타냅니다.
131
129
 
132
130
  이 데이터는 매 스크레이프 시점에 `PLATFORM_SHELL`을 쿼리하여 갱신됩니다. 초기화 시 환경 라벨을 제공할 수 있습니다.
133
131
 
134
- ```typescript
132
+ ```ts
135
133
  MetricsModule.forRoot({
136
134
  platformTelemetry: {
137
135
  env: 'production',
138
136
  instance: 'web-01',
139
137
  },
140
- })
138
+ });
141
139
  ```
142
140
 
143
141
  ### 런타임 플랫폼 텔레메트리 스크레이프 계약
@@ -145,17 +143,17 @@ MetricsModule.forRoot({
145
143
  플랫폼 텔레메트리는 매 `/metrics` 스크레이프마다 `PLATFORM_SHELL`을 resolve하여 `fluo_component_ready`와 `fluo_component_health`를 갱신합니다.
146
144
 
147
145
  - `PLATFORM_SHELL` 등록 자체가 빠진 경우에는 스크레이프가 계속 성공하고 플랫폼 텔레메트리 시리즈만 생략됩니다.
148
- - 이전 스크레이프에서 플랫폼 텔레메트리를 노출한 뒤 `PLATFORM_SHELL`을 사용할 수 없게 되면, stale `fluo_component_ready` 및 `fluo_component_health` 시리즈를 제거한 뒤 메트릭을 반환합니다.
146
+ - 직전 성공 스크레이프에서 플랫폼 텔레메트리를 노출한 뒤 `PLATFORM_SHELL`을 사용할 수 없게 되면, stale `fluo_component_ready` 및 `fluo_component_health` 시리즈를 제거한 뒤 메트릭을 반환합니다.
149
147
  - 그 외의 `PLATFORM_SHELL` resolve 실패는 조용히 삼키지 않고 스크레이프 실패로 그대로 드러납니다.
150
148
 
151
149
  ### 기본 프로세스/Node 메트릭 비활성화
152
150
 
153
151
  `defaultMetrics`의 기본값은 `true`입니다. 따라서 별도 설정이 없으면 Registry마다 Prometheus 기본 프로세스/Node.js collector를 한 번 등록합니다. 최소 Registry만 노출하고 싶다면 비활성화하세요.
154
152
 
155
- ```typescript
153
+ ```ts
156
154
  MetricsModule.forRoot({
157
155
  defaultMetrics: false,
158
- })
156
+ });
159
157
  ```
160
158
 
161
159
  ## 공개 API
@@ -165,17 +163,20 @@ MetricsModule.forRoot({
165
163
  - `METER_PROVIDER` (Token)
166
164
  - `PrometheusMeterProvider`
167
165
  - `HttpMetricsMiddleware` 및 HTTP path-label 옵션 타입
166
+ - `provider`(현재는 `'prometheus'`만 지원)와 endpoint `middleware`를 포함한 module option
168
167
  - `prom-client`의 `Registry`
169
168
 
170
169
  ### 운영 기본값
171
170
 
172
171
  - `path`의 기본값은 `'/metrics'`이며, `path: false`로 스크레이프 엔드포인트를 완전히 비활성화할 수 있습니다.
172
+ - scrape response는 active Registry의 Prometheus content type과 Registry contents를 사용합니다.
173
173
  - `defaultMetrics`의 기본값은 `true`이며, `defaultMetrics: false`로 해당 Registry의 Prometheus 기본 프로세스/Node.js collector를 끌 수 있습니다.
174
- - `endpointMiddleware`는 스크레이프 엔드포인트에만 route-scoped middleware를 바인딩합니다.
175
- - HTTP 메트릭은 기본적으로 템플릿 기반 경로 라벨 정규화를 사용합니다.
174
+ - `endpointMiddleware`는 class-based route-scoped middleware를 스크레이프 엔드포인트에만 바인딩합니다.
175
+ - HTTP 메트릭은 `http: true` 또는 `http` 옵션 객체를 전달한 경우에만 설치되며, 설치된 뒤에는 기본적으로 템플릿 기반 경로 라벨 정규화를 사용합니다.
176
+ - 내장 HTTP collector와 플랫폼 텔레메트리 Gauge는 같은 Registry를 공유하는 모듈 인스턴스 사이에서 framework-owned이고 예상 label schema를 가진 경우에만 재사용되며, 커스텀 애플리케이션 메트릭 이름 충돌은 Prometheus의 중복 이름 실패 동작을 유지합니다.
176
177
  - raw path 라벨은 `allowUnsafeRawPathLabelMode: true`를 명시한 bounded internal route에서만 사용해야 합니다.
177
178
  - 플랫폼 텔레메트리는 `PLATFORM_SHELL`이 실제로 누락된 경우에만 생략되며, 그 외 resolve 실패는 스크레이프를 실패시킵니다.
178
- - 이전에 노출된 플랫폼 텔레메트리 시리즈는 `PLATFORM_SHELL`을 사용할 수 없게 된 스크레이프에서 제거됩니다.
179
+ - 직전 성공 스크레이프에서 노출된 플랫폼 텔레메트리 시리즈는 `PLATFORM_SHELL`을 사용할 수 없게 된 스크레이프에서 제거됩니다.
179
180
 
180
181
  ## 관련 패키지
181
182
 
package/README.md CHANGED
@@ -33,12 +33,14 @@ import { MetricsModule } from '@fluojs/metrics';
33
33
  import { Module } from '@fluojs/core';
34
34
 
35
35
  @Module({
36
- imports: [MetricsModule.forRoot()],
36
+ imports: [MetricsModule.forRoot({ http: true })],
37
37
  })
38
38
  class AppModule {}
39
39
  ```
40
40
 
41
- `MetricsModule.forRoot()` still exposes `GET /metrics` by default. For production deployments, make that endpoint boundary explicit: either disable it with `path: false` until a platform-level proxy is in place, or attach dedicated endpoint middleware.
41
+ `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
+
43
+ The scrape endpoint returns the active `prom-client` registry output with that registry's Prometheus content type. `MetricsModule.forRoot()` creates an isolated registry unless you pass a `registry` option; pass a shared `Registry` only when framework metrics and application-defined metrics intentionally share one scrape surface.
42
44
 
43
45
  ## Common Patterns
44
46
 
@@ -53,7 +55,17 @@ MetricsModule.forRoot({
53
55
  });
54
56
  ```
55
57
 
56
- `pathLabelMode: 'raw'` is now treated as an unsafe opt-in. You must pass `allowUnsafeRawPathLabelMode: true` only when you can prove the path space is bounded.
58
+ `pathLabelMode: 'raw'` is an unsafe opt-in. You must pass `allowUnsafeRawPathLabelMode: true` only when you can prove the path space is bounded.
59
+
60
+ ### Custom path label normalization
61
+
62
+ ```ts
63
+ MetricsModule.forRoot({
64
+ http: {
65
+ pathLabelNormalizer: ({ path }) => (path.startsWith('/api/v1') ? '/api/v1/:resource' : path),
66
+ },
67
+ });
68
+ ```
57
69
 
58
70
  ### Protect or disable the metrics endpoint
59
71
 
@@ -79,6 +91,8 @@ MetricsModule.forRoot({
79
91
  });
80
92
  ```
81
93
 
94
+ `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.
95
+
82
96
  ### Share one registry for framework and app metrics
83
97
 
84
98
  ```ts
@@ -94,14 +108,16 @@ new Counter({
94
108
  });
95
109
 
96
110
  @Module({
97
- imports: [MetricsModule.forRoot({ registry })],
111
+ imports: [MetricsModule.forRoot({ http: true, registry })],
98
112
  })
99
113
  class AppModule {}
100
114
  ```
101
115
 
116
+ 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.
117
+
102
118
  ### Duplicate metric names still fail fast
103
119
 
104
- Prometheus metric names must stay unique inside a registry. Shared-registry mode keeps that behavior intact instead of silently shadowing metrics.
120
+ Prometheus metric names must stay unique inside a registry. Shared-registry mode keeps that behavior intact instead of silently shadowing metrics. If an application predefines a built-in HTTP collector or platform telemetry gauge name, `MetricsModule.forRoot()` rejects the collision instead of reusing an app-owned collector.
105
121
 
106
122
  ### Runtime platform telemetry
107
123
 
@@ -109,6 +125,7 @@ The module emits fluo-specific gauges that mirror the platform shell and registe
109
125
 
110
126
  - `fluo_component_ready`: `1` when a component is ready, otherwise `0`.
111
127
  - `fluo_component_health`: `1` when a component is healthy, otherwise `0`.
128
+ - `fluo_metrics_registry_mode`: `isolated` or `shared` for the active registry mode.
112
129
 
113
130
  The platform snapshot is refreshed during each scrape, and you can attach environment labels up front.
114
131
 
@@ -126,7 +143,7 @@ MetricsModule.forRoot({
126
143
  Platform telemetry refreshes `fluo_component_ready` and `fluo_component_health` on each `/metrics` scrape by resolving `PLATFORM_SHELL`.
127
144
 
128
145
  - If `PLATFORM_SHELL` is not registered, the scrape still succeeds and omits the platform telemetry series.
129
- - If `PLATFORM_SHELL` becomes unavailable after a previous successful scrape, stale `fluo_component_ready` and `fluo_component_health` series are removed before metrics are returned.
146
+ - 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.
130
147
  - If resolving `PLATFORM_SHELL` fails for any other reason, the scrape surfaces that failure instead of swallowing it.
131
148
 
132
149
  ### Disable default process and Node metrics
@@ -146,17 +163,20 @@ MetricsModule.forRoot({
146
163
  - `METER_PROVIDER`
147
164
  - `PrometheusMeterProvider`
148
165
  - `HttpMetricsMiddleware` and HTTP path-label option types
166
+ - Module options including `provider` (currently only `'prometheus'`) and endpoint `middleware`
149
167
  - `Registry` from `prom-client`
150
168
 
151
169
  ### Operational defaults
152
170
 
153
171
  - `path` defaults to `'/metrics'`, and `path: false` disables the scrape endpoint entirely.
172
+ - The scrape response uses the active registry's Prometheus content type and registry contents.
154
173
  - `defaultMetrics` defaults to `true`, and `defaultMetrics: false` disables Prometheus default process and Node.js collectors for that registry.
155
- - `endpointMiddleware` binds route-scoped middleware only to the scrape endpoint.
156
- - HTTP metrics default to template-normalized path labels.
174
+ - `endpointMiddleware` binds class-based route-scoped middleware only to the scrape endpoint.
175
+ - HTTP metrics are installed only when `http: true` or an `http` options object is provided, and then default to template-normalized path labels.
176
+ - Built-in HTTP collectors and platform telemetry gauges are reused when module instances share one registry only if they are framework-owned and have the expected label schema; custom application metric name collisions keep Prometheus' duplicate-name failure behavior.
157
177
  - Raw path labels require `allowUnsafeRawPathLabelMode: true` and should stay limited to bounded internal routes.
158
178
  - Platform telemetry is omitted only when `PLATFORM_SHELL` is genuinely missing; other resolution failures fail the scrape.
159
- - Stale platform telemetry series are removed when `PLATFORM_SHELL` becomes unavailable after a prior successful scrape.
179
+ - Stale platform telemetry series are removed when `PLATFORM_SHELL` becomes unavailable after the last successful scrape.
160
180
 
161
181
  ## Related Packages
162
182
 
@@ -1,5 +1,5 @@
1
1
  import type { FrameworkRequest, Middleware, MiddlewareContext, Next } from '@fluojs/http';
2
- import type { Registry } from 'prom-client';
2
+ import { type Registry } from 'prom-client';
3
3
  /** Strategy used to label request paths in emitted HTTP metrics. */
4
4
  export type HttpMetricsPathLabelMode = 'raw' | 'template';
5
5
  /** Context passed to a custom path-label normalizer. */
@@ -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,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAkB5C,oEAAoE;AACpE,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG,UAAU,CAAC;AAE1D,wDAAwD;AACxD,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,gBAAgB,CAAC;CAC3B;AAED,2EAA2E;AAC3E,MAAM,MAAM,8BAA8B,GAAG,CAAC,OAAO,EAAE,2BAA2B,KAAK,MAAM,CAAC;AAE9F,8DAA8D;AAC9D,MAAM,WAAW,4BAA4B;IAC3C,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,mBAAmB,CAAC,EAAE,8BAA8B,CAAC;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAsBD;;GAEG;AACH,qBAAa,qBAAsB,YAAW,UAAU;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;IAChD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsB;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2B;IACzD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAiC;IACtE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;gBAE9B,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,4BAAiC;IA2B1E,OAAO,CAAC,gBAAgB;IAmBlB,MAAM,CAAC,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnE,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,oBAAoB;CAsB7B"}
1
+ {"version":3,"file":"http-metrics-middleware.d.ts","sourceRoot":"","sources":["../src/http-metrics-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAC1F,OAAO,EAAsB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAqBhE,oEAAoE;AACpE,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG,UAAU,CAAC;AAE1D,wDAAwD;AACxD,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,gBAAgB,CAAC;CAC3B;AAED,2EAA2E;AAC3E,MAAM,MAAM,8BAA8B,GAAG,CAAC,OAAO,EAAE,2BAA2B,KAAK,MAAM,CAAC;AAE9F,8DAA8D;AAC9D,MAAM,WAAW,4BAA4B;IAC3C,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,mBAAmB,CAAC,EAAE,8BAA8B,CAAC;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAsBD;;GAEG;AACH,qBAAa,qBAAsB,YAAW,UAAU;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;IAChD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsB;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2B;IACzD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAiC;IACtE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;gBAE9B,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,4BAAiC;IA2B1E,OAAO,CAAC,gBAAgB;IAmBlB,MAAM,CAAC,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnE,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,oBAAoB;CAsB7B"}
@@ -1,4 +1,7 @@
1
+ import { Counter, Histogram } from 'prom-client';
1
2
  import { createPrometheusCounter, createPrometheusHistogram } from './providers/prometheus-metrics-factory.js';
3
+ const FRAMEWORK_HTTP_COUNTERS = new WeakSet();
4
+ const FRAMEWORK_HTTP_HISTOGRAMS = new WeakSet();
2
5
 
3
6
  /** Strategy used to label request paths in emitted HTTP metrics. */
4
7
 
@@ -41,17 +44,17 @@ export class HttpMetricsMiddleware {
41
44
  this.pathLabelMode = options.pathLabelMode ?? 'template';
42
45
  this.pathLabelNormalizer = options.pathLabelNormalizer;
43
46
  this.unknownPathLabel = options.unknownPathLabel ?? 'UNKNOWN';
44
- this.requestsTotal = createPrometheusCounter(registry, {
47
+ this.requestsTotal = getOrCreateHttpCounter(registry, {
45
48
  help: 'Total number of HTTP requests',
46
49
  labelNames: ['method', 'path', 'status'],
47
50
  name: 'http_requests_total'
48
51
  });
49
- this.errorsTotal = createPrometheusCounter(registry, {
52
+ this.errorsTotal = getOrCreateHttpCounter(registry, {
50
53
  help: 'Total number of HTTP error responses (4xx/5xx)',
51
54
  labelNames: ['method', 'path', 'status'],
52
55
  name: 'http_errors_total'
53
56
  });
54
- this.requestDuration = createPrometheusHistogram(registry, {
57
+ this.requestDuration = getOrCreateHttpHistogram(registry, {
55
58
  help: 'HTTP request duration in seconds',
56
59
  labelNames: ['method', 'path', 'status'],
57
60
  name: 'http_request_duration_seconds'
@@ -116,6 +119,38 @@ export class HttpMetricsMiddleware {
116
119
  }
117
120
  }
118
121
  }
122
+ function getOrCreateHttpCounter(registry, config) {
123
+ const existing = registry.getSingleMetric(config.name);
124
+ if (existing instanceof Counter) {
125
+ if (!FRAMEWORK_HTTP_COUNTERS.has(existing)) {
126
+ throw new Error(`Metric name "${config.name}" is already registered by the application. Built-in HTTP metrics require framework-owned collectors.`);
127
+ }
128
+ return existing;
129
+ }
130
+ const counter = createPrometheusCounter(registry, {
131
+ help: config.help,
132
+ labelNames: [...config.labelNames],
133
+ name: config.name
134
+ });
135
+ FRAMEWORK_HTTP_COUNTERS.add(counter);
136
+ return counter;
137
+ }
138
+ function getOrCreateHttpHistogram(registry, config) {
139
+ const existing = registry.getSingleMetric(config.name);
140
+ if (existing instanceof Histogram) {
141
+ if (!FRAMEWORK_HTTP_HISTOGRAMS.has(existing)) {
142
+ throw new Error(`Metric name "${config.name}" is already registered by the application. Built-in HTTP metrics require framework-owned collectors.`);
143
+ }
144
+ return existing;
145
+ }
146
+ const histogram = createPrometheusHistogram(registry, {
147
+ help: config.help,
148
+ labelNames: [...config.labelNames],
149
+ name: config.name
150
+ });
151
+ FRAMEWORK_HTTP_HISTOGRAMS.add(histogram);
152
+ return histogram;
153
+ }
119
154
  function normalizePathToTemplate(path, params) {
120
155
  if (!path) {
121
156
  return '/';
package/dist/index.d.ts CHANGED
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Prometheus registry constructor re-exported for applications that want custom
3
+ * application metrics and Fluo framework metrics to share one scrape endpoint.
4
+ *
5
+ * @remarks
6
+ * The implementation is `prom-client`'s `Registry`; duplicate metric names still
7
+ * follow Prometheus registry semantics and fail fast.
8
+ */
1
9
  export { Registry } from 'prom-client';
2
10
  export * from './metrics-module.js';
3
11
  export * from './metrics-service.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0CAA0C,CAAC;AACzD,cAAc,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0CAA0C,CAAC;AACzD,cAAc,8BAA8B,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Prometheus registry constructor re-exported for applications that want custom
3
+ * application metrics and Fluo framework metrics to share one scrape endpoint.
4
+ *
5
+ * @remarks
6
+ * The implementation is `prom-client`'s `Registry`; duplicate metric names still
7
+ * follow Prometheus registry semantics and fail fast.
8
+ */
1
9
  export { Registry } from 'prom-client';
2
10
  export * from './metrics-module.js';
3
11
  export * from './metrics-service.js';
@@ -98,6 +98,7 @@ export class MetricsModule {
98
98
  }
99
99
  const PLATFORM_COMPONENT_LABELS = ['component_id', 'component_kind', 'operation', 'result', 'env', 'instance'];
100
100
  const REGISTRY_MODE_LABELS = ['mode'];
101
+ const FRAMEWORK_PLATFORM_GAUGES = new WeakSet();
101
102
  const HEALTH_STATUSES = ['healthy', 'unhealthy', 'degraded'];
102
103
  const READINESS_STATUSES = ['ready', 'not-ready', 'degraded'];
103
104
  const PLATFORM_SHELL_TOKEN_NAME = 'PLATFORM_SHELL';
@@ -111,14 +112,27 @@ function toHealthValue(status) {
111
112
  function getOrCreateGauge(registry, config) {
112
113
  const existing = registry.getSingleMetric(config.name);
113
114
  if (existing instanceof Gauge) {
115
+ if (!FRAMEWORK_PLATFORM_GAUGES.has(existing)) {
116
+ throw new Error(`Metric name "${config.name}" is already registered by the application. Built-in platform telemetry requires framework-owned gauges.`);
117
+ }
118
+ assertGaugeLabelSchema(existing, config);
114
119
  return existing;
115
120
  }
116
- return new Gauge({
121
+ const gauge = new Gauge({
117
122
  help: config.help,
118
123
  labelNames: [...config.labelNames],
119
124
  name: config.name,
120
125
  registers: [registry]
121
126
  });
127
+ FRAMEWORK_PLATFORM_GAUGES.add(gauge);
128
+ return gauge;
129
+ }
130
+ function assertGaugeLabelSchema(gauge, config) {
131
+ const registeredLabels = (gauge.labelNames ?? []).join(',');
132
+ const expectedLabels = config.labelNames.join(',');
133
+ if (registeredLabels !== expectedLabels) {
134
+ throw new Error(`Metric name "${config.name}" is already registered with labels [${registeredLabels}]. Built-in platform telemetry requires labels [${expectedLabels}].`);
135
+ }
122
136
  }
123
137
  class RuntimePlatformTelemetry {
124
138
  readinessGauge;
@@ -285,12 +299,13 @@ function isMissingPlatformShellResolutionError(error) {
285
299
  return false;
286
300
  }
287
301
  const containerError = error;
302
+ const message = String(containerError.message ?? '');
288
303
  const token = typeof containerError.meta?.['token'] === 'string' ? containerError.meta['token'] : undefined;
289
304
  if (token && PLATFORM_SHELL_TOKEN_NAMES.has(token)) {
290
- return containerError.message.startsWith(`No provider registered for token ${token}.`);
305
+ return message.startsWith(`No provider registered for token ${token}.`);
291
306
  }
292
307
  for (const tokenName of PLATFORM_SHELL_TOKEN_NAMES) {
293
- if (containerError.message.startsWith(`No provider registered for token ${tokenName}.`)) {
308
+ if (message.startsWith(`No provider registered for token ${tokenName}.`)) {
294
309
  return true;
295
310
  }
296
311
  }
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "monitoring",
9
9
  "observability"
10
10
  ],
11
- "version": "1.0.0-beta.2",
11
+ "version": "1.0.0-beta.4",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -36,9 +36,9 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "prom-client": "^15.1.3",
39
- "@fluojs/di": "^1.0.0-beta.2",
40
- "@fluojs/http": "^1.0.0-beta.1",
41
- "@fluojs/runtime": "^1.0.0-beta.2"
39
+ "@fluojs/di": "^1.0.0-beta.7",
40
+ "@fluojs/http": "^1.0.0-beta.10",
41
+ "@fluojs/runtime": "^1.0.0-beta.12"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^22.0.0",