@fluojs/metrics
Advanced tools
@@ -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 +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"} |
+66
-30
@@ -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, |
+5
-4
@@ -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": { |
+43
-6
@@ -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를 끌 수 있습니다. |
+43
-6
@@ -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. |
74693
11.5%920
4.19%238
18.41%5
25%+ Added
Updated
Updated
Updated