@fluojs/metrics
Advanced tools
| import type { FrameworkRequest, Middleware, MiddlewareContext, Next } from '@fluojs/http'; | ||
| import type { Registry } from 'prom-client'; | ||
| import { type Registry } from 'prom-client'; | ||
| /** Strategy used to label request paths in emitted HTTP metrics. */ | ||
@@ -4,0 +4,0 @@ export type HttpMetricsPathLabelMode = 'raw' | 'template'; |
@@ -1,1 +0,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"} | ||
| {"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"} |
@@ -0,2 +1,5 @@ | ||
| import { Counter, Histogram } from 'prom-client'; | ||
| import { createPrometheusCounter, createPrometheusHistogram } from './providers/prometheus-metrics-factory.js'; | ||
| const FRAMEWORK_HTTP_COUNTERS = new WeakSet(); | ||
| const FRAMEWORK_HTTP_HISTOGRAMS = new WeakSet(); | ||
@@ -44,3 +47,3 @@ /** Strategy used to label request paths in emitted HTTP metrics. */ | ||
| this.unknownPathLabel = options.unknownPathLabel ?? 'UNKNOWN'; | ||
| this.requestsTotal = createPrometheusCounter(registry, { | ||
| this.requestsTotal = getOrCreateHttpCounter(registry, { | ||
| help: 'Total number of HTTP requests', | ||
@@ -50,3 +53,3 @@ labelNames: ['method', 'path', 'status'], | ||
| }); | ||
| this.errorsTotal = createPrometheusCounter(registry, { | ||
| this.errorsTotal = getOrCreateHttpCounter(registry, { | ||
| help: 'Total number of HTTP error responses (4xx/5xx)', | ||
@@ -56,3 +59,3 @@ labelNames: ['method', 'path', 'status'], | ||
| }); | ||
| this.requestDuration = createPrometheusHistogram(registry, { | ||
| this.requestDuration = getOrCreateHttpHistogram(registry, { | ||
| help: 'HTTP request duration in seconds', | ||
@@ -122,2 +125,34 @@ labelNames: ['method', 'path', 'status'], | ||
| } | ||
| function getOrCreateHttpCounter(registry, config) { | ||
| const existing = registry.getSingleMetric(config.name); | ||
| if (existing instanceof Counter) { | ||
| if (!FRAMEWORK_HTTP_COUNTERS.has(existing)) { | ||
| throw new Error(`Metric name "${config.name}" is already registered by the application. Built-in HTTP metrics require framework-owned collectors.`); | ||
| } | ||
| return existing; | ||
| } | ||
| const counter = createPrometheusCounter(registry, { | ||
| help: config.help, | ||
| labelNames: [...config.labelNames], | ||
| name: config.name | ||
| }); | ||
| FRAMEWORK_HTTP_COUNTERS.add(counter); | ||
| return counter; | ||
| } | ||
| function getOrCreateHttpHistogram(registry, config) { | ||
| const existing = registry.getSingleMetric(config.name); | ||
| if (existing instanceof Histogram) { | ||
| if (!FRAMEWORK_HTTP_HISTOGRAMS.has(existing)) { | ||
| throw new Error(`Metric name "${config.name}" is already registered by the application. Built-in HTTP metrics require framework-owned collectors.`); | ||
| } | ||
| return existing; | ||
| } | ||
| const histogram = createPrometheusHistogram(registry, { | ||
| help: config.help, | ||
| labelNames: [...config.labelNames], | ||
| name: config.name | ||
| }); | ||
| FRAMEWORK_HTTP_HISTOGRAMS.add(histogram); | ||
| return histogram; | ||
| } | ||
| function normalizePathToTemplate(path, params) { | ||
@@ -124,0 +159,0 @@ if (!path) { |
+8
-0
@@ -0,1 +1,9 @@ | ||
| /** | ||
| * Prometheus registry constructor re-exported for applications that want custom | ||
| * application metrics and Fluo framework metrics to share one scrape endpoint. | ||
| * | ||
| * @remarks | ||
| * The implementation is `prom-client`'s `Registry`; duplicate metric names still | ||
| * follow Prometheus registry semantics and fail fast. | ||
| */ | ||
| export { Registry } from 'prom-client'; | ||
@@ -2,0 +10,0 @@ export * from './metrics-module.js'; |
@@ -1,1 +0,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"} | ||
| {"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"} |
+8
-0
@@ -0,1 +1,9 @@ | ||
| /** | ||
| * Prometheus registry constructor re-exported for applications that want custom | ||
| * application metrics and Fluo framework metrics to share one scrape endpoint. | ||
| * | ||
| * @remarks | ||
| * The implementation is `prom-client`'s `Registry`; duplicate metric names still | ||
| * follow Prometheus registry semantics and fail fast. | ||
| */ | ||
| export { Registry } from 'prom-client'; | ||
@@ -2,0 +10,0 @@ export * from './metrics-module.js'; |
@@ -288,8 +288,9 @@ 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)]; } }; } | ||
| 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 containerError.message.startsWith(`No provider registered for token ${token}.`); | ||
| return message.startsWith(`No provider registered for token ${token}.`); | ||
| } | ||
| for (const tokenName of PLATFORM_SHELL_TOKEN_NAMES) { | ||
| if (containerError.message.startsWith(`No provider registered for token ${tokenName}.`)) { | ||
| if (message.startsWith(`No provider registered for token ${tokenName}.`)) { | ||
| return true; | ||
@@ -296,0 +297,0 @@ } |
+4
-4
@@ -11,3 +11,3 @@ { | ||
| ], | ||
| "version": "1.0.0-beta.2", | ||
| "version": "1.0.0-beta.3", | ||
| "private": false, | ||
@@ -40,5 +40,5 @@ "license": "MIT", | ||
| "prom-client": "^15.1.3", | ||
| "@fluojs/di": "^1.0.0-beta.2", | ||
| "@fluojs/http": "^1.0.0-beta.1", | ||
| "@fluojs/runtime": "^1.0.0-beta.2" | ||
| "@fluojs/di": "^1.0.0-beta.6", | ||
| "@fluojs/http": "^1.0.0-beta.9", | ||
| "@fluojs/runtime": "^1.0.0-beta.9" | ||
| }, | ||
@@ -45,0 +45,0 @@ "devDependencies": { |
+12
-5
@@ -31,3 +31,3 @@ # @fluojs/metrics | ||
| 루트 모듈에 `MetricsModule.forRoot()`를 추가하여 기본 `/metrics` 엔드포인트를 활성화합니다. | ||
| 루트 모듈에 `MetricsModule.forRoot()`를 추가하여 기본 `/metrics` 엔드포인트를 활성화합니다. HTTP 요청 수와 지연 시간까지 계측하려면 `http: true` 또는 `http` 옵션 객체를 함께 전달합니다. | ||
@@ -40,3 +40,3 @@ ```typescript | ||
| imports: [ | ||
| MetricsModule.forRoot(), | ||
| MetricsModule.forRoot({ http: true }), | ||
| ], | ||
@@ -49,3 +49,3 @@ }) | ||
| `MetricsModule.forRoot()`는 기본적으로 `GET /metrics`를 노출합니다. 운영 환경에서는 이 경계를 명시적으로 다루세요. 플랫폼 프록시/네트워크 제어를 붙이기 전까지 `path: false`로 비활성화하거나, 전용 endpoint middleware를 연결하는 방식을 권장합니다. | ||
| `MetricsModule.forRoot()`는 기본적으로 `GET /metrics`를 노출합니다. `http: true`를 전달한 경우에만 HTTP 요청 계측 미들웨어가 설치됩니다. 운영 환경에서는 이 경계를 명시적으로 다루세요. 플랫폼 프록시/네트워크 제어를 붙이기 전까지 `path: false`로 비활성화하거나, 전용 endpoint middleware를 연결하는 방식을 권장합니다. | ||
@@ -122,3 +122,3 @@ ## 공통 패턴 | ||
| imports: [ | ||
| MetricsModule.forRoot({ registry: sharedRegistry }), | ||
| MetricsModule.forRoot({ http: true, registry: sharedRegistry }), | ||
| ], | ||
@@ -129,2 +129,8 @@ }) | ||
| 여러 `MetricsModule` 인스턴스가 같은 Registry를 의도적으로 공유하는 경우, 내장 HTTP 메트릭은 기존 `http_requests_total`, `http_errors_total`, `http_request_duration_seconds` collector를 재사용합니다. 애플리케이션이 직접 등록한 중복 메트릭 이름은 Prometheus Registry 규칙대로 계속 빠르게 실패합니다. | ||
| ### 중복 메트릭 이름은 계속 빠르게 실패합니다 | ||
| Prometheus 메트릭 이름은 하나의 Registry 안에서 고유해야 합니다. 공유 Registry 모드는 애플리케이션 메트릭의 중복 이름을 조용히 덮어쓰지 않고 이 동작을 유지합니다. | ||
| ### 런타임 플랫폼 텔레메트리 | ||
@@ -180,3 +186,4 @@ | ||
| - `endpointMiddleware`는 스크레이프 엔드포인트에만 route-scoped middleware를 바인딩합니다. | ||
| - HTTP 메트릭은 기본적으로 템플릿 기반 경로 라벨 정규화를 사용합니다. | ||
| - HTTP 메트릭은 `http: true` 또는 `http` 옵션 객체를 전달한 경우에만 설치되며, 설치된 뒤에는 기본적으로 템플릿 기반 경로 라벨 정규화를 사용합니다. | ||
| - 내장 HTTP collector는 같은 Registry를 공유하는 모듈 인스턴스 사이에서 재사용되며, 커스텀 애플리케이션 메트릭 이름 충돌은 Prometheus의 중복 이름 실패 동작을 유지합니다. | ||
| - raw path 라벨은 `allowUnsafeRawPathLabelMode: true`를 명시한 bounded internal route에서만 사용해야 합니다. | ||
@@ -183,0 +190,0 @@ - 플랫폼 텔레메트리는 `PLATFORM_SHELL`이 실제로 누락된 경우에만 생략되며, 그 외 resolve 실패는 스크레이프를 실패시킵니다. |
+7
-4
@@ -36,3 +36,3 @@ # @fluojs/metrics | ||
| @Module({ | ||
| imports: [MetricsModule.forRoot()], | ||
| imports: [MetricsModule.forRoot({ http: true })], | ||
| }) | ||
@@ -42,3 +42,3 @@ class AppModule {} | ||
| `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. | ||
| `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. | ||
@@ -99,3 +99,3 @@ ## Common Patterns | ||
| @Module({ | ||
| imports: [MetricsModule.forRoot({ registry })], | ||
| imports: [MetricsModule.forRoot({ http: true, registry })], | ||
| }) | ||
@@ -105,2 +105,4 @@ class AppModule {} | ||
| 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. | ||
| ### Duplicate metric names still fail fast | ||
@@ -160,3 +162,4 @@ | ||
| - `endpointMiddleware` binds route-scoped middleware only to the scrape endpoint. | ||
| - HTTP metrics default to template-normalized path labels. | ||
| - HTTP metrics are installed only when `http: true` or an `http` options object is provided, and then default to template-normalized path labels. | ||
| - Built-in HTTP collectors are reused when module instances share one registry; custom application metric name collisions keep Prometheus' duplicate-name failure behavior. | ||
| - Raw path labels require `allowUnsafeRawPathLabelMode: true` and should stay limited to bounded internal routes. | ||
@@ -163,0 +166,0 @@ - Platform telemetry is omitted only when `PLATFORM_SHELL` is genuinely missing; other resolution failures fail the scrape. |
58471
7.31%847
6.54%174
1.75%Updated
Updated