🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@fluojs/metrics

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fluojs/metrics - npm Package Compare versions

Comparing version
1.0.3
to
1.0.4
+1
-0
dist/metrics-module.d.ts

@@ -60,3 +60,4 @@ import { type Middleware, type MiddlewareLike } from '@fluojs/http';

static forRoot(options?: MetricsModuleOptions): ModuleType;
private static createRegistry;
}
//# sourceMappingURL=metrics-module.d.ts.map
+1
-1

@@ -1,1 +0,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,iGAAiG;IACjG,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,kFAAkF;IAClF,mBAAmB,CAAC,EAAE,8BAA8B,CAAC;IACrD,+DAA+D;IAC/D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,kGAAkG;IAClG,IAAI,CAAC,EAAE,OAAO,GAAG,kBAAkB,CAAC;IACpC,yHAAyH;IACzH,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,sHAAsH;IACtH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mGAAmG;IACnG,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAC9B,uGAAuG;IACvG,kBAAkB,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,CAAC,CAAC;IAC/D,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE;QAClB,iEAAiE;QACjE,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,iDAAiD;QACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,iFAAiF;IACjF,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,sFAAsF;AACtF,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAA2B;IAE9D;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,UAAU;CAqE/D"}
{"version":3,"file":"metrics-module.d.ts","sourceRoot":"","sources":["../src/metrics-module.ts"],"names":[],"mappings":"AAEA,OAAO,EAA8B,KAAK,UAAU,EAAE,KAAK,cAAc,EAAuB,MAAM,cAAc,CAAC;AACrH,OAAO,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,iGAAiG;IACjG,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,kFAAkF;IAClF,mBAAmB,CAAC,EAAE,8BAA8B,CAAC;IACrD,+DAA+D;IAC/D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,kGAAkG;IAClG,IAAI,CAAC,EAAE,OAAO,GAAG,kBAAkB,CAAC;IACpC,yHAAyH;IACzH,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,sHAAsH;IACtH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mGAAmG;IACnG,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAC9B,uGAAuG;IACvG,kBAAkB,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,CAAC,CAAC;IAC/D,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE;QAClB,iEAAiE;QACjE,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,iDAAiD;QACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,iFAAiF;IACjF,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,sFAAsF;AACtF,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAA2B;IAE9D;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,UAAU;IAqF9D,OAAO,CAAC,MAAM,CAAC,cAAc;CAU9B"}

@@ -6,2 +6,3 @@ 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)]; } }; }

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; }
import { Inject } from '@fluojs/core';
import { ContainerResolutionError } from '@fluojs/di';

@@ -47,20 +48,22 @@ import { Controller, Get, forRoutes } from '@fluojs/http';

const metricsPath = options.path === undefined ? '/metrics' : options.path;
const registry = options.registry ?? new PrometheusRegistry();
const metricsService = new MetricsService(registry);
const meterProvider = new PrometheusMeterProvider(registry);
const platformTelemetry = new RuntimePlatformTelemetry(registry, options.registry ? 'shared' : 'isolated', options.platformTelemetry);
if (options.defaultMetrics !== false && !MetricsModule.registeredRegistries.has(registry)) {
MetricsModule.registeredRegistries.add(registry);
collectDefaultMetrics({
register: registry
});
}
const endpointMiddleware = metricsPath ? (options.endpointMiddleware ?? []).map(middlewareClass => forRoutes(middlewareClass, metricsPath)) : [];
const middleware = [...(httpOptions ? [new HttpMetricsMiddleware(registry, httpOptions)] : []), ...endpointMiddleware, ...(options.middleware ?? [])];
const providers = [{
const registryToken = Symbol('MetricsModule.registry');
const platformTelemetryToken = Symbol('MetricsModule.platformTelemetry');
const httpMetricsMiddleware = httpOptions ? createHttpMetricsMiddleware(registryToken, httpOptions) : undefined;
const endpointMiddleware = typeof metricsPath === 'string' ? (options.endpointMiddleware ?? []).map(middlewareClass => forRoutes(middlewareClass, metricsPath)) : [];
const middleware = [...(httpMetricsMiddleware ? [httpMetricsMiddleware] : []), ...endpointMiddleware, ...(options.middleware ?? [])];
const providers = [...(httpMetricsMiddleware ? [httpMetricsMiddleware] : []), {
provide: registryToken,
useFactory: () => MetricsModule.createRegistry(options)
}, {
provide: MetricsService,
useValue: metricsService
inject: [registryToken],
useFactory: registry => new MetricsService(assertPrometheusRegistry(registry))
}, {
provide: METER_PROVIDER,
useValue: meterProvider
inject: [registryToken],
useFactory: registry => new PrometheusMeterProvider(assertPrometheusRegistry(registry))
}, {
provide: platformTelemetryToken,
inject: [registryToken],
useFactory: registry => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), options.registry ? 'shared' : 'isolated', options.platformTelemetry)
}];

