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