@bymax-one/nest-core 1.0.0 → 1.1.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/CHANGELOG.md +157 -1
- package/README.md +387 -64
- package/dist/health/index.cjs +10 -0
- package/dist/health/index.d.cts +33 -1
- package/dist/health/index.d.ts +33 -1
- package/dist/health/index.mjs +8 -0
- package/dist/index.cjs +422 -37
- package/dist/index.d.cts +237 -11
- package/dist/index.d.ts +237 -11
- package/dist/index.mjs +424 -40
- package/dist/metrics/index.cjs +12 -0
- package/dist/metrics/index.d.cts +57 -0
- package/dist/metrics/index.d.ts +57 -0
- package/dist/metrics/index.mjs +9 -0
- package/dist/openapi/index.cjs +268 -0
- package/dist/openapi/index.d.cts +44 -0
- package/dist/openapi/index.d.ts +44 -0
- package/dist/openapi/index.mjs +266 -0
- package/package.json +70 -27
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Catch, Inject, Optional, Injectable, ConfigurableModuleBuilder, Module, HttpException, Get, Res, Controller, HttpStatus } from '@nestjs/common';
|
|
2
|
-
import { HttpAdapterHost, BaseExceptionFilter, APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
|
|
1
|
+
import { Catch, Inject, Optional, Injectable, ConfigurableModuleBuilder, Module, HttpException, Logger, Get, Res, Controller, HttpStatus } from '@nestjs/common';
|
|
2
|
+
import { HttpAdapterHost, DiscoveryService, Reflector, BaseExceptionFilter, APP_FILTER, APP_INTERCEPTOR, DiscoveryModule } from '@nestjs/core';
|
|
3
3
|
import { tap, catchError, throwError } from 'rxjs';
|
|
4
4
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
@@ -14,10 +14,23 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
14
14
|
};
|
|
15
15
|
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
16
16
|
|
|
17
|
+
// src/runtime.environment.ts
|
|
18
|
+
var NON_PRODUCTION_ENVIRONMENTS = /* @__PURE__ */ new Set(["development", "test"]);
|
|
19
|
+
function isProductionRuntime(value = process.env["NODE_ENV"]) {
|
|
20
|
+
if (value === void 0) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return !NON_PRODUCTION_ENVIRONMENTS.has(value.trim().toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
// src/core.options.ts
|
|
18
27
|
var DEFAULT_HEALTH_PATH = "health";
|
|
19
28
|
var DEFAULT_INDICATOR_TIMEOUT_MS = 5e3;
|
|
20
29
|
var DEFAULT_METRICS_PATH = "metrics";
|
|
30
|
+
var DEFAULT_OPENAPI_PATH = "docs";
|
|
31
|
+
var DEFAULT_OPENAPI_JSON_PATH = "docs-json";
|
|
32
|
+
var DEFAULT_OPENAPI_TITLE = "API";
|
|
33
|
+
var DEFAULT_OPENAPI_VERSION = "1.0.0";
|
|
21
34
|
function deepFreeze(value) {
|
|
22
35
|
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
23
36
|
Object.freeze(value);
|
|
@@ -42,7 +55,9 @@ function resolveHealth(raw) {
|
|
|
42
55
|
return {
|
|
43
56
|
enabled: raw?.enabled ?? true,
|
|
44
57
|
path: raw?.path ?? DEFAULT_HEALTH_PATH,
|
|
45
|
-
|
|
58
|
+
exposeIndicatorErrors: raw?.exposeIndicatorErrors ?? false,
|
|
59
|
+
indicatorTimeoutMs: raw?.indicatorTimeoutMs ?? DEFAULT_INDICATOR_TIMEOUT_MS,
|
|
60
|
+
autoDiscover: raw?.autoDiscover ?? false
|
|
46
61
|
};
|
|
47
62
|
}
|
|
48
63
|
function resolveMetrics(raw) {
|
|
@@ -53,12 +68,44 @@ function resolveMetrics(raw) {
|
|
|
53
68
|
defaultLabels: { ...raw?.defaultLabels ?? {} }
|
|
54
69
|
};
|
|
55
70
|
}
|
|
71
|
+
function cloneServers(raw) {
|
|
72
|
+
return (raw ?? []).map(
|
|
73
|
+
(server) => server.description === void 0 ? { url: server.url } : { url: server.url, description: server.description }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function resolveOpenApi(raw) {
|
|
77
|
+
const requested = raw?.enabled ?? false;
|
|
78
|
+
const production = isProductionRuntime();
|
|
79
|
+
return {
|
|
80
|
+
enabled: requested && !production,
|
|
81
|
+
suppressedInProduction: requested && production,
|
|
82
|
+
path: raw?.path ?? DEFAULT_OPENAPI_PATH,
|
|
83
|
+
jsonPath: raw?.jsonPath ?? DEFAULT_OPENAPI_JSON_PATH,
|
|
84
|
+
title: raw?.title ?? DEFAULT_OPENAPI_TITLE,
|
|
85
|
+
description: raw?.description ?? "",
|
|
86
|
+
version: raw?.version ?? DEFAULT_OPENAPI_VERSION,
|
|
87
|
+
servers: cloneServers(raw?.servers),
|
|
88
|
+
// Structured-cloned rather than shallow-copied: the values are
|
|
89
|
+
// consumer-owned nested objects, and the deep-freeze below would otherwise
|
|
90
|
+
// reach into them.
|
|
91
|
+
securitySchemes: structuredClone(raw?.securitySchemes ?? {}),
|
|
92
|
+
includeCoreSchemas: raw?.includeCoreSchemas ?? true
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function resolveTelemetry(raw) {
|
|
96
|
+
return {
|
|
97
|
+
enabled: raw?.enabled ?? false,
|
|
98
|
+
exposeTraceId: raw?.exposeTraceId ?? false
|
|
99
|
+
};
|
|
100
|
+
}
|
|
56
101
|
function normalizeCoreOptions(raw) {
|
|
57
102
|
return deepFreeze({
|
|
58
103
|
envelope: resolveEnvelope(raw?.envelope),
|
|
59
104
|
timing: resolveTiming(raw?.timing),
|
|
60
105
|
health: resolveHealth(raw?.health),
|
|
61
|
-
metrics: resolveMetrics(raw?.metrics)
|
|
106
|
+
metrics: resolveMetrics(raw?.metrics),
|
|
107
|
+
openapi: resolveOpenApi(raw?.openapi),
|
|
108
|
+
telemetry: resolveTelemetry(raw?.telemetry)
|
|
62
109
|
});
|
|
63
110
|
}
|
|
64
111
|
normalizeCoreOptions();
|
|
@@ -69,6 +116,70 @@ var BYMAX_CORRELATION_PROVIDER = /* @__PURE__ */ Symbol("BYMAX_CORRELATION_PROVI
|
|
|
69
116
|
var BYMAX_TIMING_SINK = /* @__PURE__ */ Symbol("BYMAX_TIMING_SINK");
|
|
70
117
|
var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol("BYMAX_HEALTH_INDICATORS");
|
|
71
118
|
var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol("BYMAX_METRICS_REGISTRY");
|
|
119
|
+
var BYMAX_TRACE_CONTEXT = /* @__PURE__ */ Symbol("BYMAX_TRACE_CONTEXT");
|
|
120
|
+
|
|
121
|
+
// src/optional-peer.ts
|
|
122
|
+
function isMissingModuleError(cause) {
|
|
123
|
+
const code = cause.code;
|
|
124
|
+
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
|
|
125
|
+
}
|
|
126
|
+
function missingPeerMessage(option, peer) {
|
|
127
|
+
return `${option} is true but the optional peer ${peer} is not installed. Run: pnpm add ${peer}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/telemetry/trace-context.ts
|
|
131
|
+
var MISSING_PEER_MESSAGE = missingPeerMessage("telemetry.enabled", "@opentelemetry/api");
|
|
132
|
+
var NoopTraceContextProvider = class {
|
|
133
|
+
/**
|
|
134
|
+
* Resolve no trace context.
|
|
135
|
+
*
|
|
136
|
+
* @returns Always `undefined`.
|
|
137
|
+
*/
|
|
138
|
+
getTraceContext() {
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
async function loadOtelApi() {
|
|
143
|
+
try {
|
|
144
|
+
return await import('@opentelemetry/api');
|
|
145
|
+
} catch (cause) {
|
|
146
|
+
if (isMissingModuleError(cause)) {
|
|
147
|
+
throw new Error(MISSING_PEER_MESSAGE, { cause });
|
|
148
|
+
}
|
|
149
|
+
throw cause;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
var OtelTraceContextProvider = class {
|
|
153
|
+
/**
|
|
154
|
+
* @param api - The loaded OpenTelemetry API surface.
|
|
155
|
+
*/
|
|
156
|
+
constructor(api) {
|
|
157
|
+
this.api = api;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the identifiers of the currently active span.
|
|
161
|
+
*
|
|
162
|
+
* @returns The active trace context, or `undefined` when no valid span is
|
|
163
|
+
* recording.
|
|
164
|
+
*/
|
|
165
|
+
getTraceContext() {
|
|
166
|
+
const span = this.api.trace.getActiveSpan();
|
|
167
|
+
if (span === void 0) {
|
|
168
|
+
return void 0;
|
|
169
|
+
}
|
|
170
|
+
const spanContext = span.spanContext();
|
|
171
|
+
if (!this.api.isSpanContextValid(spanContext)) {
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
return { traceId: spanContext.traceId, spanId: spanContext.spanId };
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
async function resolveTraceContextProvider(options) {
|
|
178
|
+
if (!options.telemetry.enabled) {
|
|
179
|
+
return new NoopTraceContextProvider();
|
|
180
|
+
}
|
|
181
|
+
return new OtelTraceContextProvider(await loadOtelApi());
|
|
182
|
+
}
|
|
72
183
|
|
|
73
184
|
// src/timing/timing.clock.ts
|
|
74
185
|
var DEFAULT_MONOTONIC_CLOCK = {
|
|
@@ -99,6 +210,13 @@ var NoopTimingSink = class {
|
|
|
99
210
|
function buildDefaultProviders() {
|
|
100
211
|
return [{ provide: BYMAX_TIMING_CLOCK, useValue: DEFAULT_MONOTONIC_CLOCK }];
|
|
101
212
|
}
|
|
213
|
+
function buildTraceContextProvider() {
|
|
214
|
+
return {
|
|
215
|
+
provide: BYMAX_TRACE_CONTEXT,
|
|
216
|
+
useFactory: (options) => resolveTraceContextProvider(options),
|
|
217
|
+
inject: [BYMAX_CORE_OPTIONS]
|
|
218
|
+
};
|
|
219
|
+
}
|
|
102
220
|
|
|
103
221
|
// src/envelope/error-codes.ts
|
|
104
222
|
var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
|
|
@@ -158,7 +276,8 @@ function buildErrorEnvelope(input) {
|
|
|
158
276
|
return {
|
|
159
277
|
...base,
|
|
160
278
|
...input.details !== void 0 ? { details: input.details } : {},
|
|
161
|
-
...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
|
|
279
|
+
...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {},
|
|
280
|
+
...input.traceId !== void 0 ? { traceId: input.traceId } : {}
|
|
162
281
|
};
|
|
163
282
|
}
|
|
164
283
|
|
|
@@ -209,12 +328,13 @@ var BymaxExceptionFilter = class {
|
|
|
209
328
|
* nothing is bound, this falls back to a no-op that omits `correlationId`.
|
|
210
329
|
* @param adapterHost - Host of the live HTTP adapter, resolved lazily per catch.
|
|
211
330
|
*/
|
|
212
|
-
constructor(options, correlation, adapterHost) {
|
|
331
|
+
constructor(options, correlation, adapterHost, traceContext) {
|
|
213
332
|
this.options = options;
|
|
214
333
|
this.adapterHost = adapterHost;
|
|
215
334
|
/** Clock used to stamp the envelope timestamp; overridable in tests via fake timers. */
|
|
216
335
|
this.now = () => /* @__PURE__ */ new Date();
|
|
217
336
|
this.correlation = correlation ?? new NoopCorrelationIdProvider();
|
|
337
|
+
this.traceContext = traceContext ?? new NoopTraceContextProvider();
|
|
218
338
|
}
|
|
219
339
|
/**
|
|
220
340
|
* Format the exception into the stable envelope and reply with it.
|
|
@@ -225,6 +345,34 @@ var BymaxExceptionFilter = class {
|
|
|
225
345
|
* @param exception - The error that escaped the handler.
|
|
226
346
|
* @param host - The arguments host for the current execution context.
|
|
227
347
|
*/
|
|
348
|
+
/**
|
|
349
|
+
* Resolve one optional annotation for the envelope, treating any failure as
|
|
350
|
+
* "absent".
|
|
351
|
+
*
|
|
352
|
+
* Both annotations this filter attaches — the correlation id and the trace id
|
|
353
|
+
* — come from providers it does not own: one is supplied by the consumer, the
|
|
354
|
+
* other reads a third-party API. Their contracts say they do not throw, but
|
|
355
|
+
* this filter is the last thing standing between an error and the client, and
|
|
356
|
+
* a guarantee that depends on someone else's good behavior is not one. A
|
|
357
|
+
* failed lookup costs an optional field; an unguarded one would cost the whole
|
|
358
|
+
* response.
|
|
359
|
+
*
|
|
360
|
+
* The failure is deliberately silent, and the same reasoning applies as for
|
|
361
|
+
* the {@link BymaxExceptionFilter.onUnexpectedError} seam a few lines below:
|
|
362
|
+
* this runs while an error is already being formatted, so reporting a
|
|
363
|
+
* telemetry failure here would replace the failure the caller actually needs
|
|
364
|
+
* to see.
|
|
365
|
+
*
|
|
366
|
+
* @param read - The lookup to attempt.
|
|
367
|
+
* @returns The resolved value, or `undefined` when absent or on failure.
|
|
368
|
+
*/
|
|
369
|
+
readAnnotation(read) {
|
|
370
|
+
try {
|
|
371
|
+
return read();
|
|
372
|
+
} catch {
|
|
373
|
+
return void 0;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
228
376
|
catch(exception, host) {
|
|
229
377
|
if (host.getType() !== "http") {
|
|
230
378
|
throw exception;
|
|
@@ -233,11 +381,13 @@ var BymaxExceptionFilter = class {
|
|
|
233
381
|
const ctx = host.switchToHttp();
|
|
234
382
|
const request = ctx.getRequest();
|
|
235
383
|
const response = ctx.getResponse();
|
|
236
|
-
const correlationId = this.correlation.getCorrelationId();
|
|
384
|
+
const correlationId = this.readAnnotation(() => this.correlation.getCorrelationId());
|
|
385
|
+
const traceId = this.readAnnotation(() => this.traceContext.getTraceContext()?.traceId);
|
|
237
386
|
const context = {
|
|
238
387
|
method: String(httpAdapter.getRequestMethod(request)),
|
|
239
388
|
path: String(httpAdapter.getRequestUrl(request)),
|
|
240
|
-
...correlationId !== void 0 ? { correlationId } : {}
|
|
389
|
+
...correlationId !== void 0 ? { correlationId } : {},
|
|
390
|
+
...traceId !== void 0 ? { traceId } : {}
|
|
241
391
|
};
|
|
242
392
|
const envelope = this.buildEnvelope(exception, context);
|
|
243
393
|
httpAdapter.reply(response, envelope, envelope.statusCode);
|
|
@@ -331,7 +481,11 @@ var BymaxExceptionFilter = class {
|
|
|
331
481
|
path: context.path,
|
|
332
482
|
now: this.now,
|
|
333
483
|
...details !== void 0 ? { details } : {},
|
|
334
|
-
...context.correlationId !== void 0 ? { correlationId: context.correlationId } : {}
|
|
484
|
+
...context.correlationId !== void 0 ? { correlationId: context.correlationId } : {},
|
|
485
|
+
// Gated separately from the context above: the trace id reaches the
|
|
486
|
+
// observability seam either way, and the response body only when the
|
|
487
|
+
// operator opted into publishing it.
|
|
488
|
+
...this.options.telemetry.exposeTraceId && context.traceId !== void 0 ? { traceId: context.traceId } : {}
|
|
335
489
|
});
|
|
336
490
|
}
|
|
337
491
|
/**
|
|
@@ -353,7 +507,9 @@ BymaxExceptionFilter = __decorateClass([
|
|
|
353
507
|
__decorateParam(0, Inject(BYMAX_CORE_OPTIONS)),
|
|
354
508
|
__decorateParam(1, Optional()),
|
|
355
509
|
__decorateParam(1, Inject(BYMAX_CORRELATION_PROVIDER)),
|
|
356
|
-
__decorateParam(2, Inject(HttpAdapterHost))
|
|
510
|
+
__decorateParam(2, Inject(HttpAdapterHost)),
|
|
511
|
+
__decorateParam(3, Optional()),
|
|
512
|
+
__decorateParam(3, Inject(BYMAX_TRACE_CONTEXT))
|
|
357
513
|
], BymaxExceptionFilter);
|
|
358
514
|
|
|
359
515
|
// src/timing/request-info.accessor.ts
|
|
@@ -389,11 +545,16 @@ var TimingInterceptor = class {
|
|
|
389
545
|
* @param clock - Monotonic clock seam; defaults to `performance.now()`, and
|
|
390
546
|
* is bound explicitly through {@link BYMAX_TIMING_CLOCK} so tests inject a
|
|
391
547
|
* stub advancing by controlled amounts.
|
|
548
|
+
* @param traceContext - Reads the active span's identifiers. Injected with
|
|
549
|
+
* `@Optional()` so this interceptor stays constructible on its own; when
|
|
550
|
+
* nothing resolves, a no-op resolves no trace and the sample simply omits
|
|
551
|
+
* the fields.
|
|
392
552
|
*/
|
|
393
|
-
constructor(options, sink, clock = DEFAULT_MONOTONIC_CLOCK) {
|
|
553
|
+
constructor(options, sink, clock = DEFAULT_MONOTONIC_CLOCK, traceContext) {
|
|
394
554
|
this.options = options;
|
|
395
555
|
this.clock = clock;
|
|
396
556
|
this.sink = sink ?? new NoopTimingSink();
|
|
557
|
+
this.traceContext = traceContext ?? new NoopTraceContextProvider();
|
|
397
558
|
}
|
|
398
559
|
/**
|
|
399
560
|
* Measure the handler chain and record exactly one sample per completed
|
|
@@ -454,9 +615,22 @@ var TimingInterceptor = class {
|
|
|
454
615
|
const durationMs = this.clock.now() - start;
|
|
455
616
|
const threshold = this.options.timing.slowRequestThresholdMs;
|
|
456
617
|
const slow = threshold !== void 0 && durationMs > threshold;
|
|
457
|
-
|
|
618
|
+
let trace;
|
|
619
|
+
try {
|
|
620
|
+
trace = this.traceContext.getTraceContext();
|
|
621
|
+
} catch {
|
|
622
|
+
}
|
|
458
623
|
try {
|
|
459
|
-
this.sink.record(
|
|
624
|
+
this.sink.record({
|
|
625
|
+
method,
|
|
626
|
+
route,
|
|
627
|
+
statusCode,
|
|
628
|
+
durationMs,
|
|
629
|
+
slow,
|
|
630
|
+
// Spread rather than assigned: an absent trace must leave the keys off
|
|
631
|
+
// the sample entirely, so a sink cannot mistake `undefined` for an id.
|
|
632
|
+
...trace !== void 0 ? { traceId: trace.traceId, spanId: trace.spanId } : {}
|
|
633
|
+
});
|
|
460
634
|
} catch {
|
|
461
635
|
}
|
|
462
636
|
}
|
|
@@ -466,7 +640,9 @@ TimingInterceptor = __decorateClass([
|
|
|
466
640
|
__decorateParam(0, Inject(BYMAX_CORE_OPTIONS)),
|
|
467
641
|
__decorateParam(1, Optional()),
|
|
468
642
|
__decorateParam(1, Inject(BYMAX_TIMING_SINK)),
|
|
469
|
-
__decorateParam(2, Inject(BYMAX_TIMING_CLOCK))
|
|
643
|
+
__decorateParam(2, Inject(BYMAX_TIMING_CLOCK)),
|
|
644
|
+
__decorateParam(3, Optional()),
|
|
645
|
+
__decorateParam(3, Inject(BYMAX_TRACE_CONTEXT))
|
|
470
646
|
], TimingInterceptor);
|
|
471
647
|
|
|
472
648
|
// src/passthrough.providers.ts
|
|
@@ -513,6 +689,53 @@ function selectAsyncExceptionFilter(options, correlation, adapterHost) {
|
|
|
513
689
|
function selectAsyncTimingInterceptor(options, sink, clock) {
|
|
514
690
|
return options.timing.enabled ? new TimingInterceptor(options, sink, clock) : new PassThroughInterceptor();
|
|
515
691
|
}
|
|
692
|
+
|
|
693
|
+
// src/discovery.ts
|
|
694
|
+
function labelFor(className, token) {
|
|
695
|
+
return className === "" ? String(token) : className;
|
|
696
|
+
}
|
|
697
|
+
function findMarkedProviders(discovery, reflector, metadataKey) {
|
|
698
|
+
const marked = [];
|
|
699
|
+
for (const wrapper of discovery.getProviders()) {
|
|
700
|
+
const metatype = wrapper.metatype;
|
|
701
|
+
if (typeof metatype !== "function") {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (reflector.get(metadataKey, metatype) !== true) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
marked.push({ instance: wrapper.instance, label: labelFor(metatype.name, wrapper.name) });
|
|
708
|
+
}
|
|
709
|
+
return marked;
|
|
710
|
+
}
|
|
711
|
+
var BYMAX_HEALTH_INDICATOR_METADATA = "bymax-one:health-indicator";
|
|
712
|
+
|
|
713
|
+
// src/health/health.discovery.ts
|
|
714
|
+
function isIndicator(instance) {
|
|
715
|
+
if (instance === null || instance === void 0) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
const candidate = instance;
|
|
719
|
+
return typeof candidate.name === "string" && candidate.name !== "" && typeof candidate.check === "function";
|
|
720
|
+
}
|
|
721
|
+
function discoverIndicators(discovery, reflector) {
|
|
722
|
+
const discovered = [];
|
|
723
|
+
for (const marked of findMarkedProviders(discovery, reflector, BYMAX_HEALTH_INDICATOR_METADATA)) {
|
|
724
|
+
if (!isIndicator(marked.instance)) {
|
|
725
|
+
throw new Error(
|
|
726
|
+
`[BymaxCoreModule] "${marked.label}" is marked with @BymaxHealthIndicator() but does not implement IHealthIndicator: it must expose a non-empty "name" and a "check" method.`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
discovered.push(marked.instance);
|
|
730
|
+
}
|
|
731
|
+
return discovered.sort((left, right) => left.name.localeCompare(right.name));
|
|
732
|
+
}
|
|
733
|
+
function mergeIndicators(explicit, discovered) {
|
|
734
|
+
const claimed = new Set(explicit.map((indicator) => indicator.name));
|
|
735
|
+
return [...explicit, ...discovered.filter((indicator) => !claimed.has(indicator.name))];
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/health/health.service.ts
|
|
516
739
|
var MAX_ERROR_MESSAGE_LENGTH = 300;
|
|
517
740
|
var TRUNCATION_ELLIPSIS = "...";
|
|
518
741
|
function summarizeRejection(reason) {
|
|
@@ -527,18 +750,18 @@ function summarizeRejection(reason) {
|
|
|
527
750
|
}
|
|
528
751
|
return `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH - TRUNCATION_ELLIPSIS.length)}${TRUNCATION_ELLIPSIS}`;
|
|
529
752
|
}
|
|
530
|
-
async function runIndicator(indicator, timeoutMs) {
|
|
753
|
+
async function runIndicator(indicator, timeoutMs, exposeErrors, logger) {
|
|
531
754
|
let timer;
|
|
532
755
|
const timedOut = new Promise((resolve) => {
|
|
533
756
|
timer = setTimeout(() => {
|
|
534
757
|
resolve({ name: indicator.name, status: "down", details: { timedOutAfterMs: timeoutMs } });
|
|
535
758
|
}, timeoutMs);
|
|
536
759
|
});
|
|
537
|
-
const checked = Promise.resolve().then(() => indicator.check()).then((result) => ({ ...result, name: indicator.name })).catch((reason) =>
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
details: { error:
|
|
541
|
-
})
|
|
760
|
+
const checked = Promise.resolve().then(() => indicator.check()).then((result) => ({ ...result, name: indicator.name })).catch((reason) => {
|
|
761
|
+
const message = summarizeRejection(reason);
|
|
762
|
+
logger.warn(`Health indicator "${indicator.name}" reported down: ${message}`);
|
|
763
|
+
return exposeErrors ? { name: indicator.name, status: "down", details: { error: message } } : { name: indicator.name, status: "down" };
|
|
764
|
+
});
|
|
542
765
|
try {
|
|
543
766
|
return await Promise.race([checked, timedOut]);
|
|
544
767
|
} finally {
|
|
@@ -547,16 +770,71 @@ async function runIndicator(indicator, timeoutMs) {
|
|
|
547
770
|
}
|
|
548
771
|
var HealthService = class {
|
|
549
772
|
/**
|
|
550
|
-
* @param indicators - Every registered indicator; empty when none
|
|
551
|
-
* Injected with `@Optional()`: `BymaxCoreModule` binds no local
|
|
552
|
-
* this token, so a consumer's own `BYMAX_HEALTH_INDICATORS`
|
|
553
|
-
* their own, globally-visible module) is not shadowed by one;
|
|
554
|
-
* is bound, this defaults to an empty array.
|
|
773
|
+
* @param indicators - Every explicitly registered indicator; empty when none
|
|
774
|
+
* resolve. Injected with `@Optional()`: `BymaxCoreModule` binds no local
|
|
775
|
+
* default for this token, so a consumer's own `BYMAX_HEALTH_INDICATORS`
|
|
776
|
+
* binding (from their own, globally-visible module) is not shadowed by one;
|
|
777
|
+
* when nothing is bound, this defaults to an empty array.
|
|
555
778
|
* @param options - Resolved core options; supplies `indicatorTimeoutMs`.
|
|
779
|
+
* @param discovery - Nest's provider-graph reader, present only when
|
|
780
|
+
* `DiscoveryModule` is imported, which `BymaxCoreModule` does exactly when
|
|
781
|
+
* discovery can be needed. Optional so this service stays constructible
|
|
782
|
+
* without it when the feature is off.
|
|
783
|
+
* @param reflector - Nest's metadata reader, used to match the indicator
|
|
784
|
+
* marker. Optional for the same reason.
|
|
556
785
|
*/
|
|
557
|
-
constructor(indicators = [], options) {
|
|
786
|
+
constructor(indicators = [], options, discovery, reflector) {
|
|
558
787
|
this.indicators = indicators;
|
|
559
788
|
this.options = options;
|
|
789
|
+
this.discovery = discovery;
|
|
790
|
+
this.reflector = reflector;
|
|
791
|
+
/**
|
|
792
|
+
* Nest's own logger, scoped to this class. The failure reason of a `down`
|
|
793
|
+
* indicator is written here rather than into the HTTP response, so the
|
|
794
|
+
* diagnostic survives without being served to whoever can reach the probe.
|
|
795
|
+
*/
|
|
796
|
+
this.logger = new Logger(HealthService.name);
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Resolve the readiness set once the whole container is instantiated.
|
|
800
|
+
*
|
|
801
|
+
* Bound to `onApplicationBootstrap`, not `onModuleInit`: module-init hooks run
|
|
802
|
+
* concurrently across modules, so a provider this scan needs may not exist
|
|
803
|
+
* yet. Running here also means a provider marked as an indicator but not
|
|
804
|
+
* implementing the contract fails the boot, instead of failing the first
|
|
805
|
+
* readiness probe in production.
|
|
806
|
+
*/
|
|
807
|
+
onApplicationBootstrap() {
|
|
808
|
+
this.resolveIndicators();
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* The effective readiness set, computed on first use and memoized.
|
|
812
|
+
*
|
|
813
|
+
* @returns The explicit indicators, plus the discovered ones when the feature
|
|
814
|
+
* is enabled.
|
|
815
|
+
* @throws Error When discovery is enabled but Nest's discovery services are
|
|
816
|
+
* not reachable, which means this service was constructed outside
|
|
817
|
+
* `BymaxCoreModule`; a silent fallback would leave an operator believing
|
|
818
|
+
* checks are running that never run.
|
|
819
|
+
*/
|
|
820
|
+
resolveIndicators() {
|
|
821
|
+
if (this.effectiveIndicators !== void 0) {
|
|
822
|
+
return this.effectiveIndicators;
|
|
823
|
+
}
|
|
824
|
+
if (!this.options.health.enabled || !this.options.health.autoDiscover) {
|
|
825
|
+
this.effectiveIndicators = this.indicators;
|
|
826
|
+
return this.effectiveIndicators;
|
|
827
|
+
}
|
|
828
|
+
if (this.discovery === void 0 || this.reflector === void 0) {
|
|
829
|
+
throw new Error(
|
|
830
|
+
"[BymaxCoreModule] health.autoDiscover is enabled but Nest's DiscoveryService is not available. Register the health feature through BymaxCoreModule, which imports DiscoveryModule for it."
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
this.effectiveIndicators = mergeIndicators(
|
|
834
|
+
this.indicators,
|
|
835
|
+
discoverIndicators(this.discovery, this.reflector)
|
|
836
|
+
);
|
|
837
|
+
return this.effectiveIndicators;
|
|
560
838
|
}
|
|
561
839
|
/**
|
|
562
840
|
* Liveness check: the process is up and able to respond. Runs no
|
|
@@ -568,7 +846,7 @@ var HealthService = class {
|
|
|
568
846
|
return { status: "ok", checks: [] };
|
|
569
847
|
}
|
|
570
848
|
/**
|
|
571
|
-
* Readiness check: run every
|
|
849
|
+
* Readiness check: run every indicator in the effective set concurrently and
|
|
572
850
|
* aggregate the results. `status` is `'ok'` only when every indicator
|
|
573
851
|
* reports `up`; an empty indicator list is vacuously `'ok'`.
|
|
574
852
|
*
|
|
@@ -576,8 +854,11 @@ var HealthService = class {
|
|
|
576
854
|
*/
|
|
577
855
|
async checkReadiness() {
|
|
578
856
|
const timeoutMs = this.options.health.indicatorTimeoutMs;
|
|
857
|
+
const exposeErrors = this.options.health.exposeIndicatorErrors;
|
|
579
858
|
const checks = await Promise.all(
|
|
580
|
-
this.
|
|
859
|
+
this.resolveIndicators().map(
|
|
860
|
+
(indicator) => runIndicator(indicator, timeoutMs, exposeErrors, this.logger)
|
|
861
|
+
)
|
|
581
862
|
);
|
|
582
863
|
const status = checks.every((check) => check.status === "up") ? "ok" : "error";
|
|
583
864
|
return { status, checks };
|
|
@@ -587,7 +868,11 @@ HealthService = __decorateClass([
|
|
|
587
868
|
Injectable(),
|
|
588
869
|
__decorateParam(0, Optional()),
|
|
589
870
|
__decorateParam(0, Inject(BYMAX_HEALTH_INDICATORS)),
|
|
590
|
-
__decorateParam(1, Inject(BYMAX_CORE_OPTIONS))
|
|
871
|
+
__decorateParam(1, Inject(BYMAX_CORE_OPTIONS)),
|
|
872
|
+
__decorateParam(2, Optional()),
|
|
873
|
+
__decorateParam(2, Inject(DiscoveryService)),
|
|
874
|
+
__decorateParam(3, Optional()),
|
|
875
|
+
__decorateParam(3, Inject(Reflector))
|
|
591
876
|
], HealthService);
|
|
592
877
|
|
|
593
878
|
// src/health/health.controller.ts
|
|
@@ -637,6 +922,98 @@ function createHealthController(registeredPath) {
|
|
|
637
922
|
], HealthController);
|
|
638
923
|
return HealthController;
|
|
639
924
|
}
|
|
925
|
+
var BYMAX_METRICS_CONTRIBUTOR_METADATA = "bymax-one:metrics-contributor";
|
|
926
|
+
|
|
927
|
+
// src/metrics/metrics.contribution.ts
|
|
928
|
+
function isContributor(instance) {
|
|
929
|
+
if (instance === null || instance === void 0) {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
return typeof instance.registerMetrics === "function";
|
|
933
|
+
}
|
|
934
|
+
function discoverContributors(discovery, reflector) {
|
|
935
|
+
const found = [];
|
|
936
|
+
for (const marked of findMarkedProviders(
|
|
937
|
+
discovery,
|
|
938
|
+
reflector,
|
|
939
|
+
BYMAX_METRICS_CONTRIBUTOR_METADATA
|
|
940
|
+
)) {
|
|
941
|
+
if (!isContributor(marked.instance)) {
|
|
942
|
+
throw new Error(
|
|
943
|
+
`[BymaxCoreModule] "${marked.label}" is marked with @BymaxMetricsContributor() but does not implement IMetricsContributor: it must expose a "registerMetrics" method.`
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
found.push({ contributor: marked.instance, label: marked.label });
|
|
947
|
+
}
|
|
948
|
+
return found.sort((left, right) => left.label.localeCompare(right.label));
|
|
949
|
+
}
|
|
950
|
+
function applyContributions(contributors, registry) {
|
|
951
|
+
for (const { contributor, label } of contributors) {
|
|
952
|
+
try {
|
|
953
|
+
contributor.registerMetrics(registry);
|
|
954
|
+
} catch (cause) {
|
|
955
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
956
|
+
throw new Error(`[BymaxCoreModule] "${label}" failed to register its metrics: ${reason}`, {
|
|
957
|
+
cause
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
var MetricsContributionRunner = class {
|
|
963
|
+
/**
|
|
964
|
+
* @param options - Resolved core options; the metrics gate is read from here.
|
|
965
|
+
* @param registry - The registry the scrape endpoint serves. On the async path
|
|
966
|
+
* this resolves to a guarded placeholder while metrics are disabled, which is
|
|
967
|
+
* why the gate below runs before the registry is ever passed on.
|
|
968
|
+
* @param discovery - Nest's provider-graph reader, present when
|
|
969
|
+
* `DiscoveryModule` is imported. Optional so this service stays constructible
|
|
970
|
+
* without it.
|
|
971
|
+
* @param reflector - Nest's metadata reader, used to match the marker.
|
|
972
|
+
*/
|
|
973
|
+
constructor(options, registry, discovery, reflector) {
|
|
974
|
+
this.options = options;
|
|
975
|
+
this.registry = registry;
|
|
976
|
+
this.discovery = discovery;
|
|
977
|
+
this.reflector = reflector;
|
|
978
|
+
/** Nest's logger, scoped to the module, for the one line this feature writes. */
|
|
979
|
+
this.logger = new Logger("BymaxCoreModule");
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Find the contributors and let each register its collectors.
|
|
983
|
+
*
|
|
984
|
+
* @throws Error When a marked provider is not a contributor, when one fails to
|
|
985
|
+
* register, or when metrics are enabled without Nest's discovery services —
|
|
986
|
+
* which means this runner was constructed outside `BymaxCoreModule`, and a
|
|
987
|
+
* silent skip would leave an operator with an endpoint quietly missing every
|
|
988
|
+
* metric a library publishes.
|
|
989
|
+
*/
|
|
990
|
+
onApplicationBootstrap() {
|
|
991
|
+
if (!this.options.metrics.enabled) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (this.discovery === void 0 || this.reflector === void 0) {
|
|
995
|
+
throw new Error(
|
|
996
|
+
"[BymaxCoreModule] metrics are enabled but Nest's DiscoveryService is not available. Register the metrics feature through BymaxCoreModule, which imports DiscoveryModule for it."
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
const contributors = discoverContributors(this.discovery, this.reflector);
|
|
1000
|
+
applyContributions(contributors, this.registry);
|
|
1001
|
+
if (contributors.length > 0) {
|
|
1002
|
+
this.logger.log(
|
|
1003
|
+
`Registered metrics from ${contributors.length} contributor(s): ${contributors.map(({ label }) => label).join(", ")}`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
MetricsContributionRunner = __decorateClass([
|
|
1009
|
+
Injectable(),
|
|
1010
|
+
__decorateParam(0, Inject(BYMAX_CORE_OPTIONS)),
|
|
1011
|
+
__decorateParam(1, Inject(BYMAX_METRICS_REGISTRY)),
|
|
1012
|
+
__decorateParam(2, Optional()),
|
|
1013
|
+
__decorateParam(2, Inject(DiscoveryService)),
|
|
1014
|
+
__decorateParam(3, Optional()),
|
|
1015
|
+
__decorateParam(3, Inject(Reflector))
|
|
1016
|
+
], MetricsContributionRunner);
|
|
640
1017
|
function assertControllerMatchesOptions2(options, registeredPath) {
|
|
641
1018
|
assertAsyncFeatureEnabled("metrics", options.metrics.enabled);
|
|
642
1019
|
if (options.metrics.path !== registeredPath) {
|
|
@@ -678,17 +1055,13 @@ function createMetricsController(registeredPath) {
|
|
|
678
1055
|
}
|
|
679
1056
|
|
|
680
1057
|
// src/metrics/metrics.registry.ts
|
|
681
|
-
var
|
|
682
|
-
function isMissingModuleError(cause) {
|
|
683
|
-
const code = cause.code;
|
|
684
|
-
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
|
|
685
|
-
}
|
|
1058
|
+
var MISSING_PEER_MESSAGE2 = missingPeerMessage("metrics.enabled", "prom-client");
|
|
686
1059
|
async function loadPromClient() {
|
|
687
1060
|
try {
|
|
688
1061
|
return await import('prom-client');
|
|
689
1062
|
} catch (cause) {
|
|
690
1063
|
if (isMissingModuleError(cause)) {
|
|
691
|
-
throw new Error(
|
|
1064
|
+
throw new Error(MISSING_PEER_MESSAGE2, { cause });
|
|
692
1065
|
}
|
|
693
1066
|
throw cause;
|
|
694
1067
|
}
|
|
@@ -834,8 +1207,12 @@ function buildSyncProviders(resolved) {
|
|
|
834
1207
|
if (resolved.health.enabled) {
|
|
835
1208
|
providers.push(HealthService);
|
|
836
1209
|
}
|
|
1210
|
+
if (resolved.telemetry.enabled) {
|
|
1211
|
+
providers.push(buildTraceContextProvider());
|
|
1212
|
+
}
|
|
837
1213
|
if (resolved.metrics.enabled) {
|
|
838
1214
|
providers.push(buildMetricsRegistryProvider());
|
|
1215
|
+
providers.push(MetricsContributionRunner);
|
|
839
1216
|
if (resolved.timing.enabled) {
|
|
840
1217
|
providers.push(buildMetricsTimingSinkProvider());
|
|
841
1218
|
}
|
|
@@ -870,10 +1247,11 @@ function buildAsyncSlots() {
|
|
|
870
1247
|
}
|
|
871
1248
|
];
|
|
872
1249
|
}
|
|
873
|
-
function augmentModule(base, providers, controllers, exportTokens = [BYMAX_CORE_OPTIONS]) {
|
|
1250
|
+
function augmentModule(base, providers, controllers, exportTokens = [BYMAX_CORE_OPTIONS], imports = []) {
|
|
874
1251
|
return {
|
|
875
1252
|
...base,
|
|
876
1253
|
module: BymaxCoreModule,
|
|
1254
|
+
imports: [...base.imports ?? [], ...imports],
|
|
877
1255
|
providers: [...base.providers ?? [], ...providers],
|
|
878
1256
|
controllers: [...base.controllers ?? [], ...controllers],
|
|
879
1257
|
exports: [...base.exports ?? [], ...exportTokens]
|
|
@@ -905,11 +1283,14 @@ var BymaxCoreModule = class extends BymaxCoreModuleBase {
|
|
|
905
1283
|
exportTokens.push(BYMAX_TIMING_SINK);
|
|
906
1284
|
}
|
|
907
1285
|
}
|
|
1286
|
+
const scansProviders = resolved.health.enabled && resolved.health.autoDiscover || resolved.metrics.enabled;
|
|
1287
|
+
const imports = scansProviders ? [DiscoveryModule] : [];
|
|
908
1288
|
return augmentModule(
|
|
909
1289
|
super.forRoot(options),
|
|
910
1290
|
providers,
|
|
911
1291
|
buildControllers(resolved),
|
|
912
|
-
exportTokens
|
|
1292
|
+
exportTokens,
|
|
1293
|
+
imports
|
|
913
1294
|
);
|
|
914
1295
|
}
|
|
915
1296
|
/**
|
|
@@ -943,13 +1324,16 @@ var BymaxCoreModule = class extends BymaxCoreModuleBase {
|
|
|
943
1324
|
...buildAsyncSlots(),
|
|
944
1325
|
HealthService,
|
|
945
1326
|
buildMetricsRegistryProvider(),
|
|
946
|
-
buildMetricsTimingSinkProvider()
|
|
1327
|
+
buildMetricsTimingSinkProvider(),
|
|
1328
|
+
MetricsContributionRunner,
|
|
1329
|
+
buildTraceContextProvider()
|
|
947
1330
|
];
|
|
948
1331
|
return augmentModule(
|
|
949
1332
|
super.forRootAsync(options),
|
|
950
1333
|
providers,
|
|
951
1334
|
[createHealthController(DEFAULT_HEALTH_PATH), createMetricsController(DEFAULT_METRICS_PATH)],
|
|
952
|
-
[BYMAX_CORE_OPTIONS, BYMAX_TIMING_SINK, BYMAX_METRICS_REGISTRY]
|
|
1335
|
+
[BYMAX_CORE_OPTIONS, BYMAX_TIMING_SINK, BYMAX_METRICS_REGISTRY],
|
|
1336
|
+
[DiscoveryModule]
|
|
953
1337
|
);
|
|
954
1338
|
}
|
|
955
1339
|
};
|
|
@@ -957,4 +1341,4 @@ BymaxCoreModule = __decorateClass([
|
|
|
957
1341
|
Module({})
|
|
958
1342
|
], BymaxCoreModule);
|
|
959
1343
|
|
|
960
|
-
export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_UNAUTHORIZED, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxCoreModule, BymaxExceptionFilter, TimingInterceptor, buildErrorEnvelope, codeForStatus };
|
|
1344
|
+
export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxCoreModule, BymaxExceptionFilter, TimingInterceptor, buildErrorEnvelope, codeForStatus };
|