@fluojs/metrics
Advanced tools
@@ -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,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"} | ||
| {"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;AA4BhE,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;IAuB1E,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"} |
@@ -5,2 +5,3 @@ import { Counter, Histogram } from 'prom-client'; | ||
| const FRAMEWORK_HTTP_HISTOGRAMS = new WeakSet(); | ||
| const FRAMEWORK_HTTP_COLLECTOR_CONFIGURATION = new WeakMap(); | ||
@@ -42,8 +43,6 @@ /** Strategy used to label request paths in emitted HTTP metrics. */ | ||
| constructor(registry, options = {}) { | ||
| if (options.pathLabelMode === 'raw' && options.allowUnsafeRawPathLabelMode !== true) { | ||
| throw new Error('HttpMetricsMiddleware pathLabelMode "raw" is disabled by default. Pass allowUnsafeRawPathLabelMode: true only when you have bounded path cardinality.'); | ||
| } | ||
| this.pathLabelMode = options.pathLabelMode ?? 'template'; | ||
| this.pathLabelNormalizer = options.pathLabelNormalizer; | ||
| this.unknownPathLabel = options.unknownPathLabel ?? 'UNKNOWN'; | ||
| const collectorConfiguration = resolveHttpMetricsCollectorConfiguration(options); | ||
| this.pathLabelMode = collectorConfiguration.pathLabelMode; | ||
| this.pathLabelNormalizer = collectorConfiguration.pathLabelNormalizer; | ||
| this.unknownPathLabel = collectorConfiguration.unknownPathLabel; | ||
| this.requestsTotal = getOrCreateHttpCounter(registry, { | ||
@@ -53,3 +52,3 @@ help: 'Total number of HTTP requests', | ||
| name: 'http_requests_total' | ||
| }); | ||
| }, collectorConfiguration); | ||
| this.errorsTotal = getOrCreateHttpCounter(registry, { | ||
@@ -59,3 +58,3 @@ help: 'Total number of HTTP error responses (4xx/5xx)', | ||
| name: 'http_errors_total' | ||
| }); | ||
| }, collectorConfiguration); | ||
| this.requestDuration = getOrCreateHttpHistogram(registry, { | ||
@@ -65,3 +64,3 @@ help: 'HTTP request duration in seconds', | ||
| name: 'http_request_duration_seconds' | ||
| }); | ||
| }, collectorConfiguration); | ||
| } | ||
@@ -127,3 +126,3 @@ resolvePathLabel(request) { | ||
| } | ||
| function getOrCreateHttpCounter(registry, config) { | ||
| function getOrCreateHttpCounter(registry, config, collectorConfiguration) { | ||
| const existing = registry.getSingleMetric(config.name); | ||
@@ -135,2 +134,3 @@ if (existing instanceof Counter) { | ||
| assertHttpMetricLabelSchema(existing, config); | ||
| assertHttpMetricConfiguration(existing, config.name, collectorConfiguration); | ||
| return existing; | ||
@@ -144,5 +144,6 @@ } | ||
| FRAMEWORK_HTTP_COUNTERS.add(counter); | ||
| FRAMEWORK_HTTP_COLLECTOR_CONFIGURATION.set(counter, collectorConfiguration); | ||
| return counter; | ||
| } | ||
| function getOrCreateHttpHistogram(registry, config) { | ||
| function getOrCreateHttpHistogram(registry, config, collectorConfiguration) { | ||
| const existing = registry.getSingleMetric(config.name); | ||
@@ -154,2 +155,3 @@ if (existing instanceof Histogram) { | ||
| assertHttpMetricLabelSchema(existing, config); | ||
| assertHttpMetricConfiguration(existing, config.name, collectorConfiguration); | ||
| return existing; | ||
@@ -163,2 +165,3 @@ } | ||
| FRAMEWORK_HTTP_HISTOGRAMS.add(histogram); | ||
| FRAMEWORK_HTTP_COLLECTOR_CONFIGURATION.set(histogram, collectorConfiguration); | ||
| return histogram; | ||
@@ -173,2 +176,28 @@ } | ||
| } | ||
| function resolveHttpMetricsCollectorConfiguration(options) { | ||
| if (options.pathLabelMode === 'raw' && options.allowUnsafeRawPathLabelMode !== true) { | ||
| throw new Error('HttpMetricsMiddleware pathLabelMode "raw" is disabled by default. Pass allowUnsafeRawPathLabelMode: true only when you have bounded path cardinality.'); | ||
| } | ||
| return { | ||
| pathLabelMode: options.pathLabelMode ?? 'template', | ||
| pathLabelNormalizer: options.pathLabelNormalizer, | ||
| unknownPathLabel: options.unknownPathLabel ?? 'UNKNOWN' | ||
| }; | ||
| } | ||
| function assertHttpMetricConfiguration(metric, metricName, expected) { | ||
| const registered = FRAMEWORK_HTTP_COLLECTOR_CONFIGURATION.get(metric); | ||
| if (!registered) { | ||
| throw new Error(`Metric name "${metricName}" is already registered as a framework-owned HTTP collector without path-label configuration metadata. Built-in HTTP metrics require matching path-label configuration before reuse.`); | ||
| } | ||
| if (hasSameHttpMetricConfiguration(registered, expected)) { | ||
| return; | ||
| } | ||
| throw new Error(`Metric name "${metricName}" is already registered with framework HTTP path-label configuration ${describeHttpMetricConfiguration(registered)}. Built-in HTTP metrics require matching path-label configuration before reuse; received ${describeHttpMetricConfiguration(expected)}.`); | ||
| } | ||
| function hasSameHttpMetricConfiguration(left, right) { | ||
| return left.pathLabelMode === right.pathLabelMode && left.pathLabelNormalizer === right.pathLabelNormalizer && left.unknownPathLabel === right.unknownPathLabel; | ||
| } | ||
| function describeHttpMetricConfiguration(configuration) { | ||
| return `pathLabelMode="${configuration.pathLabelMode}", pathLabelNormalizer=${configuration.pathLabelNormalizer ? 'custom' : 'none'}, unknownPathLabel="${configuration.unknownPathLabel}"`; | ||
| } | ||
| function normalizePathToTemplate(path, params) { | ||
@@ -175,0 +204,0 @@ if (!path) { |
@@ -45,2 +45,3 @@ import { type Middleware, type MiddlewareLike } from '@fluojs/http'; | ||
| private static registeredRegistries; | ||
| private static httpInstrumentationRegistrations; | ||
| /** | ||
@@ -47,0 +48,0 @@ * Register framework metrics, optional HTTP middleware, and a scrape endpoint. |
@@ -1,1 +0,1 @@ | ||
| {"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"} | ||
| {"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,EAEL,KAAK,UAAU,EAIhB,MAAM,iBAAiB,CAAC;AAEzB,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;IAC9D,OAAO,CAAC,MAAM,CAAC,gCAAgC,CAA4D;IAE3G;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,UAAU;IA6H9D,OAAO,CAAC,MAAM,CAAC,cAAc;CAU9B"} |
+216
-64
@@ -10,2 +10,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)]; } }; } | ||
| import { defineModule, PLATFORM_SHELL } from '@fluojs/runtime'; | ||
| import { RUNTIME_CONTAINER } from '@fluojs/runtime/internal'; | ||
| import { collectDefaultMetrics, Gauge, Registry as PrometheusRegistry } from 'prom-client'; | ||
@@ -26,2 +27,3 @@ import { HttpMetricsMiddleware } from './http-metrics-middleware.js'; | ||
| static registeredRegistries = new WeakSet(); | ||
| static httpInstrumentationRegistrations = new WeakMap(); | ||
@@ -49,13 +51,42 @@ /** | ||
| const metricsPath = options.path === undefined ? '/metrics' : options.path; | ||
| const registryToken = Symbol('MetricsModule.registry'); | ||
| let registryToken = Symbol('MetricsModule.registry'); | ||
| const platformTelemetryToken = Symbol('MetricsModule.platformTelemetry'); | ||
| const httpMetricsMiddleware = httpOptions ? createHttpMetricsMiddleware(registryToken, httpOptions) : undefined; | ||
| let 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] : []), { | ||
| const middleware = [...endpointMiddleware, ...(options.middleware ?? [])]; | ||
| const registryProvider = { | ||
| provide: registryToken, | ||
| useFactory: () => MetricsModule.createRegistry(options) | ||
| }, { | ||
| }; | ||
| const imports = []; | ||
| const validationProviders = []; | ||
| let includeRuntimeRegistryProvider = httpMetricsMiddleware === undefined; | ||
| if (httpOptions && httpMetricsMiddleware) { | ||
| const existingRegistration = options.registry ? MetricsModule.httpInstrumentationRegistrations.get(options.registry) : undefined; | ||
| if (existingRegistration) { | ||
| registryToken = existingRegistration.registryToken; | ||
| httpMetricsMiddleware = undefined; | ||
| validationProviders.push(createHttpCollectorValidationProvider(registryToken, httpOptions)); | ||
| imports.push(existingRegistration.moduleType); | ||
| } else { | ||
| includeRuntimeRegistryProvider = false; | ||
| class MetricsHttpInstrumentationModule {} | ||
| defineModule(MetricsHttpInstrumentationModule, { | ||
| exports: [registryToken], | ||
| global: true, | ||
| middleware: [httpMetricsMiddleware], | ||
| providers: [registryProvider, httpMetricsMiddleware] | ||
| }); | ||
| if (options.registry) { | ||
| MetricsModule.httpInstrumentationRegistrations.set(options.registry, { | ||
| moduleType: MetricsHttpInstrumentationModule, | ||
| registryToken | ||
| }); | ||
| } | ||
| imports.push(MetricsHttpInstrumentationModule); | ||
| } | ||
| } | ||
| const providers = [...(includeRuntimeRegistryProvider ? [registryProvider] : []), ...validationProviders, { | ||
| provide: MetricsService, | ||
| inject: [registryToken], | ||
| inject: [registryToken, platformTelemetryToken], | ||
| useFactory: registry => new MetricsService(assertPrometheusRegistry(registry)) | ||
@@ -68,4 +99,4 @@ }, { | ||
| provide: platformTelemetryToken, | ||
| inject: [registryToken], | ||
| useFactory: registry => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), options.registry ? 'shared' : 'isolated', options.platformTelemetry) | ||
| inject: [registryToken, RUNTIME_CONTAINER], | ||
| useFactory: (registry, container) => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), assertRuntimeContainer(container), options.registry ? 'shared' : 'isolated', options.platformTelemetry) | ||
| }]; | ||
@@ -91,3 +122,3 @@ const controllers = []; | ||
| ctx.response.setHeader('content-type', this.registry.contentType); | ||
| return this.platformTelemetry.collectMetrics(ctx, this.registry); | ||
| return this.platformTelemetry.collectMetrics(this.registry); | ||
| } | ||
@@ -103,2 +134,4 @@ static { | ||
| controllers, | ||
| exports: [MetricsService, METER_PROVIDER], | ||
| imports, | ||
| middleware, | ||
@@ -123,6 +156,6 @@ providers | ||
| const FRAMEWORK_PLATFORM_GAUGES = new WeakSet(); | ||
| const PLATFORM_TELEMETRY_REGISTRY_STATES = new WeakMap(); | ||
| const HEALTH_STATUSES = ['healthy', 'unhealthy', 'degraded']; | ||
| const READINESS_STATUSES = ['ready', 'not-ready', 'degraded']; | ||
| const PLATFORM_SHELL_TOKEN_NAME = 'PLATFORM_SHELL'; | ||
| const PLATFORM_SHELL_TOKEN_NAMES = new Set([PLATFORM_SHELL_TOKEN_NAME, String(PLATFORM_SHELL)]); | ||
| const PLATFORM_SHELL_TOKEN_NAMES = new Set([String(PLATFORM_SHELL)]); | ||
| function createHttpMetricsMiddleware(registryToken, httpOptions) { | ||
@@ -148,2 +181,9 @@ let _initClass2; | ||
| } | ||
| function createHttpCollectorValidationProvider(registryToken, httpOptions) { | ||
| return { | ||
| provide: Symbol('MetricsModule.httpCollectorValidation'), | ||
| inject: [registryToken], | ||
| useFactory: registry => new HttpMetricsMiddleware(assertPrometheusRegistry(registry), httpOptions) | ||
| }; | ||
| } | ||
| function assertPrometheusRegistry(value) { | ||
@@ -155,2 +195,25 @@ if (!(value instanceof PrometheusRegistry)) { | ||
| } | ||
| function assertRuntimeContainer(value) { | ||
| if (!isRuntimeContainer(value)) { | ||
| throw new Error('MetricsModule runtime container provider resolved an invalid container.'); | ||
| } | ||
| return value; | ||
| } | ||
| function isRuntimeContainer(value) { | ||
| return typeof value === 'object' && value !== null && 'resolve' in value && typeof value.resolve === 'function'; | ||
| } | ||
| function getRuntimePlatformTelemetryRegistryState(registry) { | ||
| const existing = PLATFORM_TELEMETRY_REGISTRY_STATES.get(registry); | ||
| if (existing) { | ||
| return existing; | ||
| } | ||
| const state = { | ||
| lastHealthStatuses: new Map(), | ||
| lastReadinessStatuses: new Map(), | ||
| registrations: [], | ||
| scrapeChain: Promise.resolve() | ||
| }; | ||
| PLATFORM_TELEMETRY_REGISTRY_STATES.set(registry, state); | ||
| return state; | ||
| } | ||
| function toReadinessValue(status) { | ||
@@ -191,8 +254,9 @@ return status === 'ready' ? 1 : 0; | ||
| registryModeGauge; | ||
| lastHealthStatuses = new Map(); | ||
| lastReadinessStatuses = new Map(); | ||
| scrapeChain = Promise.resolve(); | ||
| constructor(registry, registryMode, labels = {}) { | ||
| telemetryState; | ||
| constructor(registry, container, registryMode, labels = {}) { | ||
| this.registry = registry; | ||
| this.container = container; | ||
| this.registryMode = registryMode; | ||
| this.labels = labels; | ||
| this.telemetryState = getRuntimePlatformTelemetryRegistryState(registry); | ||
| this.readinessGauge = getOrCreateGauge(registry, { | ||
@@ -214,18 +278,50 @@ help: 'Runtime platform component readiness from shared platform snapshot semantics.', | ||
| this.registryModeGauge.labels(this.registryMode).set(1); | ||
| this.installRegistryRefresh(); | ||
| } | ||
| async collectMetrics(ctx, registry) { | ||
| const collect = this.scrapeChain.then(async () => { | ||
| await this.refresh(ctx); | ||
| return registry.metrics(); | ||
| }); | ||
| this.scrapeChain = collect.then(() => undefined, () => undefined); | ||
| return collect; | ||
| collectMetrics(registry) { | ||
| return registry.metrics(); | ||
| } | ||
| async refresh(ctx) { | ||
| const platformShell = await this.resolvePlatformShell(ctx); | ||
| if (!platformShell) { | ||
| this.clearPlatformTelemetry(); | ||
| onModuleDestroy() { | ||
| const registrationIndex = this.telemetryState.registrations.lastIndexOf(this); | ||
| if (registrationIndex >= 0) { | ||
| this.telemetryState.registrations.splice(registrationIndex, 1); | ||
| } | ||
| if (this.telemetryState.registrations.length > 0) { | ||
| return; | ||
| } | ||
| const snapshot = await platformShell.snapshot(); | ||
| const originalMetrics = this.telemetryState.originalMetrics; | ||
| if (originalMetrics) { | ||
| this.registry.metrics = originalMetrics; | ||
| this.telemetryState.originalMetrics = undefined; | ||
| } | ||
| } | ||
| installRegistryRefresh() { | ||
| this.telemetryState.registrations.push(this); | ||
| if (this.telemetryState.originalMetrics) { | ||
| return; | ||
| } | ||
| const registry = this.registry; | ||
| const telemetryState = this.telemetryState; | ||
| const originalMetrics = registry.metrics; | ||
| telemetryState.originalMetrics = originalMetrics; | ||
| registry.metrics = async () => { | ||
| const activeRegistration = telemetryState.registrations.at(-1); | ||
| await activeRegistration?.refresh(); | ||
| return await originalMetrics.call(registry); | ||
| }; | ||
| } | ||
| async refresh() { | ||
| const collect = this.telemetryState.scrapeChain.then(async () => { | ||
| const platformShell = await this.resolvePlatformShell(); | ||
| if (!platformShell) { | ||
| this.clearPlatformTelemetry(); | ||
| return; | ||
| } | ||
| const snapshot = await platformShell.snapshot(); | ||
| this.syncSnapshot(snapshot); | ||
| }); | ||
| this.telemetryState.scrapeChain = collect.then(() => undefined, () => undefined); | ||
| await collect; | ||
| } | ||
| syncSnapshot(snapshot) { | ||
| const env = this.labels?.env ?? 'unknown'; | ||
@@ -245,7 +341,5 @@ const instance = this.labels?.instance ?? 'local'; | ||
| this.syncGaugeStatuses({ | ||
| currentStatuses: new Map(components.map(component => [this.toComponentKey(component.id, component.kind), component.health])), | ||
| env, | ||
| currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.health), | ||
| gauge: this.healthGauge, | ||
| instance, | ||
| lastStatuses: this.lastHealthStatuses, | ||
| lastStatuses: this.telemetryState.lastHealthStatuses, | ||
| operation: 'health', | ||
@@ -256,7 +350,5 @@ statuses: HEALTH_STATUSES, | ||
| this.syncGaugeStatuses({ | ||
| currentStatuses: new Map(components.map(component => [this.toComponentKey(component.id, component.kind), component.readiness])), | ||
| env, | ||
| currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.readiness), | ||
| gauge: this.readinessGauge, | ||
| instance, | ||
| lastStatuses: this.lastReadinessStatuses, | ||
| lastStatuses: this.telemetryState.lastReadinessStatuses, | ||
| operation: 'readiness', | ||
@@ -269,6 +361,4 @@ statuses: READINESS_STATUSES, | ||
| this.clearGaugeStatuses({ | ||
| env: this.labels?.env ?? 'unknown', | ||
| gauge: this.healthGauge, | ||
| instance: this.labels?.instance ?? 'local', | ||
| lastStatuses: this.lastHealthStatuses, | ||
| lastStatuses: this.telemetryState.lastHealthStatuses, | ||
| operation: 'health', | ||
@@ -278,6 +368,4 @@ statuses: HEALTH_STATUSES | ||
| this.clearGaugeStatuses({ | ||
| env: this.labels?.env ?? 'unknown', | ||
| gauge: this.readinessGauge, | ||
| instance: this.labels?.instance ?? 'local', | ||
| lastStatuses: this.lastReadinessStatuses, | ||
| lastStatuses: this.telemetryState.lastReadinessStatuses, | ||
| operation: 'readiness', | ||
@@ -288,5 +376,3 @@ statuses: READINESS_STATUSES | ||
| clearGaugeStatuses({ | ||
| env, | ||
| gauge, | ||
| instance, | ||
| lastStatuses, | ||
@@ -296,4 +382,8 @@ operation, | ||
| }) { | ||
| for (const componentKey of lastStatuses.keys()) { | ||
| const [componentId, componentKind] = this.fromComponentKey(componentKey); | ||
| for (const { | ||
| componentId, | ||
| componentKind, | ||
| env, | ||
| instance | ||
| } of this.componentStatusEntries(lastStatuses)) { | ||
| for (const status of statuses) { | ||
@@ -307,5 +397,3 @@ gauge.remove(componentId, componentKind, operation, status, env, instance); | ||
| currentStatuses, | ||
| env, | ||
| gauge, | ||
| instance, | ||
| lastStatuses, | ||
@@ -316,8 +404,13 @@ operation, | ||
| }) { | ||
| for (const [componentKey, previousStatus] of lastStatuses) { | ||
| const nextStatus = currentStatuses.get(componentKey); | ||
| for (const { | ||
| componentId, | ||
| componentKind, | ||
| env, | ||
| instance, | ||
| status: previousStatus | ||
| } of this.componentStatusEntries(lastStatuses)) { | ||
| const nextStatus = this.getComponentStatus(currentStatuses, env, instance, componentId, componentKind); | ||
| if (nextStatus === previousStatus) { | ||
| continue; | ||
| } | ||
| const [componentId, componentKind] = this.fromComponentKey(componentKey); | ||
| for (const status of statuses) { | ||
@@ -330,23 +423,76 @@ if (status !== previousStatus) { | ||
| } | ||
| for (const [componentKey, currentStatus] of currentStatuses) { | ||
| const [componentId, componentKind] = this.fromComponentKey(componentKey); | ||
| for (const { | ||
| componentId, | ||
| componentKind, | ||
| env, | ||
| instance, | ||
| status: currentStatus | ||
| } of this.componentStatusEntries(currentStatuses)) { | ||
| gauge.labels(componentId, componentKind, operation, currentStatus, env, instance).set(toMetricValue(currentStatus)); | ||
| } | ||
| lastStatuses.clear(); | ||
| for (const [componentKey, currentStatus] of currentStatuses) { | ||
| lastStatuses.set(componentKey, currentStatus); | ||
| for (const { | ||
| componentId, | ||
| componentKind, | ||
| env, | ||
| instance, | ||
| status: currentStatus | ||
| } of this.componentStatusEntries(currentStatuses)) { | ||
| this.setComponentStatus(lastStatuses, env, instance, componentId, componentKind, currentStatus); | ||
| } | ||
| } | ||
| fromComponentKey(componentKey) { | ||
| const separatorIndex = componentKey.indexOf('::'); | ||
| return [componentKey.slice(0, separatorIndex), componentKey.slice(separatorIndex + 2)]; | ||
| toComponentStatusMap(components, env, instance, getStatus) { | ||
| const statuses = new Map(); | ||
| for (const component of components) { | ||
| this.setComponentStatus(statuses, env, instance, component.id, component.kind, getStatus(component)); | ||
| } | ||
| return statuses; | ||
| } | ||
| toComponentKey(componentId, componentKind) { | ||
| return `${componentId}::${componentKind}`; | ||
| setComponentStatus(statuses, env, instance, componentId, componentKind, status) { | ||
| let statusByInstance = statuses.get(env); | ||
| if (!statusByInstance) { | ||
| statusByInstance = new Map(); | ||
| statuses.set(env, statusByInstance); | ||
| } | ||
| let statusByComponent = statusByInstance.get(instance); | ||
| if (!statusByComponent) { | ||
| statusByComponent = new Map(); | ||
| statusByInstance.set(instance, statusByComponent); | ||
| } | ||
| let statusByKind = statusByComponent.get(componentId); | ||
| if (!statusByKind) { | ||
| statusByKind = new Map(); | ||
| statusByComponent.set(componentId, statusByKind); | ||
| } | ||
| statusByKind.set(componentKind, status); | ||
| } | ||
| async resolvePlatformShell(ctx) { | ||
| getComponentStatus(statuses, env, instance, componentId, componentKind) { | ||
| return statuses.get(env)?.get(instance)?.get(componentId)?.get(componentKind); | ||
| } | ||
| *componentStatusEntries(statuses) { | ||
| for (const [env, statusByInstance] of statuses) { | ||
| for (const [instance, statusByComponent] of statusByInstance) { | ||
| for (const [componentId, statusByKind] of statusByComponent) { | ||
| for (const [componentKind, status] of statusByKind) { | ||
| yield { | ||
| componentId, | ||
| componentKind, | ||
| env, | ||
| instance, | ||
| status | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| async resolvePlatformShell() { | ||
| const hasPlatformShell = hasContainerToken(this.container, PLATFORM_SHELL); | ||
| if (hasPlatformShell === false) { | ||
| return undefined; | ||
| } | ||
| try { | ||
| return await ctx.container.resolve(PLATFORM_SHELL); | ||
| return await this.container.resolve(PLATFORM_SHELL); | ||
| } catch (error) { | ||
| if (isMissingPlatformShellResolutionError(error)) { | ||
| if (hasPlatformShell !== true && isMissingPlatformShellResolutionError(error)) { | ||
| return undefined; | ||
@@ -358,2 +504,9 @@ } | ||
| } | ||
| function hasContainerToken(container, token) { | ||
| const has = container.has; | ||
| if (typeof has !== 'function') { | ||
| return undefined; | ||
| } | ||
| return has.call(container, token); | ||
| } | ||
| function isMissingPlatformShellResolutionError(error) { | ||
@@ -365,4 +518,3 @@ if (!(error instanceof ContainerResolutionError)) { | ||
| 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.'; | ||
| return token !== undefined && PLATFORM_SHELL_TOKEN_NAMES.has(token); | ||
| } | ||
@@ -369,0 +521,0 @@ function resolveHttpOptions(http) { |
+5
-5
@@ -11,3 +11,3 @@ { | ||
| ], | ||
| "version": "1.0.4", | ||
| "version": "2.0.0", | ||
| "private": false, | ||
@@ -40,6 +40,6 @@ "license": "MIT", | ||
| "prom-client": "^15.1.3", | ||
| "@fluojs/core": "^1.0.3", | ||
| "@fluojs/http": "^1.1.2", | ||
| "@fluojs/di": "^1.1.0", | ||
| "@fluojs/runtime": "^1.1.8" | ||
| "@fluojs/core": "^1.1.0", | ||
| "@fluojs/di": "^2.0.0", | ||
| "@fluojs/http": "^2.0.1", | ||
| "@fluojs/runtime": "^2.0.1" | ||
| }, | ||
@@ -46,0 +46,0 @@ "devDependencies": { |
+8
-7
@@ -161,3 +161,3 @@ # @fluojs/metrics | ||
| 여러 `MetricsModule` 인스턴스가 같은 Registry를 의도적으로 공유하는 경우, 내장 HTTP 메트릭은 기존 `http_requests_total`, `http_errors_total`, `http_request_duration_seconds` collector를 재사용합니다. 내장 플랫폼 텔레메트리 Gauge도 같은 ownership 규칙을 따릅니다. 모듈이 만든 `fluo_component_ready`, `fluo_component_health`, `fluo_metrics_registry_mode` Gauge는 framework ownership과 label schema가 일치할 때만 재사용합니다. 애플리케이션이 직접 등록한 중복 메트릭 이름은 Prometheus Registry 규칙대로 계속 빠르게 실패합니다. | ||
| 여러 `MetricsModule` 인스턴스가 같은 Registry를 의도적으로 공유하는 경우, 내장 HTTP 메트릭은 framework ownership, label schema, effective path-label configuration이 모두 일치할 때만 기존 `http_requests_total`, `http_errors_total`, `http_request_duration_seconds` collector를 재사용합니다. Path-label compatibility 검사는 `pathLabelMode`, 정확히 같은 `pathLabelNormalizer` 함수 참조, `unknownPathLabel` fallback 의미론을 포함하므로 서로 다른 HTTP series policy를 하나의 collector set에 섞는 module instance는 빠르게 실패합니다. 내장 플랫폼 텔레메트리 Gauge도 같은 ownership 규칙을 따릅니다. 모듈이 만든 `fluo_component_ready`, `fluo_component_health`, `fluo_metrics_registry_mode` Gauge는 framework ownership과 label schema가 일치할 때만 재사용합니다. 플랫폼 텔레메트리 상태는 재사용된 Registry별로 추적되므로, 이후 스크레이프는 이전 module instance가 남긴 stale component readiness/health series를 제거한 뒤 메트릭을 반환합니다. Registry scrape wrapper는 최신 active module registration을 사용하며 마지막 registration이 종료되면 Registry의 원래 `metrics()` 함수를 복원합니다. 애플리케이션이 직접 등록한 중복 메트릭 이름은 Prometheus Registry 규칙대로 계속 빠르게 실패합니다. | ||
@@ -174,5 +174,5 @@ ### 중복 메트릭 이름은 계속 빠르게 실패합니다 | ||
| - `fluo_component_health`: 정상 상태 시 1, 아닐 시 0. | ||
| - `fluo_metrics_registry_mode`: active registry mode가 `isolated`인지 `shared`인지 나타냅니다. | ||
| - `fluo_metrics_registry_mode`: active registry mode를 `mode="isolated"` 또는 `mode="shared"` label과 gauge value `1`로 나타냅니다. | ||
| 이 데이터는 매 스크레이프 시점에 `PLATFORM_SHELL`을 쿼리하여 갱신됩니다. 초기화 시 환경 라벨을 제공할 수 있습니다. | ||
| 이 데이터는 built-in `/metrics` controller와 `MetricsService.getRegistry().metrics()`를 사용하는 advanced custom scraper를 포함해 active Registry가 스크레이프될 때마다 `PLATFORM_SHELL`을 쿼리하여 갱신됩니다. 초기화 시 환경 라벨을 제공할 수 있습니다. | ||
@@ -190,6 +190,6 @@ ```ts | ||
| 플랫폼 텔레메트리는 매 `/metrics` 스크레이프마다 `PLATFORM_SHELL`을 resolve하여 `fluo_component_ready`와 `fluo_component_health`를 갱신합니다. | ||
| 플랫폼 텔레메트리는 built-in `/metrics` controller 또는 `MetricsService.getRegistry()`를 사용하는 advanced custom scraper가 active Registry를 스크레이프할 때마다 `PLATFORM_SHELL`을 resolve하여 `fluo_component_ready`와 `fluo_component_health`를 갱신합니다. | ||
| - `PLATFORM_SHELL` 등록 자체가 빠진 경우에는 스크레이프가 계속 성공하고 플랫폼 텔레메트리 시리즈만 생략됩니다. | ||
| - 직전 성공 스크레이프에서 플랫폼 텔레메트리를 노출한 뒤 `PLATFORM_SHELL`을 사용할 수 없게 되면, stale `fluo_component_ready` 및 `fluo_component_health` 시리즈를 제거한 뒤 메트릭을 반환합니다. | ||
| - 직전 성공 스크레이프에서 플랫폼 텔레메트리를 노출한 뒤 `PLATFORM_SHELL`을 사용할 수 없게 되거나 이후 module instance가 재사용된 Registry를 다른 플랫폼 snapshot으로 갱신하면, stale `fluo_component_ready` 및 `fluo_component_health` 시리즈를 제거한 뒤 메트릭을 반환합니다. | ||
| - 그 외의 `PLATFORM_SHELL` resolve 실패는 조용히 삼키지 않고 스크레이프 실패로 그대로 드러납니다. | ||
@@ -226,6 +226,7 @@ | ||
| - HTTP 메트릭은 `http: true` 또는 `http` 옵션 객체를 전달한 경우에만 설치되며, 설치된 뒤에는 기본적으로 템플릿 기반 경로 라벨 정규화를 사용합니다. | ||
| - 내장 HTTP collector와 플랫폼 텔레메트리 Gauge는 같은 Registry를 공유하는 모듈 인스턴스 사이에서 framework-owned이고 예상 label schema를 가진 경우에만 재사용되며, 커스텀 애플리케이션 메트릭 이름 충돌은 Prometheus의 중복 이름 실패 동작을 유지합니다. | ||
| - 내장 HTTP collector는 같은 Registry를 공유하는 모듈 인스턴스 사이에서 framework-owned이고 예상 label schema 및 일치하는 path-label configuration을 가진 경우에만 재사용됩니다. 플랫폼 텔레메트리 Gauge는 framework-owned이고 예상 label schema를 가진 경우에만 재사용되며, 커스텀 애플리케이션 메트릭 이름 충돌은 Prometheus의 중복 이름 실패 동작을 유지합니다. | ||
| - Shared Registry 텔레메트리 refresh는 소유 metrics module이 하나라도 active인 동안 유지되고 마지막 module이 종료되면 원래 `metrics()` 함수를 복원합니다. | ||
| - raw path 라벨은 `allowUnsafeRawPathLabelMode: true`를 명시한 bounded internal route에서만 사용해야 합니다. | ||
| - 플랫폼 텔레메트리는 `PLATFORM_SHELL`이 실제로 누락된 경우에만 생략되며, 그 외 resolve 실패는 스크레이프를 실패시킵니다. | ||
| - 직전 성공 스크레이프에서 노출된 플랫폼 텔레메트리 시리즈는 `PLATFORM_SHELL`을 사용할 수 없게 된 스크레이프에서 제거됩니다. | ||
| - 직전 성공 스크레이프에서 노출된 플랫폼 텔레메트리 시리즈는 `PLATFORM_SHELL`을 사용할 수 없게 된 스크레이프 또는 이후 module instance가 재사용된 Registry를 다른 플랫폼 snapshot으로 갱신한 스크레이프에서 제거됩니다. | ||
@@ -232,0 +233,0 @@ ## 관련 패키지 |
+7
-6
@@ -161,3 +161,3 @@ # @fluojs/metrics | ||
| 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. Built-in platform telemetry gauges follow the same ownership rule: module-created `fluo_component_ready`, `fluo_component_health`, and `fluo_metrics_registry_mode` gauges are reused only when their framework ownership and label schema match. Application-defined duplicate names still fail fast. | ||
| 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 only when their framework ownership, label schema, and effective path-label configuration match. The path-label compatibility check includes `pathLabelMode`, the exact `pathLabelNormalizer` function reference, and `unknownPathLabel` fallback semantics, so incompatible module instances fail fast instead of mixing different HTTP series policies into one collector set. Built-in platform telemetry gauges follow the same ownership rule: module-created `fluo_component_ready`, `fluo_component_health`, and `fluo_metrics_registry_mode` gauges are reused only when their framework ownership and label schema match. Platform telemetry state is tracked per reused registry, so a later scrape replaces stale module-owned component readiness and health series from an earlier module instance before metrics are returned. The registry scrape wrapper keeps using the latest active module registration and restores the Registry's original `metrics()` function after the last registration closes. Application-defined duplicate names still fail fast. | ||
@@ -174,5 +174,5 @@ ### Duplicate metric names still fail fast | ||
| - `fluo_component_health`: `1` when a component is healthy, otherwise `0`. | ||
| - `fluo_metrics_registry_mode`: `isolated` or `shared` for the active registry mode. | ||
| - `fluo_metrics_registry_mode`: gauge value `1` with a `mode="isolated"` or `mode="shared"` label for the active registry mode. | ||
| The platform snapshot is refreshed during each scrape, and you can attach environment labels up front. | ||
| The platform snapshot is refreshed during each registry scrape, including advanced `MetricsService.getRegistry().metrics()` scrape paths, and you can attach environment labels up front. | ||
@@ -190,3 +190,3 @@ ```ts | ||
| Platform telemetry refreshes `fluo_component_ready` and `fluo_component_health` on each `/metrics` scrape by resolving `PLATFORM_SHELL`. | ||
| Platform telemetry refreshes `fluo_component_ready` and `fluo_component_health` on each active registry scrape by resolving `PLATFORM_SHELL`, whether the scrape flows through the built-in `/metrics` controller or an advanced custom scraper using `MetricsService.getRegistry()`. | ||
@@ -226,6 +226,7 @@ - If `PLATFORM_SHELL` is not registered, the scrape still succeeds and omits the platform telemetry series. | ||
| - 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 and platform telemetry gauges are reused when module instances share one registry only if they are framework-owned and have the expected label schema; custom application metric name collisions keep Prometheus' duplicate-name failure behavior. | ||
| - Built-in HTTP collectors are reused when module instances share one registry only if they are framework-owned, have the expected label schema, and use matching path-label configuration; platform telemetry gauges are reused only if they are framework-owned and have the expected label schema; custom application metric name collisions keep Prometheus' duplicate-name failure behavior. | ||
| - Shared Registry telemetry refresh remains installed while any owning metrics module is active and restores the original `metrics()` function after the last module closes. | ||
| - Raw path labels require `allowUnsafeRawPathLabelMode: true` and should stay limited to bounded internal routes. | ||
| - Platform telemetry is omitted only when `PLATFORM_SHELL` is genuinely missing; other resolution failures fail the scrape. | ||
| - Stale platform telemetry series are removed when `PLATFORM_SHELL` becomes unavailable after the last successful scrape. | ||
| - Stale platform telemetry series are removed when `PLATFORM_SHELL` becomes unavailable after the last successful scrape or when a later module instance refreshes a reused registry with a different platform snapshot. | ||
@@ -232,0 +233,0 @@ ## Related Packages |
85019
13.82%1103
19.89%239
0.42%+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated