@bymax-one/nest-core 1.0.1 → 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 +121 -1
- package/README.md +354 -54
- 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 +406 -31
- package/dist/index.d.cts +224 -11
- package/dist/index.d.ts +224 -11
- package/dist/index.mjs +407 -33
- 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 +46 -16
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Catch, Inject, Optional, Injectable, ConfigurableModuleBuilder, Module, HttpException, Logger, Get, Res, Controller, HttpStatus } from '@nestjs/common';
|
|
2
|
-
import { HttpAdapterHost, BaseExceptionFilter, APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
|
|
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);
|
|
@@ -43,7 +56,8 @@ function resolveHealth(raw) {
|
|
|
43
56
|
enabled: raw?.enabled ?? true,
|
|
44
57
|
path: raw?.path ?? DEFAULT_HEALTH_PATH,
|
|
45
58
|
exposeIndicatorErrors: raw?.exposeIndicatorErrors ?? false,
|
|
46
|
-
indicatorTimeoutMs: raw?.indicatorTimeoutMs ?? DEFAULT_INDICATOR_TIMEOUT_MS
|
|
59
|
+
indicatorTimeoutMs: raw?.indicatorTimeoutMs ?? DEFAULT_INDICATOR_TIMEOUT_MS,
|
|
60
|
+
autoDiscover: raw?.autoDiscover ?? false
|
|
47
61
|
};
|
|
48
62
|
}
|
|
49
63
|
function resolveMetrics(raw) {
|
|
@@ -54,12 +68,44 @@ function resolveMetrics(raw) {
|
|
|
54
68
|
defaultLabels: { ...raw?.defaultLabels ?? {} }
|
|
55
69
|
};
|
|
56
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
|
+
}
|
|
57
101
|
function normalizeCoreOptions(raw) {
|
|
58
102
|
return deepFreeze({
|
|
59
103
|
envelope: resolveEnvelope(raw?.envelope),
|
|
60
104
|
timing: resolveTiming(raw?.timing),
|
|
61
105
|
health: resolveHealth(raw?.health),
|
|
62
|
-
metrics: resolveMetrics(raw?.metrics)
|
|
106
|
+
metrics: resolveMetrics(raw?.metrics),
|
|
107
|
+
openapi: resolveOpenApi(raw?.openapi),
|
|
108
|
+
telemetry: resolveTelemetry(raw?.telemetry)
|
|
63
109
|
});
|
|
64
110
|
}
|
|
65
111
|
normalizeCoreOptions();
|
|
@@ -70,6 +116,70 @@ var BYMAX_CORRELATION_PROVIDER = /* @__PURE__ */ Symbol("BYMAX_CORRELATION_PROVI
|
|
|
70
116
|
var BYMAX_TIMING_SINK = /* @__PURE__ */ Symbol("BYMAX_TIMING_SINK");
|
|
71
117
|
var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol("BYMAX_HEALTH_INDICATORS");
|
|
72
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
|
+
}
|
|
73
183
|
|
|
74
184
|
// src/timing/timing.clock.ts
|
|
75
185
|
var DEFAULT_MONOTONIC_CLOCK = {
|
|
@@ -100,6 +210,13 @@ var NoopTimingSink = class {
|
|
|
100
210
|
function buildDefaultProviders() {
|
|
101
211
|
return [{ provide: BYMAX_TIMING_CLOCK, useValue: DEFAULT_MONOTONIC_CLOCK }];
|
|
102
212
|
}
|
|
213
|
+
function buildTraceContextProvider() {
|
|
214
|
+
return {
|
|
215
|
+
provide: BYMAX_TRACE_CONTEXT,
|
|
216
|
+
useFactory: (options) => resolveTraceContextProvider(options),
|
|
217
|
+
inject: [BYMAX_CORE_OPTIONS]
|
|
218
|
+
};
|
|
219
|
+
}
|
|
103
220
|
|
|
104
221
|
// src/envelope/error-codes.ts
|
|
105
222
|
var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
|
|
@@ -159,7 +276,8 @@ function buildErrorEnvelope(input) {
|
|
|
159
276
|
return {
|
|
160
277
|
...base,
|
|
161
278
|
...input.details !== void 0 ? { details: input.details } : {},
|
|
162
|
-
...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
|
|
279
|
+
...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {},
|
|
280
|
+
...input.traceId !== void 0 ? { traceId: input.traceId } : {}
|
|
163
281
|
};
|
|
164
282
|
}
|
|
165
283
|
|
|
@@ -210,12 +328,13 @@ var BymaxExceptionFilter = class {
|
|
|
210
328
|
* nothing is bound, this falls back to a no-op that omits `correlationId`.
|
|
211
329
|
* @param adapterHost - Host of the live HTTP adapter, resolved lazily per catch.
|
|
212
330
|
*/
|
|
213
|
-
constructor(options, correlation, adapterHost) {
|
|
331
|
+
constructor(options, correlation, adapterHost, traceContext) {
|
|
214
332
|
this.options = options;
|
|
215
333
|
this.adapterHost = adapterHost;
|
|
216
334
|
/** Clock used to stamp the envelope timestamp; overridable in tests via fake timers. */
|
|
217
335
|
this.now = () => /* @__PURE__ */ new Date();
|
|
218
336
|
this.correlation = correlation ?? new NoopCorrelationIdProvider();
|
|
337
|
+
this.traceContext = traceContext ?? new NoopTraceContextProvider();
|
|
219
338
|
}
|
|
220
339
|
/**
|
|
221
340
|
* Format the exception into the stable envelope and reply with it.
|
|
@@ -226,6 +345,34 @@ var BymaxExceptionFilter = class {
|
|
|
226
345
|
* @param exception - The error that escaped the handler.
|
|
227
346
|
* @param host - The arguments host for the current execution context.
|
|
228
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
|
+
}
|
|
229
376
|
catch(exception, host) {
|
|
230
377
|
if (host.getType() !== "http") {
|
|
231
378
|
throw exception;
|
|
@@ -234,11 +381,13 @@ var BymaxExceptionFilter = class {
|
|
|
234
381
|
const ctx = host.switchToHttp();
|
|
235
382
|
const request = ctx.getRequest();
|
|
236
383
|
const response = ctx.getResponse();
|
|
237
|
-
const correlationId = this.correlation.getCorrelationId();
|
|
384
|
+
const correlationId = this.readAnnotation(() => this.correlation.getCorrelationId());
|
|
385
|
+
const traceId = this.readAnnotation(() => this.traceContext.getTraceContext()?.traceId);
|
|
238
386
|
const context = {
|
|
239
387
|
method: String(httpAdapter.getRequestMethod(request)),
|
|
240
388
|
path: String(httpAdapter.getRequestUrl(request)),
|
|
241
|
-
...correlationId !== void 0 ? { correlationId } : {}
|
|
389
|
+
...correlationId !== void 0 ? { correlationId } : {},
|
|
390
|
+
...traceId !== void 0 ? { traceId } : {}
|
|
242
391
|
};
|
|
243
392
|
const envelope = this.buildEnvelope(exception, context);
|
|
244
393
|
httpAdapter.reply(response, envelope, envelope.statusCode);
|
|
@@ -332,7 +481,11 @@ var BymaxExceptionFilter = class {
|
|
|
332
481
|
path: context.path,
|
|
333
482
|
now: this.now,
|
|
334
483
|
...details !== void 0 ? { details } : {},
|
|
335
|
-
...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 } : {}
|
|
336
489
|
});
|
|
337
490
|
}
|
|
338
491
|
/**
|
|
@@ -354,7 +507,9 @@ BymaxExceptionFilter = __decorateClass([
|
|
|
354
507
|
__decorateParam(0, Inject(BYMAX_CORE_OPTIONS)),
|
|
355
508
|
__decorateParam(1, Optional()),
|
|
356
509
|
__decorateParam(1, Inject(BYMAX_CORRELATION_PROVIDER)),
|
|
357
|
-
__decorateParam(2, Inject(HttpAdapterHost))
|
|
510
|
+
__decorateParam(2, Inject(HttpAdapterHost)),
|
|
511
|
+
__decorateParam(3, Optional()),
|
|
512
|
+
__decorateParam(3, Inject(BYMAX_TRACE_CONTEXT))
|
|
358
513
|
], BymaxExceptionFilter);
|
|
359
514
|
|
|
360
515
|
// src/timing/request-info.accessor.ts
|
|
@@ -390,11 +545,16 @@ var TimingInterceptor = class {
|
|
|
390
545
|
* @param clock - Monotonic clock seam; defaults to `performance.now()`, and
|
|
391
546
|
* is bound explicitly through {@link BYMAX_TIMING_CLOCK} so tests inject a
|
|
392
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.
|
|
393
552
|
*/
|
|
394
|
-
constructor(options, sink, clock = DEFAULT_MONOTONIC_CLOCK) {
|
|
553
|
+
constructor(options, sink, clock = DEFAULT_MONOTONIC_CLOCK, traceContext) {
|
|
395
554
|
this.options = options;
|
|
396
555
|
this.clock = clock;
|
|
397
556
|
this.sink = sink ?? new NoopTimingSink();
|
|
557
|
+
this.traceContext = traceContext ?? new NoopTraceContextProvider();
|
|
398
558
|
}
|
|
399
559
|
/**
|
|
400
560
|
* Measure the handler chain and record exactly one sample per completed
|
|
@@ -455,9 +615,22 @@ var TimingInterceptor = class {
|
|
|
455
615
|
const durationMs = this.clock.now() - start;
|
|
456
616
|
const threshold = this.options.timing.slowRequestThresholdMs;
|
|
457
617
|
const slow = threshold !== void 0 && durationMs > threshold;
|
|
458
|
-
|
|
618
|
+
let trace;
|
|
619
|
+
try {
|
|
620
|
+
trace = this.traceContext.getTraceContext();
|
|
621
|
+
} catch {
|
|
622
|
+
}
|
|
459
623
|
try {
|
|
460
|
-
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
|
+
});
|
|
461
634
|
} catch {
|
|
462
635
|
}
|
|
463
636
|
}
|
|
@@ -467,7 +640,9 @@ TimingInterceptor = __decorateClass([
|
|
|
467
640
|
__decorateParam(0, Inject(BYMAX_CORE_OPTIONS)),
|
|
468
641
|
__decorateParam(1, Optional()),
|
|
469
642
|
__decorateParam(1, Inject(BYMAX_TIMING_SINK)),
|
|
470
|
-
__decorateParam(2, Inject(BYMAX_TIMING_CLOCK))
|
|
643
|
+
__decorateParam(2, Inject(BYMAX_TIMING_CLOCK)),
|
|
644
|
+
__decorateParam(3, Optional()),
|
|
645
|
+
__decorateParam(3, Inject(BYMAX_TRACE_CONTEXT))
|
|
471
646
|
], TimingInterceptor);
|
|
472
647
|
|
|
473
648
|
// src/passthrough.providers.ts
|
|
@@ -514,6 +689,53 @@ function selectAsyncExceptionFilter(options, correlation, adapterHost) {
|
|
|
514
689
|
function selectAsyncTimingInterceptor(options, sink, clock) {
|
|
515
690
|
return options.timing.enabled ? new TimingInterceptor(options, sink, clock) : new PassThroughInterceptor();
|
|
516
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
|
|
517
739
|
var MAX_ERROR_MESSAGE_LENGTH = 300;
|
|
518
740
|
var TRUNCATION_ELLIPSIS = "...";
|
|
519
741
|
function summarizeRejection(reason) {
|
|
@@ -548,16 +770,24 @@ async function runIndicator(indicator, timeoutMs, exposeErrors, logger) {
|
|
|
548
770
|
}
|
|
549
771
|
var HealthService = class {
|
|
550
772
|
/**
|
|
551
|
-
* @param indicators - Every registered indicator; empty when none
|
|
552
|
-
* Injected with `@Optional()`: `BymaxCoreModule` binds no local
|
|
553
|
-
* this token, so a consumer's own `BYMAX_HEALTH_INDICATORS`
|
|
554
|
-
* their own, globally-visible module) is not shadowed by one;
|
|
555
|
-
* 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.
|
|
556
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.
|
|
557
785
|
*/
|
|
558
|
-
constructor(indicators = [], options) {
|
|
786
|
+
constructor(indicators = [], options, discovery, reflector) {
|
|
559
787
|
this.indicators = indicators;
|
|
560
788
|
this.options = options;
|
|
789
|
+
this.discovery = discovery;
|
|
790
|
+
this.reflector = reflector;
|
|
561
791
|
/**
|
|
562
792
|
* Nest's own logger, scoped to this class. The failure reason of a `down`
|
|
563
793
|
* indicator is written here rather than into the HTTP response, so the
|
|
@@ -565,6 +795,47 @@ var HealthService = class {
|
|
|
565
795
|
*/
|
|
566
796
|
this.logger = new Logger(HealthService.name);
|
|
567
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;
|
|
838
|
+
}
|
|
568
839
|
/**
|
|
569
840
|
* Liveness check: the process is up and able to respond. Runs no
|
|
570
841
|
* indicators, so it never depends on the health of anything else.
|
|
@@ -575,7 +846,7 @@ var HealthService = class {
|
|
|
575
846
|
return { status: "ok", checks: [] };
|
|
576
847
|
}
|
|
577
848
|
/**
|
|
578
|
-
* Readiness check: run every
|
|
849
|
+
* Readiness check: run every indicator in the effective set concurrently and
|
|
579
850
|
* aggregate the results. `status` is `'ok'` only when every indicator
|
|
580
851
|
* reports `up`; an empty indicator list is vacuously `'ok'`.
|
|
581
852
|
*
|
|
@@ -585,7 +856,7 @@ var HealthService = class {
|
|
|
585
856
|
const timeoutMs = this.options.health.indicatorTimeoutMs;
|
|
586
857
|
const exposeErrors = this.options.health.exposeIndicatorErrors;
|
|
587
858
|
const checks = await Promise.all(
|
|
588
|
-
this.
|
|
859
|
+
this.resolveIndicators().map(
|
|
589
860
|
(indicator) => runIndicator(indicator, timeoutMs, exposeErrors, this.logger)
|
|
590
861
|
)
|
|
591
862
|
);
|
|
@@ -597,7 +868,11 @@ HealthService = __decorateClass([
|
|
|
597
868
|
Injectable(),
|
|
598
869
|
__decorateParam(0, Optional()),
|
|
599
870
|
__decorateParam(0, Inject(BYMAX_HEALTH_INDICATORS)),
|
|
600
|
-
__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))
|
|
601
876
|
], HealthService);
|
|
602
877
|
|
|
603
878
|
// src/health/health.controller.ts
|
|
@@ -647,6 +922,98 @@ function createHealthController(registeredPath) {
|
|
|
647
922
|
], HealthController);
|
|
648
923
|
return HealthController;
|
|
649
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);
|
|
650
1017
|
function assertControllerMatchesOptions2(options, registeredPath) {
|
|
651
1018
|
assertAsyncFeatureEnabled("metrics", options.metrics.enabled);
|
|
652
1019
|
if (options.metrics.path !== registeredPath) {
|
|
@@ -688,17 +1055,13 @@ function createMetricsController(registeredPath) {
|
|
|
688
1055
|
}
|
|
689
1056
|
|
|
690
1057
|
// src/metrics/metrics.registry.ts
|
|
691
|
-
var
|
|
692
|
-
function isMissingModuleError(cause) {
|
|
693
|
-
const code = cause.code;
|
|
694
|
-
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
|
|
695
|
-
}
|
|
1058
|
+
var MISSING_PEER_MESSAGE2 = missingPeerMessage("metrics.enabled", "prom-client");
|
|
696
1059
|
async function loadPromClient() {
|
|
697
1060
|
try {
|
|
698
1061
|
return await import('prom-client');
|
|
699
1062
|
} catch (cause) {
|
|
700
1063
|
if (isMissingModuleError(cause)) {
|
|
701
|
-
throw new Error(
|
|
1064
|
+
throw new Error(MISSING_PEER_MESSAGE2, { cause });
|
|
702
1065
|
}
|
|
703
1066
|
throw cause;
|
|
704
1067
|
}
|
|
@@ -844,8 +1207,12 @@ function buildSyncProviders(resolved) {
|
|
|
844
1207
|
if (resolved.health.enabled) {
|
|
845
1208
|
providers.push(HealthService);
|
|
846
1209
|
}
|
|
1210
|
+
if (resolved.telemetry.enabled) {
|
|
1211
|
+
providers.push(buildTraceContextProvider());
|
|
1212
|
+
}
|
|
847
1213
|
if (resolved.metrics.enabled) {
|
|
848
1214
|
providers.push(buildMetricsRegistryProvider());
|
|
1215
|
+
providers.push(MetricsContributionRunner);
|
|
849
1216
|
if (resolved.timing.enabled) {
|
|
850
1217
|
providers.push(buildMetricsTimingSinkProvider());
|
|
851
1218
|
}
|
|
@@ -880,10 +1247,11 @@ function buildAsyncSlots() {
|
|
|
880
1247
|
}
|
|
881
1248
|
];
|
|
882
1249
|
}
|
|
883
|
-
function augmentModule(base, providers, controllers, exportTokens = [BYMAX_CORE_OPTIONS]) {
|
|
1250
|
+
function augmentModule(base, providers, controllers, exportTokens = [BYMAX_CORE_OPTIONS], imports = []) {
|
|
884
1251
|
return {
|
|
885
1252
|
...base,
|
|
886
1253
|
module: BymaxCoreModule,
|
|
1254
|
+
imports: [...base.imports ?? [], ...imports],
|
|
887
1255
|
providers: [...base.providers ?? [], ...providers],
|
|
888
1256
|
controllers: [...base.controllers ?? [], ...controllers],
|
|
889
1257
|
exports: [...base.exports ?? [], ...exportTokens]
|
|
@@ -915,11 +1283,14 @@ var BymaxCoreModule = class extends BymaxCoreModuleBase {
|
|
|
915
1283
|
exportTokens.push(BYMAX_TIMING_SINK);
|
|
916
1284
|
}
|
|
917
1285
|
}
|
|
1286
|
+
const scansProviders = resolved.health.enabled && resolved.health.autoDiscover || resolved.metrics.enabled;
|
|
1287
|
+
const imports = scansProviders ? [DiscoveryModule] : [];
|
|
918
1288
|
return augmentModule(
|
|
919
1289
|
super.forRoot(options),
|
|
920
1290
|
providers,
|
|
921
1291
|
buildControllers(resolved),
|
|
922
|
-
exportTokens
|
|
1292
|
+
exportTokens,
|
|
1293
|
+
imports
|
|
923
1294
|
);
|
|
924
1295
|
}
|
|
925
1296
|
/**
|
|
@@ -953,13 +1324,16 @@ var BymaxCoreModule = class extends BymaxCoreModuleBase {
|
|
|
953
1324
|
...buildAsyncSlots(),
|
|
954
1325
|
HealthService,
|
|
955
1326
|
buildMetricsRegistryProvider(),
|
|
956
|
-
buildMetricsTimingSinkProvider()
|
|
1327
|
+
buildMetricsTimingSinkProvider(),
|
|
1328
|
+
MetricsContributionRunner,
|
|
1329
|
+
buildTraceContextProvider()
|
|
957
1330
|
];
|
|
958
1331
|
return augmentModule(
|
|
959
1332
|
super.forRootAsync(options),
|
|
960
1333
|
providers,
|
|
961
1334
|
[createHealthController(DEFAULT_HEALTH_PATH), createMetricsController(DEFAULT_METRICS_PATH)],
|
|
962
|
-
[BYMAX_CORE_OPTIONS, BYMAX_TIMING_SINK, BYMAX_METRICS_REGISTRY]
|
|
1335
|
+
[BYMAX_CORE_OPTIONS, BYMAX_TIMING_SINK, BYMAX_METRICS_REGISTRY],
|
|
1336
|
+
[DiscoveryModule]
|
|
963
1337
|
);
|
|
964
1338
|
}
|
|
965
1339
|
};
|
|
@@ -967,4 +1341,4 @@ BymaxCoreModule = __decorateClass([
|
|
|
967
1341
|
Module({})
|
|
968
1342
|
], BymaxCoreModule);
|
|
969
1343
|
|
|
970
|
-
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 };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var common = require('@nestjs/common');
|
|
4
|
+
|
|
5
|
+
// src/metrics/metrics.contract.ts
|
|
6
|
+
var BYMAX_METRICS_CONTRIBUTOR_METADATA = "bymax-one:metrics-contributor";
|
|
7
|
+
function BymaxMetricsContributor() {
|
|
8
|
+
return common.SetMetadata(BYMAX_METRICS_CONTRIBUTOR_METADATA, true);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
exports.BYMAX_METRICS_CONTRIBUTOR_METADATA = BYMAX_METRICS_CONTRIBUTOR_METADATA;
|
|
12
|
+
exports.BymaxMetricsContributor = BymaxMetricsContributor;
|