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

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
@@ -28,7 +28,7 @@ pnpm add @fluojs/metrics
28
28
 
29
29
  ## 빠른 시작
30
30
 
31
- 루트 모듈에 `MetricsModule.forRoot()`를 추가하여 기본 `/metrics` 엔드포인트를 활성화합니다.
31
+ 루트 모듈에 `MetricsModule.forRoot()`를 추가하여 기본 `/metrics` 엔드포인트를 활성화합니다. HTTP 요청 수와 지연 시간까지 계측하려면 `http: true` 또는 `http` 옵션 객체를 함께 전달합니다.
32
32
 
33
33
  ```typescript
34
34
  import { Module } from '@fluojs/core';
@@ -36,7 +36,7 @@ import { MetricsModule } from '@fluojs/metrics';
36
36
 
37
37
  @Module({
38
38
  imports: [
39
- MetricsModule.forRoot(),
39
+ MetricsModule.forRoot({ http: true }),
40
40
  ],
41
41
  })
42
42
  class AppModule {}
@@ -44,7 +44,7 @@ class AppModule {}
44
44
  // GET /metrics → Prometheus 텍스트 형식
45
45
  ```
46
46
 
47
- `MetricsModule.forRoot()`는 기본적으로 `GET /metrics`를 노출합니다. 운영 환경에서는 이 경계를 명시적으로 다루세요. 플랫폼 프록시/네트워크 제어를 붙이기 전까지 `path: false`로 비활성화하거나, 전용 endpoint middleware를 연결하는 방식을 권장합니다.
47
+ `MetricsModule.forRoot()`는 기본적으로 `GET /metrics`를 노출합니다. `http: true`를 전달한 경우에만 HTTP 요청 계측 미들웨어가 설치됩니다. 운영 환경에서는 이 경계를 명시적으로 다루세요. 플랫폼 프록시/네트워크 제어를 붙이기 전까지 `path: false`로 비활성화하거나, 전용 endpoint middleware를 연결하는 방식을 권장합니다.
48
48
 
49
49
  ## 공통 패턴
50
50
 
