@fluojs/metrics 1.0.0-beta.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fluo contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ko.md ADDED
@@ -0,0 +1,185 @@
1
+ # @fluojs/metrics
2
+
3
+ <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
+
5
+ fluo 애플리케이션을 위한 Prometheus 메트릭 노출 모듈입니다. `MetricsModule`을 마운트하여 Node.js 기본 메트릭과 선택적인 저지수(low-cardinality) HTTP 요청 모니터링 기능이 포함된 `/metrics` 엔드포인트를 제공합니다.
6
+
7
+ ## 목차
8
+
9
+ - [설치](#설치)
10
+ - [사용 시점](#사용-시점)
11
+ - [빠른 시작](#빠른-시작)
12
+ - [공통 패턴](#공통-패턴)
13
+ - [공개 API](#공개-api)
14
+ - [관련 패키지](#관련-패키지)
15
+ - [예제 소스](#예제-소스)
16
+
17
+ ## 설치
18
+
19
+ ```bash
20
+ pnpm add @fluojs/metrics
21
+ ```
22
+
23
+ ## 사용 시점
24
+
25
+ - Prometheus나 VictoriaMetrics 수집기에 애플리케이션 및 시스템 메트릭을 내보내야 할 때.
26
+ - 수동 계측 없이 자동화된 HTTP 요청 지연 시간 및 횟수 메트릭을 원할 때.
27
+ - 메트릭 데이터를 fluo의 런타임 헬스체크 및 준비 상태와 동기화하고 싶을 때.
28
+
29
+ ## 빠른 시작
30
+
31
+ 루트 모듈에 `MetricsModule.forRoot()`를 추가하여 기본 `/metrics` 엔드포인트를 활성화합니다.
32
+
33
+ ```typescript
34
+ import { Module } from '@fluojs/core';
35
+ import { MetricsModule } from '@fluojs/metrics';
36
+
37
+ @Module({
38
+ imports: [
39
+ MetricsModule.forRoot(),
40
+ ],
41
+ })
42
+ class AppModule {}
43
+
44
+ // GET /metrics → Prometheus 텍스트 형식
45
+ ```
46
+
47
+ `MetricsModule.forRoot()`는 기본적으로 `GET /metrics`를 노출합니다. 운영 환경에서는 이 경계를 명시적으로 다루세요. 플랫폼 프록시/네트워크 제어를 붙이기 전까지 `path: false`로 비활성화하거나, 전용 endpoint middleware를 연결하는 방식을 권장합니다.
48
+
49
+ ## 공통 패턴
50
+
51
+ ### HTTP 라벨 정규화
52
+
53
+ `MetricsModule`은 HTTP 메트릭을 수집하는 미들웨어를 포함합니다. 기본적으로 경로 라벨을 템플릿 형태(예: `/users/123` → `/users/:id`)로 정규화하여, 라벨 카디널리티(cardinality) 폭발을 방지합니다.
54
+
55
+ ```typescript
56
+ MetricsModule.forRoot({
57
+ http: {
58
+ pathLabelMode: 'template', // 기본 동작
59
+ unknownPathLabel: 'UNKNOWN',
60
+ },
61
+ })
62
+ ```
63
+
64
+ `pathLabelMode: 'raw'`는 이제 안전하지 않은 명시적 opt-in으로 취급됩니다. 경로 공간이 유한하다는 것을 보장할 수 있을 때만 `allowUnsafeRawPathLabelMode: true`와 함께 사용하세요.
65
+
66
+ ### 메트릭 엔드포인트 보호 또는 비활성화
67
+
68
+ ```typescript
69
+ import { ForbiddenException, type MiddlewareContext, type Next } from '@fluojs/http';
70
+
71
+ class MetricsTokenMiddleware {
72
+ async handle(context: MiddlewareContext, next: Next): Promise<void> {
73
+ if (context.request.headers['x-metrics-token'] !== 'secret-token') {
74
+ throw new ForbiddenException('Metrics endpoint requires x-metrics-token.');
75
+ }
76
+
77
+ await next();
78
+ }
79
+ }
80
+
81
+ MetricsModule.forRoot({
82
+ endpointMiddleware: [MetricsTokenMiddleware],
83
+ });
84
+
85
+ MetricsModule.forRoot({
86
+ path: false,
87
+ });
88
+ ```
89
+
90
+ 특수한 경로 매핑이 필요한 경우 커스텀 normalizer를 제공할 수 있습니다.
91
+
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` 인스턴스를 전달하세요.
103
+
104
+ ```typescript
105
+ import { Registry, Counter } from 'prom-client';
106
+ import { MetricsModule, MetricsService } from '@fluojs/metrics';
107
+
108
+ const sharedRegistry = new Registry();
109
+
110
+ // 커스텀 메트릭 등록
111
+ const ordersTotal = new Counter({
112
+ name: 'orders_total',
113
+ help: '처리된 총 주문 수',
114
+ registers: [sharedRegistry],
115
+ });
116
+
117
+ @Module({
118
+ imports: [
119
+ MetricsModule.forRoot({ registry: sharedRegistry }),
120
+ ],
121
+ })
122
+ class AppModule {}
123
+ ```
124
+
125
+ ### 런타임 플랫폼 텔레메트리
126
+
127
+ 이 모듈은 플랫폼 셸 및 등록된 컴포넌트의 내부 상태를 반영하는 fluo 전용 Gauge를 자동으로 생성합니다.
128
+
129
+ - `fluo_component_ready`: 준비 완료 시 1, 아닐 시 0.
130
+ - `fluo_component_health`: 정상 상태 시 1, 아닐 시 0.
131
+
132
+ 이 데이터는 매 스크레이프 시점에 `PLATFORM_SHELL`을 쿼리하여 갱신됩니다. 초기화 시 환경 라벨을 제공할 수 있습니다.
133
+
134
+ ```typescript
135
+ MetricsModule.forRoot({
136
+ platformTelemetry: {
137
+ env: 'production',
138
+ instance: 'web-01',
139
+ },
140
+ })
141
+ ```
142
+
143
+ ### 런타임 플랫폼 텔레메트리 스크레이프 계약
144
+
145
+ 플랫폼 텔레메트리는 매 `/metrics` 스크레이프마다 `PLATFORM_SHELL`을 resolve하여 `fluo_component_ready`와 `fluo_component_health`를 갱신합니다.
146
+
147
+ - `PLATFORM_SHELL` 등록 자체가 빠진 경우에는 스크레이프가 계속 성공하고 플랫폼 텔레메트리 시리즈만 생략됩니다.
148
+ - 그 외의 `PLATFORM_SHELL` resolve 실패는 조용히 삼키지 않고 스크레이프 실패로 그대로 드러납니다.
149
+
150
+ ### 기본 프로세스/Node 메트릭 비활성화
151
+
152
+ `defaultMetrics`의 기본값은 `true`입니다. 따라서 별도 설정이 없으면 Registry마다 Prometheus 기본 프로세스/Node.js collector를 한 번 등록합니다. 최소 Registry만 노출하고 싶다면 비활성화하세요.
153
+
154
+ ```typescript
155
+ MetricsModule.forRoot({
156
+ defaultMetrics: false,
157
+ })
158
+ ```
159
+
160
+ ## 공개 API
161
+
162
+ - `MetricsModule.forRoot(options)`
163
+ - `MetricsService`
164
+ - `METER_PROVIDER` (Token)
165
+ - 카운터, 게이지, 히스토그램 및 레지스트리 접근을 위한 Prometheus 기반 헬퍼
166
+
167
+ ### 운영 기본값
168
+
169
+ - `path`의 기본값은 `'/metrics'`이며, `path: false`로 스크레이프 엔드포인트를 완전히 비활성화할 수 있습니다.
170
+ - `defaultMetrics`의 기본값은 `true`이며, `defaultMetrics: false`로 해당 Registry의 Prometheus 기본 프로세스/Node.js collector를 끌 수 있습니다.
171
+ - `endpointMiddleware`는 스크레이프 엔드포인트에만 route-scoped middleware를 바인딩합니다.
172
+ - HTTP 메트릭은 기본적으로 템플릿 기반 경로 라벨 정규화를 사용합니다.
173
+ - raw path 라벨은 `allowUnsafeRawPathLabelMode: true`를 명시한 bounded internal route에서만 사용해야 합니다.
174
+ - 플랫폼 텔레메트리는 `PLATFORM_SHELL`이 실제로 누락된 경우에만 생략되며, 그 외 resolve 실패는 스크레이프를 실패시킵니다.
175
+
176
+ ## 관련 패키지
177
+
178
+ - `@fluojs/http`: 컨트롤러 및 미들웨어 인프라를 제공합니다.
179
+ - `@fluojs/runtime`: 플랫폼 셸 및 모듈 정의 로직을 제공합니다.
180
+ - `@fluojs/terminus`: 헬스체크 통합을 위해 메트릭과 함께 자주 사용됩니다.
181
+
182
+ ## 예제 소스
183
+
184
+ - `examples/ops-metrics-terminus/src/app.ts`: Metrics 및 Terminus 모듈 구성 예제.
185
+ - `packages/metrics/src/metrics-module.test.ts`: 다양한 레지스트리 모드 및 HTTP 정규화 테스트.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # @fluojs/metrics
2
+
3
+ <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
+
5
+ Prometheus metrics exposure for fluo applications, including framework-aware HTTP metrics and platform telemetry.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [When to Use](#when-to-use)
11
+ - [Quick Start](#quick-start)
12
+ - [Common Patterns](#common-patterns)
13
+ - [Public API](#public-api)
14
+ - [Related Packages](#related-packages)
15
+ - [Example Sources](#example-sources)
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pnpm add @fluojs/metrics
21
+ ```
22
+
23
+ ## When to Use
24
+
25
+ - when your app should expose a `/metrics` endpoint for Prometheus-compatible scraping
26
+ - when HTTP latency and request counts should be instrumented without hand-written middleware
27
+ - when application telemetry should stay aligned with fluo readiness and health state
28
+
29
+ ## Quick Start
30
+
31
+ ```ts
32
+ import { MetricsModule } from '@fluojs/metrics';
33
+ import { Module } from '@fluojs/core';
34
+
35
+ @Module({
36
+ imports: [MetricsModule.forRoot()],
37
+ })
38
+ class AppModule {}
39
+ ```
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.
42
+
43
+ ## Common Patterns
44
+
45
+ ### Normalize HTTP path labels
46
+
47
+ ```ts
48
+ MetricsModule.forRoot({
49
+ http: {
50
+ pathLabelMode: 'template',
51
+ unknownPathLabel: 'UNKNOWN',
52
+ },
53
+ });
54
+ ```
55
+
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.
57
+
58
+ ### Protect or disable the metrics endpoint
59
+
60
+ ```ts
61
+ import { ForbiddenException, type MiddlewareContext, type Next } from '@fluojs/http';
62
+
63
+ class MetricsTokenMiddleware {
64
+ async handle(context: MiddlewareContext, next: Next): Promise<void> {
65
+ if (context.request.headers['x-metrics-token'] !== 'secret-token') {
66
+ throw new ForbiddenException('Metrics endpoint requires x-metrics-token.');
67
+ }
68
+
69
+ await next();
70
+ }
71
+ }
72
+
73
+ MetricsModule.forRoot({
74
+ endpointMiddleware: [MetricsTokenMiddleware],
75
+ });
76
+
77
+ MetricsModule.forRoot({
78
+ path: false,
79
+ });
80
+ ```
81
+
82
+ ### Share one registry for framework and app metrics
83
+
84
+ ```ts
85
+ import { Counter, Registry } from 'prom-client';
86
+ import { MetricsModule } from '@fluojs/metrics';
87
+
88
+ const registry = new Registry();
89
+
90
+ new Counter({
91
+ name: 'orders_total',
92
+ help: 'Total orders processed',
93
+ registers: [registry],
94
+ });
95
+
96
+ @Module({
97
+ imports: [MetricsModule.forRoot({ registry })],
98
+ })
99
+ class AppModule {}
100
+ ```
101
+
102
+ ### Duplicate metric names still fail fast
103
+
104
+ Prometheus metric names must stay unique inside a registry. Shared-registry mode keeps that behavior intact instead of silently shadowing metrics.
105
+
106
+ ### Runtime platform telemetry
107
+
108
+ The module emits fluo-specific gauges that mirror the platform shell and registered component state.
109
+
110
+ - `fluo_component_ready`: `1` when a component is ready, otherwise `0`.
111
+ - `fluo_component_health`: `1` when a component is healthy, otherwise `0`.
112
+
113
+ The platform snapshot is refreshed during each scrape, and you can attach environment labels up front.
114
+
115
+ ```ts
116
+ MetricsModule.forRoot({
117
+ platformTelemetry: {
118
+ env: 'production',
119
+ instance: 'web-01',
120
+ },
121
+ });
122
+ ```
123
+
124
+ ### Runtime platform telemetry scrape contract
125
+
126
+ Platform telemetry refreshes `fluo_component_ready` and `fluo_component_health` on each `/metrics` scrape by resolving `PLATFORM_SHELL`.
127
+
128
+ - If `PLATFORM_SHELL` is not registered, the scrape still succeeds and omits the platform telemetry series.
129
+ - If resolving `PLATFORM_SHELL` fails for any other reason, the scrape surfaces that failure instead of swallowing it.
130
+
131
+ ### Disable default process and Node metrics
132
+
133
+ `defaultMetrics` defaults to `true`, so `MetricsModule.forRoot()` registers Prometheus default process and Node.js collectors once per registry unless you opt out.
134
+
135
+ ```ts
136
+ MetricsModule.forRoot({
137
+ defaultMetrics: false,
138
+ });
139
+ ```
140
+
141
+ ## Public API
142
+
143
+ - `MetricsModule.forRoot(options)`
144
+ - `MetricsService`
145
+ - `METER_PROVIDER`
146
+ - Prometheus-backed helpers for counters, gauges, histograms, and registry access
147
+
148
+ ### Operational defaults
149
+
150
+ - `path` defaults to `'/metrics'`, and `path: false` disables the scrape endpoint entirely.
151
+ - `defaultMetrics` defaults to `true`, and `defaultMetrics: false` disables Prometheus default process and Node.js collectors for that registry.
152
+ - `endpointMiddleware` binds route-scoped middleware only to the scrape endpoint.
153
+ - HTTP metrics default to template-normalized path labels.
154
+ - Raw path labels require `allowUnsafeRawPathLabelMode: true` and should stay limited to bounded internal routes.
155
+ - Platform telemetry is omitted only when `PLATFORM_SHELL` is genuinely missing; other resolution failures fail the scrape.
156
+
157
+ ## Related Packages
158
+
159
+ - `@fluojs/http`: contributes the request lifecycle that HTTP metrics observe
160
+ - `@fluojs/runtime`: provides platform state used by runtime telemetry gauges
161
+ - `@fluojs/terminus`: commonly paired with metrics for ops visibility
162
+
163
+ ## Example Sources
164
+
165
+ - `examples/ops-metrics-terminus/src/app.ts`
166
+ - `packages/metrics/src/metrics-module.test.ts`
@@ -0,0 +1,37 @@
1
+ import type { FrameworkRequest, Middleware, MiddlewareContext, Next } from '@fluojs/http';
2
+ import type { Registry } from 'prom-client';
3
+ /** Strategy used to label request paths in emitted HTTP metrics. */
4
+ export type HttpMetricsPathLabelMode = 'raw' | 'template';
5
+ /** Context passed to a custom path-label normalizer. */
6
+ export interface HttpMetricsPathLabelContext {
7
+ method: string;
8
+ params: Readonly<Record<string, string>>;
9
+ path: string;
10
+ request: FrameworkRequest;
11
+ }
12
+ /** Callback that resolves the final path label used in emitted metrics. */
13
+ export type HttpMetricsPathLabelNormalizer = (context: HttpMetricsPathLabelContext) => string;
14
+ /** Options that tune HTTP request metric label generation. */
15
+ export interface HttpMetricsMiddlewareOptions {
16
+ pathLabelMode?: HttpMetricsPathLabelMode;
17
+ pathLabelNormalizer?: HttpMetricsPathLabelNormalizer;
18
+ unknownPathLabel?: string;
19
+ allowUnsafeRawPathLabelMode?: boolean;
20
+ }
21
+ /**
22
+ * Middleware that records HTTP request totals, failures, and latency.
23
+ */
24
+ export declare class HttpMetricsMiddleware implements Middleware {
25
+ private readonly requestsTotal;
26
+ private readonly errorsTotal;
27
+ private readonly requestDuration;
28
+ private readonly pathLabelMode;
29
+ private readonly pathLabelNormalizer?;
30
+ private readonly unknownPathLabel;
31
+ constructor(registry: Registry, options?: HttpMetricsMiddlewareOptions);
32
+ private resolvePathLabel;
33
+ handle(context: MiddlewareContext, next: Next): Promise<void>;
34
+ private resolveStatusCode;
35
+ private recordRequestMetrics;
36
+ }
37
+ //# sourceMappingURL=http-metrics-middleware.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,155 @@
1
+ import { createPrometheusCounter, createPrometheusHistogram } from './providers/prometheus-metrics-factory.js';
2
+
3
+ /** Strategy used to label request paths in emitted HTTP metrics. */
4
+
5
+ /** Context passed to a custom path-label normalizer. */
6
+
7
+ /** Callback that resolves the final path label used in emitted metrics. */
8
+
9
+ /** Options that tune HTTP request metric label generation. */
10
+
11
+ function readErrorStatusCode(error) {
12
+ if (typeof error !== 'object' || error === null) {
13
+ return undefined;
14
+ }
15
+ const candidate = error;
16
+ const fromStatus = typeof candidate.status === 'number' ? candidate.status : undefined;
17
+ const fromStatusCode = typeof candidate.statusCode === 'number' ? candidate.statusCode : undefined;
18
+ if (fromStatus !== undefined && Number.isFinite(fromStatus)) {
19
+ return fromStatus;
20
+ }
21
+ if (fromStatusCode !== undefined && Number.isFinite(fromStatusCode)) {
22
+ return fromStatusCode;
23
+ }
24
+ return undefined;
25
+ }
26
+
27
+ /**
28
+ * Middleware that records HTTP request totals, failures, and latency.
29
+ */
30
+ export class HttpMetricsMiddleware {
31
+ requestsTotal;
32
+ errorsTotal;
33
+ requestDuration;
34
+ pathLabelMode;
35
+ pathLabelNormalizer;
36
+ unknownPathLabel;
37
+ constructor(registry, options = {}) {
38
+ if (options.pathLabelMode === 'raw' && options.allowUnsafeRawPathLabelMode !== true) {
39
+ throw new Error('HttpMetricsMiddleware pathLabelMode "raw" is disabled by default. Pass allowUnsafeRawPathLabelMode: true only when you have bounded path cardinality.');
40
+ }
41
+ this.pathLabelMode = options.pathLabelMode ?? 'template';
42
+ this.pathLabelNormalizer = options.pathLabelNormalizer;
43
+ this.unknownPathLabel = options.unknownPathLabel ?? 'UNKNOWN';
44
+ this.requestsTotal = createPrometheusCounter(registry, {
45
+ help: 'Total number of HTTP requests',
46
+ labelNames: ['method', 'path', 'status'],
47
+ name: 'http_requests_total'
48
+ });
49
+ this.errorsTotal = createPrometheusCounter(registry, {
50
+ help: 'Total number of HTTP error responses (4xx/5xx)',
51
+ labelNames: ['method', 'path', 'status'],
52
+ name: 'http_errors_total'
53
+ });
54
+ this.requestDuration = createPrometheusHistogram(registry, {
55
+ help: 'HTTP request duration in seconds',
56
+ labelNames: ['method', 'path', 'status'],
57
+ name: 'http_request_duration_seconds'
58
+ });
59
+ }
60
+ resolvePathLabel(request) {
61
+ if (this.pathLabelNormalizer) {
62
+ const normalized = this.pathLabelNormalizer({
63
+ method: request.method,
64
+ params: request.params,
65
+ path: request.path,
66
+ request
67
+ });
68
+ return normalized.trim() || this.unknownPathLabel;
69
+ }
70
+ if (this.pathLabelMode === 'raw') {
71
+ return request.path;
72
+ }
73
+ const normalized = normalizePathToTemplate(request.path, request.params);
74
+ return normalized || this.unknownPathLabel;
75
+ }
76
+ async handle(context, next) {
77
+ const start = performance.now();
78
+ const method = context.request.method;
79
+ const path = this.resolvePathLabel(context.request);
80
+ let requestError;
81
+ try {
82
+ await next();
83
+ } catch (error) {
84
+ requestError = error;
85
+ throw error;
86
+ } finally {
87
+ const durationSeconds = (performance.now() - start) / 1000;
88
+ this.recordRequestMetrics(method, path, this.resolveStatusCode(context.response.statusCode, requestError), durationSeconds, requestError);
89
+ }
90
+ }
91
+ resolveStatusCode(responseStatusCode, requestError) {
92
+ if (responseStatusCode !== undefined) {
93
+ return responseStatusCode;
94
+ }
95
+ if (requestError === undefined) {
96
+ return 200;
97
+ }
98
+ return readErrorStatusCode(requestError) ?? 500;
99
+ }
100
+ recordRequestMetrics(method, path, statusCode, durationSeconds, requestError) {
101
+ const baseLabels = {
102
+ method,
103
+ path,
104
+ status: String(statusCode)
105
+ };
106
+ const requestLabels = {
107
+ ...baseLabels
108
+ };
109
+ const errorLabels = statusCode >= 400 || requestError !== undefined ? {
110
+ ...baseLabels
111
+ } : undefined;
112
+ this.requestsTotal.inc(requestLabels);
113
+ this.requestDuration.observe(baseLabels, durationSeconds);
114
+ if (errorLabels) {
115
+ this.errorsTotal.inc(errorLabels);
116
+ }
117
+ }
118
+ }
119
+ function normalizePathToTemplate(path, params) {
120
+ if (!path) {
121
+ return '/';
122
+ }
123
+ const normalizedSegments = [];
124
+ const paramEntries = Object.entries(params);
125
+ const usedParamKeys = new Set();
126
+ for (const segment of path.split('/')) {
127
+ if (!segment) {
128
+ continue;
129
+ }
130
+ const decoded = safeDecodeURIComponent(segment);
131
+ let normalizedSegment = segment;
132
+ for (const [paramKey, paramValue] of paramEntries) {
133
+ if (usedParamKeys.has(paramKey)) {
134
+ continue;
135
+ }
136
+ if (segment === paramValue || decoded === paramValue) {
137
+ normalizedSegment = `:${paramKey}`;
138
+ usedParamKeys.add(paramKey);
139
+ break;
140
+ }
141
+ }
142
+ normalizedSegments.push(normalizedSegment);
143
+ }
144
+ if (normalizedSegments.length === 0) {
145
+ return '/';
146
+ }
147
+ return `/${normalizedSegments.join('/')}`;
148
+ }
149
+ function safeDecodeURIComponent(value) {
150
+ try {
151
+ return decodeURIComponent(value);
152
+ } catch {
153
+ return value;
154
+ }
155
+ }
@@ -0,0 +1,7 @@
1
+ export { Registry } from 'prom-client';
2
+ export * from './metrics-module.js';
3
+ export * from './metrics-service.js';
4
+ export * from './providers/meter-provider.js';
5
+ export * from './providers/prometheus-meter-provider.js';
6
+ export * from './http-metrics-middleware.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { Registry } from 'prom-client';
2
+ export * from './metrics-module.js';
3
+ export * from './metrics-service.js';
4
+ export * from './providers/meter-provider.js';
5
+ export * from './providers/prometheus-meter-provider.js';
6
+ export * from './http-metrics-middleware.js';
@@ -0,0 +1,48 @@
1
+ import { type Middleware, type MiddlewareLike } from '@fluojs/http';
2
+ import { type ModuleType } from '@fluojs/runtime';
3
+ import { type Registry } from 'prom-client';
4
+ import { type HttpMetricsPathLabelMode, type HttpMetricsPathLabelNormalizer } from './http-metrics-middleware.js';
5
+ /** HTTP-specific metric labeling options exposed by `MetricsModule.forRoot(...)`. */
6
+ export interface MetricsHttpOptions {
7
+ pathLabelMode?: HttpMetricsPathLabelMode;
8
+ pathLabelNormalizer?: HttpMetricsPathLabelNormalizer;
9
+ unknownPathLabel?: string;
10
+ allowUnsafeRawPathLabelMode?: boolean;
11
+ }
12
+ /**
13
+ * Module options for exposing Prometheus metrics and runtime platform telemetry.
14
+ */
15
+ export interface MetricsModuleOptions {
16
+ http?: boolean | MetricsHttpOptions;
17
+ path?: string | false;
18
+ provider?: 'prometheus';
19
+ defaultMetrics?: boolean;
20
+ middleware?: MiddlewareLike[];
21
+ endpointMiddleware?: Array<new (...args: any[]) => Middleware>;
22
+ platformTelemetry?: {
23
+ env?: string;
24
+ instance?: string;
25
+ };
26
+ /** External Prometheus registry to share between built-in and custom metrics. */
27
+ registry?: Registry;
28
+ }
29
+ /** Module entry point that exposes `/metrics` and optional HTTP/runtime telemetry. */
30
+ export declare class MetricsModule {
31
+ private static registeredRegistries;
32
+ /**
33
+ * Register framework metrics, optional HTTP middleware, and a scrape endpoint.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * MetricsModule.forRoot({
38
+ * http: { pathLabelMode: 'template' },
39
+ * registry: new Registry(),
40
+ * });
41
+ * ```
42
+ *
43
+ * @param options Metrics endpoint, registry, HTTP middleware, and runtime telemetry configuration.
44
+ * @returns A runtime module that exposes metrics through the configured path.
45
+ */
46
+ static forRoot(options?: MetricsModuleOptions): ModuleType;
47
+ }
48
+ //# sourceMappingURL=metrics-module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics-module.d.ts","sourceRoot":"","sources":["../src/metrics-module.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,UAAU,EAAE,KAAK,cAAc,EAAuB,MAAM,cAAc,CAAC;AACrH,OAAO,EAAgB,KAAK,UAAU,EAA8C,MAAM,iBAAiB,CAAC;AAC5G,OAAO,EAAgE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE1G,OAAO,EAGL,KAAK,wBAAwB,EAC7B,KAAK,8BAA8B,EACpC,MAAM,8BAA8B,CAAC;AAKtC,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,mBAAmB,CAAC,EAAE,8BAA8B,CAAC;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,OAAO,GAAG,kBAAkB,CAAC;IACpC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACtB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAC9B,kBAAkB,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,CAAC,CAAC;IAC/D,iBAAiB,CAAC,EAAE;QAClB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,iFAAiF;IACjF,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,sFAAsF;AACtF,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAA2B;IAE9D;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,UAAU;CAqE/D"}
@@ -0,0 +1,277 @@
1
+ function _applyDecs(e, t, n, r, o, i) { var a, c, u, s, f, l, p, d = Symbol.metadata || Symbol.for("Symbol.metadata"), m = Object.defineProperty, h = Object.create, y = [h(null), h(null)], v = t.length; function g(t, n, r) { return function (o, i) { n && (i = o, o = e); for (var a = 0; a < t.length; a++) i = t[a].apply(o, r ? [i] : []); return r ? i : o; }; } function b(e, t, n, r) { if ("function" != typeof e && (r || void 0 !== e)) throw new TypeError(t + " must " + (n || "be") + " a function" + (r ? "" : " or undefined")); return e; } function applyDec(e, t, n, r, o, i, u, s, f, l, p) { function d(e) { if (!p(e)) throw new TypeError("Attempted to access private element on non-instance"); } var h = [].concat(t[0]), v = t[3], w = !u, D = 1 === o, S = 3 === o, j = 4 === o, E = 2 === o; function I(t, n, r) { return function (o, i) { return n && (i = o, o = e), r && r(o), P[t].call(o, i); }; } if (!w) { var P = {}, k = [], F = S ? "get" : j || D ? "set" : "value"; if (f ? (l || D ? P = { get: _setFunctionName(function () { return v(this); }, r, "get"), set: function (e) { t[4](this, e); } } : P[F] = v, l || _setFunctionName(P[F], r, E ? "" : F)) : l || (P = Object.getOwnPropertyDescriptor(e, r)), !l && !f) { if ((c = y[+s][r]) && 7 !== (c ^ o)) throw Error("Decorating two elements with the same name (" + P[F].name + ") is not supported yet"); y[+s][r] = o < 3 ? 1 : o; } } for (var N = e, O = h.length - 1; O >= 0; O -= n ? 2 : 1) { var T = b(h[O], "A decorator", "be", !0), z = n ? h[O - 1] : void 0, A = {}, H = { kind: ["field", "accessor", "method", "getter", "setter", "class"][o], name: r, metadata: a, addInitializer: function (e, t) { if (e.v) throw new TypeError("attempted to call addInitializer after decoration was finished"); b(t, "An initializer", "be", !0), i.push(t); }.bind(null, A) }; if (w) c = T.call(z, N, H), A.v = 1, b(c, "class decorators", "return") && (N = c);else if (H.static = s, H.private = f, c = H.access = { has: f ? p.bind() : function (e) { return r in e; } }, j || (c.get = f ? E ? function (e) { return d(e), P.value; } : I("get", 0, d) : function (e) { return e[r]; }), E || S || (c.set = f ? I("set", 0, d) : function (e, t) { e[r] = t; }), N = T.call(z, D ? { get: P.get, set: P.set } : P[F], H), A.v = 1, D) { if ("object" == typeof N && N) (c = b(N.get, "accessor.get")) && (P.get = c), (c = b(N.set, "accessor.set")) && (P.set = c), (c = b(N.init, "accessor.init")) && k.unshift(c);else if (void 0 !== N) throw new TypeError("accessor decorators must return an object with get, set, or init properties or undefined"); } else b(N, (l ? "field" : "method") + " decorators", "return") && (l ? k.unshift(N) : P[F] = N); } return o < 2 && u.push(g(k, s, 1), g(i, s, 0)), l || w || (f ? D ? u.splice(-1, 0, I("get", s), I("set", s)) : u.push(E ? P[F] : b.call.bind(P[F])) : m(e, r, P)), N; } function w(e) { return m(e, d, { configurable: !0, enumerable: !0, value: a }); } return void 0 !== i && (a = i[d]), a = h(null == a ? null : a), f = [], l = function (e) { e && f.push(g(e)); }, p = function (t, r) { for (var i = 0; i < n.length; i++) { var a = n[i], c = a[1], l = 7 & c; if ((8 & c) == t && !l == r) { var p = a[2], d = !!a[3], m = 16 & c; applyDec(t ? e : e.prototype, a, m, d ? "#" + p : _toPropertyKey(p), l, l < 2 ? [] : t ? s = s || [] : u = u || [], f, !!t, d, r, t && d ? function (t) { return _checkInRHS(t) === e; } : o); } } }, p(8, 0), p(0, 0), p(8, 1), p(0, 1), l(u), l(s), c = f, v || w(e), { e: c, get c() { var n = []; return v && [w(e = applyDec(e, [t], r, e.name, 5, n)), g(n, 1)]; } }; }
2
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
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
+ 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
+ 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 { ContainerResolutionError } from '@fluojs/di';
7
+ import { Controller, Get, forRoutes } from '@fluojs/http';
8
+ import { defineModule, PLATFORM_SHELL } from '@fluojs/runtime';
9
+ import { collectDefaultMetrics, Gauge, Registry as PrometheusRegistry } from 'prom-client';
10
+ import { HttpMetricsMiddleware } from './http-metrics-middleware.js';
11
+ import { METER_PROVIDER } from './providers/meter-provider.js';
12
+ import { MetricsService } from './metrics-service.js';
13
+ import { PrometheusMeterProvider } from './providers/prometheus-meter-provider.js';
14
+
15
+ /** HTTP-specific metric labeling options exposed by `MetricsModule.forRoot(...)`. */
16
+
17
+ /**
18
+ * Module options for exposing Prometheus metrics and runtime platform telemetry.
19
+ */
20
+
21
+ /** Module entry point that exposes `/metrics` and optional HTTP/runtime telemetry. */
22
+ export class MetricsModule {
23
+ static registeredRegistries = new WeakSet();
24
+
25
+ /**
26
+ * Register framework metrics, optional HTTP middleware, and a scrape endpoint.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * MetricsModule.forRoot({
31
+ * http: { pathLabelMode: 'template' },
32
+ * registry: new Registry(),
33
+ * });
34
+ * ```
35
+ *
36
+ * @param options Metrics endpoint, registry, HTTP middleware, and runtime telemetry configuration.
37
+ * @returns A runtime module that exposes metrics through the configured path.
38
+ */
39
+ static forRoot(options = {}) {
40
+ const provider = options.provider ?? 'prometheus';
41
+ if (provider !== 'prometheus') {
42
+ throw new Error(`MetricsModule provider "${provider}" is not supported. Use provider "prometheus".`);
43
+ }
44
+ const httpOptions = resolveHttpOptions(options.http);
45
+ const metricsPath = options.path === undefined ? '/metrics' : options.path;
46
+ const registry = options.registry ?? new PrometheusRegistry();
47
+ const metricsService = new MetricsService(registry);
48
+ const meterProvider = new PrometheusMeterProvider(registry);
49
+ const platformTelemetry = new RuntimePlatformTelemetry(registry, options.registry ? 'shared' : 'isolated', options.platformTelemetry);
50
+ if (options.defaultMetrics !== false && !MetricsModule.registeredRegistries.has(registry)) {
51
+ MetricsModule.registeredRegistries.add(registry);
52
+ collectDefaultMetrics({
53
+ register: registry
54
+ });
55
+ }
56
+ const endpointMiddleware = metricsPath ? (options.endpointMiddleware ?? []).map(middlewareClass => forRoutes(middlewareClass, metricsPath)) : [];
57
+ const middleware = [...endpointMiddleware, ...(httpOptions ? [new HttpMetricsMiddleware(registry, httpOptions)] : []), ...(options.middleware ?? [])];
58
+ const providers = [{
59
+ provide: MetricsService,
60
+ useValue: metricsService
61
+ }, {
62
+ provide: METER_PROVIDER,
63
+ useValue: meterProvider
64
+ }];
65
+ const controllers = [];
66
+ if (typeof metricsPath === 'string') {
67
+ let _initProto, _initClass;
68
+ const metricsRoutePath = metricsPath;
69
+ let _MetricsController;
70
+ class MetricsController {
71
+ static {
72
+ ({
73
+ e: [_initProto],
74
+ c: [_MetricsController, _initClass]
75
+ } = _applyDecs(this, [Controller('')], [[Get(metricsRoutePath), 2, "getMetrics"]]));
76
+ }
77
+ constructor() {
78
+ _initProto(this);
79
+ }
80
+ async getMetrics(_input, ctx) {
81
+ ctx.response.setHeader('content-type', registry.contentType);
82
+ return platformTelemetry.collectMetrics(ctx, registry);
83
+ }
84
+ static {
85
+ _initClass();
86
+ }
87
+ }
88
+ controllers.push(_MetricsController);
89
+ }
90
+ class MetricsRuntimeModule {}
91
+ defineModule(MetricsRuntimeModule, {
92
+ controllers,
93
+ middleware,
94
+ providers
95
+ });
96
+ return MetricsRuntimeModule;
97
+ }
98
+ }
99
+ const PLATFORM_COMPONENT_LABELS = ['component_id', 'component_kind', 'operation', 'result', 'env', 'instance'];
100
+ const REGISTRY_MODE_LABELS = ['mode'];
101
+ const HEALTH_STATUSES = ['healthy', 'unhealthy', 'degraded'];
102
+ const READINESS_STATUSES = ['ready', 'not-ready', 'degraded'];
103
+ const PLATFORM_SHELL_TOKEN_NAME = 'PLATFORM_SHELL';
104
+ const PLATFORM_SHELL_TOKEN_NAMES = new Set([PLATFORM_SHELL_TOKEN_NAME, String(PLATFORM_SHELL)]);
105
+ function toReadinessValue(status) {
106
+ return status === 'ready' ? 1 : 0;
107
+ }
108
+ function toHealthValue(status) {
109
+ return status === 'healthy' ? 1 : 0;
110
+ }
111
+ function getOrCreateGauge(registry, config) {
112
+ const existing = registry.getSingleMetric(config.name);
113
+ if (existing instanceof Gauge) {
114
+ return existing;
115
+ }
116
+ return new Gauge({
117
+ help: config.help,
118
+ labelNames: [...config.labelNames],
119
+ name: config.name,
120
+ registers: [registry]
121
+ });
122
+ }
123
+ class RuntimePlatformTelemetry {
124
+ readinessGauge;
125
+ healthGauge;
126
+ registryModeGauge;
127
+ lastHealthStatuses = new Map();
128
+ lastReadinessStatuses = new Map();
129
+ scrapeChain = Promise.resolve();
130
+ constructor(registry, registryMode, labels = {}) {
131
+ this.registryMode = registryMode;
132
+ this.labels = labels;
133
+ this.readinessGauge = getOrCreateGauge(registry, {
134
+ help: 'Runtime platform component readiness from shared platform snapshot semantics.',
135
+ labelNames: PLATFORM_COMPONENT_LABELS,
136
+ name: 'fluo_component_ready'
137
+ });
138
+ this.healthGauge = getOrCreateGauge(registry, {
139
+ help: 'Runtime platform component health from shared platform snapshot semantics.',
140
+ labelNames: PLATFORM_COMPONENT_LABELS,
141
+ name: 'fluo_component_health'
142
+ });
143
+ this.registryModeGauge = getOrCreateGauge(registry, {
144
+ help: 'Metrics module registry mode: isolated or shared.',
145
+ labelNames: REGISTRY_MODE_LABELS,
146
+ name: 'fluo_metrics_registry_mode'
147
+ });
148
+ this.registryModeGauge.labels(this.registryMode).set(1);
149
+ }
150
+ async collectMetrics(ctx, registry) {
151
+ const collect = this.scrapeChain.then(async () => {
152
+ await this.refresh(ctx);
153
+ return registry.metrics();
154
+ });
155
+ this.scrapeChain = collect.then(() => undefined, () => undefined);
156
+ return collect;
157
+ }
158
+ async refresh(ctx) {
159
+ const platformShell = await this.resolvePlatformShell(ctx);
160
+ if (!platformShell) {
161
+ return;
162
+ }
163
+ const snapshot = await platformShell.snapshot();
164
+ const env = this.labels?.env ?? 'unknown';
165
+ const instance = this.labels?.instance ?? 'local';
166
+ const components = [{
167
+ health: snapshot.health.status,
168
+ id: 'runtime.shell',
169
+ kind: 'runtime',
170
+ readiness: snapshot.readiness.status
171
+ }, ...snapshot.components.map(component => ({
172
+ health: component.health.status,
173
+ id: component.id,
174
+ kind: component.kind,
175
+ readiness: component.readiness.status
176
+ }))];
177
+ this.syncGaugeStatuses({
178
+ currentStatuses: new Map(components.map(component => [this.toComponentKey(component.id, component.kind), component.health])),
179
+ env,
180
+ gauge: this.healthGauge,
181
+ instance,
182
+ lastStatuses: this.lastHealthStatuses,
183
+ operation: 'health',
184
+ statuses: HEALTH_STATUSES,
185
+ toMetricValue: toHealthValue
186
+ });
187
+ this.syncGaugeStatuses({
188
+ currentStatuses: new Map(components.map(component => [this.toComponentKey(component.id, component.kind), component.readiness])),
189
+ env,
190
+ gauge: this.readinessGauge,
191
+ instance,
192
+ lastStatuses: this.lastReadinessStatuses,
193
+ operation: 'readiness',
194
+ statuses: READINESS_STATUSES,
195
+ toMetricValue: toReadinessValue
196
+ });
197
+ }
198
+ syncGaugeStatuses({
199
+ currentStatuses,
200
+ env,
201
+ gauge,
202
+ instance,
203
+ lastStatuses,
204
+ operation,
205
+ statuses,
206
+ toMetricValue
207
+ }) {
208
+ for (const [componentKey, previousStatus] of lastStatuses) {
209
+ const nextStatus = currentStatuses.get(componentKey);
210
+ if (nextStatus === previousStatus) {
211
+ continue;
212
+ }
213
+ const [componentId, componentKind] = this.fromComponentKey(componentKey);
214
+ for (const status of statuses) {
215
+ if (status !== previousStatus) {
216
+ continue;
217
+ }
218
+ gauge.remove(componentId, componentKind, operation, status, env, instance);
219
+ }
220
+ }
221
+ for (const [componentKey, currentStatus] of currentStatuses) {
222
+ const [componentId, componentKind] = this.fromComponentKey(componentKey);
223
+ gauge.labels(componentId, componentKind, operation, currentStatus, env, instance).set(toMetricValue(currentStatus));
224
+ }
225
+ lastStatuses.clear();
226
+ for (const [componentKey, currentStatus] of currentStatuses) {
227
+ lastStatuses.set(componentKey, currentStatus);
228
+ }
229
+ }
230
+ fromComponentKey(componentKey) {
231
+ const separatorIndex = componentKey.indexOf('::');
232
+ return [componentKey.slice(0, separatorIndex), componentKey.slice(separatorIndex + 2)];
233
+ }
234
+ toComponentKey(componentId, componentKind) {
235
+ return `${componentId}::${componentKind}`;
236
+ }
237
+ async resolvePlatformShell(ctx) {
238
+ try {
239
+ return await ctx.container.resolve(PLATFORM_SHELL);
240
+ } catch (error) {
241
+ if (isMissingPlatformShellResolutionError(error)) {
242
+ return undefined;
243
+ }
244
+ throw error;
245
+ }
246
+ }
247
+ }
248
+ function isMissingPlatformShellResolutionError(error) {
249
+ if (!(error instanceof ContainerResolutionError)) {
250
+ return false;
251
+ }
252
+ const containerError = error;
253
+ const token = typeof containerError.meta?.['token'] === 'string' ? containerError.meta['token'] : undefined;
254
+ if (token && PLATFORM_SHELL_TOKEN_NAMES.has(token)) {
255
+ return containerError.message.startsWith(`No provider registered for token ${token}.`);
256
+ }
257
+ for (const tokenName of PLATFORM_SHELL_TOKEN_NAMES) {
258
+ if (containerError.message.startsWith(`No provider registered for token ${tokenName}.`)) {
259
+ return true;
260
+ }
261
+ }
262
+ return false;
263
+ }
264
+ function resolveHttpOptions(http) {
265
+ if (!http) {
266
+ return undefined;
267
+ }
268
+ if (http === true) {
269
+ return {};
270
+ }
271
+ return {
272
+ allowUnsafeRawPathLabelMode: http.allowUnsafeRawPathLabelMode,
273
+ pathLabelMode: http.pathLabelMode,
274
+ pathLabelNormalizer: http.pathLabelNormalizer,
275
+ unknownPathLabel: http.unknownPathLabel
276
+ };
277
+ }
@@ -0,0 +1,36 @@
1
+ import type { Registry, CounterConfiguration, GaugeConfiguration, HistogramConfiguration } from 'prom-client';
2
+ /**
3
+ * Small facade for creating custom Prometheus metrics on the module registry.
4
+ */
5
+ export declare class MetricsService {
6
+ private readonly registry;
7
+ constructor(registry: Registry);
8
+ /**
9
+ * Create a counter on the module registry.
10
+ *
11
+ * @param config Prometheus counter configuration.
12
+ * @returns A counter registered on this module's registry.
13
+ */
14
+ counter<T extends string = string>(config: CounterConfiguration<T>): import("prom-client").Counter<T>;
15
+ /**
16
+ * Create a gauge on the module registry.
17
+ *
18
+ * @param config Prometheus gauge configuration.
19
+ * @returns A gauge registered on this module's registry.
20
+ */
21
+ gauge<T extends string = string>(config: GaugeConfiguration<T>): import("prom-client").Gauge<T>;
22
+ /**
23
+ * Create a histogram on the module registry.
24
+ *
25
+ * @param config Prometheus histogram configuration.
26
+ * @returns A histogram registered on this module's registry.
27
+ */
28
+ histogram<T extends string = string>(config: HistogramConfiguration<T>): import("prom-client").Histogram<T>;
29
+ /**
30
+ * Return the underlying Prometheus registry used by the module.
31
+ *
32
+ * @returns The registry backing all framework and custom metrics.
33
+ */
34
+ getRegistry(): Registry;
35
+ }
36
+ //# sourceMappingURL=metrics-service.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,49 @@
1
+ import { createPrometheusCounter, createPrometheusGauge, createPrometheusHistogram } from './providers/prometheus-metrics-factory.js';
2
+
3
+ /**
4
+ * Small facade for creating custom Prometheus metrics on the module registry.
5
+ */
6
+ export class MetricsService {
7
+ constructor(registry) {
8
+ this.registry = registry;
9
+ }
10
+
11
+ /**
12
+ * Create a counter on the module registry.
13
+ *
14
+ * @param config Prometheus counter configuration.
15
+ * @returns A counter registered on this module's registry.
16
+ */
17
+ counter(config) {
18
+ return createPrometheusCounter(this.registry, config);
19
+ }
20
+
21
+ /**
22
+ * Create a gauge on the module registry.
23
+ *
24
+ * @param config Prometheus gauge configuration.
25
+ * @returns A gauge registered on this module's registry.
26
+ */
27
+ gauge(config) {
28
+ return createPrometheusGauge(this.registry, config);
29
+ }
30
+
31
+ /**
32
+ * Create a histogram on the module registry.
33
+ *
34
+ * @param config Prometheus histogram configuration.
35
+ * @returns A histogram registered on this module's registry.
36
+ */
37
+ histogram(config) {
38
+ return createPrometheusHistogram(this.registry, config);
39
+ }
40
+
41
+ /**
42
+ * Return the underlying Prometheus registry used by the module.
43
+ *
44
+ * @returns The registry backing all framework and custom metrics.
45
+ */
46
+ getRegistry() {
47
+ return this.registry;
48
+ }
49
+ }
@@ -0,0 +1,22 @@
1
+ /** Counter facade exposed by the metrics abstraction layer. */
2
+ export interface MeterCounter {
3
+ inc(labels?: Record<string, string | number>, value?: number): void;
4
+ }
5
+ /** Gauge facade exposed by the metrics abstraction layer. */
6
+ export interface MeterGauge {
7
+ set(value: number, labels?: Record<string, string | number>): void;
8
+ }
9
+ /** Histogram facade exposed by the metrics abstraction layer. */
10
+ export interface MeterHistogram {
11
+ observe(value: number, labels?: Record<string, string | number>): void;
12
+ }
13
+ /** Provider interface that creates concrete metric instruments for one backend. */
14
+ export interface MeterProvider {
15
+ readonly type: 'prometheus' | 'otel' | string;
16
+ createCounter(name: string, help: string, labelNames?: string[]): MeterCounter;
17
+ createGauge(name: string, help: string, labelNames?: string[]): MeterGauge;
18
+ createHistogram(name: string, help: string, labelNames?: string[], buckets?: number[]): MeterHistogram;
19
+ }
20
+ /** Dependency-injection token for the active framework meter provider. */
21
+ export declare const METER_PROVIDER: unique symbol;
22
+ //# sourceMappingURL=meter-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meter-provider.d.ts","sourceRoot":"","sources":["../../src/providers/meter-provider.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrE;AAED,6DAA6D;AAC7D,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC;CACpE;AAED,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC;CACxE;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;IAC9C,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,YAAY,CAAC;IAC/E,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC;IAC3E,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC;CACxG;AAED,0EAA0E;AAC1E,eAAO,MAAM,cAAc,eAA4C,CAAC"}
@@ -0,0 +1,10 @@
1
+ /** Counter facade exposed by the metrics abstraction layer. */
2
+
3
+ /** Gauge facade exposed by the metrics abstraction layer. */
4
+
5
+ /** Histogram facade exposed by the metrics abstraction layer. */
6
+
7
+ /** Provider interface that creates concrete metric instruments for one backend. */
8
+
9
+ /** Dependency-injection token for the active framework meter provider. */
10
+ export const METER_PROVIDER = Symbol.for('fluo.metrics.meter-provider');
@@ -0,0 +1,12 @@
1
+ import type { Registry } from 'prom-client';
2
+ import type { MeterCounter, MeterGauge, MeterHistogram, MeterProvider } from './meter-provider.js';
3
+ /** Meter-provider implementation that maps the framework abstraction onto Prometheus. */
4
+ export declare class PrometheusMeterProvider implements MeterProvider {
5
+ private readonly registry;
6
+ readonly type: "prometheus";
7
+ constructor(registry: Registry);
8
+ createCounter(name: string, help: string, labelNames?: string[]): MeterCounter;
9
+ createGauge(name: string, help: string, labelNames?: string[]): MeterGauge;
10
+ createHistogram(name: string, help: string, labelNames?: string[], buckets?: number[]): MeterHistogram;
11
+ }
12
+ //# sourceMappingURL=prometheus-meter-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prometheus-meter-provider.d.ts","sourceRoot":"","sources":["../../src/providers/prometheus-meter-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGnG,yFAAyF;AACzF,qBAAa,uBAAwB,YAAW,aAAa;IAG/C,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAFrC,QAAQ,CAAC,IAAI,EAAG,YAAY,CAAU;gBAET,QAAQ,EAAE,QAAQ;IAE/C,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,EAAO,GAAG,YAAY;IAmBlF,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,EAAO,GAAG,UAAU;IAmB9E,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,EAAO,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc;CAmB3G"}
@@ -0,0 +1,58 @@
1
+ import { createPrometheusCounter, createPrometheusGauge, createPrometheusHistogram } from './prometheus-metrics-factory.js';
2
+
3
+ /** Meter-provider implementation that maps the framework abstraction onto Prometheus. */
4
+ export class PrometheusMeterProvider {
5
+ type = 'prometheus';
6
+ constructor(registry) {
7
+ this.registry = registry;
8
+ }
9
+ createCounter(name, help, labelNames = []) {
10
+ const counter = createPrometheusCounter(this.registry, {
11
+ help,
12
+ labelNames,
13
+ name
14
+ });
15
+ return {
16
+ inc(labels, value = 1) {
17
+ if (labels) {
18
+ counter.inc(labels, value);
19
+ return;
20
+ }
21
+ counter.inc(value);
22
+ }
23
+ };
24
+ }
25
+ createGauge(name, help, labelNames = []) {
26
+ const gauge = createPrometheusGauge(this.registry, {
27
+ help,
28
+ labelNames,
29
+ name
30
+ });
31
+ return {
32
+ set(value, labels) {
33
+ if (labels) {
34
+ gauge.set(labels, value);
35
+ return;
36
+ }
37
+ gauge.set(value);
38
+ }
39
+ };
40
+ }
41
+ createHistogram(name, help, labelNames = [], buckets) {
42
+ const histogram = createPrometheusHistogram(this.registry, {
43
+ buckets,
44
+ help,
45
+ labelNames,
46
+ name
47
+ });
48
+ return {
49
+ observe(value, labels) {
50
+ if (labels) {
51
+ histogram.observe(labels, value);
52
+ return;
53
+ }
54
+ histogram.observe(value);
55
+ }
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,26 @@
1
+ import { Counter, Gauge, Histogram, type CounterConfiguration, type GaugeConfiguration, type HistogramConfiguration, type Registry } from 'prom-client';
2
+ /**
3
+ * Create a Prometheus counter bound to the provided registry.
4
+ *
5
+ * @param registry Prometheus registry that should own the created metric.
6
+ * @param config Counter configuration forwarded to `prom-client`.
7
+ * @returns A Prometheus counter registered only on the provided registry.
8
+ */
9
+ export declare function createPrometheusCounter<T extends string = string>(registry: Registry, config: CounterConfiguration<T>): Counter<T>;
10
+ /**
11
+ * Create a Prometheus gauge bound to the provided registry.
12
+ *
13
+ * @param registry Prometheus registry that should own the created metric.
14
+ * @param config Gauge configuration forwarded to `prom-client`.
15
+ * @returns A Prometheus gauge registered only on the provided registry.
16
+ */
17
+ export declare function createPrometheusGauge<T extends string = string>(registry: Registry, config: GaugeConfiguration<T>): Gauge<T>;
18
+ /**
19
+ * Create a Prometheus histogram bound to the provided registry.
20
+ *
21
+ * @param registry Prometheus registry that should own the created metric.
22
+ * @param config Histogram configuration forwarded to `prom-client`.
23
+ * @returns A Prometheus histogram registered only on the provided registry.
24
+ */
25
+ export declare function createPrometheusHistogram<T extends string = string>(registry: Registry, config: HistogramConfiguration<T>): Histogram<T>;
26
+ //# sourceMappingURL=prometheus-metrics-factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prometheus-metrics-factory.d.ts","sourceRoot":"","sources":["../../src/providers/prometheus-metrics-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,KAAK,EACL,SAAS,EACT,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,QAAQ,EACd,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAC/D,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC9B,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAC7D,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC5B,KAAK,CAAC,CAAC,CAAC,CAEV;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EACjE,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAChC,SAAS,CAAC,CAAC,CAAC,CAEd"}
@@ -0,0 +1,43 @@
1
+ import { Counter, Gauge, Histogram } from 'prom-client';
2
+
3
+ /**
4
+ * Create a Prometheus counter bound to the provided registry.
5
+ *
6
+ * @param registry Prometheus registry that should own the created metric.
7
+ * @param config Counter configuration forwarded to `prom-client`.
8
+ * @returns A Prometheus counter registered only on the provided registry.
9
+ */
10
+ export function createPrometheusCounter(registry, config) {
11
+ return new Counter({
12
+ ...config,
13
+ registers: [registry]
14
+ });
15
+ }
16
+
17
+ /**
18
+ * Create a Prometheus gauge bound to the provided registry.
19
+ *
20
+ * @param registry Prometheus registry that should own the created metric.
21
+ * @param config Gauge configuration forwarded to `prom-client`.
22
+ * @returns A Prometheus gauge registered only on the provided registry.
23
+ */
24
+ export function createPrometheusGauge(registry, config) {
25
+ return new Gauge({
26
+ ...config,
27
+ registers: [registry]
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Create a Prometheus histogram bound to the provided registry.
33
+ *
34
+ * @param registry Prometheus registry that should own the created metric.
35
+ * @param config Histogram configuration forwarded to `prom-client`.
36
+ * @returns A Prometheus histogram registered only on the provided registry.
37
+ */
38
+ export function createPrometheusHistogram(registry, config) {
39
+ return new Histogram({
40
+ ...config,
41
+ registers: [registry]
42
+ });
43
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@fluojs/metrics",
3
+ "description": "Prometheus metrics exposure with isolated registries and low-cardinality HTTP metric middleware for Fluo.",
4
+ "keywords": [
5
+ "fluo",
6
+ "metrics",
7
+ "prometheus",
8
+ "monitoring",
9
+ "observability"
10
+ ],
11
+ "version": "1.0.0-beta.1",
12
+ "private": false,
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/fluojs/fluo.git",
17
+ "directory": "packages/metrics"
18
+ },
19
+ "engines": {
20
+ "node": ">=20.0.0"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ }
31
+ },
32
+ "main": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "dependencies": {
38
+ "prom-client": "^15.1.3",
39
+ "@fluojs/di": "^1.0.0-beta.1",
40
+ "@fluojs/runtime": "^1.0.0-beta.1",
41
+ "@fluojs/http": "^1.0.0-beta.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.0.0",
45
+ "vitest": "^3.2.4"
46
+ },
47
+ "scripts": {
48
+ "prebuild": "node ../../tooling/scripts/clean-dist.mjs",
49
+ "build": "pnpm exec babel src --extensions .ts --ignore 'src/**/*.test.ts' --out-dir dist --config-file ../../tooling/babel/babel.config.cjs && pnpm exec tsc -p tsconfig.build.json",
50
+ "typecheck": "pnpm exec tsc -p tsconfig.json --noEmit",
51
+ "test": "pnpm exec vitest run -c vitest.config.ts",
52
+ "test:watch": "pnpm exec vitest -c vitest.config.ts"
53
+ }
54
+ }