@fluojs/metrics 1.0.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +55 -23
- package/README.md +55 -22
- package/dist/http-metrics-middleware.d.ts +12 -0
- package/dist/http-metrics-middleware.d.ts.map +1 -1
- package/dist/http-metrics-middleware.js +67 -12
- package/dist/metrics-module.d.ts +7 -3
- package/dist/metrics-module.d.ts.map +1 -1
- package/dist/metrics-module.js +319 -73
- package/dist/metrics-service.d.ts +6 -1
- package/dist/metrics-service.d.ts.map +1 -1
- package/dist/metrics-service.js +6 -1
- package/dist/serialized-scrape-queue.d.ts +19 -0
- package/dist/serialized-scrape-queue.d.ts.map +1 -0
- package/dist/serialized-scrape-queue.js +43 -0
- package/package.json +9 -8
package/dist/metrics-module.js
CHANGED
|
@@ -3,15 +3,19 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
|
|
|
3
3
|
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
4
4
|
function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
|
|
5
5
|
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; }
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
6
8
|
import { Inject } from '@fluojs/core';
|
|
7
9
|
import { ContainerResolutionError } from '@fluojs/di';
|
|
8
|
-
import { Controller,
|
|
10
|
+
import { Controller, forRoutes, Get } from '@fluojs/http';
|
|
9
11
|
import { defineModule, PLATFORM_SHELL } from '@fluojs/runtime';
|
|
12
|
+
import { BOOTSTRAP_PROVIDER_TOKENS, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
10
13
|
import { collectDefaultMetrics, Gauge, Registry as PrometheusRegistry } from 'prom-client';
|
|
11
14
|
import { HttpMetricsMiddleware } from './http-metrics-middleware.js';
|
|
12
|
-
import { METER_PROVIDER } from './providers/meter-provider.js';
|
|
13
15
|
import { MetricsService } from './metrics-service.js';
|
|
16
|
+
import { METER_PROVIDER } from './providers/meter-provider.js';
|
|
14
17
|
import { PrometheusMeterProvider } from './providers/prometheus-meter-provider.js';
|
|
18
|
+
import { SerializedScrapeQueue } from './serialized-scrape-queue.js';
|
|
15
19
|
|
|
16
20
|
/** HTTP-specific metric labeling options exposed by `MetricsModule.forRoot(...)`. */
|
|
17
21
|
|
|
@@ -19,6 +23,9 @@ import { PrometheusMeterProvider } from './providers/prometheus-meter-provider.j
|
|
|
19
23
|
* Module options for exposing Prometheus metrics and runtime platform telemetry.
|
|
20
24
|
*/
|
|
21
25
|
|
|
26
|
+
/** Bootstrap provider token for a Registry shared by metrics module instances. */
|
|
27
|
+
export const METRICS_REGISTRY = Symbol.for('fluo.metrics.registry');
|
|
28
|
+
|
|
22
29
|
/** Module entry point that exposes `/metrics` and optional HTTP/runtime telemetry. */
|
|
23
30
|
export class MetricsModule {
|
|
24
31
|
static registeredRegistries = new WeakSet();
|
|
@@ -30,11 +37,10 @@ export class MetricsModule {
|
|
|
30
37
|
* ```ts
|
|
31
38
|
* MetricsModule.forRoot({
|
|
32
39
|
* http: { pathLabelMode: 'template' },
|
|
33
|
-
* registry: new Registry(),
|
|
34
40
|
* });
|
|
35
41
|
* ```
|
|
36
42
|
*
|
|
37
|
-
* @param options Metrics endpoint,
|
|
43
|
+
* @param options Metrics endpoint, HTTP middleware, and runtime telemetry configuration.
|
|
38
44
|
* @returns A runtime module that exposes metrics through the configured path.
|
|
39
45
|
*/
|
|
40
46
|
static forRoot(options = {}) {
|
|
@@ -48,13 +54,31 @@ export class MetricsModule {
|
|
|
48
54
|
const platformTelemetryToken = Symbol('MetricsModule.platformTelemetry');
|
|
49
55
|
const httpMetricsMiddleware = httpOptions ? createHttpMetricsMiddleware(registryToken, httpOptions) : undefined;
|
|
50
56
|
const endpointMiddleware = typeof metricsPath === 'string' ? (options.endpointMiddleware ?? []).map(middlewareClass => forRoutes(middlewareClass, metricsPath)) : [];
|
|
51
|
-
const middleware = [...
|
|
52
|
-
const
|
|
57
|
+
const middleware = [...endpointMiddleware, ...(options.middleware ?? [])];
|
|
58
|
+
const registryProvider = {
|
|
53
59
|
provide: registryToken,
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
inject: [RUNTIME_CONTAINER, BOOTSTRAP_PROVIDER_TOKENS],
|
|
61
|
+
useFactory: async (container, bootstrapProviderTokens) => {
|
|
62
|
+
const runtimeContainer = assertRuntimeContainer(container);
|
|
63
|
+
const configuredRegistry = assertBootstrapProviderTokens(bootstrapProviderTokens).has(METRICS_REGISTRY) ? assertPrometheusRegistry(await runtimeContainer.resolve(METRICS_REGISTRY)) : undefined;
|
|
64
|
+
return MetricsModule.createRegistry(options, configuredRegistry);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const imports = [];
|
|
68
|
+
const includeRuntimeRegistryProvider = httpMetricsMiddleware === undefined;
|
|
69
|
+
if (httpOptions && httpMetricsMiddleware) {
|
|
70
|
+
class MetricsHttpInstrumentationModule {}
|
|
71
|
+
defineModule(MetricsHttpInstrumentationModule, {
|
|
72
|
+
exports: [registryToken],
|
|
73
|
+
global: true,
|
|
74
|
+
middleware: [httpMetricsMiddleware],
|
|
75
|
+
providers: [registryProvider, httpMetricsMiddleware]
|
|
76
|
+
});
|
|
77
|
+
imports.push(MetricsHttpInstrumentationModule);
|
|
78
|
+
}
|
|
79
|
+
const providers = [...(includeRuntimeRegistryProvider ? [registryProvider] : []), {
|
|
56
80
|
provide: MetricsService,
|
|
57
|
-
inject: [registryToken],
|
|
81
|
+
inject: [registryToken, platformTelemetryToken],
|
|
58
82
|
useFactory: registry => new MetricsService(assertPrometheusRegistry(registry))
|
|
59
83
|
}, {
|
|
60
84
|
provide: METER_PROVIDER,
|
|
@@ -62,8 +86,8 @@ export class MetricsModule {
|
|
|
62
86
|
useFactory: registry => new PrometheusMeterProvider(assertPrometheusRegistry(registry))
|
|
63
87
|
}, {
|
|
64
88
|
provide: platformTelemetryToken,
|
|
65
|
-
inject: [registryToken],
|
|
66
|
-
useFactory: registry => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), options
|
|
89
|
+
inject: [registryToken, RUNTIME_CONTAINER, BOOTSTRAP_PROVIDER_TOKENS],
|
|
90
|
+
useFactory: (registry, container, bootstrapProviderTokens) => new RuntimePlatformTelemetry(assertPrometheusRegistry(registry), assertRuntimeContainer(container), resolveRegistryMode(options, assertBootstrapProviderTokens(bootstrapProviderTokens)), options.platformTelemetry)
|
|
67
91
|
}];
|
|
68
92
|
const controllers = [];
|
|
69
93
|
if (typeof metricsPath === 'string') {
|
|
@@ -78,13 +102,13 @@ export class MetricsModule {
|
|
|
78
102
|
} = _applyDecs(this, [Inject(registryToken, platformTelemetryToken), Controller('')], [[Get(metricsRoutePath), 2, "getMetrics"]]));
|
|
79
103
|
}
|
|
80
104
|
constructor(registry, platformTelemetry) {
|
|
105
|
+
_initProto(this);
|
|
81
106
|
this.registry = registry;
|
|
82
107
|
this.platformTelemetry = platformTelemetry;
|
|
83
|
-
_initProto(this);
|
|
84
108
|
}
|
|
85
109
|
async getMetrics(_input, ctx) {
|
|
86
110
|
ctx.response.setHeader('content-type', this.registry.contentType);
|
|
87
|
-
return this.platformTelemetry.collectMetrics(
|
|
111
|
+
return this.platformTelemetry.collectMetrics(this.registry);
|
|
88
112
|
}
|
|
89
113
|
static {
|
|
90
114
|
_initClass();
|
|
@@ -95,42 +119,74 @@ export class MetricsModule {
|
|
|
95
119
|
class MetricsRuntimeModule {}
|
|
96
120
|
defineModule(MetricsRuntimeModule, {
|
|
97
121
|
controllers,
|
|
122
|
+
exports: [MetricsService, METER_PROVIDER],
|
|
123
|
+
imports,
|
|
98
124
|
middleware,
|
|
99
125
|
providers
|
|
100
126
|
});
|
|
101
127
|
return MetricsRuntimeModule;
|
|
102
128
|
}
|
|
103
|
-
static createRegistry(options) {
|
|
104
|
-
const registry = options.registry ?? new PrometheusRegistry();
|
|
129
|
+
static createRegistry(options, configuredRegistry) {
|
|
130
|
+
const registry = configuredRegistry ?? options.registry ?? new PrometheusRegistry();
|
|
105
131
|
if (options.defaultMetrics !== false && !MetricsModule.registeredRegistries.has(registry)) {
|
|
132
|
+
assertNoDefaultMetricCollisions(registry);
|
|
133
|
+
const existingMetricNames = new Set(registry.getMetricsAsArray().map(metric => metric.name));
|
|
134
|
+
try {
|
|
135
|
+
collectDefaultMetrics({
|
|
136
|
+
register: registry
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
for (const metric of registry.getMetricsAsArray()) {
|
|
140
|
+
if (!existingMetricNames.has(metric.name)) {
|
|
141
|
+
registry.removeSingleMetric(metric.name);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
106
146
|
MetricsModule.registeredRegistries.add(registry);
|
|
107
|
-
collectDefaultMetrics({
|
|
108
|
-
register: registry
|
|
109
|
-
});
|
|
110
147
|
}
|
|
111
148
|
return registry;
|
|
112
149
|
}
|
|
113
150
|
}
|
|
114
151
|
const PLATFORM_COMPONENT_LABELS = ['component_id', 'component_kind', 'operation', 'result', 'env', 'instance'];
|
|
115
152
|
const REGISTRY_MODE_LABELS = ['mode'];
|
|
153
|
+
const require = createRequire(import.meta.url);
|
|
154
|
+
// prom-client exposes collector IDs publicly but not their metric names. This v15.1.3-only
|
|
155
|
+
// private metadata seam keeps collision preflight side-effect-free; the exact package pin and
|
|
156
|
+
// runtime-support regression must be updated together before changing prom-client.
|
|
157
|
+
const DEFAULT_METRIC_COLLECTORS = collectDefaultMetrics.metricsList.map(collectorName => {
|
|
158
|
+
const collector = require(`prom-client/lib/metrics/${collectorName}`);
|
|
159
|
+
if (!hasDefaultMetricNames(collector)) {
|
|
160
|
+
throw new Error(`prom-client default collector "${collectorName}" does not expose metricNames.`);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
collectorName,
|
|
164
|
+
metricNames: collector.metricNames
|
|
165
|
+
};
|
|
166
|
+
});
|
|
116
167
|
const FRAMEWORK_PLATFORM_GAUGES = new WeakSet();
|
|
168
|
+
const PLATFORM_TELEMETRY_REGISTRY_STATES = new WeakMap();
|
|
169
|
+
const HTTP_INSTRUMENTATION_OWNERS = new WeakMap();
|
|
117
170
|
const HEALTH_STATUSES = ['healthy', 'unhealthy', 'degraded'];
|
|
118
171
|
const READINESS_STATUSES = ['ready', 'not-ready', 'degraded'];
|
|
119
|
-
const
|
|
120
|
-
const PLATFORM_SHELL_TOKEN_NAMES = new Set([PLATFORM_SHELL_TOKEN_NAME, String(PLATFORM_SHELL)]);
|
|
172
|
+
const PLATFORM_SHELL_TOKEN_NAMES = new Set([String(PLATFORM_SHELL)]);
|
|
121
173
|
function createHttpMetricsMiddleware(registryToken, httpOptions) {
|
|
122
174
|
let _initClass2;
|
|
123
175
|
let _MetricsHttpMiddlewar;
|
|
124
176
|
class MetricsHttpMiddleware {
|
|
125
177
|
static {
|
|
126
|
-
[_MetricsHttpMiddlewar, _initClass2] = _applyDecs(this, [Inject(registryToken)], []).c;
|
|
178
|
+
[_MetricsHttpMiddlewar, _initClass2] = _applyDecs(this, [Inject(registryToken, RUNTIME_CONTAINER)], []).c;
|
|
127
179
|
}
|
|
128
180
|
delegate;
|
|
129
|
-
constructor(registry) {
|
|
130
|
-
|
|
181
|
+
constructor(registry, container) {
|
|
182
|
+
const httpMetricsMiddleware = new HttpMetricsMiddleware(registry, httpOptions);
|
|
183
|
+
const runtimeContainer = assertRuntimeContainer(container);
|
|
184
|
+
if (claimsHttpInstrumentationOwnership(runtimeContainer, registry)) {
|
|
185
|
+
this.delegate = httpMetricsMiddleware;
|
|
186
|
+
}
|
|
131
187
|
}
|
|
132
188
|
handle(context, next) {
|
|
133
|
-
return this.delegate.handle(context, next);
|
|
189
|
+
return this.delegate ? this.delegate.handle(context, next) : next();
|
|
134
190
|
}
|
|
135
191
|
static {
|
|
136
192
|
_initClass2();
|
|
@@ -138,12 +194,93 @@ function createHttpMetricsMiddleware(registryToken, httpOptions) {
|
|
|
138
194
|
}
|
|
139
195
|
return _MetricsHttpMiddlewar;
|
|
140
196
|
}
|
|
197
|
+
function claimsHttpInstrumentationOwnership(container, registry) {
|
|
198
|
+
let registryOwners = HTTP_INSTRUMENTATION_OWNERS.get(container);
|
|
199
|
+
if (!registryOwners) {
|
|
200
|
+
registryOwners = new WeakSet();
|
|
201
|
+
HTTP_INSTRUMENTATION_OWNERS.set(container, registryOwners);
|
|
202
|
+
}
|
|
203
|
+
if (registryOwners.has(registry)) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
registryOwners.add(registry);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
141
209
|
function assertPrometheusRegistry(value) {
|
|
142
210
|
if (!(value instanceof PrometheusRegistry)) {
|
|
143
211
|
throw new Error('MetricsModule registry provider resolved an invalid Prometheus registry.');
|
|
144
212
|
}
|
|
145
213
|
return value;
|
|
146
214
|
}
|
|
215
|
+
function hasDefaultMetricNames(value) {
|
|
216
|
+
return typeof value === 'function' && 'metricNames' in value && Array.isArray(value.metricNames) && value.metricNames.every(metricName => typeof metricName === 'string');
|
|
217
|
+
}
|
|
218
|
+
function assertNoDefaultMetricCollisions(registry) {
|
|
219
|
+
for (const {
|
|
220
|
+
collectorName,
|
|
221
|
+
metricNames
|
|
222
|
+
} of DEFAULT_METRIC_COLLECTORS) {
|
|
223
|
+
if (!isDefaultCollectorActive(collectorName)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
for (const metricName of metricNames) {
|
|
227
|
+
if (registry.getSingleMetric(metricName)) {
|
|
228
|
+
throw new Error(`A metric with the name ${metricName} has already been registered.`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function isDefaultCollectorActive(collectorName) {
|
|
234
|
+
if (collectorName === 'processHandles') {
|
|
235
|
+
return typeof Reflect.get(process, '_getActiveHandles') === 'function';
|
|
236
|
+
}
|
|
237
|
+
if (collectorName === 'processRequests') {
|
|
238
|
+
return typeof Reflect.get(process, '_getActiveRequests') === 'function';
|
|
239
|
+
}
|
|
240
|
+
if (collectorName === 'processOpenFileDescriptors') {
|
|
241
|
+
return process.platform === 'linux';
|
|
242
|
+
}
|
|
243
|
+
if (collectorName === 'processMaxFileDescriptors') {
|
|
244
|
+
try {
|
|
245
|
+
return readFileSync('/proc/self/limits', 'utf8').split('\n').some(line => line.startsWith('Max open files'));
|
|
246
|
+
} catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
function assertRuntimeContainer(value) {
|
|
253
|
+
if (!isRuntimeContainer(value)) {
|
|
254
|
+
throw new Error('MetricsModule runtime container provider resolved an invalid container.');
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
function assertBootstrapProviderTokens(value) {
|
|
259
|
+
if (!(value instanceof Set)) {
|
|
260
|
+
throw new Error('MetricsModule bootstrap provider token metadata resolved invalidly.');
|
|
261
|
+
}
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
function resolveRegistryMode(options, bootstrapProviderTokens) {
|
|
265
|
+
return options.registry || bootstrapProviderTokens.has(METRICS_REGISTRY) ? 'shared' : 'isolated';
|
|
266
|
+
}
|
|
267
|
+
function isRuntimeContainer(value) {
|
|
268
|
+
return typeof value === 'object' && value !== null && 'resolve' in value && typeof value.resolve === 'function';
|
|
269
|
+
}
|
|
270
|
+
function getRuntimePlatformTelemetryRegistryState(registry) {
|
|
271
|
+
const existing = PLATFORM_TELEMETRY_REGISTRY_STATES.get(registry);
|
|
272
|
+
if (existing) {
|
|
273
|
+
return existing;
|
|
274
|
+
}
|
|
275
|
+
const state = {
|
|
276
|
+
lastHealthStatuses: new Map(),
|
|
277
|
+
lastReadinessStatuses: new Map(),
|
|
278
|
+
registrations: [],
|
|
279
|
+
scrapeQueue: new SerializedScrapeQueue()
|
|
280
|
+
};
|
|
281
|
+
PLATFORM_TELEMETRY_REGISTRY_STATES.set(registry, state);
|
|
282
|
+
return state;
|
|
283
|
+
}
|
|
147
284
|
function toReadinessValue(status) {
|
|
148
285
|
return status === 'ready' ? 1 : 0;
|
|
149
286
|
}
|
|
@@ -175,16 +312,23 @@ function assertGaugeLabelSchema(gauge, config) {
|
|
|
175
312
|
throw new Error(`Metric name "${config.name}" is already registered with labels [${registeredLabels}]. Built-in platform telemetry requires labels [${expectedLabels}].`);
|
|
176
313
|
}
|
|
177
314
|
}
|
|
315
|
+
function removeFrameworkGauge(registry, gauge, metricName) {
|
|
316
|
+
if (!FRAMEWORK_PLATFORM_GAUGES.has(gauge) || registry.getSingleMetric(metricName) !== gauge) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
registry.removeSingleMetric(metricName);
|
|
320
|
+
}
|
|
178
321
|
class RuntimePlatformTelemetry {
|
|
179
322
|
readinessGauge;
|
|
180
323
|
healthGauge;
|
|
181
324
|
registryModeGauge;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
325
|
+
telemetryState;
|
|
326
|
+
constructor(registry, container, registryMode, labels = {}) {
|
|
327
|
+
this.registry = registry;
|
|
328
|
+
this.container = container;
|
|
186
329
|
this.registryMode = registryMode;
|
|
187
330
|
this.labels = labels;
|
|
331
|
+
this.telemetryState = getRuntimePlatformTelemetryRegistryState(registry);
|
|
188
332
|
this.readinessGauge = getOrCreateGauge(registry, {
|
|
189
333
|
help: 'Runtime platform component readiness from shared platform snapshot semantics.',
|
|
190
334
|
labelNames: PLATFORM_COMPONENT_LABELS,
|
|
@@ -201,22 +345,67 @@ class RuntimePlatformTelemetry {
|
|
|
201
345
|
name: 'fluo_metrics_registry_mode'
|
|
202
346
|
});
|
|
203
347
|
this.registryModeGauge.labels(this.registryMode).set(1);
|
|
348
|
+
this.installRegistryRefresh();
|
|
204
349
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
350
|
+
collectMetrics(registry) {
|
|
351
|
+
return registry.metrics();
|
|
352
|
+
}
|
|
353
|
+
async onModuleDestroy() {
|
|
354
|
+
const registrationIndex = this.telemetryState.registrations.lastIndexOf(this);
|
|
355
|
+
if (registrationIndex >= 0) {
|
|
356
|
+
this.telemetryState.registrations.splice(registrationIndex, 1);
|
|
357
|
+
}
|
|
358
|
+
if (this.telemetryState.registrations.length > 0) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
await this.telemetryState.scrapeQueue.drain(() => {
|
|
362
|
+
if (this.telemetryState.registrations.length > 0) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const originalMetrics = this.telemetryState.originalMetrics;
|
|
366
|
+
if (originalMetrics) {
|
|
367
|
+
this.clearPlatformTelemetry();
|
|
368
|
+
removeFrameworkGauge(this.registry, this.readinessGauge, 'fluo_component_ready');
|
|
369
|
+
removeFrameworkGauge(this.registry, this.healthGauge, 'fluo_component_health');
|
|
370
|
+
removeFrameworkGauge(this.registry, this.registryModeGauge, 'fluo_metrics_registry_mode');
|
|
371
|
+
this.registry.metrics = originalMetrics;
|
|
372
|
+
this.telemetryState.originalMetrics = undefined;
|
|
373
|
+
}
|
|
209
374
|
});
|
|
210
|
-
this.scrapeChain = collect.then(() => undefined, () => undefined);
|
|
211
|
-
return collect;
|
|
212
375
|
}
|
|
213
|
-
|
|
214
|
-
|
|
376
|
+
installRegistryRefresh() {
|
|
377
|
+
this.telemetryState.registrations.push(this);
|
|
378
|
+
if (this.telemetryState.originalMetrics) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const registry = this.registry;
|
|
382
|
+
const telemetryState = this.telemetryState;
|
|
383
|
+
const originalMetrics = registry.metrics;
|
|
384
|
+
telemetryState.originalMetrics = originalMetrics;
|
|
385
|
+
registry.metrics = () => {
|
|
386
|
+
return telemetryState.scrapeQueue.enqueue(async () => {
|
|
387
|
+
const activeRegistration = telemetryState.registrations.at(-1);
|
|
388
|
+
if (!activeRegistration) {
|
|
389
|
+
return originalMetrics.call(registry);
|
|
390
|
+
}
|
|
391
|
+
return activeRegistration.collectScrape(() => originalMetrics.call(registry));
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
async collectScrape(render) {
|
|
396
|
+
await this.refresh();
|
|
397
|
+
return await render();
|
|
398
|
+
}
|
|
399
|
+
async refresh() {
|
|
400
|
+
const platformShell = await this.resolvePlatformShell();
|
|
215
401
|
if (!platformShell) {
|
|
216
402
|
this.clearPlatformTelemetry();
|
|
217
403
|
return;
|
|
218
404
|
}
|
|
219
405
|
const snapshot = await platformShell.snapshot();
|
|
406
|
+
this.syncSnapshot(snapshot);
|
|
407
|
+
}
|
|
408
|
+
syncSnapshot(snapshot) {
|
|
220
409
|
const env = this.labels?.env ?? 'unknown';
|
|
221
410
|
const instance = this.labels?.instance ?? 'local';
|
|
222
411
|
const components = [{
|
|
@@ -231,21 +420,17 @@ class RuntimePlatformTelemetry {
|
|
|
231
420
|
readiness: component.readiness.status
|
|
232
421
|
}))];
|
|
233
422
|
this.syncGaugeStatuses({
|
|
234
|
-
currentStatuses:
|
|
235
|
-
env,
|
|
423
|
+
currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.health),
|
|
236
424
|
gauge: this.healthGauge,
|
|
237
|
-
|
|
238
|
-
lastStatuses: this.lastHealthStatuses,
|
|
425
|
+
lastStatuses: this.telemetryState.lastHealthStatuses,
|
|
239
426
|
operation: 'health',
|
|
240
427
|
statuses: HEALTH_STATUSES,
|
|
241
428
|
toMetricValue: toHealthValue
|
|
242
429
|
});
|
|
243
430
|
this.syncGaugeStatuses({
|
|
244
|
-
currentStatuses:
|
|
245
|
-
env,
|
|
431
|
+
currentStatuses: this.toComponentStatusMap(components, env, instance, component => component.readiness),
|
|
246
432
|
gauge: this.readinessGauge,
|
|
247
|
-
|
|
248
|
-
lastStatuses: this.lastReadinessStatuses,
|
|
433
|
+
lastStatuses: this.telemetryState.lastReadinessStatuses,
|
|
249
434
|
operation: 'readiness',
|
|
250
435
|
statuses: READINESS_STATUSES,
|
|
251
436
|
toMetricValue: toReadinessValue
|
|
@@ -253,32 +438,30 @@ class RuntimePlatformTelemetry {
|
|
|
253
438
|
}
|
|
254
439
|
clearPlatformTelemetry() {
|
|
255
440
|
this.clearGaugeStatuses({
|
|
256
|
-
env: this.labels?.env ?? 'unknown',
|
|
257
441
|
gauge: this.healthGauge,
|
|
258
|
-
|
|
259
|
-
lastStatuses: this.lastHealthStatuses,
|
|
442
|
+
lastStatuses: this.telemetryState.lastHealthStatuses,
|
|
260
443
|
operation: 'health',
|
|
261
444
|
statuses: HEALTH_STATUSES
|
|
262
445
|
});
|
|
263
446
|
this.clearGaugeStatuses({
|
|
264
|
-
env: this.labels?.env ?? 'unknown',
|
|
265
447
|
gauge: this.readinessGauge,
|
|
266
|
-
|
|
267
|
-
lastStatuses: this.lastReadinessStatuses,
|
|
448
|
+
lastStatuses: this.telemetryState.lastReadinessStatuses,
|
|
268
449
|
operation: 'readiness',
|
|
269
450
|
statuses: READINESS_STATUSES
|
|
270
451
|
});
|
|
271
452
|
}
|
|
272
453
|
clearGaugeStatuses({
|
|
273
|
-
env,
|
|
274
454
|
gauge,
|
|
275
|
-
instance,
|
|
276
455
|
lastStatuses,
|
|
277
456
|
operation,
|
|
278
457
|
statuses
|
|
279
458
|
}) {
|
|
280
|
-
for (const
|
|
281
|
-
|
|
459
|
+
for (const {
|
|
460
|
+
componentId,
|
|
461
|
+
componentKind,
|
|
462
|
+
env,
|
|
463
|
+
instance
|
|
464
|
+
} of this.componentStatusEntries(lastStatuses)) {
|
|
282
465
|
for (const status of statuses) {
|
|
283
466
|
gauge.remove(componentId, componentKind, operation, status, env, instance);
|
|
284
467
|
}
|
|
@@ -287,20 +470,23 @@ class RuntimePlatformTelemetry {
|
|
|
287
470
|
}
|
|
288
471
|
syncGaugeStatuses({
|
|
289
472
|
currentStatuses,
|
|
290
|
-
env,
|
|
291
473
|
gauge,
|
|
292
|
-
instance,
|
|
293
474
|
lastStatuses,
|
|
294
475
|
operation,
|
|
295
476
|
statuses,
|
|
296
477
|
toMetricValue
|
|
297
478
|
}) {
|
|
298
|
-
for (const
|
|
299
|
-
|
|
479
|
+
for (const {
|
|
480
|
+
componentId,
|
|
481
|
+
componentKind,
|
|
482
|
+
env,
|
|
483
|
+
instance,
|
|
484
|
+
status: previousStatus
|
|
485
|
+
} of this.componentStatusEntries(lastStatuses)) {
|
|
486
|
+
const nextStatus = this.getComponentStatus(currentStatuses, env, instance, componentId, componentKind);
|
|
300
487
|
if (nextStatus === previousStatus) {
|
|
301
488
|
continue;
|
|
302
489
|
}
|
|
303
|
-
const [componentId, componentKind] = this.fromComponentKey(componentKey);
|
|
304
490
|
for (const status of statuses) {
|
|
305
491
|
if (status !== previousStatus) {
|
|
306
492
|
continue;
|
|
@@ -308,41 +494,100 @@ class RuntimePlatformTelemetry {
|
|
|
308
494
|
gauge.remove(componentId, componentKind, operation, status, env, instance);
|
|
309
495
|
}
|
|
310
496
|
}
|
|
311
|
-
for (const
|
|
312
|
-
|
|
497
|
+
for (const {
|
|
498
|
+
componentId,
|
|
499
|
+
componentKind,
|
|
500
|
+
env,
|
|
501
|
+
instance,
|
|
502
|
+
status: currentStatus
|
|
503
|
+
} of this.componentStatusEntries(currentStatuses)) {
|
|
313
504
|
gauge.labels(componentId, componentKind, operation, currentStatus, env, instance).set(toMetricValue(currentStatus));
|
|
314
505
|
}
|
|
315
506
|
lastStatuses.clear();
|
|
316
|
-
for (const
|
|
317
|
-
|
|
507
|
+
for (const {
|
|
508
|
+
componentId,
|
|
509
|
+
componentKind,
|
|
510
|
+
env,
|
|
511
|
+
instance,
|
|
512
|
+
status: currentStatus
|
|
513
|
+
} of this.componentStatusEntries(currentStatuses)) {
|
|
514
|
+
this.setComponentStatus(lastStatuses, env, instance, componentId, componentKind, currentStatus);
|
|
318
515
|
}
|
|
319
516
|
}
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
517
|
+
toComponentStatusMap(components, env, instance, getStatus) {
|
|
518
|
+
const statuses = new Map();
|
|
519
|
+
for (const component of components) {
|
|
520
|
+
this.setComponentStatus(statuses, env, instance, component.id, component.kind, getStatus(component));
|
|
521
|
+
}
|
|
522
|
+
return statuses;
|
|
323
523
|
}
|
|
324
|
-
|
|
325
|
-
|
|
524
|
+
setComponentStatus(statuses, env, instance, componentId, componentKind, status) {
|
|
525
|
+
let statusByInstance = statuses.get(env);
|
|
526
|
+
if (!statusByInstance) {
|
|
527
|
+
statusByInstance = new Map();
|
|
528
|
+
statuses.set(env, statusByInstance);
|
|
529
|
+
}
|
|
530
|
+
let statusByComponent = statusByInstance.get(instance);
|
|
531
|
+
if (!statusByComponent) {
|
|
532
|
+
statusByComponent = new Map();
|
|
533
|
+
statusByInstance.set(instance, statusByComponent);
|
|
534
|
+
}
|
|
535
|
+
let statusByKind = statusByComponent.get(componentId);
|
|
536
|
+
if (!statusByKind) {
|
|
537
|
+
statusByKind = new Map();
|
|
538
|
+
statusByComponent.set(componentId, statusByKind);
|
|
539
|
+
}
|
|
540
|
+
statusByKind.set(componentKind, status);
|
|
541
|
+
}
|
|
542
|
+
getComponentStatus(statuses, env, instance, componentId, componentKind) {
|
|
543
|
+
return statuses.get(env)?.get(instance)?.get(componentId)?.get(componentKind);
|
|
544
|
+
}
|
|
545
|
+
*componentStatusEntries(statuses) {
|
|
546
|
+
for (const [env, statusByInstance] of statuses) {
|
|
547
|
+
for (const [instance, statusByComponent] of statusByInstance) {
|
|
548
|
+
for (const [componentId, statusByKind] of statusByComponent) {
|
|
549
|
+
for (const [componentKind, status] of statusByKind) {
|
|
550
|
+
yield {
|
|
551
|
+
componentId,
|
|
552
|
+
componentKind,
|
|
553
|
+
env,
|
|
554
|
+
instance,
|
|
555
|
+
status
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
326
561
|
}
|
|
327
|
-
async resolvePlatformShell(
|
|
562
|
+
async resolvePlatformShell() {
|
|
563
|
+
const hasPlatformShell = hasContainerToken(this.container, PLATFORM_SHELL);
|
|
564
|
+
if (hasPlatformShell === false) {
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
328
567
|
try {
|
|
329
|
-
return await
|
|
568
|
+
return await this.container.resolve(PLATFORM_SHELL);
|
|
330
569
|
} catch (error) {
|
|
331
|
-
if (isMissingPlatformShellResolutionError(error)) {
|
|
570
|
+
if (hasPlatformShell !== true && isMissingPlatformShellResolutionError(error)) {
|
|
332
571
|
return undefined;
|
|
333
572
|
}
|
|
334
573
|
throw error;
|
|
335
574
|
}
|
|
336
575
|
}
|
|
337
576
|
}
|
|
577
|
+
function hasContainerToken(container, token) {
|
|
578
|
+
const has = container.has;
|
|
579
|
+
if (typeof has !== 'function') {
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
582
|
+
return has.call(container, token);
|
|
583
|
+
}
|
|
338
584
|
function isMissingPlatformShellResolutionError(error) {
|
|
339
585
|
if (!(error instanceof ContainerResolutionError)) {
|
|
340
586
|
return false;
|
|
341
587
|
}
|
|
342
588
|
const containerError = error;
|
|
343
589
|
const token = typeof containerError.meta?.token === 'string' ? containerError.meta.token : undefined;
|
|
344
|
-
|
|
345
|
-
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.';
|
|
590
|
+
return token !== undefined && PLATFORM_SHELL_TOKEN_NAMES.has(token);
|
|
346
591
|
}
|
|
347
592
|
function resolveHttpOptions(http) {
|
|
348
593
|
if (!http) {
|
|
@@ -356,6 +601,7 @@ function resolveHttpOptions(http) {
|
|
|
356
601
|
}
|
|
357
602
|
return {
|
|
358
603
|
allowUnsafeRawPathLabelMode: http.allowUnsafeRawPathLabelMode,
|
|
604
|
+
durationHistogramBuckets: http.durationHistogramBuckets,
|
|
359
605
|
pathLabelMode: http.pathLabelMode,
|
|
360
606
|
pathLabelNormalizer: http.pathLabelNormalizer,
|
|
361
607
|
unknownPathLabel: http.unknownPathLabel
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { Registry, CounterConfiguration, GaugeConfiguration, HistogramConfiguration } from 'prom-client';
|
|
2
2
|
/**
|
|
3
|
-
* Small facade for creating custom Prometheus metrics on
|
|
3
|
+
* Small facade for creating custom Prometheus metrics on its `MetricsModule` registry.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* `MetricsService` is non-global: a module can inject it when it directly imports a
|
|
7
|
+
* `MetricsModule.forRoot(...)` registration or imports a module that re-exports
|
|
8
|
+
* `MetricsService`; unrelated sibling modules do not receive it automatically.
|
|
4
9
|
*/
|
|
5
10
|
export declare class MetricsService {
|
|
6
11
|
private readonly registry;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics-service.d.ts","sourceRoot":"","sources":["../src/metrics-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAIrB
|
|
1
|
+
{"version":3,"file":"metrics-service.d.ts","sourceRoot":"","sources":["../src/metrics-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAIrB;;;;;;;GAOG;AACH,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,QAAQ;IAE/C;;;;;OAKG;IACH,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAIlE;;;;;OAKG;IACH,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAI9D;;;;;OAKG;IACH,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC;IAItE;;;;OAIG;IACH,WAAW,IAAI,QAAQ;CAGxB"}
|
package/dist/metrics-service.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { createPrometheusCounter, createPrometheusGauge, createPrometheusHistogram } from './providers/prometheus-metrics-factory.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Small facade for creating custom Prometheus metrics on
|
|
4
|
+
* Small facade for creating custom Prometheus metrics on its `MetricsModule` registry.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* `MetricsService` is non-global: a module can inject it when it directly imports a
|
|
8
|
+
* `MetricsModule.forRoot(...)` registration or imports a module that re-exports
|
|
9
|
+
* `MetricsService`; unrelated sibling modules do not receive it automatically.
|
|
5
10
|
*/
|
|
6
11
|
export class MetricsService {
|
|
7
12
|
constructor(registry) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Observable state for package-internal serialized scrape scheduling. */
|
|
2
|
+
export interface SerializedScrapeQueueState {
|
|
3
|
+
readonly isRunning: boolean;
|
|
4
|
+
readonly queued: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Serializes refresh-and-render scrape work while preserving individual results.
|
|
8
|
+
*
|
|
9
|
+
* Failed work does not poison later queue entries.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SerializedScrapeQueue {
|
|
12
|
+
private isRunning;
|
|
13
|
+
private queued;
|
|
14
|
+
private tail;
|
|
15
|
+
get state(): SerializedScrapeQueueState;
|
|
16
|
+
enqueue<T>(task: () => Promise<T>): Promise<T>;
|
|
17
|
+
drain(finalizer: () => void): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=serialized-scrape-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialized-scrape-queue.d.ts","sourceRoot":"","sources":["../src/serialized-scrape-queue.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,IAAI,CAAoC;IAEhD,IAAI,KAAK,IAAI,0BAA0B,CAKtC;IAED,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAqBxC,KAAK,CAAC,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAalD"}
|