@@ -116,12 +116,18 @@ const ordersTotal = new Counter({
116
116
 
117
117
  @Module({
118
118
  imports: [
119
- MetricsModule.forRoot({ registry: sharedRegistry }),
119
+ MetricsModule.forRoot({ http: true, registry: sharedRegistry }),
120
120
  ],
121
121
  })
122
122
  class AppModule {}
123
123
  ```
124
124
 
125
+ 여러 `MetricsModule` 인스턴스가 같은 Registry를 의도적으로 공유하는 경우, 내장 HTTP 메트릭은 기존 `http_requests_total`, `http_errors_total`, `http_request_duration_seconds` collector를 재사용합니다. 애플리케이션이 직접 등록한 중복 메트릭 이름은 Prometheus Registry 규칙대로 계속 빠르게 실패합니다.
126
+
127
+ ### 중복 메트릭 이름은 계속 빠르게 실패합니다
128
+
129
+ Prometheus 메트릭 이름은 하나의 Registry 안에서 고유해야 합니다. 공유 Registry 모드는 애플리케이션 메트릭의 중복 이름을 조용히 덮어쓰지 않고 이 동작을 유지합니다.
130
+
125
131
  ### 런타임 플랫폼 텔레메트리
126
132
 
127
133
  이 모듈은 플랫폼 셸 및 등록된 컴포넌트의 내부 상태를 반영하는 fluo 전용 Gauge를 자동으로 생성합니다.
@@ -172,7 +178,8 @@ MetricsModule.forRoot({
172
178
  - `path`의 기본값은 `'/metrics'`이며, `path: false`로 스크레이프 엔드포인트를 완전히 비활성화할 수 있습니다.
173
179
  - `defaultMetrics`의 기본값은 `true`이며, `defaultMetrics: false`로 해당 Registry의 Prometheus 기본 프로세스/Node.js collector를 끌 수 있습니다.
174
180
  - `endpointMiddleware`는 스크레이프 엔드포인트에만 route-scoped middleware를 바인딩합니다.
175
- - HTTP 메트릭은 기본적으로 템플릿 기반 경로 라벨 정규화를 사용합니다.
181
+ - HTTP 메트릭은 `http: true` 또는 `http` 옵션 객체를 전달한 경우에만 설치되며, 설치된 뒤에는 기본적으로 템플릿 기반 경로 라벨 정규화를 사용합니다.
182
+ - 내장 HTTP collector는 같은 Registry를 공유하는 모듈 인스턴스 사이에서 재사용되며, 커스텀 애플리케이션 메트릭 이름 충돌은 Prometheus의 중복 이름 실패 동작을 유지합니다.
176
183
  - raw path 라벨은 `allowUnsafeRawPathLabelMode: true`를 명시한 bounded internal route에서만 사용해야 합니다.
177
184
  - 플랫폼 텔레메트리는 `PLATFORM_SHELL`이 실제로 누락된 경우에만 생략되며, 그 외 resolve 실패는 스크레이프를 실패시킵니다.
178
185
  - 이전에 노출된 플랫폼 텔레메트리 시리즈는 `PLATFORM_SHELL`을 사용할 수 없게 된 스크레이프에서 제거됩니다.
package/README.md CHANGED
@@ -33,12 +33,12 @@ 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()` still exposes `GET /metrics` by default. Pass `http: true` (or an `http` options object) when you want the module to install HTTP request instrumentation middleware. 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
42
 
43
43
  ## Common Patterns
44
44
 
@@ -94,11 +94,13 @@ new Counter({
94
94
  });
95
95
 
96
96
  @Module({
97
- imports: [MetricsModule.forRoot({ registry })],
97
+ imports: [MetricsModule.forRoot({ http: true, registry })],
98
98
  })
99
99
  class AppModule {}
100
100
  ```
101
101
 
102
+ 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. Application-defined duplicate names still fail fast.
103
+
102
104
  ### Duplicate metric names still fail fast
103
105
 
104
106
  Prometheus metric names must stay unique inside a registry. Shared-registry mode keeps that behavior intact instead of silently shadowing metrics.
@@ -153,7 +155,8 @@ MetricsModule.forRoot({
153
155
  - `path` defaults to `'/metrics'`, and `path: false` disables the scrape endpoint entirely.
154
156
  - `defaultMetrics` defaults to `true`, and `defaultMetrics: false` disables Prometheus default process and Node.js collectors for that registry.
155
157
  - `endpointMiddleware` binds route-scoped middleware only to the scrape endpoint.
156
- - HTTP metrics default to template-normalized path labels.
158
+ - HTTP metrics are installed only when `http: true` or an `http` options object is provided, and then default to template-normalized path labels.
159
+ - Built-in HTTP collectors are reused when module instances share one registry; custom application metric name collisions keep Prometheus' duplicate-name failure behavior.
157
160
  - Raw path labels require `allowUnsafeRawPathLabelMode: true` and should stay limited to bounded internal routes.
158
161
  - Platform telemetry is omitted only when `PLATFORM_SHELL` is genuinely missing; other resolution failures fail the scrape.
159
162
  - Stale platform telemetry series are removed when `PLATFORM_SHELL` becomes unavailable after a prior successful scrape.
@@ -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';
@@ -285,12 +285,13 @@ function isMissingPlatformShellResolutionError(error) {
285
285
  return false;
286
286
  }
287
287
  const containerError = error;
288
+ const message = String(containerError.message ?? '');
288
289
  const token = typeof containerError.meta?.['token'] === 'string' ? containerError.meta['token'] : undefined;
289
290
  if (token && PLATFORM_SHELL_TOKEN_NAMES.has(token)) {
290
- return containerError.message.startsWith(`No provider registered for token ${token}.`);
291
+ return message.startsWith(`No provider registered for token ${token}.`);
291
292
  }
292
293
  for (const tokenName of PLATFORM_SHELL_TOKEN_NAMES) {
293
- if (containerError.message.startsWith(`No provider registered for token ${tokenName}.`)) {
294
+ if (message.startsWith(`No provider registered for token ${tokenName}.`)) {
294
295
  return true;
295
296
  }
296
297
  }
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.3",
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.6",
40
+ "@fluojs/http": "^1.0.0-beta.9",
41
+ "@fluojs/runtime": "^1.0.0-beta.9"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^22.0.0",