@@ -77,10 +80,12 @@ const controllers = [];

c: [_MetricsController, _initClass]
} = _applyDecs(this, [Controller('')], [[Get(metricsRoutePath), 2, "getMetrics"]]));
} = _applyDecs(this, [Inject(registryToken, platformTelemetryToken), Controller('')], [[Get(metricsRoutePath), 2, "getMetrics"]]));
}
constructor() {
constructor(registry, platformTelemetry) {
this.registry = registry;
this.platformTelemetry = platformTelemetry;
_initProto(this);
}
async getMetrics(_input, ctx) {
ctx.response.setHeader('content-type', registry.contentType);
return platformTelemetry.collectMetrics(ctx, registry);
ctx.response.setHeader('content-type', this.registry.contentType);
return this.platformTelemetry.collectMetrics(ctx, this.registry);
}

@@ -101,2 +106,12 @@ static {

}
static createRegistry(options) {
const registry = options.registry ?? new PrometheusRegistry();
if (options.defaultMetrics !== false && !MetricsModule.registeredRegistries.has(registry)) {
MetricsModule.registeredRegistries.add(registry);
collectDefaultMetrics({
register: registry
});
}
return registry;
}
}

@@ -110,2 +125,28 @@ const PLATFORM_COMPONENT_LABELS = ['component_id', 'component_kind', 'operation', 'result', 'env', 'instance'];

const PLATFORM_SHELL_TOKEN_NAMES = new Set([PLATFORM_SHELL_TOKEN_NAME, String(PLATFORM_SHELL)]);
function createHttpMetricsMiddleware(registryToken, httpOptions) {
let _initClass2;
let _MetricsHttpMiddlewar;
class MetricsHttpMiddleware {
static {
[_MetricsHttpMiddlewar, _initClass2] = _applyDecs(this, [Inject(registryToken)], []).c;
}
delegate;
constructor(registry) {
this.delegate = new HttpMetricsMiddleware(registry, httpOptions);
}
handle(context, next) {
return this.delegate.handle(context, next);
}
static {
_initClass2();
}
}
return _MetricsHttpMiddlewar;
}
function assertPrometheusRegistry(value) {
if (!(value instanceof PrometheusRegistry)) {
throw new Error('MetricsModule registry provider resolved an invalid Prometheus registry.');
}
return value;
}
function toReadinessValue(status) {

@@ -307,13 +348,5 @@ return status === 'ready' ? 1 : 0;

const containerError = error;
const message = String(containerError.message ?? '');
const token = typeof containerError.meta?.['token'] === 'string' ? containerError.meta['token'] : undefined;
if (token && PLATFORM_SHELL_TOKEN_NAMES.has(token)) {
return message.startsWith(`No provider registered for token ${token}.`);
}
for (const tokenName of PLATFORM_SHELL_TOKEN_NAMES) {
if (message.startsWith(`No provider registered for token ${tokenName}.`)) {
return true;
}
}
return false;
const token = typeof containerError.meta?.token === 'string' ? containerError.meta.token : undefined;
const hint = typeof containerError.meta?.hint === 'string' ? containerError.meta.hint : undefined;
return token !== undefined && PLATFORM_SHELL_TOKEN_NAMES.has(token) && hint === 'Ensure the provider is registered in a module\'s providers array, or that the module exporting it is imported by the consuming module.';
}

@@ -327,2 +360,5 @@ function resolveHttpOptions(http) {

}
if (http.pathLabelMode === 'raw' && http.allowUnsafeRawPathLabelMode !== true) {
throw new Error('HttpMetricsMiddleware pathLabelMode "raw" is disabled by default. Pass allowUnsafeRawPathLabelMode: true only when you have bounded path cardinality.');
}
return {

@@ -329,0 +365,0 @@ allowUnsafeRawPathLabelMode: http.allowUnsafeRawPathLabelMode,

@@ -11,3 +11,3 @@ {

],
"version": "1.0.3",
"version": "1.0.4",
"private": false,

@@ -40,5 +40,6 @@ "license": "MIT",

"prom-client": "^15.1.3",
"@fluojs/di": "^1.0.3",
"@fluojs/http": "^1.1.0",
"@fluojs/runtime": "^1.1.1"
"@fluojs/core": "^1.0.3",
"@fluojs/http": "^1.1.2",
"@fluojs/di": "^1.1.0",
"@fluojs/runtime": "^1.1.8"
},

@@ -45,0 +46,0 @@ "devDependencies": {

@@ -23,2 +23,6 @@ # @fluojs/metrics

## 요구 사항
`@fluojs/metrics`는 Node.js 20 이상에서 실행됩니다. package manifest는 `engines.node >=20.0.0`을 선언합니다.
## 사용 시점

@@ -44,3 +48,3 @@

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`를 전달하세요.
Scrape endpoint는 active `prom-client` Registry output을 해당 Registry의 Prometheus content type으로 반환합니다. `MetricsModule.forRoot()`는 `registry` option을 전달하지 않는 한 application bootstrap마다 격리된 Registry를 생성합니다. 같은 dynamic module class를 다른 bootstrap에서 재사용해도 격리된 metric state는 새로 만들어집니다. framework metric과 application-defined metric이 하나의 scrape surface를 의도적으로 공유해야 할 때만 shared `Registry`를 전달하세요.

@@ -52,6 +56,7 @@ ## 공개 책임

| `MetricsModule.forRoot(...)` | Prometheus scrape endpoint, default metrics, optional HTTP instrumentation, platform telemetry, registry ownership을 wiring합니다. | `provider`는 현재 `'prometheus'`만 받습니다. `path: false`는 scrape route와 route-scoped endpoint middleware를 비활성화합니다. |
| `MetricsService` | Active Registry 위에서 custom `Counter`, `Gauge`, `Histogram`을 만들기 위한 application-facing facade입니다. | 비즈니스/application metric은 package internals 대신 이 서비스를 사용하세요. |
| `METER_PROVIDER` / `PrometheusMeterProvider` | Provider token이 필요한 first-party package integration용 low-level meter bridge입니다. | Application code는 package-level integration을 직접 조합하는 경우가 아니면 보통 이 token이 필요하지 않습니다. |
| `MetricsService` | Active Registry 위에서 custom `Counter`, `Gauge`, `Histogram`을 만드는 application-facing facade이며, 고급 Registry 공유를 위한 `getRegistry()`도 제공합니다. | 비즈니스/application metric은 collector helper를 사용하세요. `getRegistry()`는 active `prom-client` Registry를 `MetricsModule.forRoot({ registry })`로 직접 받을 수 없는 integration에 넘겨야 할 때만 사용하세요. |
| `Registry` | Shared-registry setup을 위한 `prom-client` `Registry` constructor re-export입니다. | 같은 Prometheus Registry 구현체이므로 중복 metric name은 Prometheus semantics에 따라 계속 실패합니다. |
| `METER_PROVIDER` / `PrometheusMeterProvider` / meter type | Provider token 또는 backend-neutral counter/gauge/histogram facade가 필요한 first-party package integration용 low-level meter bridge입니다. | Application code는 package-level integration을 직접 조합하는 경우가 아니면 보통 이 token이 필요하지 않습니다. 현재 bundled provider backend는 Prometheus뿐입니다. |
| `middleware` | Framework HTTP metrics와 endpoint-scoped middleware 뒤의 module middleware chain에 참여하는 module-level middleware입니다. | Route-scoped가 아니므로 scrape route만 보호하려면 `endpointMiddleware`를 사용하세요. |
| `endpointMiddleware` | 설정된 scrape endpoint에만 바인딩되는 class-based `@fluojs/http` middleware constructor입니다. | `path: false`일 때는 무시됩니다. 함수나 global middleware declaration은 이 option의 계약 밖입니다. |
| `endpointMiddleware` | 설정된 scrape endpoint에만 바인딩되는 class-based `@fluojs/http` middleware constructor입니다. | `path: false`일 때만 무시됩니다. `''`를 포함한 모든 문자열 `path`는 활성 endpoint path입니다. 함수나 global middleware declaration은 이 option의 계약 밖입니다. |

@@ -109,5 +114,35 @@ ## 공통 패턴

### Custom metric은 한 번 생성하고 재사용하기
`MetricsService.counter(...)`, `gauge(...)`, `histogram(...)`은 active Registry에 Prometheus collector를 생성합니다. 각 custom metric은 provider construction 또는 application startup 중 한 번만 만들고, business action이 발생할 때는 반환된 collector를 재사용하세요.
```ts
import { Inject } from '@fluojs/core';
import { MetricsService } from '@fluojs/metrics';
@Inject(MetricsService)
class OrdersService {
private readonly ordersCreated: ReturnType<MetricsService['counter']>;
constructor(metrics: MetricsService) {
this.ordersCreated = metrics.counter({
name: 'orders_created_total',
help: 'Total orders created',
});
}
recordOrderCreated(): void {
this.ordersCreated.inc();
}
}
```
같은 이름으로 `MetricsService.counter(...)`를 다시 호출하면 collector를 다시 만들려고 하므로 Prometheus의 duplicate-name failure behavior를 따릅니다. 요청이나 command handler마다 새로 만들지 말고 collector를 저장해 재사용하세요.
`MetricsService.getRegistry()`는 module scrape endpoint, 내장 HTTP collector, platform telemetry, service를 통해 만든 custom collector가 함께 사용하는 동일한 active `prom-client` Registry를 반환합니다. Bootstrap을 직접 소유한다면 `MetricsModule.forRoot({ registry })`에 명시적 `registry`를 전달하는 방식을 우선하세요. `getRegistry()`는 DI로 `MetricsService`를 받은 advanced integration이 이미 활성화된 Registry에 third-party Prometheus collector를 등록해야 할 때 사용합니다.
### Framework metric과 app metric이 하나의 registry를 공유하기
```ts
import { Module } from '@fluojs/core';
import { Counter, Registry } from 'prom-client';

@@ -176,5 +211,6 @@ import { MetricsModule } from '@fluojs/metrics';

- `MetricsModule.forRoot(options)`
- `MetricsService`
- `MetricsService` 및 `counter(...)`, `gauge(...)`, `histogram(...)`, `getRegistry()`
- `METER_PROVIDER` (Token)
- `PrometheusMeterProvider`
- Meter abstraction type: `MeterProvider`, `MeterCounter`, `MeterGauge`, `MeterHistogram`
- `HttpMetricsMiddleware` 및 HTTP path-label 옵션 타입

@@ -186,3 +222,4 @@ - `provider`(현재는 `'prometheus'`만 지원), module-level `middleware`, endpoint-scoped `endpointMiddleware`를 포함한 module option

- `path`의 기본값은 `'/metrics'`이며, `path: false`로 스크레이프 엔드포인트를 완전히 비활성화할 수 있습니다.
- `path`의 기본값은 `'/metrics'`입니다. `''`를 포함한 모든 문자열 path는 scrape endpoint를 노출하며, `path: false`로만 scrape endpoint를 완전히 비활성화할 수 있습니다.
- `registry`를 생략하면 application bootstrap마다 fresh isolated Registry, `MetricsService`, meter provider, telemetry collector set을 소유합니다.
- scrape response는 active Registry의 Prometheus content type과 Registry contents를 사용합니다.

@@ -189,0 +226,0 @@ - `defaultMetrics`의 기본값은 `true`이며, `defaultMetrics: false`로 해당 Registry의 Prometheus 기본 프로세스/Node.js collector를 끌 수 있습니다.

@@ -23,2 +23,6 @@ # @fluojs/metrics

## Requirements
`@fluojs/metrics` runs on Node.js 20 or newer; the package manifest declares `engines.node >=20.0.0`.
## When to Use

@@ -44,3 +48,3 @@

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.
The scrape endpoint returns the active `prom-client` registry output with that registry's Prometheus content type. `MetricsModule.forRoot()` creates an isolated registry for each application bootstrap unless you pass a `registry` option; reusing the same dynamic module class for another bootstrap receives fresh isolated metrics state. Pass a shared `Registry` only when framework metrics and application-defined metrics intentionally share one scrape surface.

@@ -52,6 +56,7 @@ ## Public Responsibilities

| `MetricsModule.forRoot(...)` | Wires the Prometheus scrape endpoint, default metrics, optional HTTP instrumentation, platform telemetry, and registry ownership. | `provider` currently accepts only `'prometheus'`; `path: false` disables the scrape route and route-scoped endpoint middleware. |
| `MetricsService` | Application-facing facade for custom `Counter`, `Gauge`, and `Histogram` metrics on the active registry. | Use this for business/application metrics instead of reaching into package internals. |
| `METER_PROVIDER` / `PrometheusMeterProvider` | Low-level meter bridge for first-party package integrations that need a provider token. | Application code usually does not need this token unless it is composing package-level integrations. |
| `MetricsService` | Application-facing facade for custom `Counter`, `Gauge`, and `Histogram` metrics on the active registry, plus `getRegistry()` for deliberate advanced registry sharing. | Use collector helpers for business/application metrics. Use `getRegistry()` only when an integration must hand the active `prom-client` Registry to code that cannot receive `MetricsModule.forRoot({ registry })` directly. |
| `Registry` | Re-export of `prom-client`'s `Registry` constructor for shared-registry setups. | It is the same Prometheus registry implementation; duplicate metric names still fail according to Prometheus semantics. |
| `METER_PROVIDER` / `PrometheusMeterProvider` / meter types | Low-level meter bridge for first-party package integrations that need a provider token or backend-neutral counter/gauge/histogram facade. | Application code usually does not need this token unless it is composing package-level integrations; the only bundled provider backend today is Prometheus. |
| `middleware` | Module-level middleware that participates in the module middleware chain after framework HTTP metrics and endpoint-scoped middleware. | It is not route-scoped; use `endpointMiddleware` when only the scrape route should be protected. |
| `endpointMiddleware` | Class-based `@fluojs/http` middleware constructors bound only to the configured scrape endpoint. | Ignored when `path: false`; functions or global middleware declarations are outside this option's contract. |
| `endpointMiddleware` | Class-based `@fluojs/http` middleware constructors bound only to the configured scrape endpoint. | Ignored only when `path: false`; any string `path`, including `''`, remains an active endpoint path. Functions or global middleware declarations are outside this option's contract. |

@@ -109,5 +114,35 @@ ## Common Patterns

### Create custom metrics once and reuse them
`MetricsService.counter(...)`, `gauge(...)`, and `histogram(...)` create Prometheus collectors on the active registry. Create each custom metric once during provider construction or application startup, then reuse the returned collector when business actions occur.
```ts
import { Inject } from '@fluojs/core';
import { MetricsService } from '@fluojs/metrics';
@Inject(MetricsService)
class OrdersService {
private readonly ordersCreated: ReturnType<MetricsService['counter']>;
constructor(metrics: MetricsService) {
this.ordersCreated = metrics.counter({
name: 'orders_created_total',
help: 'Total orders created',
});
}
recordOrderCreated(): void {
this.ordersCreated.inc();
}
}
```
Calling `MetricsService.counter(...)` again with the same name recreates the collector and follows Prometheus' duplicate-name failure behavior. Store and reuse the collector instead of creating it inside each request or command handler.
`MetricsService.getRegistry()` returns the same active `prom-client` Registry used by the module scrape endpoint, built-in HTTP collectors, platform telemetry, and custom collectors created through the service. Prefer passing an explicit `registry` to `MetricsModule.forRoot({ registry })` when you own the bootstrap. Use `getRegistry()` for advanced integrations that receive `MetricsService` through DI and need to register a third-party Prometheus collector on the already active registry.
### Share one registry for framework and app metrics
```ts
import { Module } from '@fluojs/core';
import { Counter, Registry } from 'prom-client';

@@ -176,5 +211,6 @@ import { MetricsModule } from '@fluojs/metrics';

- `MetricsModule.forRoot(options)`
- `MetricsService`
- `MetricsService`, including `counter(...)`, `gauge(...)`, `histogram(...)`, and `getRegistry()`
- `METER_PROVIDER`
- `PrometheusMeterProvider`
- Meter abstraction types: `MeterProvider`, `MeterCounter`, `MeterGauge`, and `MeterHistogram`
- `HttpMetricsMiddleware` and HTTP path-label option types

@@ -186,3 +222,4 @@ - Module options including `provider` (currently only `'prometheus'`), module-level `middleware`, and endpoint-scoped `endpointMiddleware`

- `path` defaults to `'/metrics'`, and `path: false` disables the scrape endpoint entirely.
- `path` defaults to `'/metrics'`, any string path including `''` exposes a scrape endpoint, and `path: false` disables the scrape endpoint entirely.
- When `registry` is omitted, each application bootstrap owns a fresh isolated registry, `MetricsService`, meter provider, and telemetry collector set.
- The scrape response uses the active registry's Prometheus content type and registry contents.

@@ -189,0 +226,0 @@ - `defaultMetrics` defaults to `true`, and `defaultMetrics: false` disables Prometheus default process and Node.js collectors for that registry.