@horizon-republic/nestjs-jetstream 2.9.1 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -71
- package/dist/index.cjs +2545 -607
- package/dist/index.d.cts +1562 -1082
- package/dist/index.d.ts +1562 -1082
- package/dist/index.js +2552 -611
- package/package.json +31 -17
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from2, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
var __decorateClass = (decorators, target, key, kind) => {
|
|
20
30
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
@@ -29,6 +39,7 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
|
|
|
29
39
|
// src/index.ts
|
|
30
40
|
var index_exports = {};
|
|
31
41
|
__export(index_exports, {
|
|
42
|
+
ConsumeKind: () => ConsumeKind,
|
|
32
43
|
DEFAULT_BROADCAST_CONSUMER_CONFIG: () => DEFAULT_BROADCAST_CONSUMER_CONFIG,
|
|
33
44
|
DEFAULT_BROADCAST_STREAM_CONFIG: () => DEFAULT_BROADCAST_STREAM_CONFIG,
|
|
34
45
|
DEFAULT_COMMAND_CONSUMER_CONFIG: () => DEFAULT_COMMAND_CONSUMER_CONFIG,
|
|
@@ -44,6 +55,7 @@ __export(index_exports, {
|
|
|
44
55
|
DEFAULT_ORDERED_STREAM_CONFIG: () => DEFAULT_ORDERED_STREAM_CONFIG,
|
|
45
56
|
DEFAULT_RPC_TIMEOUT: () => DEFAULT_RPC_TIMEOUT,
|
|
46
57
|
DEFAULT_SHUTDOWN_TIMEOUT: () => DEFAULT_SHUTDOWN_TIMEOUT,
|
|
58
|
+
DEFAULT_TRACES: () => DEFAULT_TRACES,
|
|
47
59
|
JETSTREAM_CODEC: () => JETSTREAM_CODEC,
|
|
48
60
|
JETSTREAM_CONNECTION: () => JETSTREAM_CONNECTION,
|
|
49
61
|
JETSTREAM_OPTIONS: () => JETSTREAM_OPTIONS,
|
|
@@ -55,15 +67,18 @@ __export(index_exports, {
|
|
|
55
67
|
JetstreamRecord: () => JetstreamRecord,
|
|
56
68
|
JetstreamRecordBuilder: () => JetstreamRecordBuilder,
|
|
57
69
|
JetstreamStrategy: () => JetstreamStrategy,
|
|
70
|
+
JetstreamTrace: () => JetstreamTrace,
|
|
58
71
|
JsonCodec: () => JsonCodec,
|
|
59
72
|
MIN_METADATA_TTL: () => MIN_METADATA_TTL,
|
|
60
73
|
MessageKind: () => MessageKind,
|
|
61
74
|
MsgpackCodec: () => MsgpackCodec,
|
|
62
75
|
NatsErrorCode: () => NatsErrorCode,
|
|
63
76
|
PatternPrefix: () => PatternPrefix,
|
|
77
|
+
PublishKind: () => PublishKind,
|
|
64
78
|
RESERVED_HEADERS: () => RESERVED_HEADERS,
|
|
65
79
|
RpcContext: () => RpcContext,
|
|
66
80
|
StreamKind: () => StreamKind,
|
|
81
|
+
TRACER_NAME: () => TRACER_NAME,
|
|
67
82
|
TransportEvent: () => TransportEvent,
|
|
68
83
|
buildBroadcastSubject: () => buildBroadcastSubject,
|
|
69
84
|
buildSubject: () => buildSubject,
|
|
@@ -80,13 +95,14 @@ __export(index_exports, {
|
|
|
80
95
|
module.exports = __toCommonJS(index_exports);
|
|
81
96
|
|
|
82
97
|
// src/jetstream.module.ts
|
|
83
|
-
var
|
|
98
|
+
var import_common21 = require("@nestjs/common");
|
|
84
99
|
|
|
85
100
|
// src/client/jetstream.client.ts
|
|
86
|
-
var
|
|
101
|
+
var import_common5 = require("@nestjs/common");
|
|
87
102
|
var import_microservices = require("@nestjs/microservices");
|
|
88
|
-
var
|
|
103
|
+
var import_api8 = require("@opentelemetry/api");
|
|
89
104
|
var import_nuid = require("@nats-io/nuid");
|
|
105
|
+
var import_transport_node = require("@nats-io/transport-node");
|
|
90
106
|
|
|
91
107
|
// src/interfaces/hooks.interface.ts
|
|
92
108
|
var MessageKind = /* @__PURE__ */ ((MessageKind2) => {
|
|
@@ -104,6 +120,10 @@ var TransportEvent = /* @__PURE__ */ ((TransportEvent2) => {
|
|
|
104
120
|
TransportEvent2["ShutdownStart"] = "shutdownStart";
|
|
105
121
|
TransportEvent2["ShutdownComplete"] = "shutdownComplete";
|
|
106
122
|
TransportEvent2["DeadLetter"] = "deadLetter";
|
|
123
|
+
TransportEvent2["ConsumerRecovered"] = "consumerRecovered";
|
|
124
|
+
TransportEvent2["HandlerCompleted"] = "handlerCompleted";
|
|
125
|
+
TransportEvent2["Published"] = "published";
|
|
126
|
+
TransportEvent2["RpcCompleted"] = "rpcCompleted";
|
|
107
127
|
return TransportEvent2;
|
|
108
128
|
})(TransportEvent || {});
|
|
109
129
|
|
|
@@ -276,6 +296,907 @@ var PatternPrefix = /* @__PURE__ */ ((PatternPrefix2) => {
|
|
|
276
296
|
var isJetStreamRpcMode = (rpc) => rpc?.mode === "jetstream";
|
|
277
297
|
var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
|
|
278
298
|
|
|
299
|
+
// src/otel/constants.ts
|
|
300
|
+
var TRACER_NAME = "@horizon-republic/nestjs-jetstream";
|
|
301
|
+
|
|
302
|
+
// src/otel/attribute-keys.ts
|
|
303
|
+
var ATTR_MESSAGING_SYSTEM = "messaging.system";
|
|
304
|
+
var ATTR_MESSAGING_DESTINATION_NAME = "messaging.destination.name";
|
|
305
|
+
var ATTR_MESSAGING_DESTINATION_TEMPLATE = "messaging.destination.template";
|
|
306
|
+
var ATTR_MESSAGING_CLIENT_ID = "messaging.client.id";
|
|
307
|
+
var ATTR_MESSAGING_OPERATION_NAME = "messaging.operation.name";
|
|
308
|
+
var ATTR_MESSAGING_OPERATION_TYPE = "messaging.operation.type";
|
|
309
|
+
var ATTR_MESSAGING_MESSAGE_BODY_SIZE = "messaging.message.body.size";
|
|
310
|
+
var ATTR_MESSAGING_MESSAGE_ID = "messaging.message.id";
|
|
311
|
+
var ATTR_MESSAGING_MESSAGE_CONVERSATION_ID = "messaging.message.conversation_id";
|
|
312
|
+
var ATTR_MESSAGING_CONSUMER_GROUP_NAME = "messaging.consumer.group.name";
|
|
313
|
+
var ATTR_MESSAGING_HEADER_PREFIX = "messaging.header.";
|
|
314
|
+
var ATTR_MESSAGING_NATS_STREAM_NAME = "messaging.nats.stream.name";
|
|
315
|
+
var ATTR_MESSAGING_NATS_STREAM_SEQUENCE = "messaging.nats.message.stream_sequence";
|
|
316
|
+
var ATTR_MESSAGING_NATS_CONSUMER_SEQUENCE = "messaging.nats.message.consumer_sequence";
|
|
317
|
+
var ATTR_MESSAGING_NATS_DELIVERY_COUNT = "messaging.nats.message.delivery_count";
|
|
318
|
+
var ATTR_MESSAGING_NATS_BODY = "messaging.nats.message.body";
|
|
319
|
+
var ATTR_MESSAGING_NATS_BODY_TRUNCATED = "messaging.nats.message.body.truncated";
|
|
320
|
+
var ATTR_SERVER_ADDRESS = "server.address";
|
|
321
|
+
var ATTR_SERVER_PORT = "server.port";
|
|
322
|
+
var ATTR_JETSTREAM_SERVICE_NAME = "jetstream.service.name";
|
|
323
|
+
var ATTR_JETSTREAM_KIND = "jetstream.kind";
|
|
324
|
+
var ATTR_JETSTREAM_RPC_REPLY_HAS_ERROR = "jetstream.rpc.reply.has_error";
|
|
325
|
+
var ATTR_JETSTREAM_RPC_REPLY_ERROR_CODE = "jetstream.rpc.reply.error.code";
|
|
326
|
+
var ATTR_JETSTREAM_PROVISIONING_ENTITY = "jetstream.provisioning.entity";
|
|
327
|
+
var ATTR_JETSTREAM_PROVISIONING_ACTION = "jetstream.provisioning.action";
|
|
328
|
+
var ATTR_JETSTREAM_PROVISIONING_NAME = "jetstream.provisioning.name";
|
|
329
|
+
var ATTR_JETSTREAM_SELF_HEALING_REASON = "jetstream.self_healing.reason";
|
|
330
|
+
var ATTR_JETSTREAM_MIGRATION_REASON = "jetstream.migration.reason";
|
|
331
|
+
var ATTR_JETSTREAM_DEAD_LETTER_REASON = "jetstream.dead_letter.reason";
|
|
332
|
+
var ATTR_JETSTREAM_SCHEDULE_TARGET = "jetstream.schedule.target";
|
|
333
|
+
var ATTR_NATS_CONNECTION_SERVER = "nats.connection.server";
|
|
334
|
+
var NATS_MSG_ID_HEADER = "Nats-Msg-Id";
|
|
335
|
+
var HOOK_PUBLISH = "publishHook";
|
|
336
|
+
var HOOK_CONSUME = "consumeHook";
|
|
337
|
+
var HOOK_RESPONSE = "responseHook";
|
|
338
|
+
var SPAN_NAME_PUBLISH = "publish";
|
|
339
|
+
var SPAN_NAME_PROCESS = "process";
|
|
340
|
+
var SPAN_NAME_SEND = "send";
|
|
341
|
+
var SPAN_NAME_DEAD_LETTER = "dead_letter";
|
|
342
|
+
var SPAN_NAME_NATS_CONNECTION = "nats.connection";
|
|
343
|
+
var SPAN_NAME_JETSTREAM_SHUTDOWN = "jetstream.shutdown";
|
|
344
|
+
var SPAN_NAME_JETSTREAM_SELF_HEALING = "jetstream.self_healing";
|
|
345
|
+
var SPAN_NAME_JETSTREAM_MIGRATION = "jetstream.migration";
|
|
346
|
+
var SPAN_NAME_JETSTREAM_PROVISIONING_PREFIX = "jetstream.provisioning.";
|
|
347
|
+
var EVENT_CONNECTION_DISCONNECTED = "connection.disconnected";
|
|
348
|
+
var EVENT_CONNECTION_RECONNECTED = "connection.reconnected";
|
|
349
|
+
var messagingHeaderAttr = (headerName) => `${ATTR_MESSAGING_HEADER_PREFIX}${headerName.toLowerCase()}`;
|
|
350
|
+
|
|
351
|
+
// src/otel/trace-kinds.ts
|
|
352
|
+
var JetstreamTrace = /* @__PURE__ */ ((JetstreamTrace2) => {
|
|
353
|
+
JetstreamTrace2["Publish"] = "publish";
|
|
354
|
+
JetstreamTrace2["Consume"] = "consume";
|
|
355
|
+
JetstreamTrace2["RpcClientSend"] = "rpc.client.send";
|
|
356
|
+
JetstreamTrace2["DeadLetter"] = "dead_letter";
|
|
357
|
+
JetstreamTrace2["ConnectionLifecycle"] = "connection.lifecycle";
|
|
358
|
+
JetstreamTrace2["SelfHealing"] = "self_healing";
|
|
359
|
+
JetstreamTrace2["Provisioning"] = "provisioning";
|
|
360
|
+
JetstreamTrace2["Migration"] = "migration";
|
|
361
|
+
JetstreamTrace2["Shutdown"] = "shutdown";
|
|
362
|
+
return JetstreamTrace2;
|
|
363
|
+
})(JetstreamTrace || {});
|
|
364
|
+
var DEFAULT_TRACES = [
|
|
365
|
+
"publish" /* Publish */,
|
|
366
|
+
"consume" /* Consume */,
|
|
367
|
+
"rpc.client.send" /* RpcClientSend */,
|
|
368
|
+
"dead_letter" /* DeadLetter */
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
// src/otel/capture.ts
|
|
372
|
+
var NEGATION_PREFIX = "!";
|
|
373
|
+
var REGEX_SPECIAL_RE = /[.+?^${}()|[\]\\*]/gu;
|
|
374
|
+
var escapeForRegex = (input) => input.replace(REGEX_SPECIAL_RE, "\\$&");
|
|
375
|
+
var globToRegex = (glob) => {
|
|
376
|
+
const escaped = escapeForRegex(glob.toLowerCase()).replace(/\\\*/gu, ".*");
|
|
377
|
+
return new RegExp(`^${escaped}$`, "u");
|
|
378
|
+
};
|
|
379
|
+
var compileHeaderAllowlist = (allowlist) => {
|
|
380
|
+
if (allowlist === true) return () => true;
|
|
381
|
+
if (allowlist === false) return () => false;
|
|
382
|
+
if (allowlist.length === 0) return () => false;
|
|
383
|
+
const includes = [];
|
|
384
|
+
const excludes = [];
|
|
385
|
+
for (const pattern of allowlist) {
|
|
386
|
+
const isExclude = pattern.startsWith(NEGATION_PREFIX);
|
|
387
|
+
const body = isExclude ? pattern.slice(NEGATION_PREFIX.length) : pattern;
|
|
388
|
+
const regex = globToRegex(body);
|
|
389
|
+
if (isExclude) excludes.push(regex);
|
|
390
|
+
else includes.push(regex);
|
|
391
|
+
}
|
|
392
|
+
return (name) => {
|
|
393
|
+
const lower = name.toLowerCase();
|
|
394
|
+
if (!includes.some((re) => re.test(lower))) return false;
|
|
395
|
+
if (excludes.some((re) => re.test(lower))) return false;
|
|
396
|
+
return true;
|
|
397
|
+
};
|
|
398
|
+
};
|
|
399
|
+
var subjectMatcherCache = /* @__PURE__ */ new WeakMap();
|
|
400
|
+
var compileSubjectAllowlist = (allowlist) => {
|
|
401
|
+
if (!allowlist || allowlist.length === 0) return () => true;
|
|
402
|
+
const cached = subjectMatcherCache.get(allowlist);
|
|
403
|
+
if (cached) return cached;
|
|
404
|
+
const regexes = allowlist.map(globToRegex);
|
|
405
|
+
const matcher = (subject) => {
|
|
406
|
+
const lower = subject.toLowerCase();
|
|
407
|
+
return regexes.some((re) => re.test(lower));
|
|
408
|
+
};
|
|
409
|
+
subjectMatcherCache.set(allowlist, matcher);
|
|
410
|
+
return matcher;
|
|
411
|
+
};
|
|
412
|
+
var subjectMatchesAllowlist = (subject, allowlist) => compileSubjectAllowlist(allowlist)(subject);
|
|
413
|
+
var HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
414
|
+
"traceparent",
|
|
415
|
+
"tracestate",
|
|
416
|
+
"baggage",
|
|
417
|
+
"sentry-trace",
|
|
418
|
+
"b3",
|
|
419
|
+
"x-b3-traceid",
|
|
420
|
+
"x-b3-spanid",
|
|
421
|
+
"x-b3-parentspanid",
|
|
422
|
+
"x-b3-sampled",
|
|
423
|
+
"x-b3-flags",
|
|
424
|
+
"uber-trace-id",
|
|
425
|
+
"x-correlation-id",
|
|
426
|
+
"x-reply-to",
|
|
427
|
+
"x-error",
|
|
428
|
+
"x-subject",
|
|
429
|
+
"x-caller-name"
|
|
430
|
+
]);
|
|
431
|
+
var isNatsServerHeader = (lower) => lower.startsWith("nats-");
|
|
432
|
+
var captureMatchingHeaders = (headers2, matcher) => {
|
|
433
|
+
if (!headers2) return {};
|
|
434
|
+
const out = {};
|
|
435
|
+
for (const key of headers2.keys()) {
|
|
436
|
+
const lower = key.toLowerCase();
|
|
437
|
+
if (HEADER_DENYLIST.has(lower)) continue;
|
|
438
|
+
if (isNatsServerHeader(lower)) continue;
|
|
439
|
+
if (!matcher(key)) continue;
|
|
440
|
+
const value = headers2.get(key);
|
|
441
|
+
if (value === "") continue;
|
|
442
|
+
out[messagingHeaderAttr(lower)] = value;
|
|
443
|
+
}
|
|
444
|
+
return out;
|
|
445
|
+
};
|
|
446
|
+
var tryUtf8Decode = (bytes) => {
|
|
447
|
+
try {
|
|
448
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
449
|
+
} catch {
|
|
450
|
+
return Buffer.from(bytes).toString("base64");
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
var captureBodyAttribute = (subject, payload, capture) => {
|
|
454
|
+
if (capture === false) return {};
|
|
455
|
+
if (payload.byteLength === 0) return {};
|
|
456
|
+
if (!subjectMatchesAllowlist(subject, capture.subjectAllowlist)) return {};
|
|
457
|
+
const { maxBytes } = capture;
|
|
458
|
+
const truncated = payload.byteLength > maxBytes;
|
|
459
|
+
const slice = truncated ? payload.subarray(0, maxBytes) : payload;
|
|
460
|
+
const decoded = tryUtf8Decode(slice);
|
|
461
|
+
const out = { [ATTR_MESSAGING_NATS_BODY]: decoded };
|
|
462
|
+
if (truncated) out[ATTR_MESSAGING_NATS_BODY_TRUNCATED] = true;
|
|
463
|
+
return out;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// src/otel/config.ts
|
|
467
|
+
var PublishKind = /* @__PURE__ */ ((PublishKind2) => {
|
|
468
|
+
PublishKind2["Event"] = "event";
|
|
469
|
+
PublishKind2["RpcRequest"] = "rpc.request";
|
|
470
|
+
PublishKind2["Broadcast"] = "broadcast";
|
|
471
|
+
PublishKind2["Ordered"] = "ordered";
|
|
472
|
+
return PublishKind2;
|
|
473
|
+
})(PublishKind || {});
|
|
474
|
+
var ConsumeKind = /* @__PURE__ */ ((ConsumeKind2) => {
|
|
475
|
+
ConsumeKind2["Event"] = "event";
|
|
476
|
+
ConsumeKind2["Rpc"] = "rpc";
|
|
477
|
+
ConsumeKind2["Broadcast"] = "broadcast";
|
|
478
|
+
ConsumeKind2["Ordered"] = "ordered";
|
|
479
|
+
return ConsumeKind2;
|
|
480
|
+
})(ConsumeKind || {});
|
|
481
|
+
var DEFAULT_CAPTURE_HEADERS = ["x-request-id"];
|
|
482
|
+
var DEFAULT_CAPTURE_BODY_MAX_BYTES = 4096;
|
|
483
|
+
var NESTJS_BARE_ERROR_MESSAGE = "Internal server error";
|
|
484
|
+
var isNestjsBareErrorSentinel = (obj) => obj.status === "error" && obj.message === NESTJS_BARE_ERROR_MESSAGE;
|
|
485
|
+
var defaultErrorClassifier = (err) => {
|
|
486
|
+
if (err === null || typeof err !== "object") return "unexpected";
|
|
487
|
+
if (!(err instanceof Error)) {
|
|
488
|
+
if (isNestjsBareErrorSentinel(err)) return "unexpected";
|
|
489
|
+
return "expected";
|
|
490
|
+
}
|
|
491
|
+
let proto = err;
|
|
492
|
+
while (proto) {
|
|
493
|
+
const name = proto.constructor?.name;
|
|
494
|
+
if (name === "RpcException" || name === "HttpException") return "expected";
|
|
495
|
+
proto = Object.getPrototypeOf(proto);
|
|
496
|
+
}
|
|
497
|
+
return "unexpected";
|
|
498
|
+
};
|
|
499
|
+
var expandTracesOption = (option) => {
|
|
500
|
+
if (option === void 0 || option === "default") return new Set(DEFAULT_TRACES);
|
|
501
|
+
if (option === "all") return new Set(Object.values(JetstreamTrace));
|
|
502
|
+
if (option === "none") return /* @__PURE__ */ new Set();
|
|
503
|
+
return new Set(option);
|
|
504
|
+
};
|
|
505
|
+
var compileHeaderMatcher = (option) => compileHeaderAllowlist(option ?? DEFAULT_CAPTURE_HEADERS);
|
|
506
|
+
var resolveCaptureBody = (option) => {
|
|
507
|
+
if (option === void 0 || option === false) return false;
|
|
508
|
+
if (option === true) return { maxBytes: DEFAULT_CAPTURE_BODY_MAX_BYTES };
|
|
509
|
+
return {
|
|
510
|
+
maxBytes: option.maxBytes ?? DEFAULT_CAPTURE_BODY_MAX_BYTES,
|
|
511
|
+
subjectAllowlist: option.subjectAllowlist
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
var resolveOtelOptions = (options = {}) => {
|
|
515
|
+
return {
|
|
516
|
+
enabled: options.enabled ?? true,
|
|
517
|
+
traces: expandTracesOption(options.traces),
|
|
518
|
+
captureHeaders: compileHeaderMatcher(options.captureHeaders),
|
|
519
|
+
captureBody: resolveCaptureBody(options.captureBody),
|
|
520
|
+
publishHook: options.publishHook,
|
|
521
|
+
consumeHook: options.consumeHook,
|
|
522
|
+
responseHook: options.responseHook,
|
|
523
|
+
shouldTracePublish: options.shouldTracePublish,
|
|
524
|
+
shouldTraceConsume: options.shouldTraceConsume,
|
|
525
|
+
errorClassifier: options.errorClassifier ?? defaultErrorClassifier
|
|
526
|
+
};
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// src/otel/internal-utils.ts
|
|
530
|
+
var import_common = require("@nestjs/common");
|
|
531
|
+
var logger = new import_common.Logger("Jetstream:Otel");
|
|
532
|
+
var safelyInvokeHook = (hookName, hook, ...args) => {
|
|
533
|
+
if (!hook) return;
|
|
534
|
+
const logHookFailure = (err) => {
|
|
535
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
536
|
+
logger.debug(`OTel ${hookName} threw: ${message}`);
|
|
537
|
+
};
|
|
538
|
+
try {
|
|
539
|
+
const result = hook(...args);
|
|
540
|
+
if (result !== null && typeof result === "object" && "then" in result && typeof result.then === "function") {
|
|
541
|
+
result.then(void 0, logHookFailure);
|
|
542
|
+
}
|
|
543
|
+
} catch (err) {
|
|
544
|
+
logHookFailure(err);
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
var stripIpv6Brackets = (host) => host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
548
|
+
var parsePort = (portRaw) => {
|
|
549
|
+
if (!portRaw) return void 0;
|
|
550
|
+
const port = Number.parseInt(portRaw, 10);
|
|
551
|
+
return Number.isInteger(port) ? port : void 0;
|
|
552
|
+
};
|
|
553
|
+
var parseServerAddress = (servers) => {
|
|
554
|
+
const raw = servers[0];
|
|
555
|
+
if (!raw) return null;
|
|
556
|
+
if (raw.includes("://")) {
|
|
557
|
+
try {
|
|
558
|
+
const url = new URL(raw);
|
|
559
|
+
if (url.hostname.length === 0) return null;
|
|
560
|
+
const host2 = stripIpv6Brackets(url.hostname);
|
|
561
|
+
const port2 = parsePort(url.port || void 0);
|
|
562
|
+
return port2 === void 0 ? { host: host2 } : { host: host2, port: port2 };
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (raw.startsWith("[")) {
|
|
568
|
+
const closeIdx = raw.indexOf("]");
|
|
569
|
+
if (closeIdx <= 0) return null;
|
|
570
|
+
const host2 = raw.slice(1, closeIdx);
|
|
571
|
+
const port2 = parsePort(raw.slice(closeIdx + 1).replace(/^:/u, ""));
|
|
572
|
+
return port2 === void 0 ? { host: host2 } : { host: host2, port: port2 };
|
|
573
|
+
}
|
|
574
|
+
const [host, portRaw] = raw.split(":");
|
|
575
|
+
if (!host) return null;
|
|
576
|
+
const port = parsePort(portRaw);
|
|
577
|
+
return port === void 0 ? { host } : { host, port };
|
|
578
|
+
};
|
|
579
|
+
var deriveOtelAttrs = (options) => ({
|
|
580
|
+
otel: resolveOtelOptions(options.otel),
|
|
581
|
+
serviceName: internalName(options.name),
|
|
582
|
+
serverEndpoint: parseServerAddress(options.servers)
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// src/otel/propagator.ts
|
|
586
|
+
var import_api = require("@opentelemetry/api");
|
|
587
|
+
var injectContext = (ctx, carrier, setter) => {
|
|
588
|
+
import_api.propagation.inject(ctx, carrier, setter);
|
|
589
|
+
};
|
|
590
|
+
var extractContext = (ctx, carrier, getter) => import_api.propagation.extract(ctx, carrier, getter);
|
|
591
|
+
|
|
592
|
+
// src/otel/tracer.ts
|
|
593
|
+
var import_api2 = require("@opentelemetry/api");
|
|
594
|
+
var PACKAGE_VERSION = true ? "2.11.0" : "0.0.0";
|
|
595
|
+
var getTracer = () => import_api2.trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
|
|
596
|
+
|
|
597
|
+
// src/otel/carrier.ts
|
|
598
|
+
var hdrsSetter = {
|
|
599
|
+
set: (headers2, key, value) => {
|
|
600
|
+
headers2.set(key, value);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
var hdrsGetter = {
|
|
604
|
+
keys: (headers2) => headers2 ? headers2.keys() : [],
|
|
605
|
+
get: (headers2, key) => {
|
|
606
|
+
if (!headers2) return void 0;
|
|
607
|
+
const all = typeof headers2.values === "function" ? headers2.values(key) : void 0;
|
|
608
|
+
if (Array.isArray(all)) {
|
|
609
|
+
const nonEmpty = all.filter((value) => value !== "");
|
|
610
|
+
if (nonEmpty.length === 0) return void 0;
|
|
611
|
+
return nonEmpty.join(",");
|
|
612
|
+
}
|
|
613
|
+
const single = headers2.get(key);
|
|
614
|
+
return single === "" ? void 0 : single;
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// src/otel/attributes.ts
|
|
619
|
+
var MESSAGING_SYSTEM = "nats";
|
|
620
|
+
var baseMessagingAttributes = (ctx) => {
|
|
621
|
+
const attrs = {
|
|
622
|
+
[ATTR_MESSAGING_SYSTEM]: MESSAGING_SYSTEM,
|
|
623
|
+
[ATTR_MESSAGING_DESTINATION_NAME]: ctx.subject,
|
|
624
|
+
[ATTR_MESSAGING_CLIENT_ID]: ctx.serviceName
|
|
625
|
+
};
|
|
626
|
+
if (ctx.serverAddress) attrs[ATTR_SERVER_ADDRESS] = ctx.serverAddress;
|
|
627
|
+
if (ctx.serverPort !== void 0) attrs[ATTR_SERVER_PORT] = ctx.serverPort;
|
|
628
|
+
if (ctx.pattern && ctx.pattern !== ctx.subject) {
|
|
629
|
+
attrs[ATTR_MESSAGING_DESTINATION_TEMPLATE] = ctx.pattern;
|
|
630
|
+
}
|
|
631
|
+
return attrs;
|
|
632
|
+
};
|
|
633
|
+
var jetstreamKindForPublish = (kind) => kind === "rpc.request" /* RpcRequest */ ? "rpc" /* Rpc */ : kind;
|
|
634
|
+
var buildPublishAttributes = (ctx) => {
|
|
635
|
+
const attrs = {
|
|
636
|
+
...baseMessagingAttributes(ctx),
|
|
637
|
+
[ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_PUBLISH,
|
|
638
|
+
[ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_SEND,
|
|
639
|
+
[ATTR_MESSAGING_MESSAGE_BODY_SIZE]: ctx.payloadBytes,
|
|
640
|
+
[ATTR_JETSTREAM_KIND]: jetstreamKindForPublish(ctx.kind)
|
|
641
|
+
};
|
|
642
|
+
if (ctx.messageId) attrs[ATTR_MESSAGING_MESSAGE_ID] = ctx.messageId;
|
|
643
|
+
if (ctx.correlationId) attrs[ATTR_MESSAGING_MESSAGE_CONVERSATION_ID] = ctx.correlationId;
|
|
644
|
+
return attrs;
|
|
645
|
+
};
|
|
646
|
+
var buildConsumeAttributes = (ctx) => {
|
|
647
|
+
const { msg, info, kind, payloadBytes } = ctx;
|
|
648
|
+
const attrs = {
|
|
649
|
+
...baseMessagingAttributes(ctx),
|
|
650
|
+
[ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_PROCESS,
|
|
651
|
+
[ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_PROCESS,
|
|
652
|
+
[ATTR_MESSAGING_MESSAGE_BODY_SIZE]: payloadBytes,
|
|
653
|
+
[ATTR_JETSTREAM_KIND]: kind
|
|
654
|
+
};
|
|
655
|
+
if (info) {
|
|
656
|
+
attrs[ATTR_MESSAGING_NATS_STREAM_NAME] = info.stream;
|
|
657
|
+
attrs[ATTR_MESSAGING_CONSUMER_GROUP_NAME] = info.consumer;
|
|
658
|
+
attrs[ATTR_MESSAGING_NATS_STREAM_SEQUENCE] = info.streamSequence;
|
|
659
|
+
attrs[ATTR_MESSAGING_NATS_CONSUMER_SEQUENCE] = info.deliverySequence;
|
|
660
|
+
attrs[ATTR_MESSAGING_NATS_DELIVERY_COUNT] = info.deliveryCount;
|
|
661
|
+
}
|
|
662
|
+
const messageId = msg.headers?.get(NATS_MSG_ID_HEADER);
|
|
663
|
+
if (messageId) attrs[ATTR_MESSAGING_MESSAGE_ID] = messageId;
|
|
664
|
+
return attrs;
|
|
665
|
+
};
|
|
666
|
+
var buildRpcClientAttributes = (ctx) => {
|
|
667
|
+
const attrs = {
|
|
668
|
+
...baseMessagingAttributes(ctx),
|
|
669
|
+
[ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_SEND,
|
|
670
|
+
[ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_SEND,
|
|
671
|
+
[ATTR_MESSAGING_MESSAGE_BODY_SIZE]: ctx.payloadBytes,
|
|
672
|
+
[ATTR_JETSTREAM_KIND]: "rpc" /* Rpc */
|
|
673
|
+
};
|
|
674
|
+
if (ctx.correlationId) attrs[ATTR_MESSAGING_MESSAGE_CONVERSATION_ID] = ctx.correlationId;
|
|
675
|
+
if (ctx.messageId) attrs[ATTR_MESSAGING_MESSAGE_ID] = ctx.messageId;
|
|
676
|
+
return attrs;
|
|
677
|
+
};
|
|
678
|
+
var buildDeadLetterAttributes = (ctx) => {
|
|
679
|
+
const attrs = {
|
|
680
|
+
...baseMessagingAttributes(ctx),
|
|
681
|
+
[ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_DEAD_LETTER,
|
|
682
|
+
[ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_PROCESS,
|
|
683
|
+
[ATTR_MESSAGING_NATS_DELIVERY_COUNT]: ctx.finalDeliveryCount
|
|
684
|
+
};
|
|
685
|
+
if (ctx.reason) attrs[ATTR_JETSTREAM_DEAD_LETTER_REASON] = ctx.reason;
|
|
686
|
+
return attrs;
|
|
687
|
+
};
|
|
688
|
+
var extractFromRpcException = (record) => {
|
|
689
|
+
const getError = record.getError;
|
|
690
|
+
if (typeof getError !== "function") return void 0;
|
|
691
|
+
return codeFromPayload(getError.call(record));
|
|
692
|
+
};
|
|
693
|
+
var extractFromHttpException = (record) => {
|
|
694
|
+
const getStatus = record.getStatus;
|
|
695
|
+
if (typeof getStatus !== "function") return void 0;
|
|
696
|
+
const status = getStatus.call(record);
|
|
697
|
+
return typeof status === "number" && Number.isFinite(status) ? `HTTP_${status}` : void 0;
|
|
698
|
+
};
|
|
699
|
+
var extractFromOwnCode = (record) => {
|
|
700
|
+
const code = record.code ?? record.errorCode;
|
|
701
|
+
return typeof code === "string" && isStableErrorCode(code) ? code : void 0;
|
|
702
|
+
};
|
|
703
|
+
var extractExpectedErrorCode = (err) => {
|
|
704
|
+
if (err === null || typeof err !== "object") return void 0;
|
|
705
|
+
const record = err;
|
|
706
|
+
if (hasAncestorNamed(err, "RpcException")) {
|
|
707
|
+
const fromPayload = extractFromRpcException(record);
|
|
708
|
+
if (fromPayload !== void 0) return fromPayload;
|
|
709
|
+
}
|
|
710
|
+
if (hasAncestorNamed(err, "HttpException")) {
|
|
711
|
+
const fromStatus = extractFromHttpException(record);
|
|
712
|
+
if (fromStatus !== void 0) return fromStatus;
|
|
713
|
+
}
|
|
714
|
+
return extractFromOwnCode(record);
|
|
715
|
+
};
|
|
716
|
+
var hasAncestorNamed = (err, name) => {
|
|
717
|
+
let proto = Object.getPrototypeOf(err);
|
|
718
|
+
while (proto) {
|
|
719
|
+
const ctorName = proto.constructor?.name;
|
|
720
|
+
if (ctorName === name) return true;
|
|
721
|
+
proto = Object.getPrototypeOf(proto);
|
|
722
|
+
}
|
|
723
|
+
return false;
|
|
724
|
+
};
|
|
725
|
+
var STABLE_ERROR_CODE_RE = /^[A-Z][A-Z0-9_]*$/u;
|
|
726
|
+
var isStableErrorCode = (value) => STABLE_ERROR_CODE_RE.test(value);
|
|
727
|
+
var codeFromPayload = (payload) => {
|
|
728
|
+
if (payload === null || payload === void 0) return void 0;
|
|
729
|
+
if (typeof payload === "string") return isStableErrorCode(payload) ? payload : void 0;
|
|
730
|
+
if (typeof payload === "object") {
|
|
731
|
+
const code = payload.code;
|
|
732
|
+
if (typeof code === "string" && isStableErrorCode(code)) return code;
|
|
733
|
+
}
|
|
734
|
+
return void 0;
|
|
735
|
+
};
|
|
736
|
+
var buildExpectedErrorAttributes = (err) => {
|
|
737
|
+
const attrs = {
|
|
738
|
+
[ATTR_JETSTREAM_RPC_REPLY_HAS_ERROR]: true
|
|
739
|
+
};
|
|
740
|
+
const code = extractExpectedErrorCode(err);
|
|
741
|
+
if (code !== void 0) attrs[ATTR_JETSTREAM_RPC_REPLY_ERROR_CODE] = code;
|
|
742
|
+
return attrs;
|
|
743
|
+
};
|
|
744
|
+
var applyExpectedErrorAttributes = (span, err) => {
|
|
745
|
+
span.setAttributes(buildExpectedErrorAttributes(err));
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// src/otel/spans/publish.ts
|
|
749
|
+
var import_common2 = require("@nestjs/common");
|
|
750
|
+
var import_api3 = require("@opentelemetry/api");
|
|
751
|
+
var logger2 = new import_common2.Logger("Jetstream:Otel");
|
|
752
|
+
var shouldTracePublishSafe = (predicate, subject, record) => {
|
|
753
|
+
if (!predicate) return true;
|
|
754
|
+
try {
|
|
755
|
+
return predicate(subject, record);
|
|
756
|
+
} catch (err) {
|
|
757
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
758
|
+
logger2.debug(`OTel shouldTracePublish threw: ${message}`);
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
var withPublishSpan = async (ctx, config, fn) => {
|
|
763
|
+
if (!config.enabled) return fn();
|
|
764
|
+
const shouldCreateSpan = config.traces.has("publish" /* Publish */) && shouldTracePublishSafe(config.shouldTracePublish, ctx.subject, ctx.record);
|
|
765
|
+
if (!shouldCreateSpan) {
|
|
766
|
+
injectContext(import_api3.context.active(), ctx.headers, hdrsSetter);
|
|
767
|
+
return fn();
|
|
768
|
+
}
|
|
769
|
+
const tracer = getTracer();
|
|
770
|
+
const span = tracer.startSpan(`${SPAN_NAME_PUBLISH} ${ctx.subject}`, {
|
|
771
|
+
kind: import_api3.SpanKind.PRODUCER,
|
|
772
|
+
attributes: {
|
|
773
|
+
...buildPublishAttributes({
|
|
774
|
+
subject: ctx.subject,
|
|
775
|
+
pattern: ctx.pattern,
|
|
776
|
+
serviceName: ctx.serviceName,
|
|
777
|
+
serverAddress: ctx.endpoint?.host,
|
|
778
|
+
serverPort: ctx.endpoint?.port,
|
|
779
|
+
kind: ctx.kind,
|
|
780
|
+
payloadBytes: ctx.payloadBytes,
|
|
781
|
+
messageId: ctx.messageId,
|
|
782
|
+
correlationId: ctx.correlationId
|
|
783
|
+
}),
|
|
784
|
+
...ctx.scheduleTarget ? { [ATTR_JETSTREAM_SCHEDULE_TARGET]: ctx.scheduleTarget } : {},
|
|
785
|
+
...captureMatchingHeaders(ctx.headers, config.captureHeaders),
|
|
786
|
+
...captureBodyAttribute(ctx.subject, ctx.payload, config.captureBody)
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
const ctxWithSpan = import_api3.trace.setSpan(import_api3.context.active(), span);
|
|
790
|
+
injectContext(ctxWithSpan, ctx.headers, hdrsSetter);
|
|
791
|
+
const start = Date.now();
|
|
792
|
+
const invokeResponseHook = (error) => {
|
|
793
|
+
import_api3.context.with(ctxWithSpan, () => {
|
|
794
|
+
safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
|
|
795
|
+
subject: ctx.subject,
|
|
796
|
+
durationMs: Date.now() - start,
|
|
797
|
+
error
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
};
|
|
801
|
+
try {
|
|
802
|
+
const result = await import_api3.context.with(ctxWithSpan, async () => {
|
|
803
|
+
safelyInvokeHook(HOOK_PUBLISH, config.publishHook, span, {
|
|
804
|
+
subject: ctx.subject,
|
|
805
|
+
record: ctx.record,
|
|
806
|
+
kind: ctx.kind
|
|
807
|
+
});
|
|
808
|
+
return fn();
|
|
809
|
+
});
|
|
810
|
+
span.setStatus({ code: import_api3.SpanStatusCode.OK });
|
|
811
|
+
invokeResponseHook();
|
|
812
|
+
return result;
|
|
813
|
+
} catch (err) {
|
|
814
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
815
|
+
span.recordException(error);
|
|
816
|
+
span.setStatus({ code: import_api3.SpanStatusCode.ERROR, message: error.message });
|
|
817
|
+
invokeResponseHook(error);
|
|
818
|
+
throw err;
|
|
819
|
+
} finally {
|
|
820
|
+
span.end();
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
// src/otel/spans/consume.ts
|
|
825
|
+
var import_api4 = require("@opentelemetry/api");
|
|
826
|
+
var isPromiseLike = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
|
|
827
|
+
var applyExpectedError = (span, err) => {
|
|
828
|
+
span.setStatus({ code: import_api4.SpanStatusCode.OK });
|
|
829
|
+
applyExpectedErrorAttributes(span, err);
|
|
830
|
+
};
|
|
831
|
+
var applyUnexpectedError = (span, err) => {
|
|
832
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
833
|
+
span.recordException(error);
|
|
834
|
+
span.setStatus({ code: import_api4.SpanStatusCode.ERROR, message: error.message });
|
|
835
|
+
};
|
|
836
|
+
var withConsumeSpan = (ctx, config, fn, options = {}) => {
|
|
837
|
+
if (!config.enabled) return fn();
|
|
838
|
+
const parentCtx = extractContext(import_api4.ROOT_CONTEXT, ctx.msg.headers, hdrsGetter);
|
|
839
|
+
const shouldCreateSpan = config.traces.has("consume" /* Consume */) && (config.shouldTraceConsume?.(ctx.subject, ctx.msg) ?? true);
|
|
840
|
+
if (!shouldCreateSpan) {
|
|
841
|
+
return import_api4.context.with(parentCtx, fn);
|
|
842
|
+
}
|
|
843
|
+
const tracer = getTracer();
|
|
844
|
+
const span = tracer.startSpan(
|
|
845
|
+
`${SPAN_NAME_PROCESS} ${ctx.subject}`,
|
|
846
|
+
{
|
|
847
|
+
kind: import_api4.SpanKind.CONSUMER,
|
|
848
|
+
attributes: {
|
|
849
|
+
...buildConsumeAttributes({
|
|
850
|
+
subject: ctx.subject,
|
|
851
|
+
pattern: ctx.pattern,
|
|
852
|
+
msg: ctx.msg,
|
|
853
|
+
info: ctx.info,
|
|
854
|
+
kind: ctx.kind,
|
|
855
|
+
payloadBytes: ctx.payloadBytes,
|
|
856
|
+
serviceName: ctx.serviceName,
|
|
857
|
+
serverAddress: ctx.endpoint?.host,
|
|
858
|
+
serverPort: ctx.endpoint?.port
|
|
859
|
+
}),
|
|
860
|
+
...captureMatchingHeaders(ctx.msg.headers, config.captureHeaders),
|
|
861
|
+
...captureBodyAttribute(ctx.subject, ctx.msg.data, config.captureBody)
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
parentCtx
|
|
865
|
+
);
|
|
866
|
+
const ctxWithSpan = import_api4.trace.setSpan(parentCtx, span);
|
|
867
|
+
const start = Date.now();
|
|
868
|
+
let finalized = false;
|
|
869
|
+
const { signal, timeoutLabel = "handler.timeout" } = options;
|
|
870
|
+
let detachAbort = null;
|
|
871
|
+
const invokeResponseHook = (durationMs, error) => {
|
|
872
|
+
import_api4.context.with(ctxWithSpan, () => {
|
|
873
|
+
safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
|
|
874
|
+
subject: ctx.subject,
|
|
875
|
+
durationMs,
|
|
876
|
+
error
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
};
|
|
880
|
+
const finishOk2 = () => {
|
|
881
|
+
if (finalized) return;
|
|
882
|
+
finalized = true;
|
|
883
|
+
detachAbort?.();
|
|
884
|
+
span.setStatus({ code: import_api4.SpanStatusCode.OK });
|
|
885
|
+
invokeResponseHook(Date.now() - start);
|
|
886
|
+
span.end();
|
|
887
|
+
};
|
|
888
|
+
const finishError2 = (err) => {
|
|
889
|
+
if (finalized) return;
|
|
890
|
+
finalized = true;
|
|
891
|
+
detachAbort?.();
|
|
892
|
+
let classification = "unexpected";
|
|
893
|
+
try {
|
|
894
|
+
classification = config.errorClassifier(err);
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
if (classification === "expected") {
|
|
898
|
+
applyExpectedError(span, err);
|
|
899
|
+
} else {
|
|
900
|
+
applyUnexpectedError(span, err);
|
|
901
|
+
}
|
|
902
|
+
invokeResponseHook(Date.now() - start, err instanceof Error ? err : new Error(String(err)));
|
|
903
|
+
span.end();
|
|
904
|
+
};
|
|
905
|
+
const onAbort = () => {
|
|
906
|
+
if (finalized) return;
|
|
907
|
+
finalized = true;
|
|
908
|
+
const error = new Error(timeoutLabel);
|
|
909
|
+
span.addEvent(timeoutLabel);
|
|
910
|
+
span.recordException(error);
|
|
911
|
+
span.setStatus({ code: import_api4.SpanStatusCode.ERROR, message: timeoutLabel });
|
|
912
|
+
invokeResponseHook(Date.now() - start, error);
|
|
913
|
+
span.end();
|
|
914
|
+
};
|
|
915
|
+
if (signal) {
|
|
916
|
+
if (signal.aborted) {
|
|
917
|
+
onAbort();
|
|
918
|
+
} else {
|
|
919
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
920
|
+
detachAbort = () => {
|
|
921
|
+
signal.removeEventListener("abort", onAbort);
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
let result;
|
|
926
|
+
try {
|
|
927
|
+
result = import_api4.context.with(ctxWithSpan, () => {
|
|
928
|
+
safelyInvokeHook(HOOK_CONSUME, config.consumeHook, span, {
|
|
929
|
+
subject: ctx.subject,
|
|
930
|
+
msg: ctx.msg,
|
|
931
|
+
handlerMetadata: ctx.handlerMetadata,
|
|
932
|
+
kind: ctx.kind
|
|
933
|
+
});
|
|
934
|
+
return fn();
|
|
935
|
+
});
|
|
936
|
+
} catch (err) {
|
|
937
|
+
finishError2(err);
|
|
938
|
+
throw err;
|
|
939
|
+
}
|
|
940
|
+
if (isPromiseLike(result)) {
|
|
941
|
+
return Promise.resolve(result).then(
|
|
942
|
+
(value) => {
|
|
943
|
+
finishOk2();
|
|
944
|
+
return value;
|
|
945
|
+
},
|
|
946
|
+
(err) => {
|
|
947
|
+
finishError2(err);
|
|
948
|
+
throw err;
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
finishOk2();
|
|
953
|
+
return result;
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
// src/otel/spans/rpc-client.ts
|
|
957
|
+
var import_common3 = require("@nestjs/common");
|
|
958
|
+
var import_api5 = require("@opentelemetry/api");
|
|
959
|
+
var logger3 = new import_common3.Logger("Jetstream:Otel");
|
|
960
|
+
var RPC_TIMEOUT_MESSAGE = "rpc.timeout";
|
|
961
|
+
var beginRpcClientSpan = (ctx, config) => {
|
|
962
|
+
if (!config.enabled) {
|
|
963
|
+
return {
|
|
964
|
+
activeContext: import_api5.context.active(),
|
|
965
|
+
finish: () => void 0
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
if (!config.traces.has("rpc.client.send" /* RpcClientSend */)) {
|
|
969
|
+
injectContext(import_api5.context.active(), ctx.headers, hdrsSetter);
|
|
970
|
+
return {
|
|
971
|
+
activeContext: import_api5.context.active(),
|
|
972
|
+
finish: () => void 0
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
const tracer = getTracer();
|
|
976
|
+
const span = tracer.startSpan(`${SPAN_NAME_SEND} ${ctx.subject}`, {
|
|
977
|
+
kind: import_api5.SpanKind.CLIENT,
|
|
978
|
+
attributes: {
|
|
979
|
+
...buildRpcClientAttributes({
|
|
980
|
+
subject: ctx.subject,
|
|
981
|
+
pattern: ctx.pattern,
|
|
982
|
+
correlationId: ctx.correlationId,
|
|
983
|
+
payloadBytes: ctx.payloadBytes,
|
|
984
|
+
messageId: ctx.messageId,
|
|
985
|
+
serviceName: ctx.serviceName,
|
|
986
|
+
serverAddress: ctx.endpoint?.host,
|
|
987
|
+
serverPort: ctx.endpoint?.port
|
|
988
|
+
}),
|
|
989
|
+
...captureMatchingHeaders(ctx.headers, config.captureHeaders),
|
|
990
|
+
...captureBodyAttribute(ctx.subject, ctx.payload, config.captureBody)
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
const ctxWithSpan = import_api5.trace.setSpan(import_api5.context.active(), span);
|
|
994
|
+
injectContext(ctxWithSpan, ctx.headers, hdrsSetter);
|
|
995
|
+
const start = Date.now();
|
|
996
|
+
let finalized = false;
|
|
997
|
+
const finish = (outcome) => {
|
|
998
|
+
if (finalized) return;
|
|
999
|
+
finalized = true;
|
|
1000
|
+
let reply;
|
|
1001
|
+
let error;
|
|
1002
|
+
switch (outcome.kind) {
|
|
1003
|
+
case "ok" /* Ok */:
|
|
1004
|
+
reply = outcome.reply;
|
|
1005
|
+
span.setStatus({ code: import_api5.SpanStatusCode.OK });
|
|
1006
|
+
break;
|
|
1007
|
+
case "reply-error" /* ReplyError */:
|
|
1008
|
+
reply = outcome.replyPayload;
|
|
1009
|
+
applyExpectedErrorAttributes(span, outcome.replyPayload);
|
|
1010
|
+
span.setStatus({ code: import_api5.SpanStatusCode.OK });
|
|
1011
|
+
break;
|
|
1012
|
+
case "timeout" /* Timeout */:
|
|
1013
|
+
error = new Error(RPC_TIMEOUT_MESSAGE);
|
|
1014
|
+
span.addEvent(RPC_TIMEOUT_MESSAGE);
|
|
1015
|
+
span.setStatus({ code: import_api5.SpanStatusCode.ERROR, message: RPC_TIMEOUT_MESSAGE });
|
|
1016
|
+
break;
|
|
1017
|
+
case "error" /* Error */:
|
|
1018
|
+
error = outcome.error;
|
|
1019
|
+
span.recordException(outcome.error);
|
|
1020
|
+
span.setStatus({ code: import_api5.SpanStatusCode.ERROR, message: outcome.error.message });
|
|
1021
|
+
break;
|
|
1022
|
+
default: {
|
|
1023
|
+
const unknownOutcome = outcome;
|
|
1024
|
+
logger3.error(`Unhandled RPC outcome: ${String(unknownOutcome.kind)}`);
|
|
1025
|
+
span.setStatus({ code: import_api5.SpanStatusCode.ERROR, message: "unknown outcome" });
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
import_api5.context.with(ctxWithSpan, () => {
|
|
1029
|
+
safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
|
|
1030
|
+
subject: ctx.subject,
|
|
1031
|
+
durationMs: Date.now() - start,
|
|
1032
|
+
reply,
|
|
1033
|
+
error
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
span.end();
|
|
1037
|
+
};
|
|
1038
|
+
return { activeContext: ctxWithSpan, finish };
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
// src/otel/spans/dead-letter.ts
|
|
1042
|
+
var import_api6 = require("@opentelemetry/api");
|
|
1043
|
+
var withDeadLetterSpan = async (ctx, config, fn) => {
|
|
1044
|
+
if (!config.enabled || !config.traces.has("dead_letter" /* DeadLetter */)) {
|
|
1045
|
+
return fn();
|
|
1046
|
+
}
|
|
1047
|
+
const parentCtx = extractContext(import_api6.ROOT_CONTEXT, ctx.msg.headers, hdrsGetter);
|
|
1048
|
+
const tracer = getTracer();
|
|
1049
|
+
const span = tracer.startSpan(
|
|
1050
|
+
`${SPAN_NAME_DEAD_LETTER} ${ctx.msg.subject}`,
|
|
1051
|
+
{
|
|
1052
|
+
kind: import_api6.SpanKind.INTERNAL,
|
|
1053
|
+
attributes: buildDeadLetterAttributes({
|
|
1054
|
+
subject: ctx.msg.subject,
|
|
1055
|
+
pattern: ctx.pattern,
|
|
1056
|
+
serviceName: ctx.serviceName,
|
|
1057
|
+
serverAddress: ctx.endpoint?.host,
|
|
1058
|
+
serverPort: ctx.endpoint?.port,
|
|
1059
|
+
finalDeliveryCount: ctx.finalDeliveryCount,
|
|
1060
|
+
reason: ctx.reason
|
|
1061
|
+
})
|
|
1062
|
+
},
|
|
1063
|
+
parentCtx
|
|
1064
|
+
);
|
|
1065
|
+
const ctxWithSpan = import_api6.trace.setSpan(parentCtx, span);
|
|
1066
|
+
const start = Date.now();
|
|
1067
|
+
const invokeResponseHook = (error) => {
|
|
1068
|
+
import_api6.context.with(ctxWithSpan, () => {
|
|
1069
|
+
safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
|
|
1070
|
+
subject: ctx.msg.subject,
|
|
1071
|
+
durationMs: Date.now() - start,
|
|
1072
|
+
error
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
};
|
|
1076
|
+
try {
|
|
1077
|
+
const result = await import_api6.context.with(ctxWithSpan, fn);
|
|
1078
|
+
span.setStatus({ code: import_api6.SpanStatusCode.OK });
|
|
1079
|
+
invokeResponseHook();
|
|
1080
|
+
return result;
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1083
|
+
span.recordException(error);
|
|
1084
|
+
span.setStatus({ code: import_api6.SpanStatusCode.ERROR, message: error.message });
|
|
1085
|
+
invokeResponseHook(error);
|
|
1086
|
+
throw err;
|
|
1087
|
+
} finally {
|
|
1088
|
+
span.end();
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
// src/otel/spans/infrastructure.ts
|
|
1093
|
+
var import_common4 = require("@nestjs/common");
|
|
1094
|
+
var import_api7 = require("@opentelemetry/api");
|
|
1095
|
+
var logger4 = new import_common4.Logger("Jetstream:Otel");
|
|
1096
|
+
var startInfraSpan = (config, traceKind, name, ctx, extraAttributes = {}) => {
|
|
1097
|
+
if (!config.enabled || !config.traces.has(traceKind)) return null;
|
|
1098
|
+
const tracer = getTracer();
|
|
1099
|
+
const attributes = {
|
|
1100
|
+
[ATTR_JETSTREAM_SERVICE_NAME]: ctx.serviceName
|
|
1101
|
+
};
|
|
1102
|
+
if (ctx.endpoint?.host) attributes[ATTR_SERVER_ADDRESS] = ctx.endpoint.host;
|
|
1103
|
+
if (ctx.endpoint?.port !== void 0) attributes[ATTR_SERVER_PORT] = ctx.endpoint.port;
|
|
1104
|
+
for (const [key, value] of Object.entries(extraAttributes)) {
|
|
1105
|
+
if (value !== void 0) attributes[key] = value;
|
|
1106
|
+
}
|
|
1107
|
+
return tracer.startSpan(name, { kind: import_api7.SpanKind.INTERNAL, attributes });
|
|
1108
|
+
};
|
|
1109
|
+
var finishOk = (span) => {
|
|
1110
|
+
span.setStatus({ code: import_api7.SpanStatusCode.OK });
|
|
1111
|
+
span.end();
|
|
1112
|
+
};
|
|
1113
|
+
var finishError = (span, err) => {
|
|
1114
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1115
|
+
span.recordException(error);
|
|
1116
|
+
span.setStatus({ code: import_api7.SpanStatusCode.ERROR, message: error.message });
|
|
1117
|
+
span.end();
|
|
1118
|
+
};
|
|
1119
|
+
var wrapInfra = async (config, traceKind, name, ctx, attributes, op) => {
|
|
1120
|
+
const span = startInfraSpan(config, traceKind, name, ctx, attributes);
|
|
1121
|
+
if (!span) return op();
|
|
1122
|
+
const ctxWithSpan = import_api7.trace.setSpan(import_api7.context.active(), span);
|
|
1123
|
+
try {
|
|
1124
|
+
const result = await import_api7.context.with(ctxWithSpan, op);
|
|
1125
|
+
finishOk(span);
|
|
1126
|
+
return result;
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
finishError(span, err);
|
|
1129
|
+
throw err;
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
var beginConnectionLifecycleSpan = (config, ctx) => {
|
|
1133
|
+
const span = startInfraSpan(
|
|
1134
|
+
config,
|
|
1135
|
+
"connection.lifecycle" /* ConnectionLifecycle */,
|
|
1136
|
+
SPAN_NAME_NATS_CONNECTION,
|
|
1137
|
+
ctx,
|
|
1138
|
+
{ [ATTR_NATS_CONNECTION_SERVER]: ctx.server }
|
|
1139
|
+
);
|
|
1140
|
+
if (!span) {
|
|
1141
|
+
return {
|
|
1142
|
+
recordEvent: () => void 0,
|
|
1143
|
+
finish: () => void 0
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
let finalized = false;
|
|
1147
|
+
return {
|
|
1148
|
+
recordEvent: (name, attributes) => {
|
|
1149
|
+
if (finalized) {
|
|
1150
|
+
logger4.debug(`recordEvent('${name}') called after connection span finished`);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
span.addEvent(name, attributes);
|
|
1154
|
+
},
|
|
1155
|
+
finish: (err) => {
|
|
1156
|
+
if (finalized) return;
|
|
1157
|
+
finalized = true;
|
|
1158
|
+
if (err === void 0) finishOk(span);
|
|
1159
|
+
else finishError(span, err);
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
};
|
|
1163
|
+
var withSelfHealingSpan = (config, ctx, op) => wrapInfra(
|
|
1164
|
+
config,
|
|
1165
|
+
"self_healing" /* SelfHealing */,
|
|
1166
|
+
SPAN_NAME_JETSTREAM_SELF_HEALING,
|
|
1167
|
+
ctx,
|
|
1168
|
+
{
|
|
1169
|
+
[ATTR_MESSAGING_NATS_STREAM_NAME]: ctx.stream,
|
|
1170
|
+
[ATTR_MESSAGING_CONSUMER_GROUP_NAME]: ctx.consumer,
|
|
1171
|
+
[ATTR_JETSTREAM_SELF_HEALING_REASON]: ctx.reason
|
|
1172
|
+
},
|
|
1173
|
+
op
|
|
1174
|
+
);
|
|
1175
|
+
var withProvisioningSpan = (config, ctx, op) => wrapInfra(
|
|
1176
|
+
config,
|
|
1177
|
+
"provisioning" /* Provisioning */,
|
|
1178
|
+
`${SPAN_NAME_JETSTREAM_PROVISIONING_PREFIX}${ctx.entity}`,
|
|
1179
|
+
ctx,
|
|
1180
|
+
{
|
|
1181
|
+
[ATTR_JETSTREAM_PROVISIONING_ENTITY]: ctx.entity,
|
|
1182
|
+
[ATTR_JETSTREAM_PROVISIONING_ACTION]: ctx.action,
|
|
1183
|
+
[ATTR_JETSTREAM_PROVISIONING_NAME]: ctx.name
|
|
1184
|
+
},
|
|
1185
|
+
op
|
|
1186
|
+
);
|
|
1187
|
+
var withMigrationSpan = (config, ctx, op) => wrapInfra(
|
|
1188
|
+
config,
|
|
1189
|
+
"migration" /* Migration */,
|
|
1190
|
+
SPAN_NAME_JETSTREAM_MIGRATION,
|
|
1191
|
+
ctx,
|
|
1192
|
+
{
|
|
1193
|
+
[ATTR_MESSAGING_NATS_STREAM_NAME]: ctx.stream,
|
|
1194
|
+
[ATTR_JETSTREAM_MIGRATION_REASON]: ctx.reason
|
|
1195
|
+
},
|
|
1196
|
+
op
|
|
1197
|
+
);
|
|
1198
|
+
var withShutdownSpan = (config, ctx, op) => wrapInfra(config, "shutdown" /* Shutdown */, SPAN_NAME_JETSTREAM_SHUTDOWN, ctx, {}, op);
|
|
1199
|
+
|
|
279
1200
|
// src/client/jetstream.record.ts
|
|
280
1201
|
var JetstreamRecord = class {
|
|
281
1202
|
constructor(data, headers2, timeout, messageId, schedule, ttl) {
|
|
@@ -428,9 +1349,14 @@ var JetstreamRecordBuilder = class {
|
|
|
428
1349
|
this.ttlDuration
|
|
429
1350
|
);
|
|
430
1351
|
}
|
|
431
|
-
/**
|
|
1352
|
+
/**
|
|
1353
|
+
* Validate that a header key is not reserved. NATS treats header names
|
|
1354
|
+
* case-insensitively, so the check is against the lowercase form to keep
|
|
1355
|
+
* `'X-Correlation-ID'`, `'x-correlation-id'`, and any other casing in
|
|
1356
|
+
* lockstep. `RESERVED_HEADERS` is defined as an all-lowercase set.
|
|
1357
|
+
*/
|
|
432
1358
|
validateHeaderKey(key) {
|
|
433
|
-
if (RESERVED_HEADERS.has(key)) {
|
|
1359
|
+
if (RESERVED_HEADERS.has(key.toLowerCase())) {
|
|
434
1360
|
throw new Error(
|
|
435
1361
|
`Header "${key}" is reserved by the JetStream transport and cannot be set manually. Reserved headers: ${[...RESERVED_HEADERS].join(", ")}`
|
|
436
1362
|
);
|
|
@@ -451,6 +1377,22 @@ var nanosToGoDuration = (nanos) => {
|
|
|
451
1377
|
|
|
452
1378
|
// src/client/jetstream.client.ts
|
|
453
1379
|
var BROADCAST_SUBJECT_PREFIX = "broadcast.";
|
|
1380
|
+
var detectEventKind = (pattern) => {
|
|
1381
|
+
if (pattern.startsWith("broadcast:" /* Broadcast */)) return "broadcast" /* Broadcast */;
|
|
1382
|
+
if (pattern.startsWith("ordered:" /* Ordered */)) return "ordered" /* Ordered */;
|
|
1383
|
+
return "event" /* Event */;
|
|
1384
|
+
};
|
|
1385
|
+
var declaredEventPattern = (pattern) => {
|
|
1386
|
+
if (pattern.startsWith("broadcast:" /* Broadcast */))
|
|
1387
|
+
return pattern.slice("broadcast:" /* Broadcast */.length);
|
|
1388
|
+
if (pattern.startsWith("ordered:" /* Ordered */)) return pattern.slice("ordered:" /* Ordered */.length);
|
|
1389
|
+
return pattern;
|
|
1390
|
+
};
|
|
1391
|
+
var eventStreamKind = (kind) => {
|
|
1392
|
+
if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
|
|
1393
|
+
if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
|
|
1394
|
+
return "ev" /* Event */;
|
|
1395
|
+
};
|
|
454
1396
|
var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
455
1397
|
constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
|
|
456
1398
|
super();
|
|
@@ -466,8 +1408,11 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
466
1408
|
this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
|
|
467
1409
|
this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
|
|
468
1410
|
this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
|
|
1411
|
+
const derived = deriveOtelAttrs(this.rootOptions);
|
|
1412
|
+
this.otel = derived.otel;
|
|
1413
|
+
this.serverEndpoint = derived.serverEndpoint;
|
|
469
1414
|
}
|
|
470
|
-
logger = new
|
|
1415
|
+
logger = new import_common5.Logger("Jetstream:Client");
|
|
471
1416
|
/** Target service name this client sends messages to. */
|
|
472
1417
|
targetName;
|
|
473
1418
|
/** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
|
|
@@ -487,6 +1432,10 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
487
1432
|
*/
|
|
488
1433
|
isCoreMode;
|
|
489
1434
|
defaultRpcTimeout;
|
|
1435
|
+
/** Resolved OpenTelemetry configuration, computed once in the constructor. */
|
|
1436
|
+
otel;
|
|
1437
|
+
/** Server endpoint parts used for `server.address` / `server.port` span attributes. */
|
|
1438
|
+
serverEndpoint;
|
|
490
1439
|
/** Shared inbox for JetStream-mode RPC responses. */
|
|
491
1440
|
inbox = null;
|
|
492
1441
|
inboxSubscription = null;
|
|
@@ -554,32 +1503,64 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
554
1503
|
if (!this.readyForPublish) await this.connect();
|
|
555
1504
|
const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
556
1505
|
const eventSubject = this.buildEventSubject(packet.pattern);
|
|
1506
|
+
const publishSubject = schedule ? this.buildScheduleSubject(eventSubject) : eventSubject;
|
|
557
1507
|
const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
1508
|
+
const encoded = this.codec.encode(data);
|
|
1509
|
+
const effectiveMsgId = messageId ?? import_nuid.nuid.next();
|
|
1510
|
+
const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
|
|
1511
|
+
const publishKind = detectEventKind(packet.pattern);
|
|
1512
|
+
const declaredPattern = declaredEventPattern(packet.pattern);
|
|
1513
|
+
const streamKind = eventStreamKind(publishKind);
|
|
1514
|
+
const startedAt = performance.now();
|
|
1515
|
+
try {
|
|
1516
|
+
await withPublishSpan(
|
|
1517
|
+
{
|
|
1518
|
+
subject: publishSubject,
|
|
1519
|
+
pattern: packet.pattern,
|
|
1520
|
+
record,
|
|
1521
|
+
kind: publishKind,
|
|
1522
|
+
payloadBytes: encoded.length,
|
|
1523
|
+
payload: encoded,
|
|
1524
|
+
messageId: effectiveMsgId,
|
|
1525
|
+
headers: msgHeaders,
|
|
1526
|
+
serviceName: this.callerName,
|
|
1527
|
+
endpoint: this.serverEndpoint,
|
|
1528
|
+
scheduleTarget: schedule ? eventSubject : void 0
|
|
1529
|
+
},
|
|
1530
|
+
this.otel,
|
|
1531
|
+
async () => {
|
|
1532
|
+
const warnIfDuplicate = (kindLabel, ack2) => {
|
|
1533
|
+
if (ack2.duplicate) {
|
|
1534
|
+
this.logger.warn(
|
|
1535
|
+
`Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
if (schedule) {
|
|
1540
|
+
const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
|
|
1541
|
+
headers: msgHeaders,
|
|
1542
|
+
msgID: effectiveMsgId,
|
|
1543
|
+
ttl,
|
|
1544
|
+
schedule: {
|
|
1545
|
+
specification: schedule.at,
|
|
1546
|
+
target: eventSubject
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
warnIfDuplicate("scheduled", ack2);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
|
|
1553
|
+
headers: msgHeaders,
|
|
1554
|
+
msgID: effectiveMsgId,
|
|
1555
|
+
ttl
|
|
1556
|
+
});
|
|
1557
|
+
warnIfDuplicate("event", ack);
|
|
567
1558
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
} else {
|
|
575
|
-
const ack = await this.connection.getJetStreamClient().publish(eventSubject, this.codec.encode(data), {
|
|
576
|
-
headers: msgHeaders,
|
|
577
|
-
msgID: messageId ?? import_nuid.nuid.next(),
|
|
578
|
-
ttl
|
|
579
|
-
});
|
|
580
|
-
if (ack.duplicate) {
|
|
581
|
-
this.logger.warn(`Duplicate event publish detected: ${eventSubject} (seq: ${ack.seq})`);
|
|
582
|
-
}
|
|
1559
|
+
);
|
|
1560
|
+
this.reportPublished(declaredPattern, streamKind, startedAt, "success");
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
this.reportPublished(declaredPattern, streamKind, startedAt, "error");
|
|
1563
|
+
throw err;
|
|
583
1564
|
}
|
|
584
1565
|
return void 0;
|
|
585
1566
|
}
|
|
@@ -608,14 +1589,17 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
608
1589
|
};
|
|
609
1590
|
let jetStreamCorrelationId = null;
|
|
610
1591
|
if (this.isCoreMode) {
|
|
611
|
-
this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(
|
|
1592
|
+
this.publishCoreRpc(subject, data, hdrs, timeout, callback, packet.pattern).catch(
|
|
1593
|
+
onUnhandled
|
|
1594
|
+
);
|
|
612
1595
|
} else {
|
|
613
1596
|
jetStreamCorrelationId = import_nuid.nuid.next();
|
|
614
1597
|
this.publishJetStreamRpc(subject, data, callback, {
|
|
615
1598
|
headers: hdrs,
|
|
616
1599
|
timeout,
|
|
617
1600
|
correlationId: jetStreamCorrelationId,
|
|
618
|
-
messageId
|
|
1601
|
+
messageId,
|
|
1602
|
+
declaredPattern: packet.pattern
|
|
619
1603
|
}).catch(onUnhandled);
|
|
620
1604
|
}
|
|
621
1605
|
return () => {
|
|
@@ -630,38 +1614,118 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
630
1614
|
};
|
|
631
1615
|
}
|
|
632
1616
|
/** Core mode: nc.request() with timeout. */
|
|
633
|
-
async publishCoreRpc(subject, data, customHeaders, timeout, callback) {
|
|
1617
|
+
async publishCoreRpc(subject, data, customHeaders, timeout, callback, declaredPattern) {
|
|
1618
|
+
const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
|
|
1619
|
+
const hdrs = this.buildHeaders(customHeaders, { subject });
|
|
1620
|
+
const encoded = this.codec.encode(data);
|
|
1621
|
+
const spanHandle = beginRpcClientSpan(
|
|
1622
|
+
{
|
|
1623
|
+
subject,
|
|
1624
|
+
payloadBytes: encoded.length,
|
|
1625
|
+
payload: encoded,
|
|
1626
|
+
headers: hdrs,
|
|
1627
|
+
serviceName: this.callerName,
|
|
1628
|
+
endpoint: this.serverEndpoint
|
|
1629
|
+
},
|
|
1630
|
+
this.otel
|
|
1631
|
+
);
|
|
1632
|
+
const startedAt = performance.now();
|
|
634
1633
|
try {
|
|
635
1634
|
const nc = this.readyForPublish ? this.connection.unwrap : await this.connect();
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1635
|
+
const response = await import_api8.context.with(
|
|
1636
|
+
spanHandle.activeContext,
|
|
1637
|
+
() => nc.request(subject, encoded, {
|
|
1638
|
+
timeout: effectiveTimeout,
|
|
1639
|
+
headers: hdrs
|
|
1640
|
+
})
|
|
1641
|
+
);
|
|
642
1642
|
const decoded = this.codec.decode(response.data);
|
|
643
1643
|
if (response.headers?.get("x-error" /* Error */)) {
|
|
1644
|
+
spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: decoded });
|
|
1645
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
1646
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
644
1647
|
callback({ err: decoded, response: null, isDisposed: true });
|
|
645
1648
|
} else {
|
|
1649
|
+
spanHandle.finish({ kind: "ok" /* Ok */, reply: decoded });
|
|
1650
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
1651
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "success");
|
|
646
1652
|
callback({ err: null, response: decoded, isDisposed: true });
|
|
647
1653
|
}
|
|
648
1654
|
} catch (err) {
|
|
649
1655
|
const error = err instanceof Error ? err : new Error("Unknown error");
|
|
650
|
-
|
|
651
|
-
|
|
1656
|
+
if (error instanceof import_transport_node.TimeoutError) {
|
|
1657
|
+
spanHandle.finish({ kind: "timeout" /* Timeout */ });
|
|
1658
|
+
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, "");
|
|
1659
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
1660
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
|
|
1661
|
+
} else {
|
|
1662
|
+
spanHandle.finish({ kind: "error" /* Error */, error });
|
|
1663
|
+
this.eventBus.emit("error" /* Error */, error, "client-rpc");
|
|
1664
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
|
|
1665
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
1666
|
+
}
|
|
652
1667
|
callback({ err: error, response: null, isDisposed: true });
|
|
653
1668
|
}
|
|
654
1669
|
}
|
|
655
1670
|
/** JetStream mode: publish to stream + wait for inbox response. */
|
|
656
1671
|
async publishJetStreamRpc(subject, data, callback, options) {
|
|
657
|
-
const { headers: customHeaders, correlationId, messageId } = options;
|
|
1672
|
+
const { headers: customHeaders, correlationId, messageId, declaredPattern } = options;
|
|
658
1673
|
const effectiveTimeout = options.timeout ?? this.defaultRpcTimeout;
|
|
659
|
-
this.
|
|
1674
|
+
const hdrs = this.buildHeaders(customHeaders, {
|
|
1675
|
+
subject,
|
|
1676
|
+
correlationId,
|
|
1677
|
+
replyTo: this.inbox ?? ""
|
|
1678
|
+
});
|
|
1679
|
+
const encoded = this.codec.encode(data);
|
|
1680
|
+
const spanHandle = beginRpcClientSpan(
|
|
1681
|
+
{
|
|
1682
|
+
subject,
|
|
1683
|
+
correlationId,
|
|
1684
|
+
payloadBytes: encoded.length,
|
|
1685
|
+
payload: encoded,
|
|
1686
|
+
messageId,
|
|
1687
|
+
headers: hdrs,
|
|
1688
|
+
serviceName: this.callerName,
|
|
1689
|
+
endpoint: this.serverEndpoint
|
|
1690
|
+
},
|
|
1691
|
+
this.otel
|
|
1692
|
+
);
|
|
1693
|
+
const startedAt = performance.now();
|
|
1694
|
+
this.pendingMessages.set(correlationId, (packet) => {
|
|
1695
|
+
if (packet.err) {
|
|
1696
|
+
if (packet.err instanceof Error) {
|
|
1697
|
+
spanHandle.finish({ kind: "error" /* Error */, error: packet.err });
|
|
1698
|
+
} else {
|
|
1699
|
+
spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: packet.err });
|
|
1700
|
+
}
|
|
1701
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
1702
|
+
} else {
|
|
1703
|
+
spanHandle.finish({ kind: "ok" /* Ok */, reply: packet.response });
|
|
1704
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "success");
|
|
1705
|
+
}
|
|
1706
|
+
callback(packet);
|
|
1707
|
+
});
|
|
1708
|
+
const timeoutId = setTimeout(() => {
|
|
1709
|
+
if (!this.pendingMessages.has(correlationId)) return;
|
|
1710
|
+
this.pendingTimeouts.delete(correlationId);
|
|
1711
|
+
this.pendingMessages.delete(correlationId);
|
|
1712
|
+
spanHandle.finish({ kind: "timeout" /* Timeout */ });
|
|
1713
|
+
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
|
|
1714
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
|
|
1715
|
+
callback({ err: new Error(RPC_TIMEOUT_MESSAGE), response: null, isDisposed: true });
|
|
1716
|
+
}, effectiveTimeout);
|
|
1717
|
+
this.pendingTimeouts.set(correlationId, timeoutId);
|
|
660
1718
|
try {
|
|
661
1719
|
if (!this.readyForPublish) await this.connect();
|
|
662
1720
|
if (!this.pendingMessages.has(correlationId)) return;
|
|
663
1721
|
if (!this.inbox) {
|
|
1722
|
+
clearTimeout(timeoutId);
|
|
1723
|
+
this.pendingTimeouts.delete(correlationId);
|
|
664
1724
|
this.pendingMessages.delete(correlationId);
|
|
1725
|
+
const inboxError = new Error("Inbox not initialized");
|
|
1726
|
+
spanHandle.finish({ kind: "error" /* Error */, error: inboxError });
|
|
1727
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
|
|
1728
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
665
1729
|
callback({
|
|
666
1730
|
err: new Error("Inbox not initialized \u2014 JetStream RPC mode requires a connected inbox"),
|
|
667
1731
|
response: null,
|
|
@@ -669,24 +1733,14 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
669
1733
|
});
|
|
670
1734
|
return;
|
|
671
1735
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
this.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
this.pendingTimeouts.set(correlationId, timeoutId);
|
|
681
|
-
const hdrs = this.buildHeaders(customHeaders, {
|
|
682
|
-
subject,
|
|
683
|
-
correlationId,
|
|
684
|
-
replyTo: this.inbox
|
|
685
|
-
});
|
|
686
|
-
await this.connection.getJetStreamClient().publish(subject, this.codec.encode(data), {
|
|
687
|
-
headers: hdrs,
|
|
688
|
-
msgID: messageId ?? import_nuid.nuid.next()
|
|
689
|
-
});
|
|
1736
|
+
await import_api8.context.with(
|
|
1737
|
+
spanHandle.activeContext,
|
|
1738
|
+
() => this.connection.getJetStreamClient().publish(subject, encoded, {
|
|
1739
|
+
headers: hdrs,
|
|
1740
|
+
msgID: messageId ?? import_nuid.nuid.next()
|
|
1741
|
+
})
|
|
1742
|
+
);
|
|
1743
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
690
1744
|
} catch (err) {
|
|
691
1745
|
const existingTimeout = this.pendingTimeouts.get(correlationId);
|
|
692
1746
|
if (existingTimeout) {
|
|
@@ -696,10 +1750,34 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
|
|
|
696
1750
|
if (!this.pendingMessages.has(correlationId)) return;
|
|
697
1751
|
this.pendingMessages.delete(correlationId);
|
|
698
1752
|
const error = err instanceof Error ? err : new Error("Unknown error");
|
|
699
|
-
|
|
1753
|
+
spanHandle.finish({ kind: "error" /* Error */, error });
|
|
1754
|
+
this.eventBus.emit("error" /* Error */, error, `jetstream-rpc-publish:${subject}`);
|
|
1755
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
|
|
1756
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
700
1757
|
callback({ err: error, response: null, isDisposed: true });
|
|
701
1758
|
}
|
|
702
1759
|
}
|
|
1760
|
+
// hasHook is per-emit so late subscribers (JetstreamMetricsService during
|
|
1761
|
+
// OnApplicationBootstrap) still receive events.
|
|
1762
|
+
reportPublished(declaredPattern, kind, startedAt, status) {
|
|
1763
|
+
if (!this.eventBus.hasHook("published" /* Published */)) return;
|
|
1764
|
+
this.eventBus.emit(
|
|
1765
|
+
"published" /* Published */,
|
|
1766
|
+
declaredPattern,
|
|
1767
|
+
kind,
|
|
1768
|
+
performance.now() - startedAt,
|
|
1769
|
+
status
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
reportRpcCompleted(declaredPattern, startedAt, status) {
|
|
1773
|
+
if (!this.eventBus.hasHook("rpcCompleted" /* RpcCompleted */)) return;
|
|
1774
|
+
this.eventBus.emit(
|
|
1775
|
+
"rpcCompleted" /* RpcCompleted */,
|
|
1776
|
+
declaredPattern,
|
|
1777
|
+
performance.now() - startedAt,
|
|
1778
|
+
status
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
703
1781
|
/** Fail-fast all pending JetStream RPC callbacks on connection loss. */
|
|
704
1782
|
handleDisconnect() {
|
|
705
1783
|
this.rejectPendingRpcs(new Error("Connection lost"));
|
|
@@ -879,9 +1957,9 @@ var MsgpackCodec = class {
|
|
|
879
1957
|
};
|
|
880
1958
|
|
|
881
1959
|
// src/connection/connection.provider.ts
|
|
882
|
-
var
|
|
1960
|
+
var import_common6 = require("@nestjs/common");
|
|
883
1961
|
var import_transport_node2 = require("@nats-io/transport-node");
|
|
884
|
-
var
|
|
1962
|
+
var import_jetstream8 = require("@nats-io/jetstream");
|
|
885
1963
|
var import_rxjs = require("rxjs");
|
|
886
1964
|
var DEFAULT_OPTIONS = {
|
|
887
1965
|
maxReconnectAttempts: -1,
|
|
@@ -891,6 +1969,10 @@ var ConnectionProvider = class {
|
|
|
891
1969
|
constructor(options, eventBus) {
|
|
892
1970
|
this.options = options;
|
|
893
1971
|
this.eventBus = eventBus;
|
|
1972
|
+
const derived = deriveOtelAttrs(options);
|
|
1973
|
+
this.otel = derived.otel;
|
|
1974
|
+
this.otelServiceName = derived.serviceName;
|
|
1975
|
+
this.otelEndpoint = derived.serverEndpoint;
|
|
894
1976
|
this.nc$ = (0, import_rxjs.defer)(() => this.getConnection()).pipe(
|
|
895
1977
|
(0, import_rxjs.shareReplay)({ bufferSize: 1, refCount: false })
|
|
896
1978
|
);
|
|
@@ -903,12 +1985,16 @@ var ConnectionProvider = class {
|
|
|
903
1985
|
nc$;
|
|
904
1986
|
/** Live stream of connection status events (no replay). */
|
|
905
1987
|
status$;
|
|
906
|
-
logger = new
|
|
1988
|
+
logger = new import_common6.Logger("Jetstream:Connection");
|
|
907
1989
|
connection = null;
|
|
908
1990
|
connectionPromise = null;
|
|
909
1991
|
jsClient = null;
|
|
910
1992
|
jsmInstance = null;
|
|
911
1993
|
jsmPromise = null;
|
|
1994
|
+
otel;
|
|
1995
|
+
otelServiceName;
|
|
1996
|
+
otelEndpoint;
|
|
1997
|
+
lifecycleSpan = null;
|
|
912
1998
|
/**
|
|
913
1999
|
* Establish NATS connection. Idempotent — returns cached connection on subsequent calls.
|
|
914
2000
|
*
|
|
@@ -951,7 +2037,7 @@ var ConnectionProvider = class {
|
|
|
951
2037
|
if (!this.connection || this.connection.isClosed()) {
|
|
952
2038
|
throw new Error("Not connected \u2014 call getConnection() before getJetStreamClient()");
|
|
953
2039
|
}
|
|
954
|
-
this.jsClient ??= (0,
|
|
2040
|
+
this.jsClient ??= (0, import_jetstream8.jetstream)(this.connection);
|
|
955
2041
|
return this.jsClient;
|
|
956
2042
|
}
|
|
957
2043
|
/** Direct access to the raw NATS connection, or `null` if not yet connected. */
|
|
@@ -970,218 +2056,942 @@ var ConnectionProvider = class {
|
|
|
970
2056
|
} catch {
|
|
971
2057
|
}
|
|
972
2058
|
}
|
|
973
|
-
if (!this.connection || this.connection.isClosed()) return;
|
|
2059
|
+
if (!this.connection || this.connection.isClosed()) return;
|
|
2060
|
+
try {
|
|
2061
|
+
await withShutdownSpan(
|
|
2062
|
+
this.otel,
|
|
2063
|
+
{ serviceName: this.otelServiceName, endpoint: this.otelEndpoint },
|
|
2064
|
+
async () => {
|
|
2065
|
+
try {
|
|
2066
|
+
await this.connection?.drain();
|
|
2067
|
+
await this.connection?.closed();
|
|
2068
|
+
} catch {
|
|
2069
|
+
try {
|
|
2070
|
+
await this.connection?.close();
|
|
2071
|
+
} catch {
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
);
|
|
2076
|
+
} finally {
|
|
2077
|
+
this.lifecycleSpan?.finish();
|
|
2078
|
+
this.lifecycleSpan = null;
|
|
2079
|
+
this.connection = null;
|
|
2080
|
+
this.connectionPromise = null;
|
|
2081
|
+
this.jsClient = null;
|
|
2082
|
+
this.jsmInstance = null;
|
|
2083
|
+
this.jsmPromise = null;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
async initJetStreamManager() {
|
|
2087
|
+
try {
|
|
2088
|
+
const nc = await this.getConnection();
|
|
2089
|
+
this.jsmInstance = await (0, import_jetstream8.jetstreamManager)(nc);
|
|
2090
|
+
this.logger.log("JetStream manager initialized");
|
|
2091
|
+
return this.jsmInstance;
|
|
2092
|
+
} finally {
|
|
2093
|
+
this.jsmPromise = null;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
/** Internal: establish the physical connection with reconnect monitoring. */
|
|
2097
|
+
async establish() {
|
|
2098
|
+
try {
|
|
2099
|
+
const nc = await (0, import_transport_node2.connect)({
|
|
2100
|
+
...DEFAULT_OPTIONS,
|
|
2101
|
+
// Default the NATS connection name to the OTel-derived service name so
|
|
2102
|
+
// `nats server info` lines up with span attributes, but let user-supplied
|
|
2103
|
+
// `connectionOptions.name` win when set.
|
|
2104
|
+
name: this.otelServiceName,
|
|
2105
|
+
...this.options.connectionOptions,
|
|
2106
|
+
servers: this.options.servers
|
|
2107
|
+
});
|
|
2108
|
+
this.connection = nc;
|
|
2109
|
+
this.logger.log(`NATS connection established: ${nc.getServer()}`);
|
|
2110
|
+
this.eventBus.emit("connect" /* Connect */, nc.getServer());
|
|
2111
|
+
this.lifecycleSpan?.finish();
|
|
2112
|
+
this.lifecycleSpan = beginConnectionLifecycleSpan(this.otel, {
|
|
2113
|
+
serviceName: this.otelServiceName,
|
|
2114
|
+
endpoint: this.otelEndpoint,
|
|
2115
|
+
server: nc.getServer()
|
|
2116
|
+
});
|
|
2117
|
+
this.monitorStatus(nc);
|
|
2118
|
+
return nc;
|
|
2119
|
+
} catch (err) {
|
|
2120
|
+
if (err instanceof Error && err.message.includes("REFUSED")) {
|
|
2121
|
+
throw new Error(`NATS connection refused: ${this.options.servers.join(", ")}`);
|
|
2122
|
+
}
|
|
2123
|
+
throw err;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
/** Handle a single `nc.status()` event, emitting hooks and span events. */
|
|
2127
|
+
handleStatusEvent(status, nc) {
|
|
2128
|
+
switch (status.type) {
|
|
2129
|
+
case "disconnect":
|
|
2130
|
+
this.eventBus.emit("disconnect" /* Disconnect */);
|
|
2131
|
+
this.lifecycleSpan?.recordEvent(EVENT_CONNECTION_DISCONNECTED);
|
|
2132
|
+
break;
|
|
2133
|
+
case "reconnect":
|
|
2134
|
+
this.jsClient = null;
|
|
2135
|
+
this.jsmInstance = null;
|
|
2136
|
+
this.jsmPromise = null;
|
|
2137
|
+
this.eventBus.emit("reconnect" /* Reconnect */, nc.getServer());
|
|
2138
|
+
this.lifecycleSpan?.recordEvent(EVENT_CONNECTION_RECONNECTED, {
|
|
2139
|
+
[ATTR_NATS_CONNECTION_SERVER]: nc.getServer()
|
|
2140
|
+
});
|
|
2141
|
+
break;
|
|
2142
|
+
case "error":
|
|
2143
|
+
this.eventBus.emit(
|
|
2144
|
+
"error" /* Error */,
|
|
2145
|
+
status.error,
|
|
2146
|
+
"connection"
|
|
2147
|
+
);
|
|
2148
|
+
break;
|
|
2149
|
+
case "update":
|
|
2150
|
+
case "ldm":
|
|
2151
|
+
case "reconnecting":
|
|
2152
|
+
case "ping":
|
|
2153
|
+
case "staleConnection":
|
|
2154
|
+
case "forceReconnect":
|
|
2155
|
+
case "slowConsumer":
|
|
2156
|
+
case "close":
|
|
2157
|
+
break;
|
|
2158
|
+
default: {
|
|
2159
|
+
const _exhaustive = status;
|
|
2160
|
+
const unknown = _exhaustive.type ?? "unknown";
|
|
2161
|
+
this.logger.warn(`Unhandled NATS status event: ${unknown}`);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
/** Subscribe to connection status events and emit hooks. */
|
|
2166
|
+
monitorStatus(nc) {
|
|
2167
|
+
void (async () => {
|
|
2168
|
+
try {
|
|
2169
|
+
for await (const status of nc.status()) {
|
|
2170
|
+
this.handleStatusEvent(status, nc);
|
|
2171
|
+
}
|
|
2172
|
+
} finally {
|
|
2173
|
+
this.lifecycleSpan?.finish();
|
|
2174
|
+
this.lifecycleSpan = null;
|
|
2175
|
+
}
|
|
2176
|
+
})().catch((err) => {
|
|
2177
|
+
this.logger.error("Status monitor error", err);
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
|
|
2182
|
+
// src/hooks/event-bus.ts
|
|
2183
|
+
var EventBus = class {
|
|
2184
|
+
hooks;
|
|
2185
|
+
logger;
|
|
2186
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
2187
|
+
constructor(logger5, hooks) {
|
|
2188
|
+
this.logger = logger5;
|
|
2189
|
+
this.hooks = hooks ?? {};
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Subscribe to a transport event. Used by built-in observers (e.g. metrics).
|
|
2193
|
+
* Multiple subscribers per event are supported; each is called independently.
|
|
2194
|
+
*/
|
|
2195
|
+
subscribe(event, handler) {
|
|
2196
|
+
const list = this.subscribers.get(event) ?? [];
|
|
2197
|
+
list.push(handler);
|
|
2198
|
+
this.subscribers.set(event, list);
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Emit a lifecycle event. Dispatches to all internal subscribers and the
|
|
2202
|
+
* registered user hook (if any).
|
|
2203
|
+
*/
|
|
2204
|
+
emit(event, ...args) {
|
|
2205
|
+
this.dispatch(event, args);
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Hot-path optimized emit for MessageRouted events.
|
|
2209
|
+
* Avoids rest/spread overhead of the generic `emit()`.
|
|
2210
|
+
*/
|
|
2211
|
+
emitMessageRouted(subject, kind) {
|
|
2212
|
+
this.dispatch("messageRouted" /* MessageRouted */, [subject, kind]);
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Check whether any listener (user hook or internal subscriber) is registered
|
|
2216
|
+
* for the given event. Used by routing hot path to elide the emit call when
|
|
2217
|
+
* no one is listening.
|
|
2218
|
+
*/
|
|
2219
|
+
hasHook(event) {
|
|
2220
|
+
return this.hooks[event] !== void 0 || (this.subscribers.get(event)?.length ?? 0) > 0;
|
|
2221
|
+
}
|
|
2222
|
+
dispatch(event, args) {
|
|
2223
|
+
const subs = this.subscribers.get(event);
|
|
2224
|
+
if (subs?.length) {
|
|
2225
|
+
for (const sub of [...subs]) {
|
|
2226
|
+
this.callHook(event, sub, ...args);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
const hook = this.hooks[event];
|
|
2230
|
+
if (hook) {
|
|
2231
|
+
this.callHook(event, hook, ...args);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
callHook(event, hook, ...args) {
|
|
2235
|
+
try {
|
|
2236
|
+
const result = hook(...args);
|
|
2237
|
+
if (result && typeof result.catch === "function") {
|
|
2238
|
+
result.catch((err) => {
|
|
2239
|
+
this.logger.error(
|
|
2240
|
+
`Async hook "${event}" rejected: ${err instanceof Error ? err.message : err}`
|
|
2241
|
+
);
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
} catch (err) {
|
|
2245
|
+
this.logger.error(
|
|
2246
|
+
`Hook "${event}" threw an error: ${err instanceof Error ? err.message : err}`
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
|
|
2252
|
+
// src/health/jetstream.health-indicator.ts
|
|
2253
|
+
var import_common7 = require("@nestjs/common");
|
|
2254
|
+
var JetstreamHealthIndicator = class {
|
|
2255
|
+
constructor(connection) {
|
|
2256
|
+
this.connection = connection;
|
|
2257
|
+
}
|
|
2258
|
+
logger = new import_common7.Logger("Jetstream:Health");
|
|
2259
|
+
/**
|
|
2260
|
+
* Plain health status check.
|
|
2261
|
+
*
|
|
2262
|
+
* Returns the current connection status without throwing.
|
|
2263
|
+
* Use this for custom health endpoints or monitoring integrations.
|
|
2264
|
+
*
|
|
2265
|
+
* @returns Connection status with server URL and RTT latency.
|
|
2266
|
+
*/
|
|
2267
|
+
async check() {
|
|
2268
|
+
const nc = this.connection.unwrap;
|
|
2269
|
+
if (!nc || nc.isClosed()) {
|
|
2270
|
+
return { connected: false, server: null, latency: null };
|
|
2271
|
+
}
|
|
2272
|
+
try {
|
|
2273
|
+
const start = performance.now();
|
|
2274
|
+
await nc.rtt();
|
|
2275
|
+
const latency = Math.round(performance.now() - start);
|
|
2276
|
+
return { connected: true, server: nc.getServer(), latency };
|
|
2277
|
+
} catch (err) {
|
|
2278
|
+
this.logger.warn(`Health check failed: ${err instanceof Error ? err.message : err}`);
|
|
2279
|
+
return { connected: false, server: nc.getServer(), latency: null };
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Terminus-compatible health check.
|
|
2284
|
+
*
|
|
2285
|
+
* Returns `{ [key]: { status: 'up', ... } }` on success.
|
|
2286
|
+
* Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
|
|
2287
|
+
*
|
|
2288
|
+
* The thrown error sets `isHealthCheckError: true` and `causes` — the
|
|
2289
|
+
* duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
|
|
2290
|
+
* health failures from unexpected exceptions. Works with both Terminus v10
|
|
2291
|
+
* (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
|
|
2292
|
+
*
|
|
2293
|
+
* @param key - Health indicator key (default: `'jetstream'`).
|
|
2294
|
+
* @returns Object with status, server, and latency under the given key.
|
|
2295
|
+
* @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
|
|
2296
|
+
*/
|
|
2297
|
+
async isHealthy(key = "jetstream") {
|
|
2298
|
+
const status = await this.check();
|
|
2299
|
+
const details = {
|
|
2300
|
+
status: status.connected ? "up" : "down",
|
|
2301
|
+
server: status.server,
|
|
2302
|
+
latency: status.latency
|
|
2303
|
+
};
|
|
2304
|
+
if (!status.connected) {
|
|
2305
|
+
const causes = { [key]: details };
|
|
2306
|
+
throw Object.assign(new Error("Jetstream health check failed"), {
|
|
2307
|
+
causes,
|
|
2308
|
+
isHealthCheckError: true
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
return { [key]: details };
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
JetstreamHealthIndicator = __decorateClass([
|
|
2315
|
+
(0, import_common7.Injectable)()
|
|
2316
|
+
], JetstreamHealthIndicator);
|
|
2317
|
+
|
|
2318
|
+
// src/metrics/metrics.module.ts
|
|
2319
|
+
var import_common11 = require("@nestjs/common");
|
|
2320
|
+
|
|
2321
|
+
// src/server/routing/pattern-registry.ts
|
|
2322
|
+
var import_common8 = require("@nestjs/common");
|
|
2323
|
+
var HANDLER_LABELS = {
|
|
2324
|
+
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
2325
|
+
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
2326
|
+
["ev" /* Event */]: "event" /* Event */,
|
|
2327
|
+
["cmd" /* Command */]: "rpc" /* Rpc */
|
|
2328
|
+
};
|
|
2329
|
+
var PatternRegistry = class {
|
|
2330
|
+
constructor(options) {
|
|
2331
|
+
this.options = options;
|
|
2332
|
+
}
|
|
2333
|
+
logger = new import_common8.Logger("Jetstream:PatternRegistry");
|
|
2334
|
+
registry = /* @__PURE__ */ new Map();
|
|
2335
|
+
// Cached after registerHandlers() — the registry is immutable from that point
|
|
2336
|
+
cachedPatterns = null;
|
|
2337
|
+
_hasEvents = false;
|
|
2338
|
+
_hasCommands = false;
|
|
2339
|
+
_hasBroadcasts = false;
|
|
2340
|
+
_hasOrdered = false;
|
|
2341
|
+
_hasMetadata = false;
|
|
2342
|
+
/**
|
|
2343
|
+
* Register all handlers from the NestJS strategy.
|
|
2344
|
+
*
|
|
2345
|
+
* @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
|
|
2346
|
+
*/
|
|
2347
|
+
registerHandlers(handlers) {
|
|
2348
|
+
const serviceName = this.options.name;
|
|
2349
|
+
for (const [pattern, handler] of handlers) {
|
|
2350
|
+
const extras = handler.extras;
|
|
2351
|
+
const isEvent = handler.isEventHandler ?? false;
|
|
2352
|
+
const isBroadcast = !!extras?.broadcast;
|
|
2353
|
+
const isOrdered = !!extras?.ordered;
|
|
2354
|
+
const meta = extras?.meta;
|
|
2355
|
+
if (isBroadcast && isOrdered) {
|
|
2356
|
+
throw new Error(
|
|
2357
|
+
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
let kind;
|
|
2361
|
+
if (isBroadcast) kind = "broadcast" /* Broadcast */;
|
|
2362
|
+
else if (isOrdered) kind = "ordered" /* Ordered */;
|
|
2363
|
+
else if (isEvent) kind = "ev" /* Event */;
|
|
2364
|
+
else kind = "cmd" /* Command */;
|
|
2365
|
+
const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
|
|
2366
|
+
this.registry.set(fullSubject, {
|
|
2367
|
+
handler,
|
|
2368
|
+
pattern,
|
|
2369
|
+
isEvent: isEvent && !isOrdered,
|
|
2370
|
+
isBroadcast,
|
|
2371
|
+
isOrdered,
|
|
2372
|
+
meta
|
|
2373
|
+
});
|
|
2374
|
+
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
2375
|
+
}
|
|
2376
|
+
this.cachedPatterns = this.buildPatternsByKind();
|
|
2377
|
+
this._hasEvents = this.cachedPatterns.events.length > 0;
|
|
2378
|
+
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
2379
|
+
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
2380
|
+
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
2381
|
+
this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
|
|
2382
|
+
this.logSummary();
|
|
2383
|
+
}
|
|
2384
|
+
/** Find handler for a full NATS subject. */
|
|
2385
|
+
getHandler(subject) {
|
|
2386
|
+
return this.registry.get(subject)?.handler ?? null;
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Resolve the declared pattern and {@link StreamKind} for a full NATS subject.
|
|
2390
|
+
*
|
|
2391
|
+
* Returns `null` when the subject is not registered. The declared pattern is
|
|
2392
|
+
* the value the user passed to `@EventPattern`/`@MessagePattern` — stable and
|
|
2393
|
+
* bounded, suitable for use as a Prometheus label without cardinality risk.
|
|
2394
|
+
*/
|
|
2395
|
+
resolveDeclared(subject) {
|
|
2396
|
+
const entry = this.registry.get(subject);
|
|
2397
|
+
if (!entry) return null;
|
|
2398
|
+
return { pattern: entry.pattern, kind: this.resolveStreamKind(entry) };
|
|
2399
|
+
}
|
|
2400
|
+
/** Get all registered broadcast patterns (for consumer filter_subject setup). */
|
|
2401
|
+
getBroadcastPatterns() {
|
|
2402
|
+
return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
|
|
2403
|
+
}
|
|
2404
|
+
hasBroadcastHandlers() {
|
|
2405
|
+
return this._hasBroadcasts;
|
|
2406
|
+
}
|
|
2407
|
+
hasRpcHandlers() {
|
|
2408
|
+
return this._hasCommands;
|
|
2409
|
+
}
|
|
2410
|
+
hasEventHandlers() {
|
|
2411
|
+
return this._hasEvents;
|
|
2412
|
+
}
|
|
2413
|
+
hasOrderedHandlers() {
|
|
2414
|
+
return this._hasOrdered;
|
|
2415
|
+
}
|
|
2416
|
+
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
2417
|
+
getOrderedSubjects() {
|
|
2418
|
+
return this.getPatternsByKind().ordered.map(
|
|
2419
|
+
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
/** Check if any registered handler has metadata. */
|
|
2423
|
+
hasMetadata() {
|
|
2424
|
+
return this._hasMetadata;
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* Get handler metadata entries for KV publishing.
|
|
2428
|
+
*
|
|
2429
|
+
* Returns a map of KV key -> metadata object for all handlers that have `meta`.
|
|
2430
|
+
* Key format: `{serviceName}.{kind}.{pattern}`.
|
|
2431
|
+
*/
|
|
2432
|
+
getMetadataEntries() {
|
|
2433
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2434
|
+
for (const entry of this.registry.values()) {
|
|
2435
|
+
if (!entry.meta) continue;
|
|
2436
|
+
const kind = this.resolveStreamKind(entry);
|
|
2437
|
+
const key = metadataKey(this.options.name, kind, entry.pattern);
|
|
2438
|
+
entries.set(key, entry.meta);
|
|
2439
|
+
}
|
|
2440
|
+
return entries;
|
|
2441
|
+
}
|
|
2442
|
+
/** Get patterns grouped by kind (cached after registration). */
|
|
2443
|
+
getPatternsByKind() {
|
|
2444
|
+
const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
|
|
2445
|
+
return {
|
|
2446
|
+
events: [...patterns.events],
|
|
2447
|
+
commands: [...patterns.commands],
|
|
2448
|
+
broadcasts: [...patterns.broadcasts],
|
|
2449
|
+
ordered: [...patterns.ordered]
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
/** Normalize a full NATS subject back to the user-facing pattern. */
|
|
2453
|
+
normalizeSubject(subject) {
|
|
2454
|
+
const name = internalName(this.options.name);
|
|
2455
|
+
const prefixes = [
|
|
2456
|
+
`${name}.${"cmd" /* Command */}.`,
|
|
2457
|
+
`${name}.${"ev" /* Event */}.`,
|
|
2458
|
+
`${name}.${"ordered" /* Ordered */}.`,
|
|
2459
|
+
`${"broadcast" /* Broadcast */}.`
|
|
2460
|
+
];
|
|
2461
|
+
for (const prefix of prefixes) {
|
|
2462
|
+
if (subject.startsWith(prefix)) {
|
|
2463
|
+
return subject.slice(prefix.length);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return subject;
|
|
2467
|
+
}
|
|
2468
|
+
buildPatternsByKind() {
|
|
2469
|
+
const events = [];
|
|
2470
|
+
const commands = [];
|
|
2471
|
+
const broadcasts = [];
|
|
2472
|
+
const ordered = [];
|
|
2473
|
+
for (const entry of this.registry.values()) {
|
|
2474
|
+
if (entry.isBroadcast) broadcasts.push(entry.pattern);
|
|
2475
|
+
else if (entry.isOrdered) ordered.push(entry.pattern);
|
|
2476
|
+
else if (entry.isEvent) events.push(entry.pattern);
|
|
2477
|
+
else commands.push(entry.pattern);
|
|
2478
|
+
}
|
|
2479
|
+
return { events, commands, broadcasts, ordered };
|
|
2480
|
+
}
|
|
2481
|
+
resolveStreamKind(entry) {
|
|
2482
|
+
if (entry.isBroadcast) return "broadcast" /* Broadcast */;
|
|
2483
|
+
if (entry.isOrdered) return "ordered" /* Ordered */;
|
|
2484
|
+
if (entry.isEvent) return "ev" /* Event */;
|
|
2485
|
+
return "cmd" /* Command */;
|
|
2486
|
+
}
|
|
2487
|
+
logSummary() {
|
|
2488
|
+
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
2489
|
+
const parts = [
|
|
2490
|
+
`${commands.length} RPC`,
|
|
2491
|
+
`${events.length} events`,
|
|
2492
|
+
`${broadcasts.length} broadcasts`
|
|
2493
|
+
];
|
|
2494
|
+
if (ordered.length > 0) {
|
|
2495
|
+
parts.push(`${ordered.length} ordered`);
|
|
2496
|
+
}
|
|
2497
|
+
this.logger.log(`Registered handlers: ${parts.join(", ")}`);
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
|
|
2501
|
+
// src/metrics/metrics.constants.ts
|
|
2502
|
+
var JETSTREAM_METRICS_CONFIG = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_CONFIG");
|
|
2503
|
+
var JETSTREAM_METRICS_REGISTRY = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_REGISTRY");
|
|
2504
|
+
var JETSTREAM_METRICS_PROM_CLIENT = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_PROM_CLIENT");
|
|
2505
|
+
var DEFAULT_METRICS_PREFIX = "jetstream_";
|
|
2506
|
+
var DEFAULT_POLL_INTERVAL_MS = 15e3;
|
|
2507
|
+
var DEFAULT_HISTOGRAM_BUCKETS = {
|
|
2508
|
+
handlerDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
|
2509
|
+
publishDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
|
|
2510
|
+
rpcDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
|
2511
|
+
};
|
|
2512
|
+
var ERROR_CONTEXT_PREFIXES = [
|
|
2513
|
+
["connection", "connection"],
|
|
2514
|
+
["codec", "codec"],
|
|
2515
|
+
["client-rpc", "publish"],
|
|
2516
|
+
["jetstream-rpc-publish", "publish"],
|
|
2517
|
+
["publish", "publish"],
|
|
2518
|
+
["message-provider", "consume"],
|
|
2519
|
+
["consume", "consume"],
|
|
2520
|
+
["core-rpc-handler", "handler"],
|
|
2521
|
+
["rpc-handler", "handler"],
|
|
2522
|
+
// EventRouter formats contexts as `${StreamKind.*}-handler:...` — the enum
|
|
2523
|
+
// uses short forms (`ev`, `ordered`, `broadcast`) so both surface in the wild.
|
|
2524
|
+
["ev-handler", "handler"],
|
|
2525
|
+
["event-handler", "handler"],
|
|
2526
|
+
["broadcast-handler", "handler"],
|
|
2527
|
+
["ordered-handler", "handler"],
|
|
2528
|
+
["handler", "handler"],
|
|
2529
|
+
["shutdown", "shutdown"]
|
|
2530
|
+
];
|
|
2531
|
+
var UNMATCHED_SUBJECT_LABEL = "<unmatched>";
|
|
2532
|
+
var STREAM_KIND_LABEL = {
|
|
2533
|
+
["ev" /* Event */]: "event",
|
|
2534
|
+
["cmd" /* Command */]: "command",
|
|
2535
|
+
["broadcast" /* Broadcast */]: "broadcast",
|
|
2536
|
+
["ordered" /* Ordered */]: "ordered"
|
|
2537
|
+
};
|
|
2538
|
+
|
|
2539
|
+
// src/metrics/metrics.service.ts
|
|
2540
|
+
var import_common10 = require("@nestjs/common");
|
|
2541
|
+
|
|
2542
|
+
// src/metrics/error-context-mapper.ts
|
|
2543
|
+
var mapErrorContext = (context7) => {
|
|
2544
|
+
if (!context7) return "other";
|
|
2545
|
+
for (const [prefix, mapped] of ERROR_CONTEXT_PREFIXES) {
|
|
2546
|
+
if (context7 === prefix || context7.startsWith(`${prefix}:`)) {
|
|
2547
|
+
return mapped;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
return "other";
|
|
2551
|
+
};
|
|
2552
|
+
|
|
2553
|
+
// src/metrics/metrics.factory.ts
|
|
2554
|
+
var createMetrics = (opts) => {
|
|
2555
|
+
const { register, promClient } = opts;
|
|
2556
|
+
const prefix = opts.prefix ?? DEFAULT_METRICS_PREFIX;
|
|
2557
|
+
const buckets = {
|
|
2558
|
+
handlerDuration: opts.buckets?.handlerDuration ?? DEFAULT_HISTOGRAM_BUCKETS.handlerDuration,
|
|
2559
|
+
publishDuration: opts.buckets?.publishDuration ?? DEFAULT_HISTOGRAM_BUCKETS.publishDuration,
|
|
2560
|
+
rpcDuration: opts.buckets?.rpcDuration ?? DEFAULT_HISTOGRAM_BUCKETS.rpcDuration
|
|
2561
|
+
};
|
|
2562
|
+
if (opts.defaultLabels && Object.keys(opts.defaultLabels).length > 0) {
|
|
2563
|
+
register.setDefaultLabels(opts.defaultLabels);
|
|
2564
|
+
}
|
|
2565
|
+
const counter = (name, help, labelNames) => new promClient.Counter({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
|
|
2566
|
+
const histogram = (name, help, labelNames, bucketArr) => new promClient.Histogram({
|
|
2567
|
+
name: `${prefix}${name}`,
|
|
2568
|
+
help,
|
|
2569
|
+
labelNames,
|
|
2570
|
+
buckets: bucketArr,
|
|
2571
|
+
registers: [register]
|
|
2572
|
+
});
|
|
2573
|
+
const gauge = (name, help, labelNames) => new promClient.Gauge({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
|
|
2574
|
+
return {
|
|
2575
|
+
messagesReceivedTotal: counter(
|
|
2576
|
+
"messages_received_total",
|
|
2577
|
+
"Total messages routed to a handler.",
|
|
2578
|
+
["stream", "subject", "kind"]
|
|
2579
|
+
),
|
|
2580
|
+
messagesProcessedTotal: counter(
|
|
2581
|
+
"messages_processed_total",
|
|
2582
|
+
"Total messages whose handler completed.",
|
|
2583
|
+
["stream", "subject", "kind", "status"]
|
|
2584
|
+
),
|
|
2585
|
+
messagesUnhandledTotal: counter(
|
|
2586
|
+
"messages_unhandled_total",
|
|
2587
|
+
"Messages received but not matching any registered handler.",
|
|
2588
|
+
["subject"]
|
|
2589
|
+
),
|
|
2590
|
+
messagesDeadLetterTotal: counter(
|
|
2591
|
+
"messages_dead_letter_total",
|
|
2592
|
+
"Messages routed to dead-letter after exhausting redelivery attempts.",
|
|
2593
|
+
["stream", "subject"]
|
|
2594
|
+
),
|
|
2595
|
+
publishTotal: counter(
|
|
2596
|
+
"publish_total",
|
|
2597
|
+
"Total publish/send operations performed by the client.",
|
|
2598
|
+
["subject", "kind", "status"]
|
|
2599
|
+
),
|
|
2600
|
+
rpcTimeoutTotal: counter("rpc_timeout_total", "RPC calls that exceeded the timeout deadline.", [
|
|
2601
|
+
"subject"
|
|
2602
|
+
]),
|
|
2603
|
+
consumerRecoveredTotal: counter(
|
|
2604
|
+
"consumer_recovered_total",
|
|
2605
|
+
"Self-healing recoveries after consume-loop failures.",
|
|
2606
|
+
["kind"]
|
|
2607
|
+
),
|
|
2608
|
+
errorsTotal: counter("errors_total", "Transport-level errors emitted on the EventBus.", [
|
|
2609
|
+
"context"
|
|
2610
|
+
]),
|
|
2611
|
+
handlerDurationSeconds: histogram(
|
|
2612
|
+
"handler_duration_seconds",
|
|
2613
|
+
"Wall-clock duration of handler execution.",
|
|
2614
|
+
["stream", "subject", "kind", "status"],
|
|
2615
|
+
buckets.handlerDuration
|
|
2616
|
+
),
|
|
2617
|
+
publishDurationSeconds: histogram(
|
|
2618
|
+
"publish_duration_seconds",
|
|
2619
|
+
"Wall-clock duration of client publish/send operations.",
|
|
2620
|
+
["subject", "kind", "status"],
|
|
2621
|
+
buckets.publishDuration
|
|
2622
|
+
),
|
|
2623
|
+
rpcDurationSeconds: histogram(
|
|
2624
|
+
"rpc_duration_seconds",
|
|
2625
|
+
"Wall-clock duration of RPC round-trips from client perspective.",
|
|
2626
|
+
["subject", "status"],
|
|
2627
|
+
buckets.rpcDuration
|
|
2628
|
+
),
|
|
2629
|
+
consumerNumPending: gauge(
|
|
2630
|
+
"consumer_num_pending",
|
|
2631
|
+
"Messages not yet delivered to this consumer.",
|
|
2632
|
+
["stream", "consumer", "kind"]
|
|
2633
|
+
),
|
|
2634
|
+
consumerNumAckPending: gauge(
|
|
2635
|
+
"consumer_num_ack_pending",
|
|
2636
|
+
"Messages delivered but not yet acked.",
|
|
2637
|
+
["stream", "consumer", "kind"]
|
|
2638
|
+
),
|
|
2639
|
+
consumerNumRedelivered: gauge(
|
|
2640
|
+
"consumer_num_redelivered",
|
|
2641
|
+
"Messages currently in redelivery state.",
|
|
2642
|
+
["stream", "consumer", "kind"]
|
|
2643
|
+
),
|
|
2644
|
+
consumerNumWaiting: gauge(
|
|
2645
|
+
"consumer_num_waiting",
|
|
2646
|
+
"Pull-request waiting count for this consumer.",
|
|
2647
|
+
["stream", "consumer", "kind"]
|
|
2648
|
+
),
|
|
2649
|
+
streamMessages: gauge("stream_messages", "Total messages stored in this stream.", ["stream"]),
|
|
2650
|
+
streamBytes: gauge("stream_bytes", "Total bytes stored in this stream.", ["stream"]),
|
|
2651
|
+
connectionUp: gauge("connection_up", "NATS connection state (1 connected, 0 disconnected).", [
|
|
2652
|
+
"server"
|
|
2653
|
+
]),
|
|
2654
|
+
metricsPollErrorsTotal: counter(
|
|
2655
|
+
"metrics_poll_errors_total",
|
|
2656
|
+
"Errors encountered while polling JetStreamManager for gauge data.",
|
|
2657
|
+
["target"]
|
|
2658
|
+
)
|
|
2659
|
+
};
|
|
2660
|
+
};
|
|
2661
|
+
|
|
2662
|
+
// src/metrics/poll-runner.ts
|
|
2663
|
+
var import_common9 = require("@nestjs/common");
|
|
2664
|
+
var PollRunner = class {
|
|
2665
|
+
constructor(opts) {
|
|
2666
|
+
this.opts = opts;
|
|
2667
|
+
}
|
|
2668
|
+
logger = new import_common9.Logger("Jetstream:Metrics:Poll");
|
|
2669
|
+
timer = null;
|
|
2670
|
+
inFlight = null;
|
|
2671
|
+
start() {
|
|
2672
|
+
if (this.timer !== null) return;
|
|
2673
|
+
if (this.opts.intervalMs <= 0) return;
|
|
2674
|
+
if (this.opts.targets.length === 0) return;
|
|
2675
|
+
this.timer = setInterval(() => {
|
|
2676
|
+
if (this.inFlight !== null) {
|
|
2677
|
+
this.logger.warn("Skipping poll tick \u2014 previous cycle still in flight");
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
this.inFlight = this.tick().finally(() => {
|
|
2681
|
+
this.inFlight = null;
|
|
2682
|
+
});
|
|
2683
|
+
}, this.opts.intervalMs);
|
|
2684
|
+
}
|
|
2685
|
+
async stop() {
|
|
2686
|
+
if (this.timer !== null) {
|
|
2687
|
+
clearInterval(this.timer);
|
|
2688
|
+
this.timer = null;
|
|
2689
|
+
}
|
|
2690
|
+
if (this.inFlight !== null) await this.inFlight;
|
|
2691
|
+
}
|
|
2692
|
+
/** @internal Visible for tests. Runs one poll cycle. */
|
|
2693
|
+
async tick() {
|
|
2694
|
+
let jsm;
|
|
974
2695
|
try {
|
|
975
|
-
await this.
|
|
976
|
-
await this.connection.closed();
|
|
2696
|
+
jsm = await this.opts.jsmFactory();
|
|
977
2697
|
} catch {
|
|
2698
|
+
this.recordPollError("jsm.connect");
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
await Promise.all([this.pollConsumers(jsm), this.pollStreams(jsm)]);
|
|
2702
|
+
}
|
|
2703
|
+
async pollConsumers(jsm) {
|
|
2704
|
+
for (const target of this.opts.targets) {
|
|
978
2705
|
try {
|
|
979
|
-
await
|
|
2706
|
+
const info = await jsm.consumers.info(target.stream, target.consumer);
|
|
2707
|
+
const labels = {
|
|
2708
|
+
stream: target.stream,
|
|
2709
|
+
consumer: target.consumer,
|
|
2710
|
+
kind: STREAM_KIND_LABEL[target.kind]
|
|
2711
|
+
};
|
|
2712
|
+
this.opts.metrics.consumerNumPending.labels(labels).set(info.num_pending);
|
|
2713
|
+
this.opts.metrics.consumerNumAckPending.labels(labels).set(info.num_ack_pending);
|
|
2714
|
+
this.opts.metrics.consumerNumRedelivered.labels(labels).set(info.num_redelivered);
|
|
2715
|
+
this.opts.metrics.consumerNumWaiting.labels(labels).set(info.num_waiting);
|
|
980
2716
|
} catch {
|
|
2717
|
+
this.recordPollError("consumer.info");
|
|
981
2718
|
}
|
|
982
|
-
} finally {
|
|
983
|
-
this.connection = null;
|
|
984
|
-
this.connectionPromise = null;
|
|
985
|
-
this.jsClient = null;
|
|
986
|
-
this.jsmInstance = null;
|
|
987
|
-
this.jsmPromise = null;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
async initJetStreamManager() {
|
|
991
|
-
try {
|
|
992
|
-
const nc = await this.getConnection();
|
|
993
|
-
this.jsmInstance = await (0, import_jetstream7.jetstreamManager)(nc);
|
|
994
|
-
this.logger.log("JetStream manager initialized");
|
|
995
|
-
return this.jsmInstance;
|
|
996
|
-
} finally {
|
|
997
|
-
this.jsmPromise = null;
|
|
998
2719
|
}
|
|
999
2720
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
});
|
|
1010
|
-
this.connection = nc;
|
|
1011
|
-
this.logger.log(`NATS connection established: ${nc.getServer()}`);
|
|
1012
|
-
this.eventBus.emit("connect" /* Connect */, nc.getServer());
|
|
1013
|
-
this.monitorStatus(nc);
|
|
1014
|
-
return nc;
|
|
1015
|
-
} catch (err) {
|
|
1016
|
-
if (err instanceof Error && err.message.includes("REFUSED")) {
|
|
1017
|
-
throw new Error(`NATS connection refused: ${this.options.servers.join(", ")}`);
|
|
2721
|
+
async pollStreams(jsm) {
|
|
2722
|
+
const uniqueStreams = new Set(this.opts.targets.map((t) => t.stream));
|
|
2723
|
+
for (const stream of uniqueStreams) {
|
|
2724
|
+
try {
|
|
2725
|
+
const info = await jsm.streams.info(stream);
|
|
2726
|
+
this.opts.metrics.streamMessages.labels({ stream }).set(info.state.messages);
|
|
2727
|
+
this.opts.metrics.streamBytes.labels({ stream }).set(info.state.bytes);
|
|
2728
|
+
} catch {
|
|
2729
|
+
this.recordPollError("stream.info");
|
|
1018
2730
|
}
|
|
1019
|
-
throw err;
|
|
1020
2731
|
}
|
|
1021
2732
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
void (async () => {
|
|
1025
|
-
for await (const status of nc.status()) {
|
|
1026
|
-
switch (status.type) {
|
|
1027
|
-
case "disconnect":
|
|
1028
|
-
this.eventBus.emit("disconnect" /* Disconnect */);
|
|
1029
|
-
break;
|
|
1030
|
-
case "reconnect":
|
|
1031
|
-
this.jsClient = null;
|
|
1032
|
-
this.jsmInstance = null;
|
|
1033
|
-
this.jsmPromise = null;
|
|
1034
|
-
this.eventBus.emit("reconnect" /* Reconnect */, nc.getServer());
|
|
1035
|
-
break;
|
|
1036
|
-
case "error":
|
|
1037
|
-
this.eventBus.emit(
|
|
1038
|
-
"error" /* Error */,
|
|
1039
|
-
status.error,
|
|
1040
|
-
"connection"
|
|
1041
|
-
);
|
|
1042
|
-
break;
|
|
1043
|
-
case "update":
|
|
1044
|
-
case "ldm":
|
|
1045
|
-
case "reconnecting":
|
|
1046
|
-
case "ping":
|
|
1047
|
-
case "staleConnection":
|
|
1048
|
-
case "forceReconnect":
|
|
1049
|
-
case "slowConsumer":
|
|
1050
|
-
case "close":
|
|
1051
|
-
break;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
})().catch((err) => {
|
|
1055
|
-
this.logger.error("Status monitor error", err);
|
|
1056
|
-
});
|
|
2733
|
+
recordPollError(target) {
|
|
2734
|
+
this.opts.metrics.metricsPollErrorsTotal.labels({ target }).inc();
|
|
1057
2735
|
}
|
|
1058
2736
|
};
|
|
1059
2737
|
|
|
1060
|
-
// src/
|
|
1061
|
-
var
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
this.
|
|
1066
|
-
this.
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
* Emit a lifecycle event. Dispatches to custom hook if registered, otherwise no-op.
|
|
1070
|
-
*
|
|
1071
|
-
* @param event - The {@link TransportEvent} to emit.
|
|
1072
|
-
* @param args - Arguments matching the hook signature for this event.
|
|
1073
|
-
*/
|
|
1074
|
-
emit(event, ...args) {
|
|
1075
|
-
const hook = this.hooks[event];
|
|
1076
|
-
if (!hook) return;
|
|
1077
|
-
this.callHook(event, hook, ...args);
|
|
2738
|
+
// src/metrics/metrics.service.ts
|
|
2739
|
+
var JetstreamMetricsService = class {
|
|
2740
|
+
constructor(eventBus, config, promClient, options, patternRegistry, connection = null) {
|
|
2741
|
+
this.eventBus = eventBus;
|
|
2742
|
+
this.config = config;
|
|
2743
|
+
this.promClient = promClient;
|
|
2744
|
+
this.options = options;
|
|
2745
|
+
this.patternRegistry = patternRegistry;
|
|
2746
|
+
this.connection = connection;
|
|
1078
2747
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
if (!
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
2748
|
+
logger = new import_common10.Logger("Jetstream:Metrics");
|
|
2749
|
+
metrics = null;
|
|
2750
|
+
pollRunner = null;
|
|
2751
|
+
activeServers = /* @__PURE__ */ new Set();
|
|
2752
|
+
async onApplicationBootstrap() {
|
|
2753
|
+
if (this.metrics !== null) return;
|
|
2754
|
+
if (!this.config.register) {
|
|
2755
|
+
throw new Error(
|
|
2756
|
+
"JetstreamMetricsService requires a prom-client Registry \u2014 none was resolved by JetstreamMetricsModule."
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
this.metrics = createMetrics({
|
|
2760
|
+
register: this.config.register,
|
|
2761
|
+
promClient: this.promClient,
|
|
2762
|
+
prefix: this.config.prefix,
|
|
2763
|
+
defaultLabels: this.config.defaultLabels,
|
|
2764
|
+
buckets: this.config.buckets
|
|
2765
|
+
});
|
|
2766
|
+
this.subscribeToEvents();
|
|
2767
|
+
this.syncInitialConnectionState();
|
|
2768
|
+
this.startPolling();
|
|
2769
|
+
this.logger.log(
|
|
2770
|
+
`Metrics enabled (prefix=${this.config.prefix ?? DEFAULT_METRICS_PREFIX}, poll=${this.getEffectivePollInterval()}ms)`
|
|
1091
2771
|
);
|
|
1092
2772
|
}
|
|
2773
|
+
async onModuleDestroy() {
|
|
2774
|
+
await this.pollRunner?.stop();
|
|
2775
|
+
this.pollRunner = null;
|
|
2776
|
+
}
|
|
2777
|
+
/** @internal Visible for tests. `0` disables polling. */
|
|
2778
|
+
getEffectivePollInterval() {
|
|
2779
|
+
return this.config.pollInterval ?? DEFAULT_POLL_INTERVAL_MS;
|
|
2780
|
+
}
|
|
1093
2781
|
/**
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
2782
|
+
* NATS connects during early bootstrap, before this service subscribes to
|
|
2783
|
+
* the EventBus — the initial `Connect` emission misses us. Mirror the
|
|
2784
|
+
* current state here so `connection_up` reflects reality the moment metrics
|
|
2785
|
+
* come online; later disconnects/reconnects update it normally.
|
|
1098
2786
|
*/
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
)
|
|
2787
|
+
syncInitialConnectionState() {
|
|
2788
|
+
const nc = this.connection?.unwrap;
|
|
2789
|
+
if (!nc) return;
|
|
2790
|
+
const server = nc.getServer();
|
|
2791
|
+
this.activeServers.add(server);
|
|
2792
|
+
this.metrics?.connectionUp.labels({ server }).set(1);
|
|
2793
|
+
}
|
|
2794
|
+
/** Skips polling for publisher-only deployments and when no kinds are active. */
|
|
2795
|
+
startPolling() {
|
|
2796
|
+
const interval = this.getEffectivePollInterval();
|
|
2797
|
+
const connection = this.connection;
|
|
2798
|
+
if (interval <= 0 || !this.patternRegistry || !connection || !this.metrics) return;
|
|
2799
|
+
const targets = this.buildPollTargets();
|
|
2800
|
+
if (targets.length === 0) return;
|
|
2801
|
+
this.pollRunner = new PollRunner({
|
|
2802
|
+
intervalMs: interval,
|
|
2803
|
+
jsmFactory: async () => connection.getJetStreamManager(),
|
|
2804
|
+
metrics: this.metrics,
|
|
2805
|
+
targets
|
|
2806
|
+
});
|
|
2807
|
+
this.pollRunner.start();
|
|
2808
|
+
}
|
|
2809
|
+
buildPollTargets() {
|
|
2810
|
+
const registry = this.patternRegistry;
|
|
2811
|
+
if (!registry) return [];
|
|
2812
|
+
const targets = [];
|
|
2813
|
+
if (registry.hasEventHandlers()) {
|
|
2814
|
+
targets.push({
|
|
2815
|
+
kind: "ev" /* Event */,
|
|
2816
|
+
stream: streamName(this.options.name, "ev" /* Event */),
|
|
2817
|
+
consumer: consumerName(this.options.name, "ev" /* Event */)
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
if (registry.hasRpcHandlers() && isJetStreamRpcMode(this.options.rpc)) {
|
|
2821
|
+
targets.push({
|
|
2822
|
+
kind: "cmd" /* Command */,
|
|
2823
|
+
stream: streamName(this.options.name, "cmd" /* Command */),
|
|
2824
|
+
consumer: consumerName(this.options.name, "cmd" /* Command */)
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
if (registry.hasBroadcastHandlers()) {
|
|
2828
|
+
targets.push({
|
|
2829
|
+
kind: "broadcast" /* Broadcast */,
|
|
2830
|
+
stream: streamName(this.options.name, "broadcast" /* Broadcast */),
|
|
2831
|
+
consumer: consumerName(this.options.name, "broadcast" /* Broadcast */)
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
return targets;
|
|
2835
|
+
}
|
|
2836
|
+
subscribeToEvents() {
|
|
2837
|
+
this.eventBus.subscribe("connect" /* Connect */, this.onConnect);
|
|
2838
|
+
this.eventBus.subscribe("disconnect" /* Disconnect */, this.onDisconnect);
|
|
2839
|
+
this.eventBus.subscribe("reconnect" /* Reconnect */, this.onReconnect);
|
|
2840
|
+
this.eventBus.subscribe("error" /* Error */, this.onError);
|
|
2841
|
+
this.eventBus.subscribe("rpcTimeout" /* RpcTimeout */, this.onRpcTimeout);
|
|
2842
|
+
this.eventBus.subscribe("messageRouted" /* MessageRouted */, this.onMessageRouted);
|
|
2843
|
+
this.eventBus.subscribe("deadLetter" /* DeadLetter */, this.onDeadLetter);
|
|
2844
|
+
this.eventBus.subscribe("consumerRecovered" /* ConsumerRecovered */, this.onConsumerRecovered);
|
|
2845
|
+
this.eventBus.subscribe("handlerCompleted" /* HandlerCompleted */, this.onHandlerCompleted);
|
|
2846
|
+
this.eventBus.subscribe("published" /* Published */, this.onPublished);
|
|
2847
|
+
this.eventBus.subscribe("rpcCompleted" /* RpcCompleted */, this.onRpcCompleted);
|
|
2848
|
+
}
|
|
2849
|
+
onConnect = (server) => {
|
|
2850
|
+
this.activeServers.add(server);
|
|
2851
|
+
this.metrics?.connectionUp.labels({ server }).set(1);
|
|
2852
|
+
};
|
|
2853
|
+
onReconnect = (server) => {
|
|
2854
|
+
this.activeServers.add(server);
|
|
2855
|
+
this.metrics?.connectionUp.labels({ server }).set(1);
|
|
2856
|
+
};
|
|
2857
|
+
onDisconnect = () => {
|
|
2858
|
+
for (const server of this.activeServers) {
|
|
2859
|
+
this.metrics?.connectionUp.labels({ server }).set(0);
|
|
1116
2860
|
}
|
|
2861
|
+
};
|
|
2862
|
+
onError = (_err, context7) => {
|
|
2863
|
+
this.metrics?.errorsTotal.labels({ context: mapErrorContext(context7) }).inc();
|
|
2864
|
+
};
|
|
2865
|
+
onRpcTimeout = (subject, _correlationId) => {
|
|
2866
|
+
const declared = this.resolveDeclared(subject);
|
|
2867
|
+
const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
|
|
2868
|
+
this.metrics?.rpcTimeoutTotal.labels({ subject: subjectLabel }).inc();
|
|
2869
|
+
};
|
|
2870
|
+
// `_kind` collapses broadcast/ordered into MessageKind.Event — we use
|
|
2871
|
+
// declared.kind from PatternRegistry for the precise label instead.
|
|
2872
|
+
onMessageRouted = (subject, _kind) => {
|
|
2873
|
+
if (!this.metrics) return;
|
|
2874
|
+
const declared = this.resolveDeclared(subject);
|
|
2875
|
+
if (!declared) {
|
|
2876
|
+
this.metrics.messagesUnhandledTotal.labels({ subject: UNMATCHED_SUBJECT_LABEL }).inc();
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
this.metrics.messagesReceivedTotal.labels({
|
|
2880
|
+
stream: streamName(this.options.name, declared.kind),
|
|
2881
|
+
subject: declared.pattern,
|
|
2882
|
+
kind: STREAM_KIND_LABEL[declared.kind]
|
|
2883
|
+
}).inc();
|
|
2884
|
+
};
|
|
2885
|
+
onDeadLetter = (info) => {
|
|
2886
|
+
const declared = this.resolveDeclared(info.subject);
|
|
2887
|
+
const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
|
|
2888
|
+
this.metrics?.messagesDeadLetterTotal.labels({ stream: info.stream, subject: subjectLabel }).inc();
|
|
2889
|
+
};
|
|
2890
|
+
onConsumerRecovered = (label, _attempts) => {
|
|
2891
|
+
const kindLabel = STREAM_KIND_LABEL[label] ?? String(label);
|
|
2892
|
+
this.metrics?.consumerRecoveredTotal.labels({ kind: kindLabel }).inc();
|
|
2893
|
+
};
|
|
2894
|
+
onHandlerCompleted = (pattern, kind, durationMs, status) => {
|
|
2895
|
+
if (!this.metrics) return;
|
|
2896
|
+
const stream = streamName(this.options.name, kind);
|
|
2897
|
+
const kindLabel = STREAM_KIND_LABEL[kind];
|
|
2898
|
+
const labels = { stream, subject: pattern, kind: kindLabel, status };
|
|
2899
|
+
this.metrics.messagesProcessedTotal.labels(labels).inc();
|
|
2900
|
+
this.metrics.handlerDurationSeconds.labels(labels).observe(durationMs / 1e3);
|
|
2901
|
+
};
|
|
2902
|
+
onPublished = (pattern, kind, durationMs, status) => {
|
|
2903
|
+
if (!this.metrics) return;
|
|
2904
|
+
const labels = { subject: pattern, kind: STREAM_KIND_LABEL[kind], status };
|
|
2905
|
+
this.metrics.publishTotal.labels(labels).inc();
|
|
2906
|
+
this.metrics.publishDurationSeconds.labels(labels).observe(durationMs / 1e3);
|
|
2907
|
+
};
|
|
2908
|
+
onRpcCompleted = (pattern, durationMs, status) => {
|
|
2909
|
+
this.metrics?.rpcDurationSeconds.labels({ subject: pattern, status }).observe(durationMs / 1e3);
|
|
2910
|
+
};
|
|
2911
|
+
resolveDeclared(subject) {
|
|
2912
|
+
return this.patternRegistry?.resolveDeclared(subject) ?? null;
|
|
1117
2913
|
}
|
|
1118
2914
|
};
|
|
2915
|
+
JetstreamMetricsService = __decorateClass([
|
|
2916
|
+
(0, import_common10.Injectable)(),
|
|
2917
|
+
__decorateParam(1, (0, import_common10.Inject)(JETSTREAM_METRICS_CONFIG)),
|
|
2918
|
+
__decorateParam(2, (0, import_common10.Inject)(JETSTREAM_METRICS_PROM_CLIENT)),
|
|
2919
|
+
__decorateParam(3, (0, import_common10.Inject)(JETSTREAM_OPTIONS)),
|
|
2920
|
+
__decorateParam(4, (0, import_common10.Optional)()),
|
|
2921
|
+
__decorateParam(5, (0, import_common10.Optional)()),
|
|
2922
|
+
__decorateParam(5, (0, import_common10.Inject)(JETSTREAM_CONNECTION))
|
|
2923
|
+
], JetstreamMetricsService);
|
|
1119
2924
|
|
|
1120
|
-
// src/
|
|
1121
|
-
var
|
|
1122
|
-
var
|
|
1123
|
-
|
|
1124
|
-
|
|
2925
|
+
// src/metrics/metrics.module.ts
|
|
2926
|
+
var PROM_CLIENT_INSTALL_MESSAGE = "prom-client is required when JetstreamModule.forRoot({ metrics: ... }) is enabled. Install it with: pnpm add prom-client";
|
|
2927
|
+
var resolvePromClient = async () => {
|
|
2928
|
+
try {
|
|
2929
|
+
return await import("prom-client");
|
|
2930
|
+
} catch {
|
|
2931
|
+
throw new Error(PROM_CLIENT_INSTALL_MESSAGE);
|
|
1125
2932
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const start = performance.now();
|
|
1142
|
-
await nc.rtt();
|
|
1143
|
-
const latency = Math.round(performance.now() - start);
|
|
1144
|
-
return { connected: true, server: nc.getServer(), latency };
|
|
1145
|
-
} catch (err) {
|
|
1146
|
-
this.logger.warn(`Health check failed: ${err instanceof Error ? err.message : err}`);
|
|
1147
|
-
return { connected: false, server: nc.getServer(), latency: null };
|
|
2933
|
+
};
|
|
2934
|
+
var normalizeMetricsConfig = (option, promClient) => {
|
|
2935
|
+
const user = option && option !== true ? option : {};
|
|
2936
|
+
return {
|
|
2937
|
+
register: user.register ?? promClient.register,
|
|
2938
|
+
prefix: user.prefix ?? DEFAULT_METRICS_PREFIX,
|
|
2939
|
+
defaultLabels: user.defaultLabels,
|
|
2940
|
+
pollInterval: user.pollInterval ?? DEFAULT_POLL_INTERVAL_MS,
|
|
2941
|
+
buckets: user.buckets
|
|
2942
|
+
};
|
|
2943
|
+
};
|
|
2944
|
+
var JetstreamMetricsModule = class {
|
|
2945
|
+
static forFeature(metricsOption) {
|
|
2946
|
+
if (!metricsOption) {
|
|
2947
|
+
return { module: JetstreamMetricsModule, providers: [], exports: [] };
|
|
1148
2948
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
2949
|
+
const promClientProvider = {
|
|
2950
|
+
provide: JETSTREAM_METRICS_PROM_CLIENT,
|
|
2951
|
+
useFactory: async () => {
|
|
2952
|
+
const mod = await resolvePromClient();
|
|
2953
|
+
return { Counter: mod.Counter, Histogram: mod.Histogram, Gauge: mod.Gauge };
|
|
2954
|
+
}
|
|
2955
|
+
};
|
|
2956
|
+
const configProvider = {
|
|
2957
|
+
provide: JETSTREAM_METRICS_CONFIG,
|
|
2958
|
+
useFactory: async () => {
|
|
2959
|
+
const mod = await resolvePromClient();
|
|
2960
|
+
return normalizeMetricsConfig(metricsOption, mod);
|
|
2961
|
+
}
|
|
2962
|
+
};
|
|
2963
|
+
const registryProvider = {
|
|
2964
|
+
provide: JETSTREAM_METRICS_REGISTRY,
|
|
2965
|
+
inject: [JETSTREAM_METRICS_CONFIG],
|
|
2966
|
+
useFactory: (cfg) => cfg.register
|
|
2967
|
+
};
|
|
2968
|
+
const serviceProvider = {
|
|
2969
|
+
provide: JetstreamMetricsService,
|
|
2970
|
+
inject: [
|
|
2971
|
+
JETSTREAM_EVENT_BUS,
|
|
2972
|
+
JETSTREAM_METRICS_CONFIG,
|
|
2973
|
+
JETSTREAM_METRICS_PROM_CLIENT,
|
|
2974
|
+
JETSTREAM_OPTIONS,
|
|
2975
|
+
{ token: PatternRegistry, optional: true },
|
|
2976
|
+
{ token: JETSTREAM_CONNECTION, optional: true }
|
|
2977
|
+
],
|
|
2978
|
+
useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection) => new JetstreamMetricsService(eventBus, cfg, runtime, opts, patternRegistry, connection)
|
|
2979
|
+
};
|
|
2980
|
+
return {
|
|
2981
|
+
module: JetstreamMetricsModule,
|
|
2982
|
+
providers: [promClientProvider, configProvider, registryProvider, serviceProvider],
|
|
2983
|
+
exports: [
|
|
2984
|
+
JetstreamMetricsService,
|
|
2985
|
+
JETSTREAM_METRICS_CONFIG,
|
|
2986
|
+
JETSTREAM_METRICS_REGISTRY,
|
|
2987
|
+
JETSTREAM_METRICS_PROM_CLIENT
|
|
2988
|
+
]
|
|
1171
2989
|
};
|
|
1172
|
-
if (!status.connected) {
|
|
1173
|
-
const causes = { [key]: details };
|
|
1174
|
-
throw Object.assign(new Error("Jetstream health check failed"), {
|
|
1175
|
-
causes,
|
|
1176
|
-
isHealthCheckError: true
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
return { [key]: details };
|
|
1180
2990
|
}
|
|
1181
2991
|
};
|
|
1182
|
-
|
|
1183
|
-
(0,
|
|
1184
|
-
],
|
|
2992
|
+
JetstreamMetricsModule = __decorateClass([
|
|
2993
|
+
(0, import_common11.Module)({})
|
|
2994
|
+
], JetstreamMetricsModule);
|
|
1185
2995
|
|
|
1186
2996
|
// src/server/strategy.ts
|
|
1187
2997
|
var import_microservices2 = require("@nestjs/microservices");
|
|
@@ -1257,6 +3067,26 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
1257
3067
|
this.messageProvider.destroy();
|
|
1258
3068
|
this.started = false;
|
|
1259
3069
|
}
|
|
3070
|
+
/**
|
|
3071
|
+
* Override NestJS `Server.addHandler` to fail-fast on duplicate pattern registration.
|
|
3072
|
+
*
|
|
3073
|
+
* The base class silently overwrites duplicate RPC handlers (last wins) and appends
|
|
3074
|
+
* duplicate event handlers to a linked list. Both behaviors are hazardous in a
|
|
3075
|
+
* JetStream context: silent overwrite drops a handler the user wrote, and double
|
|
3076
|
+
* event dispatch double-acks/double-processes the same JetStream message.
|
|
3077
|
+
*
|
|
3078
|
+
* We treat any pattern collision as a fatal misconfiguration so it surfaces at
|
|
3079
|
+
* bootstrap instead of in production traffic.
|
|
3080
|
+
*/
|
|
3081
|
+
addHandler(pattern, callback, isEventHandler = false, extras = {}) {
|
|
3082
|
+
const normalizedPattern = this.normalizePattern(pattern);
|
|
3083
|
+
if (this.messageHandlers.has(normalizedPattern)) {
|
|
3084
|
+
throw new Error(
|
|
3085
|
+
`Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice \u2014 find and remove the second declaration.`
|
|
3086
|
+
);
|
|
3087
|
+
}
|
|
3088
|
+
super.addHandler(pattern, callback, isEventHandler, extras);
|
|
3089
|
+
}
|
|
1260
3090
|
/**
|
|
1261
3091
|
* Register event listener (required by Server base class).
|
|
1262
3092
|
*
|
|
@@ -1329,7 +3159,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
|
|
|
1329
3159
|
};
|
|
1330
3160
|
|
|
1331
3161
|
// src/server/core-rpc.server.ts
|
|
1332
|
-
var
|
|
3162
|
+
var import_common12 = require("@nestjs/common");
|
|
1333
3163
|
var import_transport_node3 = require("@nats-io/transport-node");
|
|
1334
3164
|
|
|
1335
3165
|
// src/context/rpc.context.ts
|
|
@@ -1339,9 +3169,6 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
|
|
|
1339
3169
|
_retryDelay;
|
|
1340
3170
|
_shouldTerminate = false;
|
|
1341
3171
|
_terminateReason;
|
|
1342
|
-
// ---------------------------------------------------------------------------
|
|
1343
|
-
// Message accessors
|
|
1344
|
-
// ---------------------------------------------------------------------------
|
|
1345
3172
|
/**
|
|
1346
3173
|
* Get the underlying NATS message.
|
|
1347
3174
|
*
|
|
@@ -1376,9 +3203,6 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
|
|
|
1376
3203
|
isJetStream() {
|
|
1377
3204
|
return "ack" in this.args[0];
|
|
1378
3205
|
}
|
|
1379
|
-
// ---------------------------------------------------------------------------
|
|
1380
|
-
// JetStream metadata (return undefined for Core NATS messages)
|
|
1381
|
-
// ---------------------------------------------------------------------------
|
|
1382
3206
|
/** How many times this message has been delivered. */
|
|
1383
3207
|
getDeliveryCount() {
|
|
1384
3208
|
return this.asJetStream()?.info.deliveryCount;
|
|
@@ -1400,9 +3224,6 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
|
|
|
1400
3224
|
getCallerName() {
|
|
1401
3225
|
return this.getHeader("x-caller-name" /* CallerName */);
|
|
1402
3226
|
}
|
|
1403
|
-
// ---------------------------------------------------------------------------
|
|
1404
|
-
// Handler-controlled settlement
|
|
1405
|
-
// ---------------------------------------------------------------------------
|
|
1406
3227
|
/**
|
|
1407
3228
|
* Signal the transport to retry (nak) this message instead of acknowledging it.
|
|
1408
3229
|
*
|
|
@@ -1447,9 +3268,6 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
|
|
|
1447
3268
|
throw new Error(`${method}() is only available for JetStream messages`);
|
|
1448
3269
|
}
|
|
1449
3270
|
}
|
|
1450
|
-
// ---------------------------------------------------------------------------
|
|
1451
|
-
// Transport-facing state (read by EventRouter)
|
|
1452
|
-
// ---------------------------------------------------------------------------
|
|
1453
3271
|
/** @internal */
|
|
1454
3272
|
get shouldRetry() {
|
|
1455
3273
|
return this._shouldRetry;
|
|
@@ -1571,7 +3389,7 @@ var unwrapResult = (result) => {
|
|
|
1571
3389
|
}
|
|
1572
3390
|
return result;
|
|
1573
3391
|
};
|
|
1574
|
-
var
|
|
3392
|
+
var isPromiseLike2 = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
|
|
1575
3393
|
var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
1576
3394
|
let done = false;
|
|
1577
3395
|
let subscription = null;
|
|
@@ -1601,20 +3419,25 @@ var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
|
1601
3419
|
// src/server/core-rpc.server.ts
|
|
1602
3420
|
var CoreRpcServer = class {
|
|
1603
3421
|
constructor(options, connection, patternRegistry, codec, eventBus) {
|
|
1604
|
-
this.options = options;
|
|
1605
3422
|
this.connection = connection;
|
|
1606
3423
|
this.patternRegistry = patternRegistry;
|
|
1607
3424
|
this.codec = codec;
|
|
1608
3425
|
this.eventBus = eventBus;
|
|
3426
|
+
const derived = deriveOtelAttrs(options);
|
|
3427
|
+
this.otel = derived.otel;
|
|
3428
|
+
this.serviceName = derived.serviceName;
|
|
3429
|
+
this.serverEndpoint = derived.serverEndpoint;
|
|
1609
3430
|
}
|
|
1610
|
-
logger = new
|
|
3431
|
+
logger = new import_common12.Logger("Jetstream:CoreRpc");
|
|
1611
3432
|
subscription = null;
|
|
3433
|
+
otel;
|
|
3434
|
+
serviceName;
|
|
3435
|
+
serverEndpoint;
|
|
1612
3436
|
/** Start listening for RPC requests on the command subject. */
|
|
1613
3437
|
async start() {
|
|
1614
3438
|
const nc = await this.connection.getConnection();
|
|
1615
|
-
const
|
|
1616
|
-
const
|
|
1617
|
-
const queue = `${serviceName}_cmd_queue`;
|
|
3439
|
+
const subject = `${this.serviceName}.cmd.>`;
|
|
3440
|
+
const queue = `${this.serviceName}_cmd_queue`;
|
|
1618
3441
|
this.subscription = nc.subscribe(subject, {
|
|
1619
3442
|
queue,
|
|
1620
3443
|
callback: (err, msg) => {
|
|
@@ -1658,15 +3481,51 @@ var CoreRpcServer = class {
|
|
|
1658
3481
|
return;
|
|
1659
3482
|
}
|
|
1660
3483
|
const ctx = new RpcContext([msg]);
|
|
3484
|
+
const startedAt = performance.now();
|
|
1661
3485
|
try {
|
|
1662
|
-
const raw =
|
|
1663
|
-
|
|
1664
|
-
|
|
3486
|
+
const raw = await withConsumeSpan(
|
|
3487
|
+
{
|
|
3488
|
+
subject: msg.subject,
|
|
3489
|
+
msg,
|
|
3490
|
+
kind: "rpc" /* Rpc */,
|
|
3491
|
+
payloadBytes: msg.data.length,
|
|
3492
|
+
handlerMetadata: { pattern: msg.subject },
|
|
3493
|
+
serviceName: this.serviceName,
|
|
3494
|
+
endpoint: this.serverEndpoint
|
|
3495
|
+
},
|
|
3496
|
+
this.otel,
|
|
3497
|
+
() => {
|
|
3498
|
+
const out = unwrapResult(handler(data, ctx));
|
|
3499
|
+
return isPromiseLike2(out) ? out : out;
|
|
3500
|
+
}
|
|
3501
|
+
);
|
|
3502
|
+
msg.respond(this.codec.encode(raw));
|
|
3503
|
+
this.reportHandlerCompleted(msg, startedAt, "success");
|
|
1665
3504
|
} catch (err) {
|
|
1666
|
-
this.
|
|
3505
|
+
this.eventBus.emit(
|
|
3506
|
+
"error" /* Error */,
|
|
3507
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
3508
|
+
`core-rpc-handler:${msg.subject}`
|
|
3509
|
+
);
|
|
1667
3510
|
this.respondWithError(msg, err);
|
|
3511
|
+
this.reportHandlerCompleted(msg, startedAt, "error");
|
|
1668
3512
|
}
|
|
1669
3513
|
}
|
|
3514
|
+
// See EventRouter.reportHandlerCompleted for the rationale on declared
|
|
3515
|
+
// pattern + per-emit hasHook check.
|
|
3516
|
+
reportHandlerCompleted(msg, startedAt, status) {
|
|
3517
|
+
if (!this.eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
|
|
3518
|
+
const declared = this.patternRegistry.resolveDeclared(msg.subject);
|
|
3519
|
+
const pattern = declared?.pattern ?? msg.subject;
|
|
3520
|
+
const kind = declared?.kind ?? "cmd" /* Command */;
|
|
3521
|
+
this.eventBus.emit(
|
|
3522
|
+
"handlerCompleted" /* HandlerCompleted */,
|
|
3523
|
+
pattern,
|
|
3524
|
+
kind,
|
|
3525
|
+
performance.now() - startedAt,
|
|
3526
|
+
status
|
|
3527
|
+
);
|
|
3528
|
+
}
|
|
1670
3529
|
/** Send an error response back to the caller with x-error header. */
|
|
1671
3530
|
respondWithError(msg, error) {
|
|
1672
3531
|
try {
|
|
@@ -1680,8 +3539,8 @@ var CoreRpcServer = class {
|
|
|
1680
3539
|
};
|
|
1681
3540
|
|
|
1682
3541
|
// src/server/infrastructure/stream.provider.ts
|
|
1683
|
-
var
|
|
1684
|
-
var
|
|
3542
|
+
var import_common14 = require("@nestjs/common");
|
|
3543
|
+
var import_jetstream17 = require("@nats-io/jetstream");
|
|
1685
3544
|
|
|
1686
3545
|
// src/server/infrastructure/nats-error-codes.ts
|
|
1687
3546
|
var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
|
|
@@ -1747,8 +3606,8 @@ var isEqual = (a, b) => {
|
|
|
1747
3606
|
};
|
|
1748
3607
|
|
|
1749
3608
|
// src/server/infrastructure/stream-migration.ts
|
|
1750
|
-
var
|
|
1751
|
-
var
|
|
3609
|
+
var import_common13 = require("@nestjs/common");
|
|
3610
|
+
var import_jetstream16 = require("@nats-io/jetstream");
|
|
1752
3611
|
var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
|
|
1753
3612
|
var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
|
|
1754
3613
|
var SOURCING_POLL_INTERVAL_MS = 100;
|
|
@@ -1756,7 +3615,7 @@ var StreamMigration = class {
|
|
|
1756
3615
|
constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
|
|
1757
3616
|
this.sourcingTimeoutMs = sourcingTimeoutMs;
|
|
1758
3617
|
}
|
|
1759
|
-
logger = new
|
|
3618
|
+
logger = new import_common13.Logger("Jetstream:Stream");
|
|
1760
3619
|
async migrate(jsm, streamName2, newConfig) {
|
|
1761
3620
|
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
1762
3621
|
const startTime = Date.now();
|
|
@@ -1825,7 +3684,7 @@ var StreamMigration = class {
|
|
|
1825
3684
|
this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
|
|
1826
3685
|
await jsm.streams.delete(backupName);
|
|
1827
3686
|
} catch (err) {
|
|
1828
|
-
if (err instanceof
|
|
3687
|
+
if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
1829
3688
|
return;
|
|
1830
3689
|
}
|
|
1831
3690
|
throw err;
|
|
@@ -1838,9 +3697,16 @@ var StreamProvider = class {
|
|
|
1838
3697
|
constructor(options, connection) {
|
|
1839
3698
|
this.options = options;
|
|
1840
3699
|
this.connection = connection;
|
|
3700
|
+
const derived = deriveOtelAttrs(options);
|
|
3701
|
+
this.otel = derived.otel;
|
|
3702
|
+
this.otelServiceName = derived.serviceName;
|
|
3703
|
+
this.otelEndpoint = derived.serverEndpoint;
|
|
1841
3704
|
}
|
|
1842
|
-
logger = new
|
|
3705
|
+
logger = new import_common14.Logger("Jetstream:Stream");
|
|
1843
3706
|
migration = new StreamMigration();
|
|
3707
|
+
otel;
|
|
3708
|
+
otelServiceName;
|
|
3709
|
+
otelEndpoint;
|
|
1844
3710
|
/**
|
|
1845
3711
|
* Ensure all required streams exist with correct configuration.
|
|
1846
3712
|
*
|
|
@@ -1886,32 +3752,56 @@ var StreamProvider = class {
|
|
|
1886
3752
|
/** Ensure a single stream exists, creating or updating as needed. */
|
|
1887
3753
|
async ensureStream(jsm, kind) {
|
|
1888
3754
|
const config = this.buildConfig(kind);
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
3755
|
+
return withProvisioningSpan(
|
|
3756
|
+
this.otel,
|
|
3757
|
+
{
|
|
3758
|
+
serviceName: this.otelServiceName,
|
|
3759
|
+
endpoint: this.otelEndpoint,
|
|
3760
|
+
entity: "stream",
|
|
3761
|
+
name: config.name,
|
|
3762
|
+
action: "ensure"
|
|
3763
|
+
},
|
|
3764
|
+
async () => {
|
|
3765
|
+
this.logger.log(`Ensuring stream: ${config.name}`);
|
|
3766
|
+
try {
|
|
3767
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
3768
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
3769
|
+
} catch (err) {
|
|
3770
|
+
if (err instanceof import_jetstream17.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
3771
|
+
this.logger.log(`Creating stream: ${config.name}`);
|
|
3772
|
+
return await jsm.streams.add(config);
|
|
3773
|
+
}
|
|
3774
|
+
throw err;
|
|
3775
|
+
}
|
|
1897
3776
|
}
|
|
1898
|
-
|
|
1899
|
-
}
|
|
3777
|
+
);
|
|
1900
3778
|
}
|
|
1901
3779
|
/** Ensure a dead-letter queue stream exists, creating or updating as needed. */
|
|
1902
3780
|
async ensureDlqStream(jsm) {
|
|
1903
3781
|
const config = this.buildDlqConfig();
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
3782
|
+
return withProvisioningSpan(
|
|
3783
|
+
this.otel,
|
|
3784
|
+
{
|
|
3785
|
+
serviceName: this.otelServiceName,
|
|
3786
|
+
endpoint: this.otelEndpoint,
|
|
3787
|
+
entity: "stream",
|
|
3788
|
+
name: config.name,
|
|
3789
|
+
action: "ensure"
|
|
3790
|
+
},
|
|
3791
|
+
async () => {
|
|
3792
|
+
this.logger.log(`Ensuring DLQ stream: ${config.name}`);
|
|
3793
|
+
try {
|
|
3794
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
3795
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
3796
|
+
} catch (err) {
|
|
3797
|
+
if (err instanceof import_jetstream17.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
3798
|
+
this.logger.log(`Creating DLQ stream: ${config.name}`);
|
|
3799
|
+
return await jsm.streams.add(config);
|
|
3800
|
+
}
|
|
3801
|
+
throw err;
|
|
3802
|
+
}
|
|
1912
3803
|
}
|
|
1913
|
-
|
|
1914
|
-
}
|
|
3804
|
+
);
|
|
1915
3805
|
}
|
|
1916
3806
|
async handleExistingStream(jsm, currentInfo, config) {
|
|
1917
3807
|
const diff = compareStreamConfig(currentInfo.config, config);
|
|
@@ -1940,7 +3830,18 @@ var StreamProvider = class {
|
|
|
1940
3830
|
}
|
|
1941
3831
|
return currentInfo;
|
|
1942
3832
|
}
|
|
1943
|
-
await
|
|
3833
|
+
await withMigrationSpan(
|
|
3834
|
+
this.otel,
|
|
3835
|
+
{
|
|
3836
|
+
serviceName: this.otelServiceName,
|
|
3837
|
+
endpoint: this.otelEndpoint,
|
|
3838
|
+
stream: config.name,
|
|
3839
|
+
reason: diff.changes.filter((c) => c.mutability === "immutable").map((c) => c.property).join(", ")
|
|
3840
|
+
},
|
|
3841
|
+
async () => {
|
|
3842
|
+
await this.migration.migrate(jsm, config.name, config);
|
|
3843
|
+
}
|
|
3844
|
+
);
|
|
1944
3845
|
return await jsm.streams.info(config.name);
|
|
1945
3846
|
}
|
|
1946
3847
|
buildMutableOnlyConfig(config, currentConfig, diff) {
|
|
@@ -2056,16 +3957,23 @@ var StreamProvider = class {
|
|
|
2056
3957
|
};
|
|
2057
3958
|
|
|
2058
3959
|
// src/server/infrastructure/consumer.provider.ts
|
|
2059
|
-
var
|
|
2060
|
-
var
|
|
3960
|
+
var import_common15 = require("@nestjs/common");
|
|
3961
|
+
var import_jetstream19 = require("@nats-io/jetstream");
|
|
2061
3962
|
var ConsumerProvider = class {
|
|
2062
3963
|
constructor(options, connection, streamProvider, patternRegistry) {
|
|
2063
3964
|
this.options = options;
|
|
2064
3965
|
this.connection = connection;
|
|
2065
3966
|
this.streamProvider = streamProvider;
|
|
2066
3967
|
this.patternRegistry = patternRegistry;
|
|
2067
|
-
|
|
2068
|
-
|
|
3968
|
+
const derived = deriveOtelAttrs(options);
|
|
3969
|
+
this.otel = derived.otel;
|
|
3970
|
+
this.otelServiceName = derived.serviceName;
|
|
3971
|
+
this.otelEndpoint = derived.serverEndpoint;
|
|
3972
|
+
}
|
|
3973
|
+
logger = new import_common15.Logger("Jetstream:Consumer");
|
|
3974
|
+
otel;
|
|
3975
|
+
otelServiceName;
|
|
3976
|
+
otelEndpoint;
|
|
2069
3977
|
/**
|
|
2070
3978
|
* Ensure consumers exist for the specified kinds.
|
|
2071
3979
|
*
|
|
@@ -2095,17 +4003,29 @@ var ConsumerProvider = class {
|
|
|
2095
4003
|
const stream = this.streamProvider.getStreamName(kind);
|
|
2096
4004
|
const config = this.buildConfig(kind);
|
|
2097
4005
|
const name = config.durable_name;
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
4006
|
+
return withProvisioningSpan(
|
|
4007
|
+
this.otel,
|
|
4008
|
+
{
|
|
4009
|
+
serviceName: this.otelServiceName,
|
|
4010
|
+
endpoint: this.otelEndpoint,
|
|
4011
|
+
entity: "consumer",
|
|
4012
|
+
name,
|
|
4013
|
+
action: "ensure"
|
|
4014
|
+
},
|
|
4015
|
+
async () => {
|
|
4016
|
+
this.logger.log(`Ensuring consumer: ${name} on stream: ${stream}`);
|
|
4017
|
+
try {
|
|
4018
|
+
await jsm.consumers.info(stream, name);
|
|
4019
|
+
this.logger.debug(`Consumer exists, updating: ${name}`);
|
|
4020
|
+
return await jsm.consumers.update(stream, name, config);
|
|
4021
|
+
} catch (err) {
|
|
4022
|
+
if (!(err instanceof import_jetstream19.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
4023
|
+
throw err;
|
|
4024
|
+
}
|
|
4025
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
4026
|
+
}
|
|
2106
4027
|
}
|
|
2107
|
-
|
|
2108
|
-
}
|
|
4028
|
+
);
|
|
2109
4029
|
}
|
|
2110
4030
|
/**
|
|
2111
4031
|
* Recover a consumer that disappeared during runtime.
|
|
@@ -2124,16 +4044,28 @@ var ConsumerProvider = class {
|
|
|
2124
4044
|
const stream = this.streamProvider.getStreamName(kind);
|
|
2125
4045
|
const config = this.buildConfig(kind);
|
|
2126
4046
|
const name = config.durable_name;
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
4047
|
+
return withProvisioningSpan(
|
|
4048
|
+
this.otel,
|
|
4049
|
+
{
|
|
4050
|
+
serviceName: this.otelServiceName,
|
|
4051
|
+
endpoint: this.otelEndpoint,
|
|
4052
|
+
entity: "consumer",
|
|
4053
|
+
name,
|
|
4054
|
+
action: "recover"
|
|
4055
|
+
},
|
|
4056
|
+
async () => {
|
|
4057
|
+
this.logger.log(`Recovering consumer: ${name} on stream: ${stream}`);
|
|
4058
|
+
await this.assertNoMigrationInProgress(jsm, stream);
|
|
4059
|
+
try {
|
|
4060
|
+
return await jsm.consumers.info(stream, name);
|
|
4061
|
+
} catch (err) {
|
|
4062
|
+
if (!(err instanceof import_jetstream19.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
4063
|
+
throw err;
|
|
4064
|
+
}
|
|
4065
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
4066
|
+
}
|
|
2134
4067
|
}
|
|
2135
|
-
|
|
2136
|
-
}
|
|
4068
|
+
);
|
|
2137
4069
|
}
|
|
2138
4070
|
/**
|
|
2139
4071
|
* Throw if a migration backup stream exists for this stream.
|
|
@@ -2148,7 +4080,7 @@ var ConsumerProvider = class {
|
|
|
2148
4080
|
`Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
|
|
2149
4081
|
);
|
|
2150
4082
|
} catch (err) {
|
|
2151
|
-
if (err instanceof
|
|
4083
|
+
if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
2152
4084
|
return;
|
|
2153
4085
|
}
|
|
2154
4086
|
throw err;
|
|
@@ -2162,7 +4094,7 @@ var ConsumerProvider = class {
|
|
|
2162
4094
|
try {
|
|
2163
4095
|
return await jsm.consumers.add(stream, config);
|
|
2164
4096
|
} catch (addErr) {
|
|
2165
|
-
if (addErr instanceof
|
|
4097
|
+
if (addErr instanceof import_jetstream19.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
|
|
2166
4098
|
this.logger.debug(`Consumer ${name} created by another pod, using existing`);
|
|
2167
4099
|
return await jsm.consumers.info(stream, name);
|
|
2168
4100
|
}
|
|
@@ -2249,8 +4181,8 @@ var ConsumerProvider = class {
|
|
|
2249
4181
|
};
|
|
2250
4182
|
|
|
2251
4183
|
// src/server/infrastructure/message.provider.ts
|
|
2252
|
-
var
|
|
2253
|
-
var
|
|
4184
|
+
var import_common16 = require("@nestjs/common");
|
|
4185
|
+
var import_jetstream21 = require("@nats-io/jetstream");
|
|
2254
4186
|
var import_rxjs3 = require("rxjs");
|
|
2255
4187
|
var MessageProvider = class {
|
|
2256
4188
|
constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
|
|
@@ -2259,7 +4191,7 @@ var MessageProvider = class {
|
|
|
2259
4191
|
this.consumeOptionsMap = consumeOptionsMap;
|
|
2260
4192
|
this.consumerRecoveryFn = consumerRecoveryFn;
|
|
2261
4193
|
}
|
|
2262
|
-
logger = new
|
|
4194
|
+
logger = new import_common16.Logger("Jetstream:Message");
|
|
2263
4195
|
activeIterators = /* @__PURE__ */ new Set();
|
|
2264
4196
|
orderedReadyResolve = null;
|
|
2265
4197
|
orderedReadyReject = null;
|
|
@@ -2311,7 +4243,7 @@ var MessageProvider = class {
|
|
|
2311
4243
|
*/
|
|
2312
4244
|
async startOrdered(streamName2, filterSubjects, orderedConfig) {
|
|
2313
4245
|
const consumerOpts = { filter_subjects: filterSubjects };
|
|
2314
|
-
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !==
|
|
4246
|
+
if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream21.DeliverPolicy.All) {
|
|
2315
4247
|
consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
|
|
2316
4248
|
}
|
|
2317
4249
|
if (orderedConfig?.optStartSeq !== void 0) {
|
|
@@ -2357,10 +4289,13 @@ var MessageProvider = class {
|
|
|
2357
4289
|
/** Create a self-healing consumer flow for a specific kind. */
|
|
2358
4290
|
createFlow(kind, info) {
|
|
2359
4291
|
const target$ = this.getTargetSubject(kind);
|
|
2360
|
-
return this.createSelfHealingFlow(
|
|
4292
|
+
return this.createSelfHealingFlow(
|
|
4293
|
+
(onConnected) => this.consumeOnce(kind, info, target$, onConnected),
|
|
4294
|
+
info.name
|
|
4295
|
+
);
|
|
2361
4296
|
}
|
|
2362
4297
|
/** Single iteration: get consumer -> pull messages -> emit to subject. */
|
|
2363
|
-
async consumeOnce(kind, info, target
|
|
4298
|
+
async consumeOnce(kind, info, target$, onConnected) {
|
|
2364
4299
|
const js = this.connection.getJetStreamClient();
|
|
2365
4300
|
let consumer;
|
|
2366
4301
|
let consumerName2 = info.name;
|
|
@@ -2388,6 +4323,7 @@ var MessageProvider = class {
|
|
|
2388
4323
|
});
|
|
2389
4324
|
this.activeIterators.add(messages);
|
|
2390
4325
|
this.monitorConsumerHealth(messages, consumerName2);
|
|
4326
|
+
onConnected();
|
|
2391
4327
|
try {
|
|
2392
4328
|
await messages.closed();
|
|
2393
4329
|
} finally {
|
|
@@ -2442,7 +4378,7 @@ var MessageProvider = class {
|
|
|
2442
4378
|
/** Create a self-healing ordered consumer flow. */
|
|
2443
4379
|
createOrderedFlow(streamName2, consumerOpts) {
|
|
2444
4380
|
return this.createSelfHealingFlow(
|
|
2445
|
-
() => this.consumeOrderedOnce(streamName2, consumerOpts),
|
|
4381
|
+
(onConnected) => this.consumeOrderedOnce(streamName2, consumerOpts, onConnected),
|
|
2446
4382
|
"ordered" /* Ordered */,
|
|
2447
4383
|
(err) => {
|
|
2448
4384
|
if (this.orderedReadyReject) {
|
|
@@ -2456,10 +4392,15 @@ var MessageProvider = class {
|
|
|
2456
4392
|
/** Shared self-healing flow: defer -> retry with exponential backoff on error/completion. */
|
|
2457
4393
|
createSelfHealingFlow(source, label, onFirstError) {
|
|
2458
4394
|
let consecutiveFailures = 0;
|
|
2459
|
-
|
|
2460
|
-
(0
|
|
2461
|
-
|
|
2462
|
-
|
|
4395
|
+
const onConnected = () => {
|
|
4396
|
+
if (consecutiveFailures > 0) {
|
|
4397
|
+
const attempts = consecutiveFailures;
|
|
4398
|
+
this.logger.log(`Consumer ${label} recovered after ${attempts} failed attempt(s)`);
|
|
4399
|
+
this.eventBus.emit("consumerRecovered" /* ConsumerRecovered */, label, attempts);
|
|
4400
|
+
}
|
|
4401
|
+
consecutiveFailures = 0;
|
|
4402
|
+
};
|
|
4403
|
+
return (0, import_rxjs3.defer)(() => source(onConnected)).pipe(
|
|
2463
4404
|
(0, import_rxjs3.catchError)((err) => {
|
|
2464
4405
|
consecutiveFailures++;
|
|
2465
4406
|
this.logger.error(`Consumer ${label} error, will restart:`, err);
|
|
@@ -2482,7 +4423,7 @@ var MessageProvider = class {
|
|
|
2482
4423
|
);
|
|
2483
4424
|
}
|
|
2484
4425
|
/** Single iteration: create ordered consumer -> push messages into the subject. */
|
|
2485
|
-
async consumeOrderedOnce(streamName2, consumerOpts) {
|
|
4426
|
+
async consumeOrderedOnce(streamName2, consumerOpts, onConnected) {
|
|
2486
4427
|
const js = this.connection.getJetStreamClient();
|
|
2487
4428
|
const consumer = await js.consumers.get(streamName2, consumerOpts);
|
|
2488
4429
|
const orderedMessages$ = this.orderedMessages$;
|
|
@@ -2497,6 +4438,7 @@ var MessageProvider = class {
|
|
|
2497
4438
|
this.orderedReadyReject = null;
|
|
2498
4439
|
}
|
|
2499
4440
|
this.activeIterators.add(messages);
|
|
4441
|
+
onConnected();
|
|
2500
4442
|
try {
|
|
2501
4443
|
await messages.closed();
|
|
2502
4444
|
} finally {
|
|
@@ -2506,7 +4448,7 @@ var MessageProvider = class {
|
|
|
2506
4448
|
};
|
|
2507
4449
|
|
|
2508
4450
|
// src/server/infrastructure/metadata.provider.ts
|
|
2509
|
-
var
|
|
4451
|
+
var import_common17 = require("@nestjs/common");
|
|
2510
4452
|
var import_kv = require("@nats-io/kv");
|
|
2511
4453
|
var MetadataProvider = class {
|
|
2512
4454
|
constructor(options, connection) {
|
|
@@ -2515,7 +4457,7 @@ var MetadataProvider = class {
|
|
|
2515
4457
|
this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
|
|
2516
4458
|
this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
|
|
2517
4459
|
}
|
|
2518
|
-
logger = new
|
|
4460
|
+
logger = new import_common17.Logger("Jetstream:Metadata");
|
|
2519
4461
|
bucketName;
|
|
2520
4462
|
replicas;
|
|
2521
4463
|
ttl;
|
|
@@ -2607,177 +4549,14 @@ var MetadataProvider = class {
|
|
|
2607
4549
|
}
|
|
2608
4550
|
};
|
|
2609
4551
|
|
|
2610
|
-
// src/server/routing/pattern-registry.ts
|
|
2611
|
-
var import_common10 = require("@nestjs/common");
|
|
2612
|
-
var HANDLER_LABELS = {
|
|
2613
|
-
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
2614
|
-
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
2615
|
-
["ev" /* Event */]: "event" /* Event */,
|
|
2616
|
-
["cmd" /* Command */]: "rpc" /* Rpc */
|
|
2617
|
-
};
|
|
2618
|
-
var PatternRegistry = class {
|
|
2619
|
-
constructor(options) {
|
|
2620
|
-
this.options = options;
|
|
2621
|
-
}
|
|
2622
|
-
logger = new import_common10.Logger("Jetstream:PatternRegistry");
|
|
2623
|
-
registry = /* @__PURE__ */ new Map();
|
|
2624
|
-
// Cached after registerHandlers() — the registry is immutable from that point
|
|
2625
|
-
cachedPatterns = null;
|
|
2626
|
-
_hasEvents = false;
|
|
2627
|
-
_hasCommands = false;
|
|
2628
|
-
_hasBroadcasts = false;
|
|
2629
|
-
_hasOrdered = false;
|
|
2630
|
-
_hasMetadata = false;
|
|
2631
|
-
/**
|
|
2632
|
-
* Register all handlers from the NestJS strategy.
|
|
2633
|
-
*
|
|
2634
|
-
* @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
|
|
2635
|
-
*/
|
|
2636
|
-
registerHandlers(handlers) {
|
|
2637
|
-
const serviceName = this.options.name;
|
|
2638
|
-
for (const [pattern, handler] of handlers) {
|
|
2639
|
-
const extras = handler.extras;
|
|
2640
|
-
const isEvent = handler.isEventHandler ?? false;
|
|
2641
|
-
const isBroadcast = !!extras?.broadcast;
|
|
2642
|
-
const isOrdered = !!extras?.ordered;
|
|
2643
|
-
const meta = extras?.meta;
|
|
2644
|
-
if (isBroadcast && isOrdered) {
|
|
2645
|
-
throw new Error(
|
|
2646
|
-
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
2647
|
-
);
|
|
2648
|
-
}
|
|
2649
|
-
let kind;
|
|
2650
|
-
if (isBroadcast) kind = "broadcast" /* Broadcast */;
|
|
2651
|
-
else if (isOrdered) kind = "ordered" /* Ordered */;
|
|
2652
|
-
else if (isEvent) kind = "ev" /* Event */;
|
|
2653
|
-
else kind = "cmd" /* Command */;
|
|
2654
|
-
const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
|
|
2655
|
-
this.registry.set(fullSubject, {
|
|
2656
|
-
handler,
|
|
2657
|
-
pattern,
|
|
2658
|
-
isEvent: isEvent && !isOrdered,
|
|
2659
|
-
isBroadcast,
|
|
2660
|
-
isOrdered,
|
|
2661
|
-
meta
|
|
2662
|
-
});
|
|
2663
|
-
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
2664
|
-
}
|
|
2665
|
-
this.cachedPatterns = this.buildPatternsByKind();
|
|
2666
|
-
this._hasEvents = this.cachedPatterns.events.length > 0;
|
|
2667
|
-
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
2668
|
-
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
2669
|
-
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
2670
|
-
this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
|
|
2671
|
-
this.logSummary();
|
|
2672
|
-
}
|
|
2673
|
-
/** Find handler for a full NATS subject. */
|
|
2674
|
-
getHandler(subject) {
|
|
2675
|
-
return this.registry.get(subject)?.handler ?? null;
|
|
2676
|
-
}
|
|
2677
|
-
/** Get all registered broadcast patterns (for consumer filter_subject setup). */
|
|
2678
|
-
getBroadcastPatterns() {
|
|
2679
|
-
return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
|
|
2680
|
-
}
|
|
2681
|
-
hasBroadcastHandlers() {
|
|
2682
|
-
return this._hasBroadcasts;
|
|
2683
|
-
}
|
|
2684
|
-
hasRpcHandlers() {
|
|
2685
|
-
return this._hasCommands;
|
|
2686
|
-
}
|
|
2687
|
-
hasEventHandlers() {
|
|
2688
|
-
return this._hasEvents;
|
|
2689
|
-
}
|
|
2690
|
-
hasOrderedHandlers() {
|
|
2691
|
-
return this._hasOrdered;
|
|
2692
|
-
}
|
|
2693
|
-
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
2694
|
-
getOrderedSubjects() {
|
|
2695
|
-
return this.getPatternsByKind().ordered.map(
|
|
2696
|
-
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
2697
|
-
);
|
|
2698
|
-
}
|
|
2699
|
-
/** Check if any registered handler has metadata. */
|
|
2700
|
-
hasMetadata() {
|
|
2701
|
-
return this._hasMetadata;
|
|
2702
|
-
}
|
|
2703
|
-
/**
|
|
2704
|
-
* Get handler metadata entries for KV publishing.
|
|
2705
|
-
*
|
|
2706
|
-
* Returns a map of KV key -> metadata object for all handlers that have `meta`.
|
|
2707
|
-
* Key format: `{serviceName}.{kind}.{pattern}`.
|
|
2708
|
-
*/
|
|
2709
|
-
getMetadataEntries() {
|
|
2710
|
-
const entries = /* @__PURE__ */ new Map();
|
|
2711
|
-
for (const entry of this.registry.values()) {
|
|
2712
|
-
if (!entry.meta) continue;
|
|
2713
|
-
const kind = this.resolveStreamKind(entry);
|
|
2714
|
-
const key = metadataKey(this.options.name, kind, entry.pattern);
|
|
2715
|
-
entries.set(key, entry.meta);
|
|
2716
|
-
}
|
|
2717
|
-
return entries;
|
|
2718
|
-
}
|
|
2719
|
-
/** Get patterns grouped by kind (cached after registration). */
|
|
2720
|
-
getPatternsByKind() {
|
|
2721
|
-
const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
|
|
2722
|
-
return {
|
|
2723
|
-
events: [...patterns.events],
|
|
2724
|
-
commands: [...patterns.commands],
|
|
2725
|
-
broadcasts: [...patterns.broadcasts],
|
|
2726
|
-
ordered: [...patterns.ordered]
|
|
2727
|
-
};
|
|
2728
|
-
}
|
|
2729
|
-
/** Normalize a full NATS subject back to the user-facing pattern. */
|
|
2730
|
-
normalizeSubject(subject) {
|
|
2731
|
-
const name = internalName(this.options.name);
|
|
2732
|
-
const prefixes = [
|
|
2733
|
-
`${name}.${"cmd" /* Command */}.`,
|
|
2734
|
-
`${name}.${"ev" /* Event */}.`,
|
|
2735
|
-
`${name}.${"ordered" /* Ordered */}.`,
|
|
2736
|
-
`${"broadcast" /* Broadcast */}.`
|
|
2737
|
-
];
|
|
2738
|
-
for (const prefix of prefixes) {
|
|
2739
|
-
if (subject.startsWith(prefix)) {
|
|
2740
|
-
return subject.slice(prefix.length);
|
|
2741
|
-
}
|
|
2742
|
-
}
|
|
2743
|
-
return subject;
|
|
2744
|
-
}
|
|
2745
|
-
buildPatternsByKind() {
|
|
2746
|
-
const events = [];
|
|
2747
|
-
const commands = [];
|
|
2748
|
-
const broadcasts = [];
|
|
2749
|
-
const ordered = [];
|
|
2750
|
-
for (const entry of this.registry.values()) {
|
|
2751
|
-
if (entry.isBroadcast) broadcasts.push(entry.pattern);
|
|
2752
|
-
else if (entry.isOrdered) ordered.push(entry.pattern);
|
|
2753
|
-
else if (entry.isEvent) events.push(entry.pattern);
|
|
2754
|
-
else commands.push(entry.pattern);
|
|
2755
|
-
}
|
|
2756
|
-
return { events, commands, broadcasts, ordered };
|
|
2757
|
-
}
|
|
2758
|
-
resolveStreamKind(entry) {
|
|
2759
|
-
if (entry.isBroadcast) return "broadcast" /* Broadcast */;
|
|
2760
|
-
if (entry.isOrdered) return "ordered" /* Ordered */;
|
|
2761
|
-
if (entry.isEvent) return "ev" /* Event */;
|
|
2762
|
-
return "cmd" /* Command */;
|
|
2763
|
-
}
|
|
2764
|
-
logSummary() {
|
|
2765
|
-
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
2766
|
-
const parts = [
|
|
2767
|
-
`${commands.length} RPC`,
|
|
2768
|
-
`${events.length} events`,
|
|
2769
|
-
`${broadcasts.length} broadcasts`
|
|
2770
|
-
];
|
|
2771
|
-
if (ordered.length > 0) {
|
|
2772
|
-
parts.push(`${ordered.length} ordered`);
|
|
2773
|
-
}
|
|
2774
|
-
this.logger.log(`Registered handlers: ${parts.join(", ")}`);
|
|
2775
|
-
}
|
|
2776
|
-
};
|
|
2777
|
-
|
|
2778
4552
|
// src/server/routing/event.router.ts
|
|
2779
|
-
var
|
|
4553
|
+
var import_common18 = require("@nestjs/common");
|
|
2780
4554
|
var import_transport_node4 = require("@nats-io/transport-node");
|
|
4555
|
+
var eventConsumeKindFor = (kind) => {
|
|
4556
|
+
if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
|
|
4557
|
+
if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
|
|
4558
|
+
return "event" /* Event */;
|
|
4559
|
+
};
|
|
2781
4560
|
var EventRouter = class {
|
|
2782
4561
|
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
|
|
2783
4562
|
this.messageProvider = messageProvider;
|
|
@@ -2789,9 +4568,22 @@ var EventRouter = class {
|
|
|
2789
4568
|
this.ackWaitMap = ackWaitMap;
|
|
2790
4569
|
this.connection = connection;
|
|
2791
4570
|
this.options = options;
|
|
4571
|
+
if (options) {
|
|
4572
|
+
const derived = deriveOtelAttrs(options);
|
|
4573
|
+
this.otel = derived.otel;
|
|
4574
|
+
this.serviceName = derived.serviceName;
|
|
4575
|
+
this.serverEndpoint = derived.serverEndpoint;
|
|
4576
|
+
} else {
|
|
4577
|
+
this.otel = resolveOtelOptions({ enabled: false });
|
|
4578
|
+
this.serviceName = "";
|
|
4579
|
+
this.serverEndpoint = null;
|
|
4580
|
+
}
|
|
2792
4581
|
}
|
|
2793
|
-
logger = new
|
|
4582
|
+
logger = new import_common18.Logger("Jetstream:EventRouter");
|
|
2794
4583
|
subscriptions = [];
|
|
4584
|
+
otel;
|
|
4585
|
+
serviceName;
|
|
4586
|
+
serverEndpoint;
|
|
2795
4587
|
/**
|
|
2796
4588
|
* Update the max_deliver thresholds from actual NATS consumer configs.
|
|
2797
4589
|
* Called after consumers are ensured so the DLQ map reflects reality.
|
|
@@ -2821,13 +4613,24 @@ var EventRouter = class {
|
|
|
2821
4613
|
const patternRegistry = this.patternRegistry;
|
|
2822
4614
|
const codec = this.codec;
|
|
2823
4615
|
const eventBus = this.eventBus;
|
|
2824
|
-
const
|
|
4616
|
+
const logger5 = this.logger;
|
|
2825
4617
|
const deadLetterConfig = this.deadLetterConfig;
|
|
4618
|
+
const otel = this.otel;
|
|
4619
|
+
const serviceName = this.serviceName;
|
|
4620
|
+
const serverEndpoint = this.serverEndpoint;
|
|
4621
|
+
const spanKind = eventConsumeKindFor(kind);
|
|
2826
4622
|
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
2827
4623
|
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
2828
4624
|
const concurrency = this.getConcurrency(kind);
|
|
2829
4625
|
const hasDlqCheck = deadLetterConfig !== void 0;
|
|
2830
|
-
const
|
|
4626
|
+
const reportHandlerCompleted = (msg, startedAt, status) => {
|
|
4627
|
+
if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
|
|
4628
|
+
const declared = patternRegistry.resolveDeclared(msg.subject);
|
|
4629
|
+
const pattern = declared?.pattern ?? msg.subject;
|
|
4630
|
+
const declaredKind = declared?.kind ?? kind;
|
|
4631
|
+
const durationMs = performance.now() - startedAt;
|
|
4632
|
+
eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
|
|
4633
|
+
};
|
|
2831
4634
|
const isDeadLetter = (msg) => {
|
|
2832
4635
|
if (!hasDlqCheck) return false;
|
|
2833
4636
|
const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
|
|
@@ -2853,7 +4656,7 @@ var EventRouter = class {
|
|
|
2853
4656
|
const handler = patternRegistry.getHandler(subject);
|
|
2854
4657
|
if (!handler) {
|
|
2855
4658
|
msg.term(`No handler for event: ${subject}`);
|
|
2856
|
-
|
|
4659
|
+
logger5.error(`No handler for subject: ${subject}`);
|
|
2857
4660
|
return null;
|
|
2858
4661
|
}
|
|
2859
4662
|
let data;
|
|
@@ -2861,47 +4664,79 @@ var EventRouter = class {
|
|
|
2861
4664
|
data = codec.decode(msg.data);
|
|
2862
4665
|
} catch (err) {
|
|
2863
4666
|
msg.term("Decode error");
|
|
2864
|
-
|
|
4667
|
+
logger5.error(`Decode error for ${subject}:`, err);
|
|
2865
4668
|
return null;
|
|
2866
4669
|
}
|
|
2867
|
-
|
|
4670
|
+
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
2868
4671
|
return { handler, data };
|
|
2869
4672
|
} catch (err) {
|
|
2870
|
-
|
|
4673
|
+
logger5.error(`Unexpected error in ${kind} event router`, err);
|
|
2871
4674
|
try {
|
|
2872
4675
|
msg.term("Unexpected router error");
|
|
2873
4676
|
} catch (termErr) {
|
|
2874
|
-
|
|
4677
|
+
logger5.error(`Failed to terminate message ${subject}:`, termErr);
|
|
2875
4678
|
}
|
|
2876
4679
|
return null;
|
|
2877
4680
|
}
|
|
2878
4681
|
};
|
|
4682
|
+
const statusForContext = (ctx) => {
|
|
4683
|
+
if (ctx.shouldTerminate) return "terminated";
|
|
4684
|
+
if (ctx.shouldRetry) return "retried";
|
|
4685
|
+
return "success";
|
|
4686
|
+
};
|
|
2879
4687
|
const handleSafe = (msg) => {
|
|
2880
4688
|
const resolved = resolveEvent(msg);
|
|
2881
4689
|
if (resolved === null) return void 0;
|
|
2882
4690
|
const { handler, data } = resolved;
|
|
2883
4691
|
const ctx = new RpcContext([msg]);
|
|
2884
4692
|
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
4693
|
+
const startedAt = performance.now();
|
|
2885
4694
|
let pending;
|
|
2886
4695
|
try {
|
|
2887
|
-
pending =
|
|
4696
|
+
pending = withConsumeSpan(
|
|
4697
|
+
{
|
|
4698
|
+
subject: msg.subject,
|
|
4699
|
+
msg,
|
|
4700
|
+
info: msg.info,
|
|
4701
|
+
kind: spanKind,
|
|
4702
|
+
payloadBytes: msg.data.length,
|
|
4703
|
+
handlerMetadata: { pattern: msg.subject },
|
|
4704
|
+
serviceName,
|
|
4705
|
+
endpoint: serverEndpoint
|
|
4706
|
+
},
|
|
4707
|
+
otel,
|
|
4708
|
+
() => unwrapResult(handler(data, ctx))
|
|
4709
|
+
);
|
|
2888
4710
|
} catch (err) {
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
4711
|
+
eventBus.emit(
|
|
4712
|
+
"error" /* Error */,
|
|
4713
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
4714
|
+
`${kind}-handler:${msg.subject}`
|
|
4715
|
+
);
|
|
4716
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
4717
|
+
return settleFailure(msg, data, err).finally(() => {
|
|
4718
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
4719
|
+
});
|
|
2892
4720
|
}
|
|
2893
|
-
if (!
|
|
4721
|
+
if (!isPromiseLike2(pending)) {
|
|
2894
4722
|
settleSuccess(msg, ctx);
|
|
4723
|
+
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
2895
4724
|
if (stopAckExtension !== null) stopAckExtension();
|
|
2896
4725
|
return void 0;
|
|
2897
4726
|
}
|
|
2898
4727
|
return pending.then(
|
|
2899
4728
|
() => {
|
|
2900
4729
|
settleSuccess(msg, ctx);
|
|
4730
|
+
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
2901
4731
|
if (stopAckExtension !== null) stopAckExtension();
|
|
2902
4732
|
},
|
|
2903
4733
|
async (err) => {
|
|
2904
|
-
|
|
4734
|
+
eventBus.emit(
|
|
4735
|
+
"error" /* Error */,
|
|
4736
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
4737
|
+
`${kind}-handler:${msg.subject}`
|
|
4738
|
+
);
|
|
4739
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
2905
4740
|
try {
|
|
2906
4741
|
await settleFailure(msg, data, err);
|
|
2907
4742
|
} finally {
|
|
@@ -2917,42 +4752,65 @@ var EventRouter = class {
|
|
|
2917
4752
|
try {
|
|
2918
4753
|
handler = patternRegistry.getHandler(subject);
|
|
2919
4754
|
if (!handler) {
|
|
2920
|
-
|
|
4755
|
+
logger5.error(`No handler for subject: ${subject}`);
|
|
2921
4756
|
return void 0;
|
|
2922
4757
|
}
|
|
2923
4758
|
try {
|
|
2924
4759
|
data = codec.decode(msg.data);
|
|
2925
4760
|
} catch (err) {
|
|
2926
|
-
|
|
4761
|
+
logger5.error(`Decode error for ${subject}:`, err);
|
|
2927
4762
|
return void 0;
|
|
2928
4763
|
}
|
|
2929
|
-
|
|
4764
|
+
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
2930
4765
|
} catch (err) {
|
|
2931
|
-
|
|
4766
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
2932
4767
|
return void 0;
|
|
2933
4768
|
}
|
|
2934
4769
|
const ctx = new RpcContext([msg]);
|
|
2935
4770
|
const warnIfSettlementAttempted = () => {
|
|
2936
4771
|
if (ctx.shouldRetry || ctx.shouldTerminate) {
|
|
2937
|
-
|
|
4772
|
+
logger5.warn(
|
|
2938
4773
|
`retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
|
|
2939
4774
|
);
|
|
2940
4775
|
}
|
|
2941
4776
|
};
|
|
4777
|
+
const startedAt = performance.now();
|
|
2942
4778
|
let pending;
|
|
2943
4779
|
try {
|
|
2944
|
-
pending =
|
|
4780
|
+
pending = withConsumeSpan(
|
|
4781
|
+
{
|
|
4782
|
+
subject: msg.subject,
|
|
4783
|
+
msg,
|
|
4784
|
+
info: msg.info,
|
|
4785
|
+
kind: spanKind,
|
|
4786
|
+
payloadBytes: msg.data.length,
|
|
4787
|
+
handlerMetadata: { pattern: msg.subject },
|
|
4788
|
+
serviceName,
|
|
4789
|
+
endpoint: serverEndpoint
|
|
4790
|
+
},
|
|
4791
|
+
otel,
|
|
4792
|
+
() => unwrapResult(handler(data, ctx))
|
|
4793
|
+
);
|
|
2945
4794
|
} catch (err) {
|
|
2946
|
-
|
|
4795
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
4796
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
2947
4797
|
return void 0;
|
|
2948
4798
|
}
|
|
2949
|
-
if (!
|
|
4799
|
+
if (!isPromiseLike2(pending)) {
|
|
2950
4800
|
warnIfSettlementAttempted();
|
|
4801
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
2951
4802
|
return void 0;
|
|
2952
4803
|
}
|
|
2953
|
-
return pending.then(
|
|
2954
|
-
|
|
2955
|
-
|
|
4804
|
+
return pending.then(
|
|
4805
|
+
() => {
|
|
4806
|
+
warnIfSettlementAttempted();
|
|
4807
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
4808
|
+
},
|
|
4809
|
+
(err) => {
|
|
4810
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
4811
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
4812
|
+
}
|
|
4813
|
+
);
|
|
2956
4814
|
};
|
|
2957
4815
|
const route = isOrdered ? handleOrderedSafe : handleSafe;
|
|
2958
4816
|
const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
|
|
@@ -2984,7 +4842,7 @@ var EventRouter = class {
|
|
|
2984
4842
|
backlog.push(msg);
|
|
2985
4843
|
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
2986
4844
|
backlogWarned = true;
|
|
2987
|
-
|
|
4845
|
+
logger5.warn(
|
|
2988
4846
|
`${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
2989
4847
|
);
|
|
2990
4848
|
}
|
|
@@ -3000,7 +4858,7 @@ var EventRouter = class {
|
|
|
3000
4858
|
}
|
|
3001
4859
|
},
|
|
3002
4860
|
error: (err) => {
|
|
3003
|
-
|
|
4861
|
+
logger5.error(`Stream error in ${kind} router`, err);
|
|
3004
4862
|
}
|
|
3005
4863
|
});
|
|
3006
4864
|
this.subscriptions.push(subscription);
|
|
@@ -3015,14 +4873,11 @@ var EventRouter = class {
|
|
|
3015
4873
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
3016
4874
|
return void 0;
|
|
3017
4875
|
}
|
|
3018
|
-
/** Handle a dead letter: invoke callback, then term or nak based on result. */
|
|
3019
4876
|
/**
|
|
3020
|
-
*
|
|
3021
|
-
*
|
|
3022
|
-
*
|
|
3023
|
-
*
|
|
3024
|
-
* On success, terminates the message. On error, leaves it unacknowledged (nak)
|
|
3025
|
-
* so NATS can retry the delivery on the next cycle.
|
|
4877
|
+
* Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
|
|
4878
|
+
* success or `nak` on hook failure so NATS retries on the next delivery
|
|
4879
|
+
* cycle. Used when DLQ stream isn't configured, or when publishing to it
|
|
4880
|
+
* failed and we still have to surface the message somewhere observable.
|
|
3026
4881
|
*/
|
|
3027
4882
|
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
3028
4883
|
if (!this.deadLetterConfig) {
|
|
@@ -3114,20 +4969,37 @@ var EventRouter = class {
|
|
|
3114
4969
|
streamSequence: msg.info.streamSequence,
|
|
3115
4970
|
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
3116
4971
|
};
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
4972
|
+
await withDeadLetterSpan(
|
|
4973
|
+
{
|
|
4974
|
+
msg,
|
|
4975
|
+
// Pattern resolution mirrors event-routing: when a registered
|
|
4976
|
+
// pattern matches, surface it on the DLQ span so APM can filter
|
|
4977
|
+
// dead letters by handler without parsing the subject. Falls back
|
|
4978
|
+
// to the subject itself when no glob handler is in play.
|
|
4979
|
+
pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
|
|
4980
|
+
finalDeliveryCount: msg.info.deliveryCount,
|
|
4981
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
4982
|
+
serviceName: this.serviceName,
|
|
4983
|
+
endpoint: this.serverEndpoint
|
|
4984
|
+
},
|
|
4985
|
+
this.otel,
|
|
4986
|
+
async () => {
|
|
4987
|
+
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
4988
|
+
if (!this.options?.dlq) {
|
|
4989
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
4990
|
+
} else {
|
|
4991
|
+
await this.publishToDlq(msg, info, error);
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
);
|
|
3123
4995
|
}
|
|
3124
4996
|
};
|
|
3125
4997
|
|
|
3126
4998
|
// src/server/routing/rpc.router.ts
|
|
3127
|
-
var
|
|
4999
|
+
var import_common19 = require("@nestjs/common");
|
|
3128
5000
|
var import_transport_node5 = require("@nats-io/transport-node");
|
|
3129
5001
|
var RpcRouter = class {
|
|
3130
|
-
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
|
|
5002
|
+
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
|
|
3131
5003
|
this.messageProvider = messageProvider;
|
|
3132
5004
|
this.patternRegistry = patternRegistry;
|
|
3133
5005
|
this.connection = connection;
|
|
@@ -3137,13 +5009,26 @@ var RpcRouter = class {
|
|
|
3137
5009
|
this.ackWaitMap = ackWaitMap;
|
|
3138
5010
|
this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
|
|
3139
5011
|
this.concurrency = rpcOptions?.concurrency;
|
|
5012
|
+
if (options) {
|
|
5013
|
+
const derived = deriveOtelAttrs(options);
|
|
5014
|
+
this.otel = derived.otel;
|
|
5015
|
+
this.serviceName = derived.serviceName;
|
|
5016
|
+
this.serverEndpoint = derived.serverEndpoint;
|
|
5017
|
+
} else {
|
|
5018
|
+
this.otel = resolveOtelOptions({ enabled: false });
|
|
5019
|
+
this.serviceName = "";
|
|
5020
|
+
this.serverEndpoint = null;
|
|
5021
|
+
}
|
|
3140
5022
|
}
|
|
3141
|
-
logger = new
|
|
5023
|
+
logger = new import_common19.Logger("Jetstream:RpcRouter");
|
|
3142
5024
|
timeout;
|
|
3143
5025
|
concurrency;
|
|
3144
5026
|
resolvedAckExtensionInterval;
|
|
3145
5027
|
subscription = null;
|
|
3146
5028
|
cachedNc = null;
|
|
5029
|
+
otel;
|
|
5030
|
+
serviceName;
|
|
5031
|
+
serverEndpoint;
|
|
3147
5032
|
/** Lazily resolve the ack extension interval (needs ackWaitMap populated at runtime). */
|
|
3148
5033
|
get ackExtensionInterval() {
|
|
3149
5034
|
if (this.resolvedAckExtensionInterval !== void 0) return this.resolvedAckExtensionInterval;
|
|
@@ -3160,21 +5045,37 @@ var RpcRouter = class {
|
|
|
3160
5045
|
const patternRegistry = this.patternRegistry;
|
|
3161
5046
|
const codec = this.codec;
|
|
3162
5047
|
const eventBus = this.eventBus;
|
|
3163
|
-
const
|
|
5048
|
+
const logger5 = this.logger;
|
|
3164
5049
|
const timeout = this.timeout;
|
|
3165
5050
|
const ackExtensionInterval = this.ackExtensionInterval;
|
|
3166
5051
|
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
3167
5052
|
const maxActive = this.concurrency ?? Number.POSITIVE_INFINITY;
|
|
5053
|
+
const otel = this.otel;
|
|
5054
|
+
const serviceName = this.serviceName;
|
|
5055
|
+
const serverEndpoint = this.serverEndpoint;
|
|
3168
5056
|
const emitRpcTimeout = (subject, correlationId) => {
|
|
3169
5057
|
eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
|
|
3170
5058
|
};
|
|
5059
|
+
const reportHandlerCompleted = (msg, startedAt, status) => {
|
|
5060
|
+
if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
|
|
5061
|
+
const declared = patternRegistry.resolveDeclared(msg.subject);
|
|
5062
|
+
const pattern = declared?.pattern ?? msg.subject;
|
|
5063
|
+
const declaredKind = declared?.kind ?? "cmd" /* Command */;
|
|
5064
|
+
eventBus.emit(
|
|
5065
|
+
"handlerCompleted" /* HandlerCompleted */,
|
|
5066
|
+
pattern,
|
|
5067
|
+
declaredKind,
|
|
5068
|
+
performance.now() - startedAt,
|
|
5069
|
+
status
|
|
5070
|
+
);
|
|
5071
|
+
};
|
|
3171
5072
|
const publishReply = (replyTo, correlationId, payload) => {
|
|
3172
5073
|
try {
|
|
3173
5074
|
const hdrs = (0, import_transport_node5.headers)();
|
|
3174
5075
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
3175
5076
|
nc.publish(replyTo, codec.encode(payload), { headers: hdrs });
|
|
3176
5077
|
} catch (publishErr) {
|
|
3177
|
-
|
|
5078
|
+
logger5.error(`Failed to publish RPC response`, publishErr);
|
|
3178
5079
|
}
|
|
3179
5080
|
};
|
|
3180
5081
|
const publishErrorReply = (replyTo, correlationId, subject, err) => {
|
|
@@ -3184,7 +5085,7 @@ var RpcRouter = class {
|
|
|
3184
5085
|
hdrs.set("x-error" /* Error */, "true");
|
|
3185
5086
|
nc.publish(replyTo, codec.encode(serializeError(err)), { headers: hdrs });
|
|
3186
5087
|
} catch (encodeErr) {
|
|
3187
|
-
|
|
5088
|
+
logger5.error(`Failed to encode RPC error for ${subject}`, encodeErr);
|
|
3188
5089
|
}
|
|
3189
5090
|
};
|
|
3190
5091
|
const resolveCommand = (msg) => {
|
|
@@ -3193,7 +5094,7 @@ var RpcRouter = class {
|
|
|
3193
5094
|
const handler = patternRegistry.getHandler(subject);
|
|
3194
5095
|
if (!handler) {
|
|
3195
5096
|
msg.term(`No handler for RPC: ${subject}`);
|
|
3196
|
-
|
|
5097
|
+
logger5.error(`No handler for RPC subject: ${subject}`);
|
|
3197
5098
|
return null;
|
|
3198
5099
|
}
|
|
3199
5100
|
const msgHeaders = msg.headers;
|
|
@@ -3201,7 +5102,7 @@ var RpcRouter = class {
|
|
|
3201
5102
|
const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
|
|
3202
5103
|
if (!replyTo || !correlationId) {
|
|
3203
5104
|
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
3204
|
-
|
|
5105
|
+
logger5.error(`Missing headers for RPC: ${subject}`);
|
|
3205
5106
|
return null;
|
|
3206
5107
|
}
|
|
3207
5108
|
let data;
|
|
@@ -3209,17 +5110,17 @@ var RpcRouter = class {
|
|
|
3209
5110
|
data = codec.decode(msg.data);
|
|
3210
5111
|
} catch (err) {
|
|
3211
5112
|
msg.term("Decode error");
|
|
3212
|
-
|
|
5113
|
+
logger5.error(`Decode error for RPC ${subject}:`, err);
|
|
3213
5114
|
return null;
|
|
3214
5115
|
}
|
|
3215
5116
|
eventBus.emitMessageRouted(subject, "rpc" /* Rpc */);
|
|
3216
5117
|
return { handler, data, replyTo, correlationId };
|
|
3217
5118
|
} catch (err) {
|
|
3218
|
-
|
|
5119
|
+
logger5.error("Unexpected error in RPC router", err);
|
|
3219
5120
|
try {
|
|
3220
5121
|
msg.term("Unexpected router error");
|
|
3221
5122
|
} catch (termErr) {
|
|
3222
|
-
|
|
5123
|
+
logger5.error(`Failed to terminate RPC message ${subject}:`, termErr);
|
|
3223
5124
|
}
|
|
3224
5125
|
return null;
|
|
3225
5126
|
}
|
|
@@ -3231,20 +5132,45 @@ var RpcRouter = class {
|
|
|
3231
5132
|
const subject = msg.subject;
|
|
3232
5133
|
const ctx = new RpcContext([msg]);
|
|
3233
5134
|
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5135
|
+
const startedAt = performance.now();
|
|
5136
|
+
const reportHandlerError = (err) => {
|
|
5137
|
+
eventBus.emit(
|
|
5138
|
+
"error" /* Error */,
|
|
5139
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
5140
|
+
`rpc-handler:${subject}`
|
|
5141
|
+
);
|
|
5142
|
+
publishErrorReply(replyTo, correlationId, subject, err);
|
|
5143
|
+
msg.term(`Handler error: ${subject}`);
|
|
5144
|
+
};
|
|
5145
|
+
const abortController = new AbortController();
|
|
3234
5146
|
let pending;
|
|
3235
5147
|
try {
|
|
3236
|
-
pending =
|
|
5148
|
+
pending = withConsumeSpan(
|
|
5149
|
+
{
|
|
5150
|
+
subject,
|
|
5151
|
+
msg,
|
|
5152
|
+
info: msg.info,
|
|
5153
|
+
kind: "rpc" /* Rpc */,
|
|
5154
|
+
payloadBytes: msg.data.length,
|
|
5155
|
+
handlerMetadata: { pattern: subject },
|
|
5156
|
+
serviceName,
|
|
5157
|
+
endpoint: serverEndpoint
|
|
5158
|
+
},
|
|
5159
|
+
otel,
|
|
5160
|
+
() => unwrapResult(handler(data, ctx)),
|
|
5161
|
+
{ signal: abortController.signal, timeoutLabel: "rpc.handler.timeout" }
|
|
5162
|
+
);
|
|
3237
5163
|
} catch (err) {
|
|
3238
5164
|
if (stopAckExtension !== null) stopAckExtension();
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
msg.term(`Handler error: ${subject}`);
|
|
5165
|
+
reportHandlerError(err);
|
|
5166
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
3242
5167
|
return void 0;
|
|
3243
5168
|
}
|
|
3244
|
-
if (!
|
|
5169
|
+
if (!isPromiseLike2(pending)) {
|
|
3245
5170
|
if (stopAckExtension !== null) stopAckExtension();
|
|
3246
5171
|
msg.ack();
|
|
3247
5172
|
publishReply(replyTo, correlationId, pending);
|
|
5173
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
3248
5174
|
return void 0;
|
|
3249
5175
|
}
|
|
3250
5176
|
let settled = false;
|
|
@@ -3252,9 +5178,10 @@ var RpcRouter = class {
|
|
|
3252
5178
|
if (settled) return;
|
|
3253
5179
|
settled = true;
|
|
3254
5180
|
if (stopAckExtension !== null) stopAckExtension();
|
|
3255
|
-
|
|
5181
|
+
abortController.abort();
|
|
3256
5182
|
emitRpcTimeout(subject, correlationId);
|
|
3257
5183
|
msg.term("Handler timeout");
|
|
5184
|
+
reportHandlerCompleted(msg, startedAt, "terminated");
|
|
3258
5185
|
}, timeout);
|
|
3259
5186
|
return pending.then(
|
|
3260
5187
|
(result) => {
|
|
@@ -3264,15 +5191,15 @@ var RpcRouter = class {
|
|
|
3264
5191
|
if (stopAckExtension !== null) stopAckExtension();
|
|
3265
5192
|
msg.ack();
|
|
3266
5193
|
publishReply(replyTo, correlationId, result);
|
|
5194
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
3267
5195
|
},
|
|
3268
5196
|
(err) => {
|
|
3269
5197
|
if (settled) return;
|
|
3270
5198
|
settled = true;
|
|
3271
5199
|
clearTimeout(timeoutId);
|
|
3272
5200
|
if (stopAckExtension !== null) stopAckExtension();
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
msg.term(`Handler error: ${subject}`);
|
|
5201
|
+
reportHandlerError(err);
|
|
5202
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
3276
5203
|
}
|
|
3277
5204
|
);
|
|
3278
5205
|
};
|
|
@@ -3304,7 +5231,7 @@ var RpcRouter = class {
|
|
|
3304
5231
|
backlog.push(msg);
|
|
3305
5232
|
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
3306
5233
|
backlogWarned = true;
|
|
3307
|
-
|
|
5234
|
+
logger5.warn(
|
|
3308
5235
|
`RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
3309
5236
|
);
|
|
3310
5237
|
}
|
|
@@ -3320,7 +5247,7 @@ var RpcRouter = class {
|
|
|
3320
5247
|
}
|
|
3321
5248
|
},
|
|
3322
5249
|
error: (err) => {
|
|
3323
|
-
|
|
5250
|
+
logger5.error("Stream error in RPC router", err);
|
|
3324
5251
|
}
|
|
3325
5252
|
});
|
|
3326
5253
|
}
|
|
@@ -3332,14 +5259,14 @@ var RpcRouter = class {
|
|
|
3332
5259
|
};
|
|
3333
5260
|
|
|
3334
5261
|
// src/shutdown/shutdown.manager.ts
|
|
3335
|
-
var
|
|
5262
|
+
var import_common20 = require("@nestjs/common");
|
|
3336
5263
|
var ShutdownManager = class {
|
|
3337
5264
|
constructor(connection, eventBus, timeout) {
|
|
3338
5265
|
this.connection = connection;
|
|
3339
5266
|
this.eventBus = eventBus;
|
|
3340
5267
|
this.timeout = timeout;
|
|
3341
5268
|
}
|
|
3342
|
-
logger = new
|
|
5269
|
+
logger = new import_common20.Logger("Jetstream:Shutdown");
|
|
3343
5270
|
shutdownPromise;
|
|
3344
5271
|
/**
|
|
3345
5272
|
* Execute the full shutdown sequence.
|
|
@@ -3379,9 +5306,6 @@ var JetstreamModule = class {
|
|
|
3379
5306
|
this.shutdownManager = shutdownManager;
|
|
3380
5307
|
this.strategy = strategy;
|
|
3381
5308
|
}
|
|
3382
|
-
// -------------------------------------------------------------------
|
|
3383
|
-
// forRoot — global module registration
|
|
3384
|
-
// -------------------------------------------------------------------
|
|
3385
5309
|
/**
|
|
3386
5310
|
* Register the JetStream transport globally.
|
|
3387
5311
|
*
|
|
@@ -3396,21 +5320,20 @@ var JetstreamModule = class {
|
|
|
3396
5320
|
return {
|
|
3397
5321
|
module: JetstreamModule,
|
|
3398
5322
|
global: true,
|
|
5323
|
+
imports: options.metrics ? [JetstreamMetricsModule.forFeature(options.metrics)] : [],
|
|
3399
5324
|
providers,
|
|
3400
5325
|
exports: [
|
|
3401
5326
|
JETSTREAM_CONNECTION,
|
|
3402
5327
|
JETSTREAM_CODEC,
|
|
3403
5328
|
JETSTREAM_EVENT_BUS,
|
|
3404
5329
|
JETSTREAM_OPTIONS,
|
|
5330
|
+
PatternRegistry,
|
|
3405
5331
|
ShutdownManager,
|
|
3406
5332
|
JetstreamStrategy,
|
|
3407
5333
|
JetstreamHealthIndicator
|
|
3408
5334
|
]
|
|
3409
5335
|
};
|
|
3410
5336
|
}
|
|
3411
|
-
// -------------------------------------------------------------------
|
|
3412
|
-
// forRootAsync — async global module registration
|
|
3413
|
-
// -------------------------------------------------------------------
|
|
3414
5337
|
/**
|
|
3415
5338
|
* Register the JetStream transport globally with async configuration.
|
|
3416
5339
|
*
|
|
@@ -3423,25 +5346,24 @@ var JetstreamModule = class {
|
|
|
3423
5346
|
static forRootAsync(asyncOptions) {
|
|
3424
5347
|
const asyncProviders = this.createAsyncOptionsProvider(asyncOptions);
|
|
3425
5348
|
const coreProviders = this.createCoreDependentProviders();
|
|
5349
|
+
const metricsImports = asyncOptions.metrics ? [JetstreamMetricsModule.forFeature(asyncOptions.metrics)] : [];
|
|
3426
5350
|
return {
|
|
3427
5351
|
module: JetstreamModule,
|
|
3428
5352
|
global: true,
|
|
3429
|
-
imports: asyncOptions.imports ?? [],
|
|
5353
|
+
imports: [...asyncOptions.imports ?? [], ...metricsImports],
|
|
3430
5354
|
providers: [...asyncProviders, ...coreProviders],
|
|
3431
5355
|
exports: [
|
|
3432
5356
|
JETSTREAM_CONNECTION,
|
|
3433
5357
|
JETSTREAM_CODEC,
|
|
3434
5358
|
JETSTREAM_EVENT_BUS,
|
|
3435
5359
|
JETSTREAM_OPTIONS,
|
|
5360
|
+
PatternRegistry,
|
|
3436
5361
|
ShutdownManager,
|
|
3437
5362
|
JetstreamStrategy,
|
|
3438
5363
|
JetstreamHealthIndicator
|
|
3439
5364
|
]
|
|
3440
5365
|
};
|
|
3441
5366
|
}
|
|
3442
|
-
// -------------------------------------------------------------------
|
|
3443
|
-
// forFeature — per-module client registration
|
|
3444
|
-
// -------------------------------------------------------------------
|
|
3445
5367
|
/**
|
|
3446
5368
|
* Register a lightweight client proxy for a target service.
|
|
3447
5369
|
*
|
|
@@ -3467,9 +5389,6 @@ var JetstreamModule = class {
|
|
|
3467
5389
|
exports: [clientToken]
|
|
3468
5390
|
};
|
|
3469
5391
|
}
|
|
3470
|
-
// -------------------------------------------------------------------
|
|
3471
|
-
// Provider factories
|
|
3472
|
-
// -------------------------------------------------------------------
|
|
3473
5392
|
static createCoreProviders(options) {
|
|
3474
5393
|
return [
|
|
3475
5394
|
{
|
|
@@ -3487,8 +5406,8 @@ var JetstreamModule = class {
|
|
|
3487
5406
|
provide: JETSTREAM_EVENT_BUS,
|
|
3488
5407
|
inject: [JETSTREAM_OPTIONS],
|
|
3489
5408
|
useFactory: (options) => {
|
|
3490
|
-
const
|
|
3491
|
-
return new EventBus(
|
|
5409
|
+
const logger5 = new import_common21.Logger("Jetstream:Module");
|
|
5410
|
+
return new EventBus(logger5, options.hooks);
|
|
3492
5411
|
}
|
|
3493
5412
|
},
|
|
3494
5413
|
// Codec — global encode/decode
|
|
@@ -3527,10 +5446,8 @@ var JetstreamModule = class {
|
|
|
3527
5446
|
);
|
|
3528
5447
|
}
|
|
3529
5448
|
},
|
|
3530
|
-
// ---------------------------------------------------------------
|
|
3531
5449
|
// Consumer infrastructure — only created when consumer !== false.
|
|
3532
5450
|
// Providers return null when consumer is disabled (publisher-only mode).
|
|
3533
|
-
// ---------------------------------------------------------------
|
|
3534
5451
|
// PatternRegistry — subject-to-handler mapping
|
|
3535
5452
|
{
|
|
3536
5453
|
provide: PatternRegistry,
|
|
@@ -3566,8 +5483,14 @@ var JetstreamModule = class {
|
|
|
3566
5483
|
// MessageProvider — pull-based message consumption
|
|
3567
5484
|
{
|
|
3568
5485
|
provide: MessageProvider,
|
|
3569
|
-
inject: [
|
|
3570
|
-
|
|
5486
|
+
inject: [
|
|
5487
|
+
JETSTREAM_OPTIONS,
|
|
5488
|
+
JETSTREAM_CONNECTION,
|
|
5489
|
+
JETSTREAM_EVENT_BUS,
|
|
5490
|
+
ConsumerProvider,
|
|
5491
|
+
StreamProvider
|
|
5492
|
+
],
|
|
5493
|
+
useFactory: (options, connection, eventBus, consumerProvider, streamProvider) => {
|
|
3571
5494
|
if (options.consumer === false) return null;
|
|
3572
5495
|
const consumeOptionsMap = /* @__PURE__ */ new Map();
|
|
3573
5496
|
if (options.events?.consume)
|
|
@@ -3577,10 +5500,22 @@ var JetstreamModule = class {
|
|
|
3577
5500
|
if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
|
|
3578
5501
|
consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
|
|
3579
5502
|
}
|
|
3580
|
-
const
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
5503
|
+
const derived = deriveOtelAttrs(options);
|
|
5504
|
+
const { otel, serverEndpoint: otelEndpoint, serviceName: otelServiceName } = derived;
|
|
5505
|
+
const consumerRecoveryFn = consumerProvider && streamProvider ? async (kind) => withSelfHealingSpan(
|
|
5506
|
+
otel,
|
|
5507
|
+
{
|
|
5508
|
+
serviceName: otelServiceName,
|
|
5509
|
+
endpoint: otelEndpoint,
|
|
5510
|
+
consumer: consumerProvider.getConsumerName(kind),
|
|
5511
|
+
stream: streamProvider.getStreamName(kind),
|
|
5512
|
+
reason: "consumer not found"
|
|
5513
|
+
},
|
|
5514
|
+
async () => {
|
|
5515
|
+
const jsm = await connection.getJetStreamManager();
|
|
5516
|
+
return consumerProvider.recoverConsumer(jsm, kind);
|
|
5517
|
+
}
|
|
5518
|
+
) : void 0;
|
|
3584
5519
|
return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
|
|
3585
5520
|
}
|
|
3586
5521
|
},
|
|
@@ -3651,7 +5586,8 @@ var JetstreamModule = class {
|
|
|
3651
5586
|
codec,
|
|
3652
5587
|
eventBus,
|
|
3653
5588
|
rpcOptions,
|
|
3654
|
-
ackWaitMap
|
|
5589
|
+
ackWaitMap,
|
|
5590
|
+
options
|
|
3655
5591
|
);
|
|
3656
5592
|
}
|
|
3657
5593
|
},
|
|
@@ -3754,9 +5690,6 @@ var JetstreamModule = class {
|
|
|
3754
5690
|
}
|
|
3755
5691
|
];
|
|
3756
5692
|
}
|
|
3757
|
-
// -------------------------------------------------------------------
|
|
3758
|
-
// Lifecycle hooks
|
|
3759
|
-
// -------------------------------------------------------------------
|
|
3760
5693
|
/**
|
|
3761
5694
|
* Gracefully shut down the transport on application termination.
|
|
3762
5695
|
*/
|
|
@@ -3767,15 +5700,16 @@ var JetstreamModule = class {
|
|
|
3767
5700
|
}
|
|
3768
5701
|
};
|
|
3769
5702
|
JetstreamModule = __decorateClass([
|
|
3770
|
-
(0,
|
|
3771
|
-
(0,
|
|
3772
|
-
__decorateParam(0, (0,
|
|
3773
|
-
__decorateParam(0, (0,
|
|
3774
|
-
__decorateParam(1, (0,
|
|
3775
|
-
__decorateParam(1, (0,
|
|
5703
|
+
(0, import_common21.Global)(),
|
|
5704
|
+
(0, import_common21.Module)({}),
|
|
5705
|
+
__decorateParam(0, (0, import_common21.Optional)()),
|
|
5706
|
+
__decorateParam(0, (0, import_common21.Inject)(ShutdownManager)),
|
|
5707
|
+
__decorateParam(1, (0, import_common21.Optional)()),
|
|
5708
|
+
__decorateParam(1, (0, import_common21.Inject)(JetstreamStrategy))
|
|
3776
5709
|
], JetstreamModule);
|
|
3777
5710
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3778
5711
|
0 && (module.exports = {
|
|
5712
|
+
ConsumeKind,
|
|
3779
5713
|
DEFAULT_BROADCAST_CONSUMER_CONFIG,
|
|
3780
5714
|
DEFAULT_BROADCAST_STREAM_CONFIG,
|
|
3781
5715
|
DEFAULT_COMMAND_CONSUMER_CONFIG,
|
|
@@ -3791,6 +5725,7 @@ JetstreamModule = __decorateClass([
|
|
|
3791
5725
|
DEFAULT_ORDERED_STREAM_CONFIG,
|
|
3792
5726
|
DEFAULT_RPC_TIMEOUT,
|
|
3793
5727
|
DEFAULT_SHUTDOWN_TIMEOUT,
|
|
5728
|
+
DEFAULT_TRACES,
|
|
3794
5729
|
JETSTREAM_CODEC,
|
|
3795
5730
|
JETSTREAM_CONNECTION,
|
|
3796
5731
|
JETSTREAM_OPTIONS,
|
|
@@ -3802,15 +5737,18 @@ JetstreamModule = __decorateClass([
|
|
|
3802
5737
|
JetstreamRecord,
|
|
3803
5738
|
JetstreamRecordBuilder,
|
|
3804
5739
|
JetstreamStrategy,
|
|
5740
|
+
JetstreamTrace,
|
|
3805
5741
|
JsonCodec,
|
|
3806
5742
|
MIN_METADATA_TTL,
|
|
3807
5743
|
MessageKind,
|
|
3808
5744
|
MsgpackCodec,
|
|
3809
5745
|
NatsErrorCode,
|
|
3810
5746
|
PatternPrefix,
|
|
5747
|
+
PublishKind,
|
|
3811
5748
|
RESERVED_HEADERS,
|
|
3812
5749
|
RpcContext,
|
|
3813
5750
|
StreamKind,
|
|
5751
|
+
TRACER_NAME,
|
|
3814
5752
|
TransportEvent,
|
|
3815
5753
|
buildBroadcastSubject,
|
|
3816
5754
|
buildSubject,
|