@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.js
CHANGED
|
@@ -13,20 +13,22 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
|
|
|
13
13
|
// src/jetstream.module.ts
|
|
14
14
|
import {
|
|
15
15
|
Global,
|
|
16
|
-
Inject,
|
|
17
|
-
Logger as
|
|
18
|
-
Module,
|
|
19
|
-
Optional
|
|
16
|
+
Inject as Inject2,
|
|
17
|
+
Logger as Logger20,
|
|
18
|
+
Module as Module2,
|
|
19
|
+
Optional as Optional2
|
|
20
20
|
} from "@nestjs/common";
|
|
21
21
|
|
|
22
22
|
// src/client/jetstream.client.ts
|
|
23
|
-
import { Logger } from "@nestjs/common";
|
|
23
|
+
import { Logger as Logger5 } from "@nestjs/common";
|
|
24
24
|
import { ClientProxy } from "@nestjs/microservices";
|
|
25
|
+
import { context as context6 } from "@opentelemetry/api";
|
|
26
|
+
import { nuid } from "@nats-io/nuid";
|
|
25
27
|
import {
|
|
26
28
|
createInbox,
|
|
27
|
-
headers as natsHeaders
|
|
29
|
+
headers as natsHeaders,
|
|
30
|
+
TimeoutError
|
|
28
31
|
} from "@nats-io/transport-node";
|
|
29
|
-
import { nuid } from "@nats-io/nuid";
|
|
30
32
|
|
|
31
33
|
// src/interfaces/hooks.interface.ts
|
|
32
34
|
var MessageKind = /* @__PURE__ */ ((MessageKind2) => {
|
|
@@ -44,6 +46,10 @@ var TransportEvent = /* @__PURE__ */ ((TransportEvent2) => {
|
|
|
44
46
|
TransportEvent2["ShutdownStart"] = "shutdownStart";
|
|
45
47
|
TransportEvent2["ShutdownComplete"] = "shutdownComplete";
|
|
46
48
|
TransportEvent2["DeadLetter"] = "deadLetter";
|
|
49
|
+
TransportEvent2["ConsumerRecovered"] = "consumerRecovered";
|
|
50
|
+
TransportEvent2["HandlerCompleted"] = "handlerCompleted";
|
|
51
|
+
TransportEvent2["Published"] = "published";
|
|
52
|
+
TransportEvent2["RpcCompleted"] = "rpcCompleted";
|
|
47
53
|
return TransportEvent2;
|
|
48
54
|
})(TransportEvent || {});
|
|
49
55
|
|
|
@@ -224,6 +230,920 @@ var PatternPrefix = /* @__PURE__ */ ((PatternPrefix2) => {
|
|
|
224
230
|
var isJetStreamRpcMode = (rpc) => rpc?.mode === "jetstream";
|
|
225
231
|
var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
|
|
226
232
|
|
|
233
|
+
// src/otel/constants.ts
|
|
234
|
+
var TRACER_NAME = "@horizon-republic/nestjs-jetstream";
|
|
235
|
+
|
|
236
|
+
// src/otel/attribute-keys.ts
|
|
237
|
+
var ATTR_MESSAGING_SYSTEM = "messaging.system";
|
|
238
|
+
var ATTR_MESSAGING_DESTINATION_NAME = "messaging.destination.name";
|
|
239
|
+
var ATTR_MESSAGING_DESTINATION_TEMPLATE = "messaging.destination.template";
|
|
240
|
+
var ATTR_MESSAGING_CLIENT_ID = "messaging.client.id";
|
|
241
|
+
var ATTR_MESSAGING_OPERATION_NAME = "messaging.operation.name";
|
|
242
|
+
var ATTR_MESSAGING_OPERATION_TYPE = "messaging.operation.type";
|
|
243
|
+
var ATTR_MESSAGING_MESSAGE_BODY_SIZE = "messaging.message.body.size";
|
|
244
|
+
var ATTR_MESSAGING_MESSAGE_ID = "messaging.message.id";
|
|
245
|
+
var ATTR_MESSAGING_MESSAGE_CONVERSATION_ID = "messaging.message.conversation_id";
|
|
246
|
+
var ATTR_MESSAGING_CONSUMER_GROUP_NAME = "messaging.consumer.group.name";
|
|
247
|
+
var ATTR_MESSAGING_HEADER_PREFIX = "messaging.header.";
|
|
248
|
+
var ATTR_MESSAGING_NATS_STREAM_NAME = "messaging.nats.stream.name";
|
|
249
|
+
var ATTR_MESSAGING_NATS_STREAM_SEQUENCE = "messaging.nats.message.stream_sequence";
|
|
250
|
+
var ATTR_MESSAGING_NATS_CONSUMER_SEQUENCE = "messaging.nats.message.consumer_sequence";
|
|
251
|
+
var ATTR_MESSAGING_NATS_DELIVERY_COUNT = "messaging.nats.message.delivery_count";
|
|
252
|
+
var ATTR_MESSAGING_NATS_BODY = "messaging.nats.message.body";
|
|
253
|
+
var ATTR_MESSAGING_NATS_BODY_TRUNCATED = "messaging.nats.message.body.truncated";
|
|
254
|
+
var ATTR_SERVER_ADDRESS = "server.address";
|
|
255
|
+
var ATTR_SERVER_PORT = "server.port";
|
|
256
|
+
var ATTR_JETSTREAM_SERVICE_NAME = "jetstream.service.name";
|
|
257
|
+
var ATTR_JETSTREAM_KIND = "jetstream.kind";
|
|
258
|
+
var ATTR_JETSTREAM_RPC_REPLY_HAS_ERROR = "jetstream.rpc.reply.has_error";
|
|
259
|
+
var ATTR_JETSTREAM_RPC_REPLY_ERROR_CODE = "jetstream.rpc.reply.error.code";
|
|
260
|
+
var ATTR_JETSTREAM_PROVISIONING_ENTITY = "jetstream.provisioning.entity";
|
|
261
|
+
var ATTR_JETSTREAM_PROVISIONING_ACTION = "jetstream.provisioning.action";
|
|
262
|
+
var ATTR_JETSTREAM_PROVISIONING_NAME = "jetstream.provisioning.name";
|
|
263
|
+
var ATTR_JETSTREAM_SELF_HEALING_REASON = "jetstream.self_healing.reason";
|
|
264
|
+
var ATTR_JETSTREAM_MIGRATION_REASON = "jetstream.migration.reason";
|
|
265
|
+
var ATTR_JETSTREAM_DEAD_LETTER_REASON = "jetstream.dead_letter.reason";
|
|
266
|
+
var ATTR_JETSTREAM_SCHEDULE_TARGET = "jetstream.schedule.target";
|
|
267
|
+
var ATTR_NATS_CONNECTION_SERVER = "nats.connection.server";
|
|
268
|
+
var NATS_MSG_ID_HEADER = "Nats-Msg-Id";
|
|
269
|
+
var HOOK_PUBLISH = "publishHook";
|
|
270
|
+
var HOOK_CONSUME = "consumeHook";
|
|
271
|
+
var HOOK_RESPONSE = "responseHook";
|
|
272
|
+
var SPAN_NAME_PUBLISH = "publish";
|
|
273
|
+
var SPAN_NAME_PROCESS = "process";
|
|
274
|
+
var SPAN_NAME_SEND = "send";
|
|
275
|
+
var SPAN_NAME_DEAD_LETTER = "dead_letter";
|
|
276
|
+
var SPAN_NAME_NATS_CONNECTION = "nats.connection";
|
|
277
|
+
var SPAN_NAME_JETSTREAM_SHUTDOWN = "jetstream.shutdown";
|
|
278
|
+
var SPAN_NAME_JETSTREAM_SELF_HEALING = "jetstream.self_healing";
|
|
279
|
+
var SPAN_NAME_JETSTREAM_MIGRATION = "jetstream.migration";
|
|
280
|
+
var SPAN_NAME_JETSTREAM_PROVISIONING_PREFIX = "jetstream.provisioning.";
|
|
281
|
+
var EVENT_CONNECTION_DISCONNECTED = "connection.disconnected";
|
|
282
|
+
var EVENT_CONNECTION_RECONNECTED = "connection.reconnected";
|
|
283
|
+
var messagingHeaderAttr = (headerName) => `${ATTR_MESSAGING_HEADER_PREFIX}${headerName.toLowerCase()}`;
|
|
284
|
+
|
|
285
|
+
// src/otel/trace-kinds.ts
|
|
286
|
+
var JetstreamTrace = /* @__PURE__ */ ((JetstreamTrace2) => {
|
|
287
|
+
JetstreamTrace2["Publish"] = "publish";
|
|
288
|
+
JetstreamTrace2["Consume"] = "consume";
|
|
289
|
+
JetstreamTrace2["RpcClientSend"] = "rpc.client.send";
|
|
290
|
+
JetstreamTrace2["DeadLetter"] = "dead_letter";
|
|
291
|
+
JetstreamTrace2["ConnectionLifecycle"] = "connection.lifecycle";
|
|
292
|
+
JetstreamTrace2["SelfHealing"] = "self_healing";
|
|
293
|
+
JetstreamTrace2["Provisioning"] = "provisioning";
|
|
294
|
+
JetstreamTrace2["Migration"] = "migration";
|
|
295
|
+
JetstreamTrace2["Shutdown"] = "shutdown";
|
|
296
|
+
return JetstreamTrace2;
|
|
297
|
+
})(JetstreamTrace || {});
|
|
298
|
+
var DEFAULT_TRACES = [
|
|
299
|
+
"publish" /* Publish */,
|
|
300
|
+
"consume" /* Consume */,
|
|
301
|
+
"rpc.client.send" /* RpcClientSend */,
|
|
302
|
+
"dead_letter" /* DeadLetter */
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
// src/otel/capture.ts
|
|
306
|
+
var NEGATION_PREFIX = "!";
|
|
307
|
+
var REGEX_SPECIAL_RE = /[.+?^${}()|[\]\\*]/gu;
|
|
308
|
+
var escapeForRegex = (input) => input.replace(REGEX_SPECIAL_RE, "\\$&");
|
|
309
|
+
var globToRegex = (glob) => {
|
|
310
|
+
const escaped = escapeForRegex(glob.toLowerCase()).replace(/\\\*/gu, ".*");
|
|
311
|
+
return new RegExp(`^${escaped}$`, "u");
|
|
312
|
+
};
|
|
313
|
+
var compileHeaderAllowlist = (allowlist) => {
|
|
314
|
+
if (allowlist === true) return () => true;
|
|
315
|
+
if (allowlist === false) return () => false;
|
|
316
|
+
if (allowlist.length === 0) return () => false;
|
|
317
|
+
const includes = [];
|
|
318
|
+
const excludes = [];
|
|
319
|
+
for (const pattern of allowlist) {
|
|
320
|
+
const isExclude = pattern.startsWith(NEGATION_PREFIX);
|
|
321
|
+
const body = isExclude ? pattern.slice(NEGATION_PREFIX.length) : pattern;
|
|
322
|
+
const regex = globToRegex(body);
|
|
323
|
+
if (isExclude) excludes.push(regex);
|
|
324
|
+
else includes.push(regex);
|
|
325
|
+
}
|
|
326
|
+
return (name) => {
|
|
327
|
+
const lower = name.toLowerCase();
|
|
328
|
+
if (!includes.some((re) => re.test(lower))) return false;
|
|
329
|
+
if (excludes.some((re) => re.test(lower))) return false;
|
|
330
|
+
return true;
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
var subjectMatcherCache = /* @__PURE__ */ new WeakMap();
|
|
334
|
+
var compileSubjectAllowlist = (allowlist) => {
|
|
335
|
+
if (!allowlist || allowlist.length === 0) return () => true;
|
|
336
|
+
const cached = subjectMatcherCache.get(allowlist);
|
|
337
|
+
if (cached) return cached;
|
|
338
|
+
const regexes = allowlist.map(globToRegex);
|
|
339
|
+
const matcher = (subject) => {
|
|
340
|
+
const lower = subject.toLowerCase();
|
|
341
|
+
return regexes.some((re) => re.test(lower));
|
|
342
|
+
};
|
|
343
|
+
subjectMatcherCache.set(allowlist, matcher);
|
|
344
|
+
return matcher;
|
|
345
|
+
};
|
|
346
|
+
var subjectMatchesAllowlist = (subject, allowlist) => compileSubjectAllowlist(allowlist)(subject);
|
|
347
|
+
var HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
348
|
+
"traceparent",
|
|
349
|
+
"tracestate",
|
|
350
|
+
"baggage",
|
|
351
|
+
"sentry-trace",
|
|
352
|
+
"b3",
|
|
353
|
+
"x-b3-traceid",
|
|
354
|
+
"x-b3-spanid",
|
|
355
|
+
"x-b3-parentspanid",
|
|
356
|
+
"x-b3-sampled",
|
|
357
|
+
"x-b3-flags",
|
|
358
|
+
"uber-trace-id",
|
|
359
|
+
"x-correlation-id",
|
|
360
|
+
"x-reply-to",
|
|
361
|
+
"x-error",
|
|
362
|
+
"x-subject",
|
|
363
|
+
"x-caller-name"
|
|
364
|
+
]);
|
|
365
|
+
var isNatsServerHeader = (lower) => lower.startsWith("nats-");
|
|
366
|
+
var captureMatchingHeaders = (headers2, matcher) => {
|
|
367
|
+
if (!headers2) return {};
|
|
368
|
+
const out = {};
|
|
369
|
+
for (const key of headers2.keys()) {
|
|
370
|
+
const lower = key.toLowerCase();
|
|
371
|
+
if (HEADER_DENYLIST.has(lower)) continue;
|
|
372
|
+
if (isNatsServerHeader(lower)) continue;
|
|
373
|
+
if (!matcher(key)) continue;
|
|
374
|
+
const value = headers2.get(key);
|
|
375
|
+
if (value === "") continue;
|
|
376
|
+
out[messagingHeaderAttr(lower)] = value;
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
};
|
|
380
|
+
var tryUtf8Decode = (bytes) => {
|
|
381
|
+
try {
|
|
382
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
383
|
+
} catch {
|
|
384
|
+
return Buffer.from(bytes).toString("base64");
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
var captureBodyAttribute = (subject, payload, capture) => {
|
|
388
|
+
if (capture === false) return {};
|
|
389
|
+
if (payload.byteLength === 0) return {};
|
|
390
|
+
if (!subjectMatchesAllowlist(subject, capture.subjectAllowlist)) return {};
|
|
391
|
+
const { maxBytes } = capture;
|
|
392
|
+
const truncated = payload.byteLength > maxBytes;
|
|
393
|
+
const slice = truncated ? payload.subarray(0, maxBytes) : payload;
|
|
394
|
+
const decoded = tryUtf8Decode(slice);
|
|
395
|
+
const out = { [ATTR_MESSAGING_NATS_BODY]: decoded };
|
|
396
|
+
if (truncated) out[ATTR_MESSAGING_NATS_BODY_TRUNCATED] = true;
|
|
397
|
+
return out;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// src/otel/config.ts
|
|
401
|
+
var PublishKind = /* @__PURE__ */ ((PublishKind2) => {
|
|
402
|
+
PublishKind2["Event"] = "event";
|
|
403
|
+
PublishKind2["RpcRequest"] = "rpc.request";
|
|
404
|
+
PublishKind2["Broadcast"] = "broadcast";
|
|
405
|
+
PublishKind2["Ordered"] = "ordered";
|
|
406
|
+
return PublishKind2;
|
|
407
|
+
})(PublishKind || {});
|
|
408
|
+
var ConsumeKind = /* @__PURE__ */ ((ConsumeKind2) => {
|
|
409
|
+
ConsumeKind2["Event"] = "event";
|
|
410
|
+
ConsumeKind2["Rpc"] = "rpc";
|
|
411
|
+
ConsumeKind2["Broadcast"] = "broadcast";
|
|
412
|
+
ConsumeKind2["Ordered"] = "ordered";
|
|
413
|
+
return ConsumeKind2;
|
|
414
|
+
})(ConsumeKind || {});
|
|
415
|
+
var DEFAULT_CAPTURE_HEADERS = ["x-request-id"];
|
|
416
|
+
var DEFAULT_CAPTURE_BODY_MAX_BYTES = 4096;
|
|
417
|
+
var NESTJS_BARE_ERROR_MESSAGE = "Internal server error";
|
|
418
|
+
var isNestjsBareErrorSentinel = (obj) => obj.status === "error" && obj.message === NESTJS_BARE_ERROR_MESSAGE;
|
|
419
|
+
var defaultErrorClassifier = (err) => {
|
|
420
|
+
if (err === null || typeof err !== "object") return "unexpected";
|
|
421
|
+
if (!(err instanceof Error)) {
|
|
422
|
+
if (isNestjsBareErrorSentinel(err)) return "unexpected";
|
|
423
|
+
return "expected";
|
|
424
|
+
}
|
|
425
|
+
let proto = err;
|
|
426
|
+
while (proto) {
|
|
427
|
+
const name = proto.constructor?.name;
|
|
428
|
+
if (name === "RpcException" || name === "HttpException") return "expected";
|
|
429
|
+
proto = Object.getPrototypeOf(proto);
|
|
430
|
+
}
|
|
431
|
+
return "unexpected";
|
|
432
|
+
};
|
|
433
|
+
var expandTracesOption = (option) => {
|
|
434
|
+
if (option === void 0 || option === "default") return new Set(DEFAULT_TRACES);
|
|
435
|
+
if (option === "all") return new Set(Object.values(JetstreamTrace));
|
|
436
|
+
if (option === "none") return /* @__PURE__ */ new Set();
|
|
437
|
+
return new Set(option);
|
|
438
|
+
};
|
|
439
|
+
var compileHeaderMatcher = (option) => compileHeaderAllowlist(option ?? DEFAULT_CAPTURE_HEADERS);
|
|
440
|
+
var resolveCaptureBody = (option) => {
|
|
441
|
+
if (option === void 0 || option === false) return false;
|
|
442
|
+
if (option === true) return { maxBytes: DEFAULT_CAPTURE_BODY_MAX_BYTES };
|
|
443
|
+
return {
|
|
444
|
+
maxBytes: option.maxBytes ?? DEFAULT_CAPTURE_BODY_MAX_BYTES,
|
|
445
|
+
subjectAllowlist: option.subjectAllowlist
|
|
446
|
+
};
|
|
447
|
+
};
|
|
448
|
+
var resolveOtelOptions = (options = {}) => {
|
|
449
|
+
return {
|
|
450
|
+
enabled: options.enabled ?? true,
|
|
451
|
+
traces: expandTracesOption(options.traces),
|
|
452
|
+
captureHeaders: compileHeaderMatcher(options.captureHeaders),
|
|
453
|
+
captureBody: resolveCaptureBody(options.captureBody),
|
|
454
|
+
publishHook: options.publishHook,
|
|
455
|
+
consumeHook: options.consumeHook,
|
|
456
|
+
responseHook: options.responseHook,
|
|
457
|
+
shouldTracePublish: options.shouldTracePublish,
|
|
458
|
+
shouldTraceConsume: options.shouldTraceConsume,
|
|
459
|
+
errorClassifier: options.errorClassifier ?? defaultErrorClassifier
|
|
460
|
+
};
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/otel/internal-utils.ts
|
|
464
|
+
import { Logger } from "@nestjs/common";
|
|
465
|
+
var logger = new Logger("Jetstream:Otel");
|
|
466
|
+
var safelyInvokeHook = (hookName, hook, ...args) => {
|
|
467
|
+
if (!hook) return;
|
|
468
|
+
const logHookFailure = (err) => {
|
|
469
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
470
|
+
logger.debug(`OTel ${hookName} threw: ${message}`);
|
|
471
|
+
};
|
|
472
|
+
try {
|
|
473
|
+
const result = hook(...args);
|
|
474
|
+
if (result !== null && typeof result === "object" && "then" in result && typeof result.then === "function") {
|
|
475
|
+
result.then(void 0, logHookFailure);
|
|
476
|
+
}
|
|
477
|
+
} catch (err) {
|
|
478
|
+
logHookFailure(err);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var stripIpv6Brackets = (host) => host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
482
|
+
var parsePort = (portRaw) => {
|
|
483
|
+
if (!portRaw) return void 0;
|
|
484
|
+
const port = Number.parseInt(portRaw, 10);
|
|
485
|
+
return Number.isInteger(port) ? port : void 0;
|
|
486
|
+
};
|
|
487
|
+
var parseServerAddress = (servers) => {
|
|
488
|
+
const raw = servers[0];
|
|
489
|
+
if (!raw) return null;
|
|
490
|
+
if (raw.includes("://")) {
|
|
491
|
+
try {
|
|
492
|
+
const url = new URL(raw);
|
|
493
|
+
if (url.hostname.length === 0) return null;
|
|
494
|
+
const host2 = stripIpv6Brackets(url.hostname);
|
|
495
|
+
const port2 = parsePort(url.port || void 0);
|
|
496
|
+
return port2 === void 0 ? { host: host2 } : { host: host2, port: port2 };
|
|
497
|
+
} catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (raw.startsWith("[")) {
|
|
502
|
+
const closeIdx = raw.indexOf("]");
|
|
503
|
+
if (closeIdx <= 0) return null;
|
|
504
|
+
const host2 = raw.slice(1, closeIdx);
|
|
505
|
+
const port2 = parsePort(raw.slice(closeIdx + 1).replace(/^:/u, ""));
|
|
506
|
+
return port2 === void 0 ? { host: host2 } : { host: host2, port: port2 };
|
|
507
|
+
}
|
|
508
|
+
const [host, portRaw] = raw.split(":");
|
|
509
|
+
if (!host) return null;
|
|
510
|
+
const port = parsePort(portRaw);
|
|
511
|
+
return port === void 0 ? { host } : { host, port };
|
|
512
|
+
};
|
|
513
|
+
var deriveOtelAttrs = (options) => ({
|
|
514
|
+
otel: resolveOtelOptions(options.otel),
|
|
515
|
+
serviceName: internalName(options.name),
|
|
516
|
+
serverEndpoint: parseServerAddress(options.servers)
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// src/otel/propagator.ts
|
|
520
|
+
import {
|
|
521
|
+
propagation
|
|
522
|
+
} from "@opentelemetry/api";
|
|
523
|
+
var injectContext = (ctx, carrier, setter) => {
|
|
524
|
+
propagation.inject(ctx, carrier, setter);
|
|
525
|
+
};
|
|
526
|
+
var extractContext = (ctx, carrier, getter) => propagation.extract(ctx, carrier, getter);
|
|
527
|
+
|
|
528
|
+
// src/otel/tracer.ts
|
|
529
|
+
import { trace } from "@opentelemetry/api";
|
|
530
|
+
var PACKAGE_VERSION = true ? "2.11.0" : "0.0.0";
|
|
531
|
+
var getTracer = () => trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
|
|
532
|
+
|
|
533
|
+
// src/otel/carrier.ts
|
|
534
|
+
var hdrsSetter = {
|
|
535
|
+
set: (headers2, key, value) => {
|
|
536
|
+
headers2.set(key, value);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
var hdrsGetter = {
|
|
540
|
+
keys: (headers2) => headers2 ? headers2.keys() : [],
|
|
541
|
+
get: (headers2, key) => {
|
|
542
|
+
if (!headers2) return void 0;
|
|
543
|
+
const all = typeof headers2.values === "function" ? headers2.values(key) : void 0;
|
|
544
|
+
if (Array.isArray(all)) {
|
|
545
|
+
const nonEmpty = all.filter((value) => value !== "");
|
|
546
|
+
if (nonEmpty.length === 0) return void 0;
|
|
547
|
+
return nonEmpty.join(",");
|
|
548
|
+
}
|
|
549
|
+
const single = headers2.get(key);
|
|
550
|
+
return single === "" ? void 0 : single;
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// src/otel/attributes.ts
|
|
555
|
+
var MESSAGING_SYSTEM = "nats";
|
|
556
|
+
var baseMessagingAttributes = (ctx) => {
|
|
557
|
+
const attrs = {
|
|
558
|
+
[ATTR_MESSAGING_SYSTEM]: MESSAGING_SYSTEM,
|
|
559
|
+
[ATTR_MESSAGING_DESTINATION_NAME]: ctx.subject,
|
|
560
|
+
[ATTR_MESSAGING_CLIENT_ID]: ctx.serviceName
|
|
561
|
+
};
|
|
562
|
+
if (ctx.serverAddress) attrs[ATTR_SERVER_ADDRESS] = ctx.serverAddress;
|
|
563
|
+
if (ctx.serverPort !== void 0) attrs[ATTR_SERVER_PORT] = ctx.serverPort;
|
|
564
|
+
if (ctx.pattern && ctx.pattern !== ctx.subject) {
|
|
565
|
+
attrs[ATTR_MESSAGING_DESTINATION_TEMPLATE] = ctx.pattern;
|
|
566
|
+
}
|
|
567
|
+
return attrs;
|
|
568
|
+
};
|
|
569
|
+
var jetstreamKindForPublish = (kind) => kind === "rpc.request" /* RpcRequest */ ? "rpc" /* Rpc */ : kind;
|
|
570
|
+
var buildPublishAttributes = (ctx) => {
|
|
571
|
+
const attrs = {
|
|
572
|
+
...baseMessagingAttributes(ctx),
|
|
573
|
+
[ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_PUBLISH,
|
|
574
|
+
[ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_SEND,
|
|
575
|
+
[ATTR_MESSAGING_MESSAGE_BODY_SIZE]: ctx.payloadBytes,
|
|
576
|
+
[ATTR_JETSTREAM_KIND]: jetstreamKindForPublish(ctx.kind)
|
|
577
|
+
};
|
|
578
|
+
if (ctx.messageId) attrs[ATTR_MESSAGING_MESSAGE_ID] = ctx.messageId;
|
|
579
|
+
if (ctx.correlationId) attrs[ATTR_MESSAGING_MESSAGE_CONVERSATION_ID] = ctx.correlationId;
|
|
580
|
+
return attrs;
|
|
581
|
+
};
|
|
582
|
+
var buildConsumeAttributes = (ctx) => {
|
|
583
|
+
const { msg, info, kind, payloadBytes } = ctx;
|
|
584
|
+
const attrs = {
|
|
585
|
+
...baseMessagingAttributes(ctx),
|
|
586
|
+
[ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_PROCESS,
|
|
587
|
+
[ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_PROCESS,
|
|
588
|
+
[ATTR_MESSAGING_MESSAGE_BODY_SIZE]: payloadBytes,
|
|
589
|
+
[ATTR_JETSTREAM_KIND]: kind
|
|
590
|
+
};
|
|
591
|
+
if (info) {
|
|
592
|
+
attrs[ATTR_MESSAGING_NATS_STREAM_NAME] = info.stream;
|
|
593
|
+
attrs[ATTR_MESSAGING_CONSUMER_GROUP_NAME] = info.consumer;
|
|
594
|
+
attrs[ATTR_MESSAGING_NATS_STREAM_SEQUENCE] = info.streamSequence;
|
|
595
|
+
attrs[ATTR_MESSAGING_NATS_CONSUMER_SEQUENCE] = info.deliverySequence;
|
|
596
|
+
attrs[ATTR_MESSAGING_NATS_DELIVERY_COUNT] = info.deliveryCount;
|
|
597
|
+
}
|
|
598
|
+
const messageId = msg.headers?.get(NATS_MSG_ID_HEADER);
|
|
599
|
+
if (messageId) attrs[ATTR_MESSAGING_MESSAGE_ID] = messageId;
|
|
600
|
+
return attrs;
|
|
601
|
+
};
|
|
602
|
+
var buildRpcClientAttributes = (ctx) => {
|
|
603
|
+
const attrs = {
|
|
604
|
+
...baseMessagingAttributes(ctx),
|
|
605
|
+
[ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_SEND,
|
|
606
|
+
[ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_SEND,
|
|
607
|
+
[ATTR_MESSAGING_MESSAGE_BODY_SIZE]: ctx.payloadBytes,
|
|
608
|
+
[ATTR_JETSTREAM_KIND]: "rpc" /* Rpc */
|
|
609
|
+
};
|
|
610
|
+
if (ctx.correlationId) attrs[ATTR_MESSAGING_MESSAGE_CONVERSATION_ID] = ctx.correlationId;
|
|
611
|
+
if (ctx.messageId) attrs[ATTR_MESSAGING_MESSAGE_ID] = ctx.messageId;
|
|
612
|
+
return attrs;
|
|
613
|
+
};
|
|
614
|
+
var buildDeadLetterAttributes = (ctx) => {
|
|
615
|
+
const attrs = {
|
|
616
|
+
...baseMessagingAttributes(ctx),
|
|
617
|
+
[ATTR_MESSAGING_OPERATION_NAME]: SPAN_NAME_DEAD_LETTER,
|
|
618
|
+
[ATTR_MESSAGING_OPERATION_TYPE]: SPAN_NAME_PROCESS,
|
|
619
|
+
[ATTR_MESSAGING_NATS_DELIVERY_COUNT]: ctx.finalDeliveryCount
|
|
620
|
+
};
|
|
621
|
+
if (ctx.reason) attrs[ATTR_JETSTREAM_DEAD_LETTER_REASON] = ctx.reason;
|
|
622
|
+
return attrs;
|
|
623
|
+
};
|
|
624
|
+
var extractFromRpcException = (record) => {
|
|
625
|
+
const getError = record.getError;
|
|
626
|
+
if (typeof getError !== "function") return void 0;
|
|
627
|
+
return codeFromPayload(getError.call(record));
|
|
628
|
+
};
|
|
629
|
+
var extractFromHttpException = (record) => {
|
|
630
|
+
const getStatus = record.getStatus;
|
|
631
|
+
if (typeof getStatus !== "function") return void 0;
|
|
632
|
+
const status = getStatus.call(record);
|
|
633
|
+
return typeof status === "number" && Number.isFinite(status) ? `HTTP_${status}` : void 0;
|
|
634
|
+
};
|
|
635
|
+
var extractFromOwnCode = (record) => {
|
|
636
|
+
const code = record.code ?? record.errorCode;
|
|
637
|
+
return typeof code === "string" && isStableErrorCode(code) ? code : void 0;
|
|
638
|
+
};
|
|
639
|
+
var extractExpectedErrorCode = (err) => {
|
|
640
|
+
if (err === null || typeof err !== "object") return void 0;
|
|
641
|
+
const record = err;
|
|
642
|
+
if (hasAncestorNamed(err, "RpcException")) {
|
|
643
|
+
const fromPayload = extractFromRpcException(record);
|
|
644
|
+
if (fromPayload !== void 0) return fromPayload;
|
|
645
|
+
}
|
|
646
|
+
if (hasAncestorNamed(err, "HttpException")) {
|
|
647
|
+
const fromStatus = extractFromHttpException(record);
|
|
648
|
+
if (fromStatus !== void 0) return fromStatus;
|
|
649
|
+
}
|
|
650
|
+
return extractFromOwnCode(record);
|
|
651
|
+
};
|
|
652
|
+
var hasAncestorNamed = (err, name) => {
|
|
653
|
+
let proto = Object.getPrototypeOf(err);
|
|
654
|
+
while (proto) {
|
|
655
|
+
const ctorName = proto.constructor?.name;
|
|
656
|
+
if (ctorName === name) return true;
|
|
657
|
+
proto = Object.getPrototypeOf(proto);
|
|
658
|
+
}
|
|
659
|
+
return false;
|
|
660
|
+
};
|
|
661
|
+
var STABLE_ERROR_CODE_RE = /^[A-Z][A-Z0-9_]*$/u;
|
|
662
|
+
var isStableErrorCode = (value) => STABLE_ERROR_CODE_RE.test(value);
|
|
663
|
+
var codeFromPayload = (payload) => {
|
|
664
|
+
if (payload === null || payload === void 0) return void 0;
|
|
665
|
+
if (typeof payload === "string") return isStableErrorCode(payload) ? payload : void 0;
|
|
666
|
+
if (typeof payload === "object") {
|
|
667
|
+
const code = payload.code;
|
|
668
|
+
if (typeof code === "string" && isStableErrorCode(code)) return code;
|
|
669
|
+
}
|
|
670
|
+
return void 0;
|
|
671
|
+
};
|
|
672
|
+
var buildExpectedErrorAttributes = (err) => {
|
|
673
|
+
const attrs = {
|
|
674
|
+
[ATTR_JETSTREAM_RPC_REPLY_HAS_ERROR]: true
|
|
675
|
+
};
|
|
676
|
+
const code = extractExpectedErrorCode(err);
|
|
677
|
+
if (code !== void 0) attrs[ATTR_JETSTREAM_RPC_REPLY_ERROR_CODE] = code;
|
|
678
|
+
return attrs;
|
|
679
|
+
};
|
|
680
|
+
var applyExpectedErrorAttributes = (span, err) => {
|
|
681
|
+
span.setAttributes(buildExpectedErrorAttributes(err));
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// src/otel/spans/publish.ts
|
|
685
|
+
import { Logger as Logger2 } from "@nestjs/common";
|
|
686
|
+
import { SpanKind, SpanStatusCode, context, trace as trace2 } from "@opentelemetry/api";
|
|
687
|
+
var logger2 = new Logger2("Jetstream:Otel");
|
|
688
|
+
var shouldTracePublishSafe = (predicate, subject, record) => {
|
|
689
|
+
if (!predicate) return true;
|
|
690
|
+
try {
|
|
691
|
+
return predicate(subject, record);
|
|
692
|
+
} catch (err) {
|
|
693
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
694
|
+
logger2.debug(`OTel shouldTracePublish threw: ${message}`);
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
var withPublishSpan = async (ctx, config, fn) => {
|
|
699
|
+
if (!config.enabled) return fn();
|
|
700
|
+
const shouldCreateSpan = config.traces.has("publish" /* Publish */) && shouldTracePublishSafe(config.shouldTracePublish, ctx.subject, ctx.record);
|
|
701
|
+
if (!shouldCreateSpan) {
|
|
702
|
+
injectContext(context.active(), ctx.headers, hdrsSetter);
|
|
703
|
+
return fn();
|
|
704
|
+
}
|
|
705
|
+
const tracer = getTracer();
|
|
706
|
+
const span = tracer.startSpan(`${SPAN_NAME_PUBLISH} ${ctx.subject}`, {
|
|
707
|
+
kind: SpanKind.PRODUCER,
|
|
708
|
+
attributes: {
|
|
709
|
+
...buildPublishAttributes({
|
|
710
|
+
subject: ctx.subject,
|
|
711
|
+
pattern: ctx.pattern,
|
|
712
|
+
serviceName: ctx.serviceName,
|
|
713
|
+
serverAddress: ctx.endpoint?.host,
|
|
714
|
+
serverPort: ctx.endpoint?.port,
|
|
715
|
+
kind: ctx.kind,
|
|
716
|
+
payloadBytes: ctx.payloadBytes,
|
|
717
|
+
messageId: ctx.messageId,
|
|
718
|
+
correlationId: ctx.correlationId
|
|
719
|
+
}),
|
|
720
|
+
...ctx.scheduleTarget ? { [ATTR_JETSTREAM_SCHEDULE_TARGET]: ctx.scheduleTarget } : {},
|
|
721
|
+
...captureMatchingHeaders(ctx.headers, config.captureHeaders),
|
|
722
|
+
...captureBodyAttribute(ctx.subject, ctx.payload, config.captureBody)
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
const ctxWithSpan = trace2.setSpan(context.active(), span);
|
|
726
|
+
injectContext(ctxWithSpan, ctx.headers, hdrsSetter);
|
|
727
|
+
const start = Date.now();
|
|
728
|
+
const invokeResponseHook = (error) => {
|
|
729
|
+
context.with(ctxWithSpan, () => {
|
|
730
|
+
safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
|
|
731
|
+
subject: ctx.subject,
|
|
732
|
+
durationMs: Date.now() - start,
|
|
733
|
+
error
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
};
|
|
737
|
+
try {
|
|
738
|
+
const result = await context.with(ctxWithSpan, async () => {
|
|
739
|
+
safelyInvokeHook(HOOK_PUBLISH, config.publishHook, span, {
|
|
740
|
+
subject: ctx.subject,
|
|
741
|
+
record: ctx.record,
|
|
742
|
+
kind: ctx.kind
|
|
743
|
+
});
|
|
744
|
+
return fn();
|
|
745
|
+
});
|
|
746
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
747
|
+
invokeResponseHook();
|
|
748
|
+
return result;
|
|
749
|
+
} catch (err) {
|
|
750
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
751
|
+
span.recordException(error);
|
|
752
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
|
753
|
+
invokeResponseHook(error);
|
|
754
|
+
throw err;
|
|
755
|
+
} finally {
|
|
756
|
+
span.end();
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
// src/otel/spans/consume.ts
|
|
761
|
+
import {
|
|
762
|
+
ROOT_CONTEXT,
|
|
763
|
+
SpanKind as SpanKind2,
|
|
764
|
+
SpanStatusCode as SpanStatusCode2,
|
|
765
|
+
context as context2,
|
|
766
|
+
trace as trace3
|
|
767
|
+
} from "@opentelemetry/api";
|
|
768
|
+
var isPromiseLike = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
|
|
769
|
+
var applyExpectedError = (span, err) => {
|
|
770
|
+
span.setStatus({ code: SpanStatusCode2.OK });
|
|
771
|
+
applyExpectedErrorAttributes(span, err);
|
|
772
|
+
};
|
|
773
|
+
var applyUnexpectedError = (span, err) => {
|
|
774
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
775
|
+
span.recordException(error);
|
|
776
|
+
span.setStatus({ code: SpanStatusCode2.ERROR, message: error.message });
|
|
777
|
+
};
|
|
778
|
+
var withConsumeSpan = (ctx, config, fn, options = {}) => {
|
|
779
|
+
if (!config.enabled) return fn();
|
|
780
|
+
const parentCtx = extractContext(ROOT_CONTEXT, ctx.msg.headers, hdrsGetter);
|
|
781
|
+
const shouldCreateSpan = config.traces.has("consume" /* Consume */) && (config.shouldTraceConsume?.(ctx.subject, ctx.msg) ?? true);
|
|
782
|
+
if (!shouldCreateSpan) {
|
|
783
|
+
return context2.with(parentCtx, fn);
|
|
784
|
+
}
|
|
785
|
+
const tracer = getTracer();
|
|
786
|
+
const span = tracer.startSpan(
|
|
787
|
+
`${SPAN_NAME_PROCESS} ${ctx.subject}`,
|
|
788
|
+
{
|
|
789
|
+
kind: SpanKind2.CONSUMER,
|
|
790
|
+
attributes: {
|
|
791
|
+
...buildConsumeAttributes({
|
|
792
|
+
subject: ctx.subject,
|
|
793
|
+
pattern: ctx.pattern,
|
|
794
|
+
msg: ctx.msg,
|
|
795
|
+
info: ctx.info,
|
|
796
|
+
kind: ctx.kind,
|
|
797
|
+
payloadBytes: ctx.payloadBytes,
|
|
798
|
+
serviceName: ctx.serviceName,
|
|
799
|
+
serverAddress: ctx.endpoint?.host,
|
|
800
|
+
serverPort: ctx.endpoint?.port
|
|
801
|
+
}),
|
|
802
|
+
...captureMatchingHeaders(ctx.msg.headers, config.captureHeaders),
|
|
803
|
+
...captureBodyAttribute(ctx.subject, ctx.msg.data, config.captureBody)
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
parentCtx
|
|
807
|
+
);
|
|
808
|
+
const ctxWithSpan = trace3.setSpan(parentCtx, span);
|
|
809
|
+
const start = Date.now();
|
|
810
|
+
let finalized = false;
|
|
811
|
+
const { signal, timeoutLabel = "handler.timeout" } = options;
|
|
812
|
+
let detachAbort = null;
|
|
813
|
+
const invokeResponseHook = (durationMs, error) => {
|
|
814
|
+
context2.with(ctxWithSpan, () => {
|
|
815
|
+
safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
|
|
816
|
+
subject: ctx.subject,
|
|
817
|
+
durationMs,
|
|
818
|
+
error
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
};
|
|
822
|
+
const finishOk2 = () => {
|
|
823
|
+
if (finalized) return;
|
|
824
|
+
finalized = true;
|
|
825
|
+
detachAbort?.();
|
|
826
|
+
span.setStatus({ code: SpanStatusCode2.OK });
|
|
827
|
+
invokeResponseHook(Date.now() - start);
|
|
828
|
+
span.end();
|
|
829
|
+
};
|
|
830
|
+
const finishError2 = (err) => {
|
|
831
|
+
if (finalized) return;
|
|
832
|
+
finalized = true;
|
|
833
|
+
detachAbort?.();
|
|
834
|
+
let classification = "unexpected";
|
|
835
|
+
try {
|
|
836
|
+
classification = config.errorClassifier(err);
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
if (classification === "expected") {
|
|
840
|
+
applyExpectedError(span, err);
|
|
841
|
+
} else {
|
|
842
|
+
applyUnexpectedError(span, err);
|
|
843
|
+
}
|
|
844
|
+
invokeResponseHook(Date.now() - start, err instanceof Error ? err : new Error(String(err)));
|
|
845
|
+
span.end();
|
|
846
|
+
};
|
|
847
|
+
const onAbort = () => {
|
|
848
|
+
if (finalized) return;
|
|
849
|
+
finalized = true;
|
|
850
|
+
const error = new Error(timeoutLabel);
|
|
851
|
+
span.addEvent(timeoutLabel);
|
|
852
|
+
span.recordException(error);
|
|
853
|
+
span.setStatus({ code: SpanStatusCode2.ERROR, message: timeoutLabel });
|
|
854
|
+
invokeResponseHook(Date.now() - start, error);
|
|
855
|
+
span.end();
|
|
856
|
+
};
|
|
857
|
+
if (signal) {
|
|
858
|
+
if (signal.aborted) {
|
|
859
|
+
onAbort();
|
|
860
|
+
} else {
|
|
861
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
862
|
+
detachAbort = () => {
|
|
863
|
+
signal.removeEventListener("abort", onAbort);
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
let result;
|
|
868
|
+
try {
|
|
869
|
+
result = context2.with(ctxWithSpan, () => {
|
|
870
|
+
safelyInvokeHook(HOOK_CONSUME, config.consumeHook, span, {
|
|
871
|
+
subject: ctx.subject,
|
|
872
|
+
msg: ctx.msg,
|
|
873
|
+
handlerMetadata: ctx.handlerMetadata,
|
|
874
|
+
kind: ctx.kind
|
|
875
|
+
});
|
|
876
|
+
return fn();
|
|
877
|
+
});
|
|
878
|
+
} catch (err) {
|
|
879
|
+
finishError2(err);
|
|
880
|
+
throw err;
|
|
881
|
+
}
|
|
882
|
+
if (isPromiseLike(result)) {
|
|
883
|
+
return Promise.resolve(result).then(
|
|
884
|
+
(value) => {
|
|
885
|
+
finishOk2();
|
|
886
|
+
return value;
|
|
887
|
+
},
|
|
888
|
+
(err) => {
|
|
889
|
+
finishError2(err);
|
|
890
|
+
throw err;
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
finishOk2();
|
|
895
|
+
return result;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
// src/otel/spans/rpc-client.ts
|
|
899
|
+
import { Logger as Logger3 } from "@nestjs/common";
|
|
900
|
+
import { SpanKind as SpanKind3, SpanStatusCode as SpanStatusCode3, context as context3, trace as trace4 } from "@opentelemetry/api";
|
|
901
|
+
var logger3 = new Logger3("Jetstream:Otel");
|
|
902
|
+
var RPC_TIMEOUT_MESSAGE = "rpc.timeout";
|
|
903
|
+
var beginRpcClientSpan = (ctx, config) => {
|
|
904
|
+
if (!config.enabled) {
|
|
905
|
+
return {
|
|
906
|
+
activeContext: context3.active(),
|
|
907
|
+
finish: () => void 0
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
if (!config.traces.has("rpc.client.send" /* RpcClientSend */)) {
|
|
911
|
+
injectContext(context3.active(), ctx.headers, hdrsSetter);
|
|
912
|
+
return {
|
|
913
|
+
activeContext: context3.active(),
|
|
914
|
+
finish: () => void 0
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
const tracer = getTracer();
|
|
918
|
+
const span = tracer.startSpan(`${SPAN_NAME_SEND} ${ctx.subject}`, {
|
|
919
|
+
kind: SpanKind3.CLIENT,
|
|
920
|
+
attributes: {
|
|
921
|
+
...buildRpcClientAttributes({
|
|
922
|
+
subject: ctx.subject,
|
|
923
|
+
pattern: ctx.pattern,
|
|
924
|
+
correlationId: ctx.correlationId,
|
|
925
|
+
payloadBytes: ctx.payloadBytes,
|
|
926
|
+
messageId: ctx.messageId,
|
|
927
|
+
serviceName: ctx.serviceName,
|
|
928
|
+
serverAddress: ctx.endpoint?.host,
|
|
929
|
+
serverPort: ctx.endpoint?.port
|
|
930
|
+
}),
|
|
931
|
+
...captureMatchingHeaders(ctx.headers, config.captureHeaders),
|
|
932
|
+
...captureBodyAttribute(ctx.subject, ctx.payload, config.captureBody)
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
const ctxWithSpan = trace4.setSpan(context3.active(), span);
|
|
936
|
+
injectContext(ctxWithSpan, ctx.headers, hdrsSetter);
|
|
937
|
+
const start = Date.now();
|
|
938
|
+
let finalized = false;
|
|
939
|
+
const finish = (outcome) => {
|
|
940
|
+
if (finalized) return;
|
|
941
|
+
finalized = true;
|
|
942
|
+
let reply;
|
|
943
|
+
let error;
|
|
944
|
+
switch (outcome.kind) {
|
|
945
|
+
case "ok" /* Ok */:
|
|
946
|
+
reply = outcome.reply;
|
|
947
|
+
span.setStatus({ code: SpanStatusCode3.OK });
|
|
948
|
+
break;
|
|
949
|
+
case "reply-error" /* ReplyError */:
|
|
950
|
+
reply = outcome.replyPayload;
|
|
951
|
+
applyExpectedErrorAttributes(span, outcome.replyPayload);
|
|
952
|
+
span.setStatus({ code: SpanStatusCode3.OK });
|
|
953
|
+
break;
|
|
954
|
+
case "timeout" /* Timeout */:
|
|
955
|
+
error = new Error(RPC_TIMEOUT_MESSAGE);
|
|
956
|
+
span.addEvent(RPC_TIMEOUT_MESSAGE);
|
|
957
|
+
span.setStatus({ code: SpanStatusCode3.ERROR, message: RPC_TIMEOUT_MESSAGE });
|
|
958
|
+
break;
|
|
959
|
+
case "error" /* Error */:
|
|
960
|
+
error = outcome.error;
|
|
961
|
+
span.recordException(outcome.error);
|
|
962
|
+
span.setStatus({ code: SpanStatusCode3.ERROR, message: outcome.error.message });
|
|
963
|
+
break;
|
|
964
|
+
default: {
|
|
965
|
+
const unknownOutcome = outcome;
|
|
966
|
+
logger3.error(`Unhandled RPC outcome: ${String(unknownOutcome.kind)}`);
|
|
967
|
+
span.setStatus({ code: SpanStatusCode3.ERROR, message: "unknown outcome" });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
context3.with(ctxWithSpan, () => {
|
|
971
|
+
safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
|
|
972
|
+
subject: ctx.subject,
|
|
973
|
+
durationMs: Date.now() - start,
|
|
974
|
+
reply,
|
|
975
|
+
error
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
span.end();
|
|
979
|
+
};
|
|
980
|
+
return { activeContext: ctxWithSpan, finish };
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// src/otel/spans/dead-letter.ts
|
|
984
|
+
import { ROOT_CONTEXT as ROOT_CONTEXT2, SpanKind as SpanKind4, SpanStatusCode as SpanStatusCode4, context as context4, trace as trace5 } from "@opentelemetry/api";
|
|
985
|
+
var withDeadLetterSpan = async (ctx, config, fn) => {
|
|
986
|
+
if (!config.enabled || !config.traces.has("dead_letter" /* DeadLetter */)) {
|
|
987
|
+
return fn();
|
|
988
|
+
}
|
|
989
|
+
const parentCtx = extractContext(ROOT_CONTEXT2, ctx.msg.headers, hdrsGetter);
|
|
990
|
+
const tracer = getTracer();
|
|
991
|
+
const span = tracer.startSpan(
|
|
992
|
+
`${SPAN_NAME_DEAD_LETTER} ${ctx.msg.subject}`,
|
|
993
|
+
{
|
|
994
|
+
kind: SpanKind4.INTERNAL,
|
|
995
|
+
attributes: buildDeadLetterAttributes({
|
|
996
|
+
subject: ctx.msg.subject,
|
|
997
|
+
pattern: ctx.pattern,
|
|
998
|
+
serviceName: ctx.serviceName,
|
|
999
|
+
serverAddress: ctx.endpoint?.host,
|
|
1000
|
+
serverPort: ctx.endpoint?.port,
|
|
1001
|
+
finalDeliveryCount: ctx.finalDeliveryCount,
|
|
1002
|
+
reason: ctx.reason
|
|
1003
|
+
})
|
|
1004
|
+
},
|
|
1005
|
+
parentCtx
|
|
1006
|
+
);
|
|
1007
|
+
const ctxWithSpan = trace5.setSpan(parentCtx, span);
|
|
1008
|
+
const start = Date.now();
|
|
1009
|
+
const invokeResponseHook = (error) => {
|
|
1010
|
+
context4.with(ctxWithSpan, () => {
|
|
1011
|
+
safelyInvokeHook(HOOK_RESPONSE, config.responseHook, span, {
|
|
1012
|
+
subject: ctx.msg.subject,
|
|
1013
|
+
durationMs: Date.now() - start,
|
|
1014
|
+
error
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
};
|
|
1018
|
+
try {
|
|
1019
|
+
const result = await context4.with(ctxWithSpan, fn);
|
|
1020
|
+
span.setStatus({ code: SpanStatusCode4.OK });
|
|
1021
|
+
invokeResponseHook();
|
|
1022
|
+
return result;
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1025
|
+
span.recordException(error);
|
|
1026
|
+
span.setStatus({ code: SpanStatusCode4.ERROR, message: error.message });
|
|
1027
|
+
invokeResponseHook(error);
|
|
1028
|
+
throw err;
|
|
1029
|
+
} finally {
|
|
1030
|
+
span.end();
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
// src/otel/spans/infrastructure.ts
|
|
1035
|
+
import { Logger as Logger4 } from "@nestjs/common";
|
|
1036
|
+
import {
|
|
1037
|
+
SpanKind as SpanKind5,
|
|
1038
|
+
SpanStatusCode as SpanStatusCode5,
|
|
1039
|
+
context as context5,
|
|
1040
|
+
trace as trace6
|
|
1041
|
+
} from "@opentelemetry/api";
|
|
1042
|
+
var logger4 = new Logger4("Jetstream:Otel");
|
|
1043
|
+
var startInfraSpan = (config, traceKind, name, ctx, extraAttributes = {}) => {
|
|
1044
|
+
if (!config.enabled || !config.traces.has(traceKind)) return null;
|
|
1045
|
+
const tracer = getTracer();
|
|
1046
|
+
const attributes = {
|
|
1047
|
+
[ATTR_JETSTREAM_SERVICE_NAME]: ctx.serviceName
|
|
1048
|
+
};
|
|
1049
|
+
if (ctx.endpoint?.host) attributes[ATTR_SERVER_ADDRESS] = ctx.endpoint.host;
|
|
1050
|
+
if (ctx.endpoint?.port !== void 0) attributes[ATTR_SERVER_PORT] = ctx.endpoint.port;
|
|
1051
|
+
for (const [key, value] of Object.entries(extraAttributes)) {
|
|
1052
|
+
if (value !== void 0) attributes[key] = value;
|
|
1053
|
+
}
|
|
1054
|
+
return tracer.startSpan(name, { kind: SpanKind5.INTERNAL, attributes });
|
|
1055
|
+
};
|
|
1056
|
+
var finishOk = (span) => {
|
|
1057
|
+
span.setStatus({ code: SpanStatusCode5.OK });
|
|
1058
|
+
span.end();
|
|
1059
|
+
};
|
|
1060
|
+
var finishError = (span, err) => {
|
|
1061
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1062
|
+
span.recordException(error);
|
|
1063
|
+
span.setStatus({ code: SpanStatusCode5.ERROR, message: error.message });
|
|
1064
|
+
span.end();
|
|
1065
|
+
};
|
|
1066
|
+
var wrapInfra = async (config, traceKind, name, ctx, attributes, op) => {
|
|
1067
|
+
const span = startInfraSpan(config, traceKind, name, ctx, attributes);
|
|
1068
|
+
if (!span) return op();
|
|
1069
|
+
const ctxWithSpan = trace6.setSpan(context5.active(), span);
|
|
1070
|
+
try {
|
|
1071
|
+
const result = await context5.with(ctxWithSpan, op);
|
|
1072
|
+
finishOk(span);
|
|
1073
|
+
return result;
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
finishError(span, err);
|
|
1076
|
+
throw err;
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
var beginConnectionLifecycleSpan = (config, ctx) => {
|
|
1080
|
+
const span = startInfraSpan(
|
|
1081
|
+
config,
|
|
1082
|
+
"connection.lifecycle" /* ConnectionLifecycle */,
|
|
1083
|
+
SPAN_NAME_NATS_CONNECTION,
|
|
1084
|
+
ctx,
|
|
1085
|
+
{ [ATTR_NATS_CONNECTION_SERVER]: ctx.server }
|
|
1086
|
+
);
|
|
1087
|
+
if (!span) {
|
|
1088
|
+
return {
|
|
1089
|
+
recordEvent: () => void 0,
|
|
1090
|
+
finish: () => void 0
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
let finalized = false;
|
|
1094
|
+
return {
|
|
1095
|
+
recordEvent: (name, attributes) => {
|
|
1096
|
+
if (finalized) {
|
|
1097
|
+
logger4.debug(`recordEvent('${name}') called after connection span finished`);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
span.addEvent(name, attributes);
|
|
1101
|
+
},
|
|
1102
|
+
finish: (err) => {
|
|
1103
|
+
if (finalized) return;
|
|
1104
|
+
finalized = true;
|
|
1105
|
+
if (err === void 0) finishOk(span);
|
|
1106
|
+
else finishError(span, err);
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
};
|
|
1110
|
+
var withSelfHealingSpan = (config, ctx, op) => wrapInfra(
|
|
1111
|
+
config,
|
|
1112
|
+
"self_healing" /* SelfHealing */,
|
|
1113
|
+
SPAN_NAME_JETSTREAM_SELF_HEALING,
|
|
1114
|
+
ctx,
|
|
1115
|
+
{
|
|
1116
|
+
[ATTR_MESSAGING_NATS_STREAM_NAME]: ctx.stream,
|
|
1117
|
+
[ATTR_MESSAGING_CONSUMER_GROUP_NAME]: ctx.consumer,
|
|
1118
|
+
[ATTR_JETSTREAM_SELF_HEALING_REASON]: ctx.reason
|
|
1119
|
+
},
|
|
1120
|
+
op
|
|
1121
|
+
);
|
|
1122
|
+
var withProvisioningSpan = (config, ctx, op) => wrapInfra(
|
|
1123
|
+
config,
|
|
1124
|
+
"provisioning" /* Provisioning */,
|
|
1125
|
+
`${SPAN_NAME_JETSTREAM_PROVISIONING_PREFIX}${ctx.entity}`,
|
|
1126
|
+
ctx,
|
|
1127
|
+
{
|
|
1128
|
+
[ATTR_JETSTREAM_PROVISIONING_ENTITY]: ctx.entity,
|
|
1129
|
+
[ATTR_JETSTREAM_PROVISIONING_ACTION]: ctx.action,
|
|
1130
|
+
[ATTR_JETSTREAM_PROVISIONING_NAME]: ctx.name
|
|
1131
|
+
},
|
|
1132
|
+
op
|
|
1133
|
+
);
|
|
1134
|
+
var withMigrationSpan = (config, ctx, op) => wrapInfra(
|
|
1135
|
+
config,
|
|
1136
|
+
"migration" /* Migration */,
|
|
1137
|
+
SPAN_NAME_JETSTREAM_MIGRATION,
|
|
1138
|
+
ctx,
|
|
1139
|
+
{
|
|
1140
|
+
[ATTR_MESSAGING_NATS_STREAM_NAME]: ctx.stream,
|
|
1141
|
+
[ATTR_JETSTREAM_MIGRATION_REASON]: ctx.reason
|
|
1142
|
+
},
|
|
1143
|
+
op
|
|
1144
|
+
);
|
|
1145
|
+
var withShutdownSpan = (config, ctx, op) => wrapInfra(config, "shutdown" /* Shutdown */, SPAN_NAME_JETSTREAM_SHUTDOWN, ctx, {}, op);
|
|
1146
|
+
|
|
227
1147
|
// src/client/jetstream.record.ts
|
|
228
1148
|
var JetstreamRecord = class {
|
|
229
1149
|
constructor(data, headers2, timeout, messageId, schedule, ttl) {
|
|
@@ -376,9 +1296,14 @@ var JetstreamRecordBuilder = class {
|
|
|
376
1296
|
this.ttlDuration
|
|
377
1297
|
);
|
|
378
1298
|
}
|
|
379
|
-
/**
|
|
1299
|
+
/**
|
|
1300
|
+
* Validate that a header key is not reserved. NATS treats header names
|
|
1301
|
+
* case-insensitively, so the check is against the lowercase form to keep
|
|
1302
|
+
* `'X-Correlation-ID'`, `'x-correlation-id'`, and any other casing in
|
|
1303
|
+
* lockstep. `RESERVED_HEADERS` is defined as an all-lowercase set.
|
|
1304
|
+
*/
|
|
380
1305
|
validateHeaderKey(key) {
|
|
381
|
-
if (RESERVED_HEADERS.has(key)) {
|
|
1306
|
+
if (RESERVED_HEADERS.has(key.toLowerCase())) {
|
|
382
1307
|
throw new Error(
|
|
383
1308
|
`Header "${key}" is reserved by the JetStream transport and cannot be set manually. Reserved headers: ${[...RESERVED_HEADERS].join(", ")}`
|
|
384
1309
|
);
|
|
@@ -399,6 +1324,22 @@ var nanosToGoDuration = (nanos) => {
|
|
|
399
1324
|
|
|
400
1325
|
// src/client/jetstream.client.ts
|
|
401
1326
|
var BROADCAST_SUBJECT_PREFIX = "broadcast.";
|
|
1327
|
+
var detectEventKind = (pattern) => {
|
|
1328
|
+
if (pattern.startsWith("broadcast:" /* Broadcast */)) return "broadcast" /* Broadcast */;
|
|
1329
|
+
if (pattern.startsWith("ordered:" /* Ordered */)) return "ordered" /* Ordered */;
|
|
1330
|
+
return "event" /* Event */;
|
|
1331
|
+
};
|
|
1332
|
+
var declaredEventPattern = (pattern) => {
|
|
1333
|
+
if (pattern.startsWith("broadcast:" /* Broadcast */))
|
|
1334
|
+
return pattern.slice("broadcast:" /* Broadcast */.length);
|
|
1335
|
+
if (pattern.startsWith("ordered:" /* Ordered */)) return pattern.slice("ordered:" /* Ordered */.length);
|
|
1336
|
+
return pattern;
|
|
1337
|
+
};
|
|
1338
|
+
var eventStreamKind = (kind) => {
|
|
1339
|
+
if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
|
|
1340
|
+
if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
|
|
1341
|
+
return "ev" /* Event */;
|
|
1342
|
+
};
|
|
402
1343
|
var JetstreamClient = class extends ClientProxy {
|
|
403
1344
|
constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
|
|
404
1345
|
super();
|
|
@@ -414,8 +1355,11 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
414
1355
|
this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
|
|
415
1356
|
this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
|
|
416
1357
|
this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
|
|
1358
|
+
const derived = deriveOtelAttrs(this.rootOptions);
|
|
1359
|
+
this.otel = derived.otel;
|
|
1360
|
+
this.serverEndpoint = derived.serverEndpoint;
|
|
417
1361
|
}
|
|
418
|
-
logger = new
|
|
1362
|
+
logger = new Logger5("Jetstream:Client");
|
|
419
1363
|
/** Target service name this client sends messages to. */
|
|
420
1364
|
targetName;
|
|
421
1365
|
/** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
|
|
@@ -435,6 +1379,10 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
435
1379
|
*/
|
|
436
1380
|
isCoreMode;
|
|
437
1381
|
defaultRpcTimeout;
|
|
1382
|
+
/** Resolved OpenTelemetry configuration, computed once in the constructor. */
|
|
1383
|
+
otel;
|
|
1384
|
+
/** Server endpoint parts used for `server.address` / `server.port` span attributes. */
|
|
1385
|
+
serverEndpoint;
|
|
438
1386
|
/** Shared inbox for JetStream-mode RPC responses. */
|
|
439
1387
|
inbox = null;
|
|
440
1388
|
inboxSubscription = null;
|
|
@@ -502,32 +1450,64 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
502
1450
|
if (!this.readyForPublish) await this.connect();
|
|
503
1451
|
const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
|
|
504
1452
|
const eventSubject = this.buildEventSubject(packet.pattern);
|
|
1453
|
+
const publishSubject = schedule ? this.buildScheduleSubject(eventSubject) : eventSubject;
|
|
505
1454
|
const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
1455
|
+
const encoded = this.codec.encode(data);
|
|
1456
|
+
const effectiveMsgId = messageId ?? nuid.next();
|
|
1457
|
+
const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
|
|
1458
|
+
const publishKind = detectEventKind(packet.pattern);
|
|
1459
|
+
const declaredPattern = declaredEventPattern(packet.pattern);
|
|
1460
|
+
const streamKind = eventStreamKind(publishKind);
|
|
1461
|
+
const startedAt = performance.now();
|
|
1462
|
+
try {
|
|
1463
|
+
await withPublishSpan(
|
|
1464
|
+
{
|
|
1465
|
+
subject: publishSubject,
|
|
1466
|
+
pattern: packet.pattern,
|
|
1467
|
+
record,
|
|
1468
|
+
kind: publishKind,
|
|
1469
|
+
payloadBytes: encoded.length,
|
|
1470
|
+
payload: encoded,
|
|
1471
|
+
messageId: effectiveMsgId,
|
|
1472
|
+
headers: msgHeaders,
|
|
1473
|
+
serviceName: this.callerName,
|
|
1474
|
+
endpoint: this.serverEndpoint,
|
|
1475
|
+
scheduleTarget: schedule ? eventSubject : void 0
|
|
1476
|
+
},
|
|
1477
|
+
this.otel,
|
|
1478
|
+
async () => {
|
|
1479
|
+
const warnIfDuplicate = (kindLabel, ack2) => {
|
|
1480
|
+
if (ack2.duplicate) {
|
|
1481
|
+
this.logger.warn(
|
|
1482
|
+
`Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
if (schedule) {
|
|
1487
|
+
const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
|
|
1488
|
+
headers: msgHeaders,
|
|
1489
|
+
msgID: effectiveMsgId,
|
|
1490
|
+
ttl,
|
|
1491
|
+
schedule: {
|
|
1492
|
+
specification: schedule.at,
|
|
1493
|
+
target: eventSubject
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
warnIfDuplicate("scheduled", ack2);
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
|
|
1500
|
+
headers: msgHeaders,
|
|
1501
|
+
msgID: effectiveMsgId,
|
|
1502
|
+
ttl
|
|
1503
|
+
});
|
|
1504
|
+
warnIfDuplicate("event", ack);
|
|
515
1505
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
} else {
|
|
523
|
-
const ack = await this.connection.getJetStreamClient().publish(eventSubject, this.codec.encode(data), {
|
|
524
|
-
headers: msgHeaders,
|
|
525
|
-
msgID: messageId ?? nuid.next(),
|
|
526
|
-
ttl
|
|
527
|
-
});
|
|
528
|
-
if (ack.duplicate) {
|
|
529
|
-
this.logger.warn(`Duplicate event publish detected: ${eventSubject} (seq: ${ack.seq})`);
|
|
530
|
-
}
|
|
1506
|
+
);
|
|
1507
|
+
this.reportPublished(declaredPattern, streamKind, startedAt, "success");
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
this.reportPublished(declaredPattern, streamKind, startedAt, "error");
|
|
1510
|
+
throw err;
|
|
531
1511
|
}
|
|
532
1512
|
return void 0;
|
|
533
1513
|
}
|
|
@@ -556,14 +1536,17 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
556
1536
|
};
|
|
557
1537
|
let jetStreamCorrelationId = null;
|
|
558
1538
|
if (this.isCoreMode) {
|
|
559
|
-
this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(
|
|
1539
|
+
this.publishCoreRpc(subject, data, hdrs, timeout, callback, packet.pattern).catch(
|
|
1540
|
+
onUnhandled
|
|
1541
|
+
);
|
|
560
1542
|
} else {
|
|
561
1543
|
jetStreamCorrelationId = nuid.next();
|
|
562
1544
|
this.publishJetStreamRpc(subject, data, callback, {
|
|
563
1545
|
headers: hdrs,
|
|
564
1546
|
timeout,
|
|
565
1547
|
correlationId: jetStreamCorrelationId,
|
|
566
|
-
messageId
|
|
1548
|
+
messageId,
|
|
1549
|
+
declaredPattern: packet.pattern
|
|
567
1550
|
}).catch(onUnhandled);
|
|
568
1551
|
}
|
|
569
1552
|
return () => {
|
|
@@ -578,38 +1561,118 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
578
1561
|
};
|
|
579
1562
|
}
|
|
580
1563
|
/** Core mode: nc.request() with timeout. */
|
|
581
|
-
async publishCoreRpc(subject, data, customHeaders, timeout, callback) {
|
|
1564
|
+
async publishCoreRpc(subject, data, customHeaders, timeout, callback, declaredPattern) {
|
|
1565
|
+
const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
|
|
1566
|
+
const hdrs = this.buildHeaders(customHeaders, { subject });
|
|
1567
|
+
const encoded = this.codec.encode(data);
|
|
1568
|
+
const spanHandle = beginRpcClientSpan(
|
|
1569
|
+
{
|
|
1570
|
+
subject,
|
|
1571
|
+
payloadBytes: encoded.length,
|
|
1572
|
+
payload: encoded,
|
|
1573
|
+
headers: hdrs,
|
|
1574
|
+
serviceName: this.callerName,
|
|
1575
|
+
endpoint: this.serverEndpoint
|
|
1576
|
+
},
|
|
1577
|
+
this.otel
|
|
1578
|
+
);
|
|
1579
|
+
const startedAt = performance.now();
|
|
582
1580
|
try {
|
|
583
1581
|
const nc = this.readyForPublish ? this.connection.unwrap : await this.connect();
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1582
|
+
const response = await context6.with(
|
|
1583
|
+
spanHandle.activeContext,
|
|
1584
|
+
() => nc.request(subject, encoded, {
|
|
1585
|
+
timeout: effectiveTimeout,
|
|
1586
|
+
headers: hdrs
|
|
1587
|
+
})
|
|
1588
|
+
);
|
|
590
1589
|
const decoded = this.codec.decode(response.data);
|
|
591
1590
|
if (response.headers?.get("x-error" /* Error */)) {
|
|
1591
|
+
spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: decoded });
|
|
1592
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
1593
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
592
1594
|
callback({ err: decoded, response: null, isDisposed: true });
|
|
593
1595
|
} else {
|
|
1596
|
+
spanHandle.finish({ kind: "ok" /* Ok */, reply: decoded });
|
|
1597
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
1598
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "success");
|
|
594
1599
|
callback({ err: null, response: decoded, isDisposed: true });
|
|
595
1600
|
}
|
|
596
1601
|
} catch (err) {
|
|
597
1602
|
const error = err instanceof Error ? err : new Error("Unknown error");
|
|
598
|
-
|
|
599
|
-
|
|
1603
|
+
if (error instanceof TimeoutError) {
|
|
1604
|
+
spanHandle.finish({ kind: "timeout" /* Timeout */ });
|
|
1605
|
+
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, "");
|
|
1606
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
1607
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
|
|
1608
|
+
} else {
|
|
1609
|
+
spanHandle.finish({ kind: "error" /* Error */, error });
|
|
1610
|
+
this.eventBus.emit("error" /* Error */, error, "client-rpc");
|
|
1611
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
|
|
1612
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
1613
|
+
}
|
|
600
1614
|
callback({ err: error, response: null, isDisposed: true });
|
|
601
1615
|
}
|
|
602
1616
|
}
|
|
603
1617
|
/** JetStream mode: publish to stream + wait for inbox response. */
|
|
604
1618
|
async publishJetStreamRpc(subject, data, callback, options) {
|
|
605
|
-
const { headers: customHeaders, correlationId, messageId } = options;
|
|
1619
|
+
const { headers: customHeaders, correlationId, messageId, declaredPattern } = options;
|
|
606
1620
|
const effectiveTimeout = options.timeout ?? this.defaultRpcTimeout;
|
|
607
|
-
this.
|
|
1621
|
+
const hdrs = this.buildHeaders(customHeaders, {
|
|
1622
|
+
subject,
|
|
1623
|
+
correlationId,
|
|
1624
|
+
replyTo: this.inbox ?? ""
|
|
1625
|
+
});
|
|
1626
|
+
const encoded = this.codec.encode(data);
|
|
1627
|
+
const spanHandle = beginRpcClientSpan(
|
|
1628
|
+
{
|
|
1629
|
+
subject,
|
|
1630
|
+
correlationId,
|
|
1631
|
+
payloadBytes: encoded.length,
|
|
1632
|
+
payload: encoded,
|
|
1633
|
+
messageId,
|
|
1634
|
+
headers: hdrs,
|
|
1635
|
+
serviceName: this.callerName,
|
|
1636
|
+
endpoint: this.serverEndpoint
|
|
1637
|
+
},
|
|
1638
|
+
this.otel
|
|
1639
|
+
);
|
|
1640
|
+
const startedAt = performance.now();
|
|
1641
|
+
this.pendingMessages.set(correlationId, (packet) => {
|
|
1642
|
+
if (packet.err) {
|
|
1643
|
+
if (packet.err instanceof Error) {
|
|
1644
|
+
spanHandle.finish({ kind: "error" /* Error */, error: packet.err });
|
|
1645
|
+
} else {
|
|
1646
|
+
spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: packet.err });
|
|
1647
|
+
}
|
|
1648
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
1649
|
+
} else {
|
|
1650
|
+
spanHandle.finish({ kind: "ok" /* Ok */, reply: packet.response });
|
|
1651
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "success");
|
|
1652
|
+
}
|
|
1653
|
+
callback(packet);
|
|
1654
|
+
});
|
|
1655
|
+
const timeoutId = setTimeout(() => {
|
|
1656
|
+
if (!this.pendingMessages.has(correlationId)) return;
|
|
1657
|
+
this.pendingTimeouts.delete(correlationId);
|
|
1658
|
+
this.pendingMessages.delete(correlationId);
|
|
1659
|
+
spanHandle.finish({ kind: "timeout" /* Timeout */ });
|
|
1660
|
+
this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
|
|
1661
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
|
|
1662
|
+
callback({ err: new Error(RPC_TIMEOUT_MESSAGE), response: null, isDisposed: true });
|
|
1663
|
+
}, effectiveTimeout);
|
|
1664
|
+
this.pendingTimeouts.set(correlationId, timeoutId);
|
|
608
1665
|
try {
|
|
609
1666
|
if (!this.readyForPublish) await this.connect();
|
|
610
1667
|
if (!this.pendingMessages.has(correlationId)) return;
|
|
611
1668
|
if (!this.inbox) {
|
|
1669
|
+
clearTimeout(timeoutId);
|
|
1670
|
+
this.pendingTimeouts.delete(correlationId);
|
|
612
1671
|
this.pendingMessages.delete(correlationId);
|
|
1672
|
+
const inboxError = new Error("Inbox not initialized");
|
|
1673
|
+
spanHandle.finish({ kind: "error" /* Error */, error: inboxError });
|
|
1674
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
|
|
1675
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
613
1676
|
callback({
|
|
614
1677
|
err: new Error("Inbox not initialized \u2014 JetStream RPC mode requires a connected inbox"),
|
|
615
1678
|
response: null,
|
|
@@ -617,24 +1680,14 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
617
1680
|
});
|
|
618
1681
|
return;
|
|
619
1682
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
this.
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
this.pendingTimeouts.set(correlationId, timeoutId);
|
|
629
|
-
const hdrs = this.buildHeaders(customHeaders, {
|
|
630
|
-
subject,
|
|
631
|
-
correlationId,
|
|
632
|
-
replyTo: this.inbox
|
|
633
|
-
});
|
|
634
|
-
await this.connection.getJetStreamClient().publish(subject, this.codec.encode(data), {
|
|
635
|
-
headers: hdrs,
|
|
636
|
-
msgID: messageId ?? nuid.next()
|
|
637
|
-
});
|
|
1683
|
+
await context6.with(
|
|
1684
|
+
spanHandle.activeContext,
|
|
1685
|
+
() => this.connection.getJetStreamClient().publish(subject, encoded, {
|
|
1686
|
+
headers: hdrs,
|
|
1687
|
+
msgID: messageId ?? nuid.next()
|
|
1688
|
+
})
|
|
1689
|
+
);
|
|
1690
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
|
|
638
1691
|
} catch (err) {
|
|
639
1692
|
const existingTimeout = this.pendingTimeouts.get(correlationId);
|
|
640
1693
|
if (existingTimeout) {
|
|
@@ -644,10 +1697,34 @@ var JetstreamClient = class extends ClientProxy {
|
|
|
644
1697
|
if (!this.pendingMessages.has(correlationId)) return;
|
|
645
1698
|
this.pendingMessages.delete(correlationId);
|
|
646
1699
|
const error = err instanceof Error ? err : new Error("Unknown error");
|
|
647
|
-
|
|
1700
|
+
spanHandle.finish({ kind: "error" /* Error */, error });
|
|
1701
|
+
this.eventBus.emit("error" /* Error */, error, `jetstream-rpc-publish:${subject}`);
|
|
1702
|
+
this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
|
|
1703
|
+
this.reportRpcCompleted(declaredPattern, startedAt, "error");
|
|
648
1704
|
callback({ err: error, response: null, isDisposed: true });
|
|
649
1705
|
}
|
|
650
1706
|
}
|
|
1707
|
+
// hasHook is per-emit so late subscribers (JetstreamMetricsService during
|
|
1708
|
+
// OnApplicationBootstrap) still receive events.
|
|
1709
|
+
reportPublished(declaredPattern, kind, startedAt, status) {
|
|
1710
|
+
if (!this.eventBus.hasHook("published" /* Published */)) return;
|
|
1711
|
+
this.eventBus.emit(
|
|
1712
|
+
"published" /* Published */,
|
|
1713
|
+
declaredPattern,
|
|
1714
|
+
kind,
|
|
1715
|
+
performance.now() - startedAt,
|
|
1716
|
+
status
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
reportRpcCompleted(declaredPattern, startedAt, status) {
|
|
1720
|
+
if (!this.eventBus.hasHook("rpcCompleted" /* RpcCompleted */)) return;
|
|
1721
|
+
this.eventBus.emit(
|
|
1722
|
+
"rpcCompleted" /* RpcCompleted */,
|
|
1723
|
+
declaredPattern,
|
|
1724
|
+
performance.now() - startedAt,
|
|
1725
|
+
status
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
651
1728
|
/** Fail-fast all pending JetStream RPC callbacks on connection loss. */
|
|
652
1729
|
handleDisconnect() {
|
|
653
1730
|
this.rejectPendingRpcs(new Error("Connection lost"));
|
|
@@ -827,7 +1904,7 @@ var MsgpackCodec = class {
|
|
|
827
1904
|
};
|
|
828
1905
|
|
|
829
1906
|
// src/connection/connection.provider.ts
|
|
830
|
-
import { Logger as
|
|
1907
|
+
import { Logger as Logger6 } from "@nestjs/common";
|
|
831
1908
|
import {
|
|
832
1909
|
connect
|
|
833
1910
|
} from "@nats-io/transport-node";
|
|
@@ -844,6 +1921,10 @@ var ConnectionProvider = class {
|
|
|
844
1921
|
constructor(options, eventBus) {
|
|
845
1922
|
this.options = options;
|
|
846
1923
|
this.eventBus = eventBus;
|
|
1924
|
+
const derived = deriveOtelAttrs(options);
|
|
1925
|
+
this.otel = derived.otel;
|
|
1926
|
+
this.otelServiceName = derived.serviceName;
|
|
1927
|
+
this.otelEndpoint = derived.serverEndpoint;
|
|
847
1928
|
this.nc$ = defer(() => this.getConnection()).pipe(
|
|
848
1929
|
shareReplay({ bufferSize: 1, refCount: false })
|
|
849
1930
|
);
|
|
@@ -856,12 +1937,16 @@ var ConnectionProvider = class {
|
|
|
856
1937
|
nc$;
|
|
857
1938
|
/** Live stream of connection status events (no replay). */
|
|
858
1939
|
status$;
|
|
859
|
-
logger = new
|
|
1940
|
+
logger = new Logger6("Jetstream:Connection");
|
|
860
1941
|
connection = null;
|
|
861
1942
|
connectionPromise = null;
|
|
862
1943
|
jsClient = null;
|
|
863
1944
|
jsmInstance = null;
|
|
864
1945
|
jsmPromise = null;
|
|
1946
|
+
otel;
|
|
1947
|
+
otelServiceName;
|
|
1948
|
+
otelEndpoint;
|
|
1949
|
+
lifecycleSpan = null;
|
|
865
1950
|
/**
|
|
866
1951
|
* Establish NATS connection. Idempotent — returns cached connection on subsequent calls.
|
|
867
1952
|
*
|
|
@@ -911,230 +1996,959 @@ var ConnectionProvider = class {
|
|
|
911
1996
|
get unwrap() {
|
|
912
1997
|
return this.connection;
|
|
913
1998
|
}
|
|
914
|
-
/**
|
|
915
|
-
* Gracefully shut down the connection.
|
|
916
|
-
*
|
|
917
|
-
* Sequence: drain → wait for close. Falls back to force-close on error.
|
|
918
|
-
*/
|
|
919
|
-
async shutdown() {
|
|
920
|
-
if (this.connectionPromise) {
|
|
921
|
-
try {
|
|
922
|
-
await this.connectionPromise;
|
|
923
|
-
} catch {
|
|
1999
|
+
/**
|
|
2000
|
+
* Gracefully shut down the connection.
|
|
2001
|
+
*
|
|
2002
|
+
* Sequence: drain → wait for close. Falls back to force-close on error.
|
|
2003
|
+
*/
|
|
2004
|
+
async shutdown() {
|
|
2005
|
+
if (this.connectionPromise) {
|
|
2006
|
+
try {
|
|
2007
|
+
await this.connectionPromise;
|
|
2008
|
+
} catch {
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (!this.connection || this.connection.isClosed()) return;
|
|
2012
|
+
try {
|
|
2013
|
+
await withShutdownSpan(
|
|
2014
|
+
this.otel,
|
|
2015
|
+
{ serviceName: this.otelServiceName, endpoint: this.otelEndpoint },
|
|
2016
|
+
async () => {
|
|
2017
|
+
try {
|
|
2018
|
+
await this.connection?.drain();
|
|
2019
|
+
await this.connection?.closed();
|
|
2020
|
+
} catch {
|
|
2021
|
+
try {
|
|
2022
|
+
await this.connection?.close();
|
|
2023
|
+
} catch {
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
);
|
|
2028
|
+
} finally {
|
|
2029
|
+
this.lifecycleSpan?.finish();
|
|
2030
|
+
this.lifecycleSpan = null;
|
|
2031
|
+
this.connection = null;
|
|
2032
|
+
this.connectionPromise = null;
|
|
2033
|
+
this.jsClient = null;
|
|
2034
|
+
this.jsmInstance = null;
|
|
2035
|
+
this.jsmPromise = null;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
async initJetStreamManager() {
|
|
2039
|
+
try {
|
|
2040
|
+
const nc = await this.getConnection();
|
|
2041
|
+
this.jsmInstance = await jetstreamManager(nc);
|
|
2042
|
+
this.logger.log("JetStream manager initialized");
|
|
2043
|
+
return this.jsmInstance;
|
|
2044
|
+
} finally {
|
|
2045
|
+
this.jsmPromise = null;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
/** Internal: establish the physical connection with reconnect monitoring. */
|
|
2049
|
+
async establish() {
|
|
2050
|
+
try {
|
|
2051
|
+
const nc = await connect({
|
|
2052
|
+
...DEFAULT_OPTIONS,
|
|
2053
|
+
// Default the NATS connection name to the OTel-derived service name so
|
|
2054
|
+
// `nats server info` lines up with span attributes, but let user-supplied
|
|
2055
|
+
// `connectionOptions.name` win when set.
|
|
2056
|
+
name: this.otelServiceName,
|
|
2057
|
+
...this.options.connectionOptions,
|
|
2058
|
+
servers: this.options.servers
|
|
2059
|
+
});
|
|
2060
|
+
this.connection = nc;
|
|
2061
|
+
this.logger.log(`NATS connection established: ${nc.getServer()}`);
|
|
2062
|
+
this.eventBus.emit("connect" /* Connect */, nc.getServer());
|
|
2063
|
+
this.lifecycleSpan?.finish();
|
|
2064
|
+
this.lifecycleSpan = beginConnectionLifecycleSpan(this.otel, {
|
|
2065
|
+
serviceName: this.otelServiceName,
|
|
2066
|
+
endpoint: this.otelEndpoint,
|
|
2067
|
+
server: nc.getServer()
|
|
2068
|
+
});
|
|
2069
|
+
this.monitorStatus(nc);
|
|
2070
|
+
return nc;
|
|
2071
|
+
} catch (err) {
|
|
2072
|
+
if (err instanceof Error && err.message.includes("REFUSED")) {
|
|
2073
|
+
throw new Error(`NATS connection refused: ${this.options.servers.join(", ")}`);
|
|
2074
|
+
}
|
|
2075
|
+
throw err;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
/** Handle a single `nc.status()` event, emitting hooks and span events. */
|
|
2079
|
+
handleStatusEvent(status, nc) {
|
|
2080
|
+
switch (status.type) {
|
|
2081
|
+
case "disconnect":
|
|
2082
|
+
this.eventBus.emit("disconnect" /* Disconnect */);
|
|
2083
|
+
this.lifecycleSpan?.recordEvent(EVENT_CONNECTION_DISCONNECTED);
|
|
2084
|
+
break;
|
|
2085
|
+
case "reconnect":
|
|
2086
|
+
this.jsClient = null;
|
|
2087
|
+
this.jsmInstance = null;
|
|
2088
|
+
this.jsmPromise = null;
|
|
2089
|
+
this.eventBus.emit("reconnect" /* Reconnect */, nc.getServer());
|
|
2090
|
+
this.lifecycleSpan?.recordEvent(EVENT_CONNECTION_RECONNECTED, {
|
|
2091
|
+
[ATTR_NATS_CONNECTION_SERVER]: nc.getServer()
|
|
2092
|
+
});
|
|
2093
|
+
break;
|
|
2094
|
+
case "error":
|
|
2095
|
+
this.eventBus.emit(
|
|
2096
|
+
"error" /* Error */,
|
|
2097
|
+
status.error,
|
|
2098
|
+
"connection"
|
|
2099
|
+
);
|
|
2100
|
+
break;
|
|
2101
|
+
case "update":
|
|
2102
|
+
case "ldm":
|
|
2103
|
+
case "reconnecting":
|
|
2104
|
+
case "ping":
|
|
2105
|
+
case "staleConnection":
|
|
2106
|
+
case "forceReconnect":
|
|
2107
|
+
case "slowConsumer":
|
|
2108
|
+
case "close":
|
|
2109
|
+
break;
|
|
2110
|
+
default: {
|
|
2111
|
+
const _exhaustive = status;
|
|
2112
|
+
const unknown = _exhaustive.type ?? "unknown";
|
|
2113
|
+
this.logger.warn(`Unhandled NATS status event: ${unknown}`);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
/** Subscribe to connection status events and emit hooks. */
|
|
2118
|
+
monitorStatus(nc) {
|
|
2119
|
+
void (async () => {
|
|
2120
|
+
try {
|
|
2121
|
+
for await (const status of nc.status()) {
|
|
2122
|
+
this.handleStatusEvent(status, nc);
|
|
2123
|
+
}
|
|
2124
|
+
} finally {
|
|
2125
|
+
this.lifecycleSpan?.finish();
|
|
2126
|
+
this.lifecycleSpan = null;
|
|
2127
|
+
}
|
|
2128
|
+
})().catch((err) => {
|
|
2129
|
+
this.logger.error("Status monitor error", err);
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
|
|
2134
|
+
// src/hooks/event-bus.ts
|
|
2135
|
+
var EventBus = class {
|
|
2136
|
+
hooks;
|
|
2137
|
+
logger;
|
|
2138
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
2139
|
+
constructor(logger5, hooks) {
|
|
2140
|
+
this.logger = logger5;
|
|
2141
|
+
this.hooks = hooks ?? {};
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Subscribe to a transport event. Used by built-in observers (e.g. metrics).
|
|
2145
|
+
* Multiple subscribers per event are supported; each is called independently.
|
|
2146
|
+
*/
|
|
2147
|
+
subscribe(event, handler) {
|
|
2148
|
+
const list = this.subscribers.get(event) ?? [];
|
|
2149
|
+
list.push(handler);
|
|
2150
|
+
this.subscribers.set(event, list);
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Emit a lifecycle event. Dispatches to all internal subscribers and the
|
|
2154
|
+
* registered user hook (if any).
|
|
2155
|
+
*/
|
|
2156
|
+
emit(event, ...args) {
|
|
2157
|
+
this.dispatch(event, args);
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Hot-path optimized emit for MessageRouted events.
|
|
2161
|
+
* Avoids rest/spread overhead of the generic `emit()`.
|
|
2162
|
+
*/
|
|
2163
|
+
emitMessageRouted(subject, kind) {
|
|
2164
|
+
this.dispatch("messageRouted" /* MessageRouted */, [subject, kind]);
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Check whether any listener (user hook or internal subscriber) is registered
|
|
2168
|
+
* for the given event. Used by routing hot path to elide the emit call when
|
|
2169
|
+
* no one is listening.
|
|
2170
|
+
*/
|
|
2171
|
+
hasHook(event) {
|
|
2172
|
+
return this.hooks[event] !== void 0 || (this.subscribers.get(event)?.length ?? 0) > 0;
|
|
2173
|
+
}
|
|
2174
|
+
dispatch(event, args) {
|
|
2175
|
+
const subs = this.subscribers.get(event);
|
|
2176
|
+
if (subs?.length) {
|
|
2177
|
+
for (const sub of [...subs]) {
|
|
2178
|
+
this.callHook(event, sub, ...args);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
const hook = this.hooks[event];
|
|
2182
|
+
if (hook) {
|
|
2183
|
+
this.callHook(event, hook, ...args);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
callHook(event, hook, ...args) {
|
|
2187
|
+
try {
|
|
2188
|
+
const result = hook(...args);
|
|
2189
|
+
if (result && typeof result.catch === "function") {
|
|
2190
|
+
result.catch((err) => {
|
|
2191
|
+
this.logger.error(
|
|
2192
|
+
`Async hook "${event}" rejected: ${err instanceof Error ? err.message : err}`
|
|
2193
|
+
);
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
} catch (err) {
|
|
2197
|
+
this.logger.error(
|
|
2198
|
+
`Hook "${event}" threw an error: ${err instanceof Error ? err.message : err}`
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
|
|
2204
|
+
// src/health/jetstream.health-indicator.ts
|
|
2205
|
+
import { Injectable, Logger as Logger7 } from "@nestjs/common";
|
|
2206
|
+
var JetstreamHealthIndicator = class {
|
|
2207
|
+
constructor(connection) {
|
|
2208
|
+
this.connection = connection;
|
|
2209
|
+
}
|
|
2210
|
+
logger = new Logger7("Jetstream:Health");
|
|
2211
|
+
/**
|
|
2212
|
+
* Plain health status check.
|
|
2213
|
+
*
|
|
2214
|
+
* Returns the current connection status without throwing.
|
|
2215
|
+
* Use this for custom health endpoints or monitoring integrations.
|
|
2216
|
+
*
|
|
2217
|
+
* @returns Connection status with server URL and RTT latency.
|
|
2218
|
+
*/
|
|
2219
|
+
async check() {
|
|
2220
|
+
const nc = this.connection.unwrap;
|
|
2221
|
+
if (!nc || nc.isClosed()) {
|
|
2222
|
+
return { connected: false, server: null, latency: null };
|
|
2223
|
+
}
|
|
2224
|
+
try {
|
|
2225
|
+
const start = performance.now();
|
|
2226
|
+
await nc.rtt();
|
|
2227
|
+
const latency = Math.round(performance.now() - start);
|
|
2228
|
+
return { connected: true, server: nc.getServer(), latency };
|
|
2229
|
+
} catch (err) {
|
|
2230
|
+
this.logger.warn(`Health check failed: ${err instanceof Error ? err.message : err}`);
|
|
2231
|
+
return { connected: false, server: nc.getServer(), latency: null };
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
/**
|
|
2235
|
+
* Terminus-compatible health check.
|
|
2236
|
+
*
|
|
2237
|
+
* Returns `{ [key]: { status: 'up', ... } }` on success.
|
|
2238
|
+
* Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
|
|
2239
|
+
*
|
|
2240
|
+
* The thrown error sets `isHealthCheckError: true` and `causes` — the
|
|
2241
|
+
* duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
|
|
2242
|
+
* health failures from unexpected exceptions. Works with both Terminus v10
|
|
2243
|
+
* (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
|
|
2244
|
+
*
|
|
2245
|
+
* @param key - Health indicator key (default: `'jetstream'`).
|
|
2246
|
+
* @returns Object with status, server, and latency under the given key.
|
|
2247
|
+
* @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
|
|
2248
|
+
*/
|
|
2249
|
+
async isHealthy(key = "jetstream") {
|
|
2250
|
+
const status = await this.check();
|
|
2251
|
+
const details = {
|
|
2252
|
+
status: status.connected ? "up" : "down",
|
|
2253
|
+
server: status.server,
|
|
2254
|
+
latency: status.latency
|
|
2255
|
+
};
|
|
2256
|
+
if (!status.connected) {
|
|
2257
|
+
const causes = { [key]: details };
|
|
2258
|
+
throw Object.assign(new Error("Jetstream health check failed"), {
|
|
2259
|
+
causes,
|
|
2260
|
+
isHealthCheckError: true
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
return { [key]: details };
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
JetstreamHealthIndicator = __decorateClass([
|
|
2267
|
+
Injectable()
|
|
2268
|
+
], JetstreamHealthIndicator);
|
|
2269
|
+
|
|
2270
|
+
// src/metrics/metrics.module.ts
|
|
2271
|
+
import { Module } from "@nestjs/common";
|
|
2272
|
+
|
|
2273
|
+
// src/server/routing/pattern-registry.ts
|
|
2274
|
+
import { Logger as Logger8 } from "@nestjs/common";
|
|
2275
|
+
var HANDLER_LABELS = {
|
|
2276
|
+
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
2277
|
+
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
2278
|
+
["ev" /* Event */]: "event" /* Event */,
|
|
2279
|
+
["cmd" /* Command */]: "rpc" /* Rpc */
|
|
2280
|
+
};
|
|
2281
|
+
var PatternRegistry = class {
|
|
2282
|
+
constructor(options) {
|
|
2283
|
+
this.options = options;
|
|
2284
|
+
}
|
|
2285
|
+
logger = new Logger8("Jetstream:PatternRegistry");
|
|
2286
|
+
registry = /* @__PURE__ */ new Map();
|
|
2287
|
+
// Cached after registerHandlers() — the registry is immutable from that point
|
|
2288
|
+
cachedPatterns = null;
|
|
2289
|
+
_hasEvents = false;
|
|
2290
|
+
_hasCommands = false;
|
|
2291
|
+
_hasBroadcasts = false;
|
|
2292
|
+
_hasOrdered = false;
|
|
2293
|
+
_hasMetadata = false;
|
|
2294
|
+
/**
|
|
2295
|
+
* Register all handlers from the NestJS strategy.
|
|
2296
|
+
*
|
|
2297
|
+
* @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
|
|
2298
|
+
*/
|
|
2299
|
+
registerHandlers(handlers) {
|
|
2300
|
+
const serviceName = this.options.name;
|
|
2301
|
+
for (const [pattern, handler] of handlers) {
|
|
2302
|
+
const extras = handler.extras;
|
|
2303
|
+
const isEvent = handler.isEventHandler ?? false;
|
|
2304
|
+
const isBroadcast = !!extras?.broadcast;
|
|
2305
|
+
const isOrdered = !!extras?.ordered;
|
|
2306
|
+
const meta = extras?.meta;
|
|
2307
|
+
if (isBroadcast && isOrdered) {
|
|
2308
|
+
throw new Error(
|
|
2309
|
+
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
let kind;
|
|
2313
|
+
if (isBroadcast) kind = "broadcast" /* Broadcast */;
|
|
2314
|
+
else if (isOrdered) kind = "ordered" /* Ordered */;
|
|
2315
|
+
else if (isEvent) kind = "ev" /* Event */;
|
|
2316
|
+
else kind = "cmd" /* Command */;
|
|
2317
|
+
const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
|
|
2318
|
+
this.registry.set(fullSubject, {
|
|
2319
|
+
handler,
|
|
2320
|
+
pattern,
|
|
2321
|
+
isEvent: isEvent && !isOrdered,
|
|
2322
|
+
isBroadcast,
|
|
2323
|
+
isOrdered,
|
|
2324
|
+
meta
|
|
2325
|
+
});
|
|
2326
|
+
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
2327
|
+
}
|
|
2328
|
+
this.cachedPatterns = this.buildPatternsByKind();
|
|
2329
|
+
this._hasEvents = this.cachedPatterns.events.length > 0;
|
|
2330
|
+
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
2331
|
+
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
2332
|
+
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
2333
|
+
this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
|
|
2334
|
+
this.logSummary();
|
|
2335
|
+
}
|
|
2336
|
+
/** Find handler for a full NATS subject. */
|
|
2337
|
+
getHandler(subject) {
|
|
2338
|
+
return this.registry.get(subject)?.handler ?? null;
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Resolve the declared pattern and {@link StreamKind} for a full NATS subject.
|
|
2342
|
+
*
|
|
2343
|
+
* Returns `null` when the subject is not registered. The declared pattern is
|
|
2344
|
+
* the value the user passed to `@EventPattern`/`@MessagePattern` — stable and
|
|
2345
|
+
* bounded, suitable for use as a Prometheus label without cardinality risk.
|
|
2346
|
+
*/
|
|
2347
|
+
resolveDeclared(subject) {
|
|
2348
|
+
const entry = this.registry.get(subject);
|
|
2349
|
+
if (!entry) return null;
|
|
2350
|
+
return { pattern: entry.pattern, kind: this.resolveStreamKind(entry) };
|
|
2351
|
+
}
|
|
2352
|
+
/** Get all registered broadcast patterns (for consumer filter_subject setup). */
|
|
2353
|
+
getBroadcastPatterns() {
|
|
2354
|
+
return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
|
|
2355
|
+
}
|
|
2356
|
+
hasBroadcastHandlers() {
|
|
2357
|
+
return this._hasBroadcasts;
|
|
2358
|
+
}
|
|
2359
|
+
hasRpcHandlers() {
|
|
2360
|
+
return this._hasCommands;
|
|
2361
|
+
}
|
|
2362
|
+
hasEventHandlers() {
|
|
2363
|
+
return this._hasEvents;
|
|
2364
|
+
}
|
|
2365
|
+
hasOrderedHandlers() {
|
|
2366
|
+
return this._hasOrdered;
|
|
2367
|
+
}
|
|
2368
|
+
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
2369
|
+
getOrderedSubjects() {
|
|
2370
|
+
return this.getPatternsByKind().ordered.map(
|
|
2371
|
+
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
/** Check if any registered handler has metadata. */
|
|
2375
|
+
hasMetadata() {
|
|
2376
|
+
return this._hasMetadata;
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Get handler metadata entries for KV publishing.
|
|
2380
|
+
*
|
|
2381
|
+
* Returns a map of KV key -> metadata object for all handlers that have `meta`.
|
|
2382
|
+
* Key format: `{serviceName}.{kind}.{pattern}`.
|
|
2383
|
+
*/
|
|
2384
|
+
getMetadataEntries() {
|
|
2385
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2386
|
+
for (const entry of this.registry.values()) {
|
|
2387
|
+
if (!entry.meta) continue;
|
|
2388
|
+
const kind = this.resolveStreamKind(entry);
|
|
2389
|
+
const key = metadataKey(this.options.name, kind, entry.pattern);
|
|
2390
|
+
entries.set(key, entry.meta);
|
|
2391
|
+
}
|
|
2392
|
+
return entries;
|
|
2393
|
+
}
|
|
2394
|
+
/** Get patterns grouped by kind (cached after registration). */
|
|
2395
|
+
getPatternsByKind() {
|
|
2396
|
+
const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
|
|
2397
|
+
return {
|
|
2398
|
+
events: [...patterns.events],
|
|
2399
|
+
commands: [...patterns.commands],
|
|
2400
|
+
broadcasts: [...patterns.broadcasts],
|
|
2401
|
+
ordered: [...patterns.ordered]
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
/** Normalize a full NATS subject back to the user-facing pattern. */
|
|
2405
|
+
normalizeSubject(subject) {
|
|
2406
|
+
const name = internalName(this.options.name);
|
|
2407
|
+
const prefixes = [
|
|
2408
|
+
`${name}.${"cmd" /* Command */}.`,
|
|
2409
|
+
`${name}.${"ev" /* Event */}.`,
|
|
2410
|
+
`${name}.${"ordered" /* Ordered */}.`,
|
|
2411
|
+
`${"broadcast" /* Broadcast */}.`
|
|
2412
|
+
];
|
|
2413
|
+
for (const prefix of prefixes) {
|
|
2414
|
+
if (subject.startsWith(prefix)) {
|
|
2415
|
+
return subject.slice(prefix.length);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return subject;
|
|
2419
|
+
}
|
|
2420
|
+
buildPatternsByKind() {
|
|
2421
|
+
const events = [];
|
|
2422
|
+
const commands = [];
|
|
2423
|
+
const broadcasts = [];
|
|
2424
|
+
const ordered = [];
|
|
2425
|
+
for (const entry of this.registry.values()) {
|
|
2426
|
+
if (entry.isBroadcast) broadcasts.push(entry.pattern);
|
|
2427
|
+
else if (entry.isOrdered) ordered.push(entry.pattern);
|
|
2428
|
+
else if (entry.isEvent) events.push(entry.pattern);
|
|
2429
|
+
else commands.push(entry.pattern);
|
|
2430
|
+
}
|
|
2431
|
+
return { events, commands, broadcasts, ordered };
|
|
2432
|
+
}
|
|
2433
|
+
resolveStreamKind(entry) {
|
|
2434
|
+
if (entry.isBroadcast) return "broadcast" /* Broadcast */;
|
|
2435
|
+
if (entry.isOrdered) return "ordered" /* Ordered */;
|
|
2436
|
+
if (entry.isEvent) return "ev" /* Event */;
|
|
2437
|
+
return "cmd" /* Command */;
|
|
2438
|
+
}
|
|
2439
|
+
logSummary() {
|
|
2440
|
+
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
2441
|
+
const parts = [
|
|
2442
|
+
`${commands.length} RPC`,
|
|
2443
|
+
`${events.length} events`,
|
|
2444
|
+
`${broadcasts.length} broadcasts`
|
|
2445
|
+
];
|
|
2446
|
+
if (ordered.length > 0) {
|
|
2447
|
+
parts.push(`${ordered.length} ordered`);
|
|
2448
|
+
}
|
|
2449
|
+
this.logger.log(`Registered handlers: ${parts.join(", ")}`);
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
|
|
2453
|
+
// src/metrics/metrics.constants.ts
|
|
2454
|
+
var JETSTREAM_METRICS_CONFIG = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_CONFIG");
|
|
2455
|
+
var JETSTREAM_METRICS_REGISTRY = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_REGISTRY");
|
|
2456
|
+
var JETSTREAM_METRICS_PROM_CLIENT = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_PROM_CLIENT");
|
|
2457
|
+
var DEFAULT_METRICS_PREFIX = "jetstream_";
|
|
2458
|
+
var DEFAULT_POLL_INTERVAL_MS = 15e3;
|
|
2459
|
+
var DEFAULT_HISTOGRAM_BUCKETS = {
|
|
2460
|
+
handlerDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
|
2461
|
+
publishDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
|
|
2462
|
+
rpcDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
|
2463
|
+
};
|
|
2464
|
+
var ERROR_CONTEXT_PREFIXES = [
|
|
2465
|
+
["connection", "connection"],
|
|
2466
|
+
["codec", "codec"],
|
|
2467
|
+
["client-rpc", "publish"],
|
|
2468
|
+
["jetstream-rpc-publish", "publish"],
|
|
2469
|
+
["publish", "publish"],
|
|
2470
|
+
["message-provider", "consume"],
|
|
2471
|
+
["consume", "consume"],
|
|
2472
|
+
["core-rpc-handler", "handler"],
|
|
2473
|
+
["rpc-handler", "handler"],
|
|
2474
|
+
// EventRouter formats contexts as `${StreamKind.*}-handler:...` — the enum
|
|
2475
|
+
// uses short forms (`ev`, `ordered`, `broadcast`) so both surface in the wild.
|
|
2476
|
+
["ev-handler", "handler"],
|
|
2477
|
+
["event-handler", "handler"],
|
|
2478
|
+
["broadcast-handler", "handler"],
|
|
2479
|
+
["ordered-handler", "handler"],
|
|
2480
|
+
["handler", "handler"],
|
|
2481
|
+
["shutdown", "shutdown"]
|
|
2482
|
+
];
|
|
2483
|
+
var UNMATCHED_SUBJECT_LABEL = "<unmatched>";
|
|
2484
|
+
var STREAM_KIND_LABEL = {
|
|
2485
|
+
["ev" /* Event */]: "event",
|
|
2486
|
+
["cmd" /* Command */]: "command",
|
|
2487
|
+
["broadcast" /* Broadcast */]: "broadcast",
|
|
2488
|
+
["ordered" /* Ordered */]: "ordered"
|
|
2489
|
+
};
|
|
2490
|
+
|
|
2491
|
+
// src/metrics/metrics.service.ts
|
|
2492
|
+
import {
|
|
2493
|
+
Inject,
|
|
2494
|
+
Injectable as Injectable2,
|
|
2495
|
+
Logger as Logger10,
|
|
2496
|
+
Optional
|
|
2497
|
+
} from "@nestjs/common";
|
|
2498
|
+
|
|
2499
|
+
// src/metrics/error-context-mapper.ts
|
|
2500
|
+
var mapErrorContext = (context7) => {
|
|
2501
|
+
if (!context7) return "other";
|
|
2502
|
+
for (const [prefix, mapped] of ERROR_CONTEXT_PREFIXES) {
|
|
2503
|
+
if (context7 === prefix || context7.startsWith(`${prefix}:`)) {
|
|
2504
|
+
return mapped;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
return "other";
|
|
2508
|
+
};
|
|
2509
|
+
|
|
2510
|
+
// src/metrics/metrics.factory.ts
|
|
2511
|
+
var createMetrics = (opts) => {
|
|
2512
|
+
const { register, promClient } = opts;
|
|
2513
|
+
const prefix = opts.prefix ?? DEFAULT_METRICS_PREFIX;
|
|
2514
|
+
const buckets = {
|
|
2515
|
+
handlerDuration: opts.buckets?.handlerDuration ?? DEFAULT_HISTOGRAM_BUCKETS.handlerDuration,
|
|
2516
|
+
publishDuration: opts.buckets?.publishDuration ?? DEFAULT_HISTOGRAM_BUCKETS.publishDuration,
|
|
2517
|
+
rpcDuration: opts.buckets?.rpcDuration ?? DEFAULT_HISTOGRAM_BUCKETS.rpcDuration
|
|
2518
|
+
};
|
|
2519
|
+
if (opts.defaultLabels && Object.keys(opts.defaultLabels).length > 0) {
|
|
2520
|
+
register.setDefaultLabels(opts.defaultLabels);
|
|
2521
|
+
}
|
|
2522
|
+
const counter = (name, help, labelNames) => new promClient.Counter({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
|
|
2523
|
+
const histogram = (name, help, labelNames, bucketArr) => new promClient.Histogram({
|
|
2524
|
+
name: `${prefix}${name}`,
|
|
2525
|
+
help,
|
|
2526
|
+
labelNames,
|
|
2527
|
+
buckets: bucketArr,
|
|
2528
|
+
registers: [register]
|
|
2529
|
+
});
|
|
2530
|
+
const gauge = (name, help, labelNames) => new promClient.Gauge({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
|
|
2531
|
+
return {
|
|
2532
|
+
messagesReceivedTotal: counter(
|
|
2533
|
+
"messages_received_total",
|
|
2534
|
+
"Total messages routed to a handler.",
|
|
2535
|
+
["stream", "subject", "kind"]
|
|
2536
|
+
),
|
|
2537
|
+
messagesProcessedTotal: counter(
|
|
2538
|
+
"messages_processed_total",
|
|
2539
|
+
"Total messages whose handler completed.",
|
|
2540
|
+
["stream", "subject", "kind", "status"]
|
|
2541
|
+
),
|
|
2542
|
+
messagesUnhandledTotal: counter(
|
|
2543
|
+
"messages_unhandled_total",
|
|
2544
|
+
"Messages received but not matching any registered handler.",
|
|
2545
|
+
["subject"]
|
|
2546
|
+
),
|
|
2547
|
+
messagesDeadLetterTotal: counter(
|
|
2548
|
+
"messages_dead_letter_total",
|
|
2549
|
+
"Messages routed to dead-letter after exhausting redelivery attempts.",
|
|
2550
|
+
["stream", "subject"]
|
|
2551
|
+
),
|
|
2552
|
+
publishTotal: counter(
|
|
2553
|
+
"publish_total",
|
|
2554
|
+
"Total publish/send operations performed by the client.",
|
|
2555
|
+
["subject", "kind", "status"]
|
|
2556
|
+
),
|
|
2557
|
+
rpcTimeoutTotal: counter("rpc_timeout_total", "RPC calls that exceeded the timeout deadline.", [
|
|
2558
|
+
"subject"
|
|
2559
|
+
]),
|
|
2560
|
+
consumerRecoveredTotal: counter(
|
|
2561
|
+
"consumer_recovered_total",
|
|
2562
|
+
"Self-healing recoveries after consume-loop failures.",
|
|
2563
|
+
["kind"]
|
|
2564
|
+
),
|
|
2565
|
+
errorsTotal: counter("errors_total", "Transport-level errors emitted on the EventBus.", [
|
|
2566
|
+
"context"
|
|
2567
|
+
]),
|
|
2568
|
+
handlerDurationSeconds: histogram(
|
|
2569
|
+
"handler_duration_seconds",
|
|
2570
|
+
"Wall-clock duration of handler execution.",
|
|
2571
|
+
["stream", "subject", "kind", "status"],
|
|
2572
|
+
buckets.handlerDuration
|
|
2573
|
+
),
|
|
2574
|
+
publishDurationSeconds: histogram(
|
|
2575
|
+
"publish_duration_seconds",
|
|
2576
|
+
"Wall-clock duration of client publish/send operations.",
|
|
2577
|
+
["subject", "kind", "status"],
|
|
2578
|
+
buckets.publishDuration
|
|
2579
|
+
),
|
|
2580
|
+
rpcDurationSeconds: histogram(
|
|
2581
|
+
"rpc_duration_seconds",
|
|
2582
|
+
"Wall-clock duration of RPC round-trips from client perspective.",
|
|
2583
|
+
["subject", "status"],
|
|
2584
|
+
buckets.rpcDuration
|
|
2585
|
+
),
|
|
2586
|
+
consumerNumPending: gauge(
|
|
2587
|
+
"consumer_num_pending",
|
|
2588
|
+
"Messages not yet delivered to this consumer.",
|
|
2589
|
+
["stream", "consumer", "kind"]
|
|
2590
|
+
),
|
|
2591
|
+
consumerNumAckPending: gauge(
|
|
2592
|
+
"consumer_num_ack_pending",
|
|
2593
|
+
"Messages delivered but not yet acked.",
|
|
2594
|
+
["stream", "consumer", "kind"]
|
|
2595
|
+
),
|
|
2596
|
+
consumerNumRedelivered: gauge(
|
|
2597
|
+
"consumer_num_redelivered",
|
|
2598
|
+
"Messages currently in redelivery state.",
|
|
2599
|
+
["stream", "consumer", "kind"]
|
|
2600
|
+
),
|
|
2601
|
+
consumerNumWaiting: gauge(
|
|
2602
|
+
"consumer_num_waiting",
|
|
2603
|
+
"Pull-request waiting count for this consumer.",
|
|
2604
|
+
["stream", "consumer", "kind"]
|
|
2605
|
+
),
|
|
2606
|
+
streamMessages: gauge("stream_messages", "Total messages stored in this stream.", ["stream"]),
|
|
2607
|
+
streamBytes: gauge("stream_bytes", "Total bytes stored in this stream.", ["stream"]),
|
|
2608
|
+
connectionUp: gauge("connection_up", "NATS connection state (1 connected, 0 disconnected).", [
|
|
2609
|
+
"server"
|
|
2610
|
+
]),
|
|
2611
|
+
metricsPollErrorsTotal: counter(
|
|
2612
|
+
"metrics_poll_errors_total",
|
|
2613
|
+
"Errors encountered while polling JetStreamManager for gauge data.",
|
|
2614
|
+
["target"]
|
|
2615
|
+
)
|
|
2616
|
+
};
|
|
2617
|
+
};
|
|
2618
|
+
|
|
2619
|
+
// src/metrics/poll-runner.ts
|
|
2620
|
+
import { Logger as Logger9 } from "@nestjs/common";
|
|
2621
|
+
var PollRunner = class {
|
|
2622
|
+
constructor(opts) {
|
|
2623
|
+
this.opts = opts;
|
|
2624
|
+
}
|
|
2625
|
+
logger = new Logger9("Jetstream:Metrics:Poll");
|
|
2626
|
+
timer = null;
|
|
2627
|
+
inFlight = null;
|
|
2628
|
+
start() {
|
|
2629
|
+
if (this.timer !== null) return;
|
|
2630
|
+
if (this.opts.intervalMs <= 0) return;
|
|
2631
|
+
if (this.opts.targets.length === 0) return;
|
|
2632
|
+
this.timer = setInterval(() => {
|
|
2633
|
+
if (this.inFlight !== null) {
|
|
2634
|
+
this.logger.warn("Skipping poll tick \u2014 previous cycle still in flight");
|
|
2635
|
+
return;
|
|
924
2636
|
}
|
|
2637
|
+
this.inFlight = this.tick().finally(() => {
|
|
2638
|
+
this.inFlight = null;
|
|
2639
|
+
});
|
|
2640
|
+
}, this.opts.intervalMs);
|
|
2641
|
+
}
|
|
2642
|
+
async stop() {
|
|
2643
|
+
if (this.timer !== null) {
|
|
2644
|
+
clearInterval(this.timer);
|
|
2645
|
+
this.timer = null;
|
|
925
2646
|
}
|
|
926
|
-
if (
|
|
2647
|
+
if (this.inFlight !== null) await this.inFlight;
|
|
2648
|
+
}
|
|
2649
|
+
/** @internal Visible for tests. Runs one poll cycle. */
|
|
2650
|
+
async tick() {
|
|
2651
|
+
let jsm;
|
|
927
2652
|
try {
|
|
928
|
-
await this.
|
|
929
|
-
await this.connection.closed();
|
|
2653
|
+
jsm = await this.opts.jsmFactory();
|
|
930
2654
|
} catch {
|
|
2655
|
+
this.recordPollError("jsm.connect");
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
await Promise.all([this.pollConsumers(jsm), this.pollStreams(jsm)]);
|
|
2659
|
+
}
|
|
2660
|
+
async pollConsumers(jsm) {
|
|
2661
|
+
for (const target of this.opts.targets) {
|
|
931
2662
|
try {
|
|
932
|
-
await
|
|
2663
|
+
const info = await jsm.consumers.info(target.stream, target.consumer);
|
|
2664
|
+
const labels = {
|
|
2665
|
+
stream: target.stream,
|
|
2666
|
+
consumer: target.consumer,
|
|
2667
|
+
kind: STREAM_KIND_LABEL[target.kind]
|
|
2668
|
+
};
|
|
2669
|
+
this.opts.metrics.consumerNumPending.labels(labels).set(info.num_pending);
|
|
2670
|
+
this.opts.metrics.consumerNumAckPending.labels(labels).set(info.num_ack_pending);
|
|
2671
|
+
this.opts.metrics.consumerNumRedelivered.labels(labels).set(info.num_redelivered);
|
|
2672
|
+
this.opts.metrics.consumerNumWaiting.labels(labels).set(info.num_waiting);
|
|
933
2673
|
} catch {
|
|
2674
|
+
this.recordPollError("consumer.info");
|
|
934
2675
|
}
|
|
935
|
-
} finally {
|
|
936
|
-
this.connection = null;
|
|
937
|
-
this.connectionPromise = null;
|
|
938
|
-
this.jsClient = null;
|
|
939
|
-
this.jsmInstance = null;
|
|
940
|
-
this.jsmPromise = null;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
async initJetStreamManager() {
|
|
944
|
-
try {
|
|
945
|
-
const nc = await this.getConnection();
|
|
946
|
-
this.jsmInstance = await jetstreamManager(nc);
|
|
947
|
-
this.logger.log("JetStream manager initialized");
|
|
948
|
-
return this.jsmInstance;
|
|
949
|
-
} finally {
|
|
950
|
-
this.jsmPromise = null;
|
|
951
2676
|
}
|
|
952
2677
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
});
|
|
963
|
-
this.connection = nc;
|
|
964
|
-
this.logger.log(`NATS connection established: ${nc.getServer()}`);
|
|
965
|
-
this.eventBus.emit("connect" /* Connect */, nc.getServer());
|
|
966
|
-
this.monitorStatus(nc);
|
|
967
|
-
return nc;
|
|
968
|
-
} catch (err) {
|
|
969
|
-
if (err instanceof Error && err.message.includes("REFUSED")) {
|
|
970
|
-
throw new Error(`NATS connection refused: ${this.options.servers.join(", ")}`);
|
|
2678
|
+
async pollStreams(jsm) {
|
|
2679
|
+
const uniqueStreams = new Set(this.opts.targets.map((t) => t.stream));
|
|
2680
|
+
for (const stream of uniqueStreams) {
|
|
2681
|
+
try {
|
|
2682
|
+
const info = await jsm.streams.info(stream);
|
|
2683
|
+
this.opts.metrics.streamMessages.labels({ stream }).set(info.state.messages);
|
|
2684
|
+
this.opts.metrics.streamBytes.labels({ stream }).set(info.state.bytes);
|
|
2685
|
+
} catch {
|
|
2686
|
+
this.recordPollError("stream.info");
|
|
971
2687
|
}
|
|
972
|
-
throw err;
|
|
973
2688
|
}
|
|
974
2689
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
void (async () => {
|
|
978
|
-
for await (const status of nc.status()) {
|
|
979
|
-
switch (status.type) {
|
|
980
|
-
case "disconnect":
|
|
981
|
-
this.eventBus.emit("disconnect" /* Disconnect */);
|
|
982
|
-
break;
|
|
983
|
-
case "reconnect":
|
|
984
|
-
this.jsClient = null;
|
|
985
|
-
this.jsmInstance = null;
|
|
986
|
-
this.jsmPromise = null;
|
|
987
|
-
this.eventBus.emit("reconnect" /* Reconnect */, nc.getServer());
|
|
988
|
-
break;
|
|
989
|
-
case "error":
|
|
990
|
-
this.eventBus.emit(
|
|
991
|
-
"error" /* Error */,
|
|
992
|
-
status.error,
|
|
993
|
-
"connection"
|
|
994
|
-
);
|
|
995
|
-
break;
|
|
996
|
-
case "update":
|
|
997
|
-
case "ldm":
|
|
998
|
-
case "reconnecting":
|
|
999
|
-
case "ping":
|
|
1000
|
-
case "staleConnection":
|
|
1001
|
-
case "forceReconnect":
|
|
1002
|
-
case "slowConsumer":
|
|
1003
|
-
case "close":
|
|
1004
|
-
break;
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
})().catch((err) => {
|
|
1008
|
-
this.logger.error("Status monitor error", err);
|
|
1009
|
-
});
|
|
2690
|
+
recordPollError(target) {
|
|
2691
|
+
this.opts.metrics.metricsPollErrorsTotal.labels({ target }).inc();
|
|
1010
2692
|
}
|
|
1011
2693
|
};
|
|
1012
2694
|
|
|
1013
|
-
// src/
|
|
1014
|
-
var
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
this.
|
|
1019
|
-
this.
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
* Emit a lifecycle event. Dispatches to custom hook if registered, otherwise no-op.
|
|
1023
|
-
*
|
|
1024
|
-
* @param event - The {@link TransportEvent} to emit.
|
|
1025
|
-
* @param args - Arguments matching the hook signature for this event.
|
|
1026
|
-
*/
|
|
1027
|
-
emit(event, ...args) {
|
|
1028
|
-
const hook = this.hooks[event];
|
|
1029
|
-
if (!hook) return;
|
|
1030
|
-
this.callHook(event, hook, ...args);
|
|
2695
|
+
// src/metrics/metrics.service.ts
|
|
2696
|
+
var JetstreamMetricsService = class {
|
|
2697
|
+
constructor(eventBus, config, promClient, options, patternRegistry, connection = null) {
|
|
2698
|
+
this.eventBus = eventBus;
|
|
2699
|
+
this.config = config;
|
|
2700
|
+
this.promClient = promClient;
|
|
2701
|
+
this.options = options;
|
|
2702
|
+
this.patternRegistry = patternRegistry;
|
|
2703
|
+
this.connection = connection;
|
|
1031
2704
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
if (!
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
2705
|
+
logger = new Logger10("Jetstream:Metrics");
|
|
2706
|
+
metrics = null;
|
|
2707
|
+
pollRunner = null;
|
|
2708
|
+
activeServers = /* @__PURE__ */ new Set();
|
|
2709
|
+
async onApplicationBootstrap() {
|
|
2710
|
+
if (this.metrics !== null) return;
|
|
2711
|
+
if (!this.config.register) {
|
|
2712
|
+
throw new Error(
|
|
2713
|
+
"JetstreamMetricsService requires a prom-client Registry \u2014 none was resolved by JetstreamMetricsModule."
|
|
2714
|
+
);
|
|
2715
|
+
}
|
|
2716
|
+
this.metrics = createMetrics({
|
|
2717
|
+
register: this.config.register,
|
|
2718
|
+
promClient: this.promClient,
|
|
2719
|
+
prefix: this.config.prefix,
|
|
2720
|
+
defaultLabels: this.config.defaultLabels,
|
|
2721
|
+
buckets: this.config.buckets
|
|
2722
|
+
});
|
|
2723
|
+
this.subscribeToEvents();
|
|
2724
|
+
this.syncInitialConnectionState();
|
|
2725
|
+
this.startPolling();
|
|
2726
|
+
this.logger.log(
|
|
2727
|
+
`Metrics enabled (prefix=${this.config.prefix ?? DEFAULT_METRICS_PREFIX}, poll=${this.getEffectivePollInterval()}ms)`
|
|
1044
2728
|
);
|
|
1045
2729
|
}
|
|
2730
|
+
async onModuleDestroy() {
|
|
2731
|
+
await this.pollRunner?.stop();
|
|
2732
|
+
this.pollRunner = null;
|
|
2733
|
+
}
|
|
2734
|
+
/** @internal Visible for tests. `0` disables polling. */
|
|
2735
|
+
getEffectivePollInterval() {
|
|
2736
|
+
return this.config.pollInterval ?? DEFAULT_POLL_INTERVAL_MS;
|
|
2737
|
+
}
|
|
1046
2738
|
/**
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
2739
|
+
* NATS connects during early bootstrap, before this service subscribes to
|
|
2740
|
+
* the EventBus — the initial `Connect` emission misses us. Mirror the
|
|
2741
|
+
* current state here so `connection_up` reflects reality the moment metrics
|
|
2742
|
+
* come online; later disconnects/reconnects update it normally.
|
|
1051
2743
|
*/
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
)
|
|
2744
|
+
syncInitialConnectionState() {
|
|
2745
|
+
const nc = this.connection?.unwrap;
|
|
2746
|
+
if (!nc) return;
|
|
2747
|
+
const server = nc.getServer();
|
|
2748
|
+
this.activeServers.add(server);
|
|
2749
|
+
this.metrics?.connectionUp.labels({ server }).set(1);
|
|
2750
|
+
}
|
|
2751
|
+
/** Skips polling for publisher-only deployments and when no kinds are active. */
|
|
2752
|
+
startPolling() {
|
|
2753
|
+
const interval = this.getEffectivePollInterval();
|
|
2754
|
+
const connection = this.connection;
|
|
2755
|
+
if (interval <= 0 || !this.patternRegistry || !connection || !this.metrics) return;
|
|
2756
|
+
const targets = this.buildPollTargets();
|
|
2757
|
+
if (targets.length === 0) return;
|
|
2758
|
+
this.pollRunner = new PollRunner({
|
|
2759
|
+
intervalMs: interval,
|
|
2760
|
+
jsmFactory: async () => connection.getJetStreamManager(),
|
|
2761
|
+
metrics: this.metrics,
|
|
2762
|
+
targets
|
|
2763
|
+
});
|
|
2764
|
+
this.pollRunner.start();
|
|
2765
|
+
}
|
|
2766
|
+
buildPollTargets() {
|
|
2767
|
+
const registry = this.patternRegistry;
|
|
2768
|
+
if (!registry) return [];
|
|
2769
|
+
const targets = [];
|
|
2770
|
+
if (registry.hasEventHandlers()) {
|
|
2771
|
+
targets.push({
|
|
2772
|
+
kind: "ev" /* Event */,
|
|
2773
|
+
stream: streamName(this.options.name, "ev" /* Event */),
|
|
2774
|
+
consumer: consumerName(this.options.name, "ev" /* Event */)
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
if (registry.hasRpcHandlers() && isJetStreamRpcMode(this.options.rpc)) {
|
|
2778
|
+
targets.push({
|
|
2779
|
+
kind: "cmd" /* Command */,
|
|
2780
|
+
stream: streamName(this.options.name, "cmd" /* Command */),
|
|
2781
|
+
consumer: consumerName(this.options.name, "cmd" /* Command */)
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
if (registry.hasBroadcastHandlers()) {
|
|
2785
|
+
targets.push({
|
|
2786
|
+
kind: "broadcast" /* Broadcast */,
|
|
2787
|
+
stream: streamName(this.options.name, "broadcast" /* Broadcast */),
|
|
2788
|
+
consumer: consumerName(this.options.name, "broadcast" /* Broadcast */)
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
return targets;
|
|
2792
|
+
}
|
|
2793
|
+
subscribeToEvents() {
|
|
2794
|
+
this.eventBus.subscribe("connect" /* Connect */, this.onConnect);
|
|
2795
|
+
this.eventBus.subscribe("disconnect" /* Disconnect */, this.onDisconnect);
|
|
2796
|
+
this.eventBus.subscribe("reconnect" /* Reconnect */, this.onReconnect);
|
|
2797
|
+
this.eventBus.subscribe("error" /* Error */, this.onError);
|
|
2798
|
+
this.eventBus.subscribe("rpcTimeout" /* RpcTimeout */, this.onRpcTimeout);
|
|
2799
|
+
this.eventBus.subscribe("messageRouted" /* MessageRouted */, this.onMessageRouted);
|
|
2800
|
+
this.eventBus.subscribe("deadLetter" /* DeadLetter */, this.onDeadLetter);
|
|
2801
|
+
this.eventBus.subscribe("consumerRecovered" /* ConsumerRecovered */, this.onConsumerRecovered);
|
|
2802
|
+
this.eventBus.subscribe("handlerCompleted" /* HandlerCompleted */, this.onHandlerCompleted);
|
|
2803
|
+
this.eventBus.subscribe("published" /* Published */, this.onPublished);
|
|
2804
|
+
this.eventBus.subscribe("rpcCompleted" /* RpcCompleted */, this.onRpcCompleted);
|
|
2805
|
+
}
|
|
2806
|
+
onConnect = (server) => {
|
|
2807
|
+
this.activeServers.add(server);
|
|
2808
|
+
this.metrics?.connectionUp.labels({ server }).set(1);
|
|
2809
|
+
};
|
|
2810
|
+
onReconnect = (server) => {
|
|
2811
|
+
this.activeServers.add(server);
|
|
2812
|
+
this.metrics?.connectionUp.labels({ server }).set(1);
|
|
2813
|
+
};
|
|
2814
|
+
onDisconnect = () => {
|
|
2815
|
+
for (const server of this.activeServers) {
|
|
2816
|
+
this.metrics?.connectionUp.labels({ server }).set(0);
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
2819
|
+
onError = (_err, context7) => {
|
|
2820
|
+
this.metrics?.errorsTotal.labels({ context: mapErrorContext(context7) }).inc();
|
|
2821
|
+
};
|
|
2822
|
+
onRpcTimeout = (subject, _correlationId) => {
|
|
2823
|
+
const declared = this.resolveDeclared(subject);
|
|
2824
|
+
const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
|
|
2825
|
+
this.metrics?.rpcTimeoutTotal.labels({ subject: subjectLabel }).inc();
|
|
2826
|
+
};
|
|
2827
|
+
// `_kind` collapses broadcast/ordered into MessageKind.Event — we use
|
|
2828
|
+
// declared.kind from PatternRegistry for the precise label instead.
|
|
2829
|
+
onMessageRouted = (subject, _kind) => {
|
|
2830
|
+
if (!this.metrics) return;
|
|
2831
|
+
const declared = this.resolveDeclared(subject);
|
|
2832
|
+
if (!declared) {
|
|
2833
|
+
this.metrics.messagesUnhandledTotal.labels({ subject: UNMATCHED_SUBJECT_LABEL }).inc();
|
|
2834
|
+
return;
|
|
1069
2835
|
}
|
|
2836
|
+
this.metrics.messagesReceivedTotal.labels({
|
|
2837
|
+
stream: streamName(this.options.name, declared.kind),
|
|
2838
|
+
subject: declared.pattern,
|
|
2839
|
+
kind: STREAM_KIND_LABEL[declared.kind]
|
|
2840
|
+
}).inc();
|
|
2841
|
+
};
|
|
2842
|
+
onDeadLetter = (info) => {
|
|
2843
|
+
const declared = this.resolveDeclared(info.subject);
|
|
2844
|
+
const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
|
|
2845
|
+
this.metrics?.messagesDeadLetterTotal.labels({ stream: info.stream, subject: subjectLabel }).inc();
|
|
2846
|
+
};
|
|
2847
|
+
onConsumerRecovered = (label, _attempts) => {
|
|
2848
|
+
const kindLabel = STREAM_KIND_LABEL[label] ?? String(label);
|
|
2849
|
+
this.metrics?.consumerRecoveredTotal.labels({ kind: kindLabel }).inc();
|
|
2850
|
+
};
|
|
2851
|
+
onHandlerCompleted = (pattern, kind, durationMs, status) => {
|
|
2852
|
+
if (!this.metrics) return;
|
|
2853
|
+
const stream = streamName(this.options.name, kind);
|
|
2854
|
+
const kindLabel = STREAM_KIND_LABEL[kind];
|
|
2855
|
+
const labels = { stream, subject: pattern, kind: kindLabel, status };
|
|
2856
|
+
this.metrics.messagesProcessedTotal.labels(labels).inc();
|
|
2857
|
+
this.metrics.handlerDurationSeconds.labels(labels).observe(durationMs / 1e3);
|
|
2858
|
+
};
|
|
2859
|
+
onPublished = (pattern, kind, durationMs, status) => {
|
|
2860
|
+
if (!this.metrics) return;
|
|
2861
|
+
const labels = { subject: pattern, kind: STREAM_KIND_LABEL[kind], status };
|
|
2862
|
+
this.metrics.publishTotal.labels(labels).inc();
|
|
2863
|
+
this.metrics.publishDurationSeconds.labels(labels).observe(durationMs / 1e3);
|
|
2864
|
+
};
|
|
2865
|
+
onRpcCompleted = (pattern, durationMs, status) => {
|
|
2866
|
+
this.metrics?.rpcDurationSeconds.labels({ subject: pattern, status }).observe(durationMs / 1e3);
|
|
2867
|
+
};
|
|
2868
|
+
resolveDeclared(subject) {
|
|
2869
|
+
return this.patternRegistry?.resolveDeclared(subject) ?? null;
|
|
1070
2870
|
}
|
|
1071
2871
|
};
|
|
2872
|
+
JetstreamMetricsService = __decorateClass([
|
|
2873
|
+
Injectable2(),
|
|
2874
|
+
__decorateParam(1, Inject(JETSTREAM_METRICS_CONFIG)),
|
|
2875
|
+
__decorateParam(2, Inject(JETSTREAM_METRICS_PROM_CLIENT)),
|
|
2876
|
+
__decorateParam(3, Inject(JETSTREAM_OPTIONS)),
|
|
2877
|
+
__decorateParam(4, Optional()),
|
|
2878
|
+
__decorateParam(5, Optional()),
|
|
2879
|
+
__decorateParam(5, Inject(JETSTREAM_CONNECTION))
|
|
2880
|
+
], JetstreamMetricsService);
|
|
1072
2881
|
|
|
1073
|
-
// src/
|
|
1074
|
-
|
|
1075
|
-
var
|
|
1076
|
-
|
|
1077
|
-
|
|
2882
|
+
// src/metrics/metrics.module.ts
|
|
2883
|
+
var PROM_CLIENT_INSTALL_MESSAGE = "prom-client is required when JetstreamModule.forRoot({ metrics: ... }) is enabled. Install it with: pnpm add prom-client";
|
|
2884
|
+
var resolvePromClient = async () => {
|
|
2885
|
+
try {
|
|
2886
|
+
return await import("prom-client");
|
|
2887
|
+
} catch {
|
|
2888
|
+
throw new Error(PROM_CLIENT_INSTALL_MESSAGE);
|
|
1078
2889
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
const start = performance.now();
|
|
1095
|
-
await nc.rtt();
|
|
1096
|
-
const latency = Math.round(performance.now() - start);
|
|
1097
|
-
return { connected: true, server: nc.getServer(), latency };
|
|
1098
|
-
} catch (err) {
|
|
1099
|
-
this.logger.warn(`Health check failed: ${err instanceof Error ? err.message : err}`);
|
|
1100
|
-
return { connected: false, server: nc.getServer(), latency: null };
|
|
2890
|
+
};
|
|
2891
|
+
var normalizeMetricsConfig = (option, promClient) => {
|
|
2892
|
+
const user = option && option !== true ? option : {};
|
|
2893
|
+
return {
|
|
2894
|
+
register: user.register ?? promClient.register,
|
|
2895
|
+
prefix: user.prefix ?? DEFAULT_METRICS_PREFIX,
|
|
2896
|
+
defaultLabels: user.defaultLabels,
|
|
2897
|
+
pollInterval: user.pollInterval ?? DEFAULT_POLL_INTERVAL_MS,
|
|
2898
|
+
buckets: user.buckets
|
|
2899
|
+
};
|
|
2900
|
+
};
|
|
2901
|
+
var JetstreamMetricsModule = class {
|
|
2902
|
+
static forFeature(metricsOption) {
|
|
2903
|
+
if (!metricsOption) {
|
|
2904
|
+
return { module: JetstreamMetricsModule, providers: [], exports: [] };
|
|
1101
2905
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
2906
|
+
const promClientProvider = {
|
|
2907
|
+
provide: JETSTREAM_METRICS_PROM_CLIENT,
|
|
2908
|
+
useFactory: async () => {
|
|
2909
|
+
const mod = await resolvePromClient();
|
|
2910
|
+
return { Counter: mod.Counter, Histogram: mod.Histogram, Gauge: mod.Gauge };
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
2913
|
+
const configProvider = {
|
|
2914
|
+
provide: JETSTREAM_METRICS_CONFIG,
|
|
2915
|
+
useFactory: async () => {
|
|
2916
|
+
const mod = await resolvePromClient();
|
|
2917
|
+
return normalizeMetricsConfig(metricsOption, mod);
|
|
2918
|
+
}
|
|
2919
|
+
};
|
|
2920
|
+
const registryProvider = {
|
|
2921
|
+
provide: JETSTREAM_METRICS_REGISTRY,
|
|
2922
|
+
inject: [JETSTREAM_METRICS_CONFIG],
|
|
2923
|
+
useFactory: (cfg) => cfg.register
|
|
2924
|
+
};
|
|
2925
|
+
const serviceProvider = {
|
|
2926
|
+
provide: JetstreamMetricsService,
|
|
2927
|
+
inject: [
|
|
2928
|
+
JETSTREAM_EVENT_BUS,
|
|
2929
|
+
JETSTREAM_METRICS_CONFIG,
|
|
2930
|
+
JETSTREAM_METRICS_PROM_CLIENT,
|
|
2931
|
+
JETSTREAM_OPTIONS,
|
|
2932
|
+
{ token: PatternRegistry, optional: true },
|
|
2933
|
+
{ token: JETSTREAM_CONNECTION, optional: true }
|
|
2934
|
+
],
|
|
2935
|
+
useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection) => new JetstreamMetricsService(eventBus, cfg, runtime, opts, patternRegistry, connection)
|
|
2936
|
+
};
|
|
2937
|
+
return {
|
|
2938
|
+
module: JetstreamMetricsModule,
|
|
2939
|
+
providers: [promClientProvider, configProvider, registryProvider, serviceProvider],
|
|
2940
|
+
exports: [
|
|
2941
|
+
JetstreamMetricsService,
|
|
2942
|
+
JETSTREAM_METRICS_CONFIG,
|
|
2943
|
+
JETSTREAM_METRICS_REGISTRY,
|
|
2944
|
+
JETSTREAM_METRICS_PROM_CLIENT
|
|
2945
|
+
]
|
|
1124
2946
|
};
|
|
1125
|
-
if (!status.connected) {
|
|
1126
|
-
const causes = { [key]: details };
|
|
1127
|
-
throw Object.assign(new Error("Jetstream health check failed"), {
|
|
1128
|
-
causes,
|
|
1129
|
-
isHealthCheckError: true
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
return { [key]: details };
|
|
1133
2947
|
}
|
|
1134
2948
|
};
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
],
|
|
2949
|
+
JetstreamMetricsModule = __decorateClass([
|
|
2950
|
+
Module({})
|
|
2951
|
+
], JetstreamMetricsModule);
|
|
1138
2952
|
|
|
1139
2953
|
// src/server/strategy.ts
|
|
1140
2954
|
import { Server } from "@nestjs/microservices";
|
|
@@ -1210,6 +3024,26 @@ var JetstreamStrategy = class extends Server {
|
|
|
1210
3024
|
this.messageProvider.destroy();
|
|
1211
3025
|
this.started = false;
|
|
1212
3026
|
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Override NestJS `Server.addHandler` to fail-fast on duplicate pattern registration.
|
|
3029
|
+
*
|
|
3030
|
+
* The base class silently overwrites duplicate RPC handlers (last wins) and appends
|
|
3031
|
+
* duplicate event handlers to a linked list. Both behaviors are hazardous in a
|
|
3032
|
+
* JetStream context: silent overwrite drops a handler the user wrote, and double
|
|
3033
|
+
* event dispatch double-acks/double-processes the same JetStream message.
|
|
3034
|
+
*
|
|
3035
|
+
* We treat any pattern collision as a fatal misconfiguration so it surfaces at
|
|
3036
|
+
* bootstrap instead of in production traffic.
|
|
3037
|
+
*/
|
|
3038
|
+
addHandler(pattern, callback, isEventHandler = false, extras = {}) {
|
|
3039
|
+
const normalizedPattern = this.normalizePattern(pattern);
|
|
3040
|
+
if (this.messageHandlers.has(normalizedPattern)) {
|
|
3041
|
+
throw new Error(
|
|
3042
|
+
`Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice \u2014 find and remove the second declaration.`
|
|
3043
|
+
);
|
|
3044
|
+
}
|
|
3045
|
+
super.addHandler(pattern, callback, isEventHandler, extras);
|
|
3046
|
+
}
|
|
1213
3047
|
/**
|
|
1214
3048
|
* Register event listener (required by Server base class).
|
|
1215
3049
|
*
|
|
@@ -1282,7 +3116,7 @@ var JetstreamStrategy = class extends Server {
|
|
|
1282
3116
|
};
|
|
1283
3117
|
|
|
1284
3118
|
// src/server/core-rpc.server.ts
|
|
1285
|
-
import { Logger as
|
|
3119
|
+
import { Logger as Logger11 } from "@nestjs/common";
|
|
1286
3120
|
import { headers as natsHeaders2 } from "@nats-io/transport-node";
|
|
1287
3121
|
|
|
1288
3122
|
// src/context/rpc.context.ts
|
|
@@ -1292,9 +3126,6 @@ var RpcContext = class extends BaseRpcContext {
|
|
|
1292
3126
|
_retryDelay;
|
|
1293
3127
|
_shouldTerminate = false;
|
|
1294
3128
|
_terminateReason;
|
|
1295
|
-
// ---------------------------------------------------------------------------
|
|
1296
|
-
// Message accessors
|
|
1297
|
-
// ---------------------------------------------------------------------------
|
|
1298
3129
|
/**
|
|
1299
3130
|
* Get the underlying NATS message.
|
|
1300
3131
|
*
|
|
@@ -1329,9 +3160,6 @@ var RpcContext = class extends BaseRpcContext {
|
|
|
1329
3160
|
isJetStream() {
|
|
1330
3161
|
return "ack" in this.args[0];
|
|
1331
3162
|
}
|
|
1332
|
-
// ---------------------------------------------------------------------------
|
|
1333
|
-
// JetStream metadata (return undefined for Core NATS messages)
|
|
1334
|
-
// ---------------------------------------------------------------------------
|
|
1335
3163
|
/** How many times this message has been delivered. */
|
|
1336
3164
|
getDeliveryCount() {
|
|
1337
3165
|
return this.asJetStream()?.info.deliveryCount;
|
|
@@ -1353,9 +3181,6 @@ var RpcContext = class extends BaseRpcContext {
|
|
|
1353
3181
|
getCallerName() {
|
|
1354
3182
|
return this.getHeader("x-caller-name" /* CallerName */);
|
|
1355
3183
|
}
|
|
1356
|
-
// ---------------------------------------------------------------------------
|
|
1357
|
-
// Handler-controlled settlement
|
|
1358
|
-
// ---------------------------------------------------------------------------
|
|
1359
3184
|
/**
|
|
1360
3185
|
* Signal the transport to retry (nak) this message instead of acknowledging it.
|
|
1361
3186
|
*
|
|
@@ -1400,9 +3225,6 @@ var RpcContext = class extends BaseRpcContext {
|
|
|
1400
3225
|
throw new Error(`${method}() is only available for JetStream messages`);
|
|
1401
3226
|
}
|
|
1402
3227
|
}
|
|
1403
|
-
// ---------------------------------------------------------------------------
|
|
1404
|
-
// Transport-facing state (read by EventRouter)
|
|
1405
|
-
// ---------------------------------------------------------------------------
|
|
1406
3228
|
/** @internal */
|
|
1407
3229
|
get shouldRetry() {
|
|
1408
3230
|
return this._shouldRetry;
|
|
@@ -1524,7 +3346,7 @@ var unwrapResult = (result) => {
|
|
|
1524
3346
|
}
|
|
1525
3347
|
return result;
|
|
1526
3348
|
};
|
|
1527
|
-
var
|
|
3349
|
+
var isPromiseLike2 = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
|
|
1528
3350
|
var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
1529
3351
|
let done = false;
|
|
1530
3352
|
let subscription = null;
|
|
@@ -1554,20 +3376,25 @@ var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
|
|
|
1554
3376
|
// src/server/core-rpc.server.ts
|
|
1555
3377
|
var CoreRpcServer = class {
|
|
1556
3378
|
constructor(options, connection, patternRegistry, codec, eventBus) {
|
|
1557
|
-
this.options = options;
|
|
1558
3379
|
this.connection = connection;
|
|
1559
3380
|
this.patternRegistry = patternRegistry;
|
|
1560
3381
|
this.codec = codec;
|
|
1561
3382
|
this.eventBus = eventBus;
|
|
3383
|
+
const derived = deriveOtelAttrs(options);
|
|
3384
|
+
this.otel = derived.otel;
|
|
3385
|
+
this.serviceName = derived.serviceName;
|
|
3386
|
+
this.serverEndpoint = derived.serverEndpoint;
|
|
1562
3387
|
}
|
|
1563
|
-
logger = new
|
|
3388
|
+
logger = new Logger11("Jetstream:CoreRpc");
|
|
1564
3389
|
subscription = null;
|
|
3390
|
+
otel;
|
|
3391
|
+
serviceName;
|
|
3392
|
+
serverEndpoint;
|
|
1565
3393
|
/** Start listening for RPC requests on the command subject. */
|
|
1566
3394
|
async start() {
|
|
1567
3395
|
const nc = await this.connection.getConnection();
|
|
1568
|
-
const
|
|
1569
|
-
const
|
|
1570
|
-
const queue = `${serviceName}_cmd_queue`;
|
|
3396
|
+
const subject = `${this.serviceName}.cmd.>`;
|
|
3397
|
+
const queue = `${this.serviceName}_cmd_queue`;
|
|
1571
3398
|
this.subscription = nc.subscribe(subject, {
|
|
1572
3399
|
queue,
|
|
1573
3400
|
callback: (err, msg) => {
|
|
@@ -1611,15 +3438,51 @@ var CoreRpcServer = class {
|
|
|
1611
3438
|
return;
|
|
1612
3439
|
}
|
|
1613
3440
|
const ctx = new RpcContext([msg]);
|
|
3441
|
+
const startedAt = performance.now();
|
|
1614
3442
|
try {
|
|
1615
|
-
const raw =
|
|
1616
|
-
|
|
1617
|
-
|
|
3443
|
+
const raw = await withConsumeSpan(
|
|
3444
|
+
{
|
|
3445
|
+
subject: msg.subject,
|
|
3446
|
+
msg,
|
|
3447
|
+
kind: "rpc" /* Rpc */,
|
|
3448
|
+
payloadBytes: msg.data.length,
|
|
3449
|
+
handlerMetadata: { pattern: msg.subject },
|
|
3450
|
+
serviceName: this.serviceName,
|
|
3451
|
+
endpoint: this.serverEndpoint
|
|
3452
|
+
},
|
|
3453
|
+
this.otel,
|
|
3454
|
+
() => {
|
|
3455
|
+
const out = unwrapResult(handler(data, ctx));
|
|
3456
|
+
return isPromiseLike2(out) ? out : out;
|
|
3457
|
+
}
|
|
3458
|
+
);
|
|
3459
|
+
msg.respond(this.codec.encode(raw));
|
|
3460
|
+
this.reportHandlerCompleted(msg, startedAt, "success");
|
|
1618
3461
|
} catch (err) {
|
|
1619
|
-
this.
|
|
3462
|
+
this.eventBus.emit(
|
|
3463
|
+
"error" /* Error */,
|
|
3464
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
3465
|
+
`core-rpc-handler:${msg.subject}`
|
|
3466
|
+
);
|
|
1620
3467
|
this.respondWithError(msg, err);
|
|
3468
|
+
this.reportHandlerCompleted(msg, startedAt, "error");
|
|
1621
3469
|
}
|
|
1622
3470
|
}
|
|
3471
|
+
// See EventRouter.reportHandlerCompleted for the rationale on declared
|
|
3472
|
+
// pattern + per-emit hasHook check.
|
|
3473
|
+
reportHandlerCompleted(msg, startedAt, status) {
|
|
3474
|
+
if (!this.eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
|
|
3475
|
+
const declared = this.patternRegistry.resolveDeclared(msg.subject);
|
|
3476
|
+
const pattern = declared?.pattern ?? msg.subject;
|
|
3477
|
+
const kind = declared?.kind ?? "cmd" /* Command */;
|
|
3478
|
+
this.eventBus.emit(
|
|
3479
|
+
"handlerCompleted" /* HandlerCompleted */,
|
|
3480
|
+
pattern,
|
|
3481
|
+
kind,
|
|
3482
|
+
performance.now() - startedAt,
|
|
3483
|
+
status
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
1623
3486
|
/** Send an error response back to the caller with x-error header. */
|
|
1624
3487
|
respondWithError(msg, error) {
|
|
1625
3488
|
try {
|
|
@@ -1633,7 +3496,7 @@ var CoreRpcServer = class {
|
|
|
1633
3496
|
};
|
|
1634
3497
|
|
|
1635
3498
|
// src/server/infrastructure/stream.provider.ts
|
|
1636
|
-
import { Logger as
|
|
3499
|
+
import { Logger as Logger13 } from "@nestjs/common";
|
|
1637
3500
|
import { JetStreamApiError as JetStreamApiError2 } from "@nats-io/jetstream";
|
|
1638
3501
|
|
|
1639
3502
|
// src/server/infrastructure/nats-error-codes.ts
|
|
@@ -1700,7 +3563,7 @@ var isEqual = (a, b) => {
|
|
|
1700
3563
|
};
|
|
1701
3564
|
|
|
1702
3565
|
// src/server/infrastructure/stream-migration.ts
|
|
1703
|
-
import { Logger as
|
|
3566
|
+
import { Logger as Logger12 } from "@nestjs/common";
|
|
1704
3567
|
import { JetStreamApiError } from "@nats-io/jetstream";
|
|
1705
3568
|
var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
|
|
1706
3569
|
var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
|
|
@@ -1709,7 +3572,7 @@ var StreamMigration = class {
|
|
|
1709
3572
|
constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
|
|
1710
3573
|
this.sourcingTimeoutMs = sourcingTimeoutMs;
|
|
1711
3574
|
}
|
|
1712
|
-
logger = new
|
|
3575
|
+
logger = new Logger12("Jetstream:Stream");
|
|
1713
3576
|
async migrate(jsm, streamName2, newConfig) {
|
|
1714
3577
|
const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
|
|
1715
3578
|
const startTime = Date.now();
|
|
@@ -1791,9 +3654,16 @@ var StreamProvider = class {
|
|
|
1791
3654
|
constructor(options, connection) {
|
|
1792
3655
|
this.options = options;
|
|
1793
3656
|
this.connection = connection;
|
|
3657
|
+
const derived = deriveOtelAttrs(options);
|
|
3658
|
+
this.otel = derived.otel;
|
|
3659
|
+
this.otelServiceName = derived.serviceName;
|
|
3660
|
+
this.otelEndpoint = derived.serverEndpoint;
|
|
1794
3661
|
}
|
|
1795
|
-
logger = new
|
|
3662
|
+
logger = new Logger13("Jetstream:Stream");
|
|
1796
3663
|
migration = new StreamMigration();
|
|
3664
|
+
otel;
|
|
3665
|
+
otelServiceName;
|
|
3666
|
+
otelEndpoint;
|
|
1797
3667
|
/**
|
|
1798
3668
|
* Ensure all required streams exist with correct configuration.
|
|
1799
3669
|
*
|
|
@@ -1839,32 +3709,56 @@ var StreamProvider = class {
|
|
|
1839
3709
|
/** Ensure a single stream exists, creating or updating as needed. */
|
|
1840
3710
|
async ensureStream(jsm, kind) {
|
|
1841
3711
|
const config = this.buildConfig(kind);
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
3712
|
+
return withProvisioningSpan(
|
|
3713
|
+
this.otel,
|
|
3714
|
+
{
|
|
3715
|
+
serviceName: this.otelServiceName,
|
|
3716
|
+
endpoint: this.otelEndpoint,
|
|
3717
|
+
entity: "stream",
|
|
3718
|
+
name: config.name,
|
|
3719
|
+
action: "ensure"
|
|
3720
|
+
},
|
|
3721
|
+
async () => {
|
|
3722
|
+
this.logger.log(`Ensuring stream: ${config.name}`);
|
|
3723
|
+
try {
|
|
3724
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
3725
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
3726
|
+
} catch (err) {
|
|
3727
|
+
if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
3728
|
+
this.logger.log(`Creating stream: ${config.name}`);
|
|
3729
|
+
return await jsm.streams.add(config);
|
|
3730
|
+
}
|
|
3731
|
+
throw err;
|
|
3732
|
+
}
|
|
1850
3733
|
}
|
|
1851
|
-
|
|
1852
|
-
}
|
|
3734
|
+
);
|
|
1853
3735
|
}
|
|
1854
3736
|
/** Ensure a dead-letter queue stream exists, creating or updating as needed. */
|
|
1855
3737
|
async ensureDlqStream(jsm) {
|
|
1856
3738
|
const config = this.buildDlqConfig();
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
3739
|
+
return withProvisioningSpan(
|
|
3740
|
+
this.otel,
|
|
3741
|
+
{
|
|
3742
|
+
serviceName: this.otelServiceName,
|
|
3743
|
+
endpoint: this.otelEndpoint,
|
|
3744
|
+
entity: "stream",
|
|
3745
|
+
name: config.name,
|
|
3746
|
+
action: "ensure"
|
|
3747
|
+
},
|
|
3748
|
+
async () => {
|
|
3749
|
+
this.logger.log(`Ensuring DLQ stream: ${config.name}`);
|
|
3750
|
+
try {
|
|
3751
|
+
const currentInfo = await jsm.streams.info(config.name);
|
|
3752
|
+
return await this.handleExistingStream(jsm, currentInfo, config);
|
|
3753
|
+
} catch (err) {
|
|
3754
|
+
if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
|
|
3755
|
+
this.logger.log(`Creating DLQ stream: ${config.name}`);
|
|
3756
|
+
return await jsm.streams.add(config);
|
|
3757
|
+
}
|
|
3758
|
+
throw err;
|
|
3759
|
+
}
|
|
1865
3760
|
}
|
|
1866
|
-
|
|
1867
|
-
}
|
|
3761
|
+
);
|
|
1868
3762
|
}
|
|
1869
3763
|
async handleExistingStream(jsm, currentInfo, config) {
|
|
1870
3764
|
const diff = compareStreamConfig(currentInfo.config, config);
|
|
@@ -1893,7 +3787,18 @@ var StreamProvider = class {
|
|
|
1893
3787
|
}
|
|
1894
3788
|
return currentInfo;
|
|
1895
3789
|
}
|
|
1896
|
-
await
|
|
3790
|
+
await withMigrationSpan(
|
|
3791
|
+
this.otel,
|
|
3792
|
+
{
|
|
3793
|
+
serviceName: this.otelServiceName,
|
|
3794
|
+
endpoint: this.otelEndpoint,
|
|
3795
|
+
stream: config.name,
|
|
3796
|
+
reason: diff.changes.filter((c) => c.mutability === "immutable").map((c) => c.property).join(", ")
|
|
3797
|
+
},
|
|
3798
|
+
async () => {
|
|
3799
|
+
await this.migration.migrate(jsm, config.name, config);
|
|
3800
|
+
}
|
|
3801
|
+
);
|
|
1897
3802
|
return await jsm.streams.info(config.name);
|
|
1898
3803
|
}
|
|
1899
3804
|
buildMutableOnlyConfig(config, currentConfig, diff) {
|
|
@@ -2009,7 +3914,7 @@ var StreamProvider = class {
|
|
|
2009
3914
|
};
|
|
2010
3915
|
|
|
2011
3916
|
// src/server/infrastructure/consumer.provider.ts
|
|
2012
|
-
import { Logger as
|
|
3917
|
+
import { Logger as Logger14 } from "@nestjs/common";
|
|
2013
3918
|
import { JetStreamApiError as JetStreamApiError3 } from "@nats-io/jetstream";
|
|
2014
3919
|
var ConsumerProvider = class {
|
|
2015
3920
|
constructor(options, connection, streamProvider, patternRegistry) {
|
|
@@ -2017,8 +3922,15 @@ var ConsumerProvider = class {
|
|
|
2017
3922
|
this.connection = connection;
|
|
2018
3923
|
this.streamProvider = streamProvider;
|
|
2019
3924
|
this.patternRegistry = patternRegistry;
|
|
2020
|
-
|
|
2021
|
-
|
|
3925
|
+
const derived = deriveOtelAttrs(options);
|
|
3926
|
+
this.otel = derived.otel;
|
|
3927
|
+
this.otelServiceName = derived.serviceName;
|
|
3928
|
+
this.otelEndpoint = derived.serverEndpoint;
|
|
3929
|
+
}
|
|
3930
|
+
logger = new Logger14("Jetstream:Consumer");
|
|
3931
|
+
otel;
|
|
3932
|
+
otelServiceName;
|
|
3933
|
+
otelEndpoint;
|
|
2022
3934
|
/**
|
|
2023
3935
|
* Ensure consumers exist for the specified kinds.
|
|
2024
3936
|
*
|
|
@@ -2048,17 +3960,29 @@ var ConsumerProvider = class {
|
|
|
2048
3960
|
const stream = this.streamProvider.getStreamName(kind);
|
|
2049
3961
|
const config = this.buildConfig(kind);
|
|
2050
3962
|
const name = config.durable_name;
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
3963
|
+
return withProvisioningSpan(
|
|
3964
|
+
this.otel,
|
|
3965
|
+
{
|
|
3966
|
+
serviceName: this.otelServiceName,
|
|
3967
|
+
endpoint: this.otelEndpoint,
|
|
3968
|
+
entity: "consumer",
|
|
3969
|
+
name,
|
|
3970
|
+
action: "ensure"
|
|
3971
|
+
},
|
|
3972
|
+
async () => {
|
|
3973
|
+
this.logger.log(`Ensuring consumer: ${name} on stream: ${stream}`);
|
|
3974
|
+
try {
|
|
3975
|
+
await jsm.consumers.info(stream, name);
|
|
3976
|
+
this.logger.debug(`Consumer exists, updating: ${name}`);
|
|
3977
|
+
return await jsm.consumers.update(stream, name, config);
|
|
3978
|
+
} catch (err) {
|
|
3979
|
+
if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
3980
|
+
throw err;
|
|
3981
|
+
}
|
|
3982
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
3983
|
+
}
|
|
2059
3984
|
}
|
|
2060
|
-
|
|
2061
|
-
}
|
|
3985
|
+
);
|
|
2062
3986
|
}
|
|
2063
3987
|
/**
|
|
2064
3988
|
* Recover a consumer that disappeared during runtime.
|
|
@@ -2077,16 +4001,28 @@ var ConsumerProvider = class {
|
|
|
2077
4001
|
const stream = this.streamProvider.getStreamName(kind);
|
|
2078
4002
|
const config = this.buildConfig(kind);
|
|
2079
4003
|
const name = config.durable_name;
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
4004
|
+
return withProvisioningSpan(
|
|
4005
|
+
this.otel,
|
|
4006
|
+
{
|
|
4007
|
+
serviceName: this.otelServiceName,
|
|
4008
|
+
endpoint: this.otelEndpoint,
|
|
4009
|
+
entity: "consumer",
|
|
4010
|
+
name,
|
|
4011
|
+
action: "recover"
|
|
4012
|
+
},
|
|
4013
|
+
async () => {
|
|
4014
|
+
this.logger.log(`Recovering consumer: ${name} on stream: ${stream}`);
|
|
4015
|
+
await this.assertNoMigrationInProgress(jsm, stream);
|
|
4016
|
+
try {
|
|
4017
|
+
return await jsm.consumers.info(stream, name);
|
|
4018
|
+
} catch (err) {
|
|
4019
|
+
if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
|
|
4020
|
+
throw err;
|
|
4021
|
+
}
|
|
4022
|
+
return await this.createConsumer(jsm, stream, name, config);
|
|
4023
|
+
}
|
|
2087
4024
|
}
|
|
2088
|
-
|
|
2089
|
-
}
|
|
4025
|
+
);
|
|
2090
4026
|
}
|
|
2091
4027
|
/**
|
|
2092
4028
|
* Throw if a migration backup stream exists for this stream.
|
|
@@ -2202,7 +4138,7 @@ var ConsumerProvider = class {
|
|
|
2202
4138
|
};
|
|
2203
4139
|
|
|
2204
4140
|
// src/server/infrastructure/message.provider.ts
|
|
2205
|
-
import { Logger as
|
|
4141
|
+
import { Logger as Logger15 } from "@nestjs/common";
|
|
2206
4142
|
import { DeliverPolicy as DeliverPolicy2 } from "@nats-io/jetstream";
|
|
2207
4143
|
import {
|
|
2208
4144
|
catchError,
|
|
@@ -2212,7 +4148,6 @@ import {
|
|
|
2212
4148
|
repeat,
|
|
2213
4149
|
Subject,
|
|
2214
4150
|
takeUntil,
|
|
2215
|
-
tap,
|
|
2216
4151
|
timer
|
|
2217
4152
|
} from "rxjs";
|
|
2218
4153
|
var MessageProvider = class {
|
|
@@ -2222,7 +4157,7 @@ var MessageProvider = class {
|
|
|
2222
4157
|
this.consumeOptionsMap = consumeOptionsMap;
|
|
2223
4158
|
this.consumerRecoveryFn = consumerRecoveryFn;
|
|
2224
4159
|
}
|
|
2225
|
-
logger = new
|
|
4160
|
+
logger = new Logger15("Jetstream:Message");
|
|
2226
4161
|
activeIterators = /* @__PURE__ */ new Set();
|
|
2227
4162
|
orderedReadyResolve = null;
|
|
2228
4163
|
orderedReadyReject = null;
|
|
@@ -2320,10 +4255,13 @@ var MessageProvider = class {
|
|
|
2320
4255
|
/** Create a self-healing consumer flow for a specific kind. */
|
|
2321
4256
|
createFlow(kind, info) {
|
|
2322
4257
|
const target$ = this.getTargetSubject(kind);
|
|
2323
|
-
return this.createSelfHealingFlow(
|
|
4258
|
+
return this.createSelfHealingFlow(
|
|
4259
|
+
(onConnected) => this.consumeOnce(kind, info, target$, onConnected),
|
|
4260
|
+
info.name
|
|
4261
|
+
);
|
|
2324
4262
|
}
|
|
2325
4263
|
/** Single iteration: get consumer -> pull messages -> emit to subject. */
|
|
2326
|
-
async consumeOnce(kind, info, target
|
|
4264
|
+
async consumeOnce(kind, info, target$, onConnected) {
|
|
2327
4265
|
const js = this.connection.getJetStreamClient();
|
|
2328
4266
|
let consumer;
|
|
2329
4267
|
let consumerName2 = info.name;
|
|
@@ -2351,6 +4289,7 @@ var MessageProvider = class {
|
|
|
2351
4289
|
});
|
|
2352
4290
|
this.activeIterators.add(messages);
|
|
2353
4291
|
this.monitorConsumerHealth(messages, consumerName2);
|
|
4292
|
+
onConnected();
|
|
2354
4293
|
try {
|
|
2355
4294
|
await messages.closed();
|
|
2356
4295
|
} finally {
|
|
@@ -2405,7 +4344,7 @@ var MessageProvider = class {
|
|
|
2405
4344
|
/** Create a self-healing ordered consumer flow. */
|
|
2406
4345
|
createOrderedFlow(streamName2, consumerOpts) {
|
|
2407
4346
|
return this.createSelfHealingFlow(
|
|
2408
|
-
() => this.consumeOrderedOnce(streamName2, consumerOpts),
|
|
4347
|
+
(onConnected) => this.consumeOrderedOnce(streamName2, consumerOpts, onConnected),
|
|
2409
4348
|
"ordered" /* Ordered */,
|
|
2410
4349
|
(err) => {
|
|
2411
4350
|
if (this.orderedReadyReject) {
|
|
@@ -2419,10 +4358,15 @@ var MessageProvider = class {
|
|
|
2419
4358
|
/** Shared self-healing flow: defer -> retry with exponential backoff on error/completion. */
|
|
2420
4359
|
createSelfHealingFlow(source, label, onFirstError) {
|
|
2421
4360
|
let consecutiveFailures = 0;
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
4361
|
+
const onConnected = () => {
|
|
4362
|
+
if (consecutiveFailures > 0) {
|
|
4363
|
+
const attempts = consecutiveFailures;
|
|
4364
|
+
this.logger.log(`Consumer ${label} recovered after ${attempts} failed attempt(s)`);
|
|
4365
|
+
this.eventBus.emit("consumerRecovered" /* ConsumerRecovered */, label, attempts);
|
|
4366
|
+
}
|
|
4367
|
+
consecutiveFailures = 0;
|
|
4368
|
+
};
|
|
4369
|
+
return defer2(() => source(onConnected)).pipe(
|
|
2426
4370
|
catchError((err) => {
|
|
2427
4371
|
consecutiveFailures++;
|
|
2428
4372
|
this.logger.error(`Consumer ${label} error, will restart:`, err);
|
|
@@ -2445,7 +4389,7 @@ var MessageProvider = class {
|
|
|
2445
4389
|
);
|
|
2446
4390
|
}
|
|
2447
4391
|
/** Single iteration: create ordered consumer -> push messages into the subject. */
|
|
2448
|
-
async consumeOrderedOnce(streamName2, consumerOpts) {
|
|
4392
|
+
async consumeOrderedOnce(streamName2, consumerOpts, onConnected) {
|
|
2449
4393
|
const js = this.connection.getJetStreamClient();
|
|
2450
4394
|
const consumer = await js.consumers.get(streamName2, consumerOpts);
|
|
2451
4395
|
const orderedMessages$ = this.orderedMessages$;
|
|
@@ -2460,6 +4404,7 @@ var MessageProvider = class {
|
|
|
2460
4404
|
this.orderedReadyReject = null;
|
|
2461
4405
|
}
|
|
2462
4406
|
this.activeIterators.add(messages);
|
|
4407
|
+
onConnected();
|
|
2463
4408
|
try {
|
|
2464
4409
|
await messages.closed();
|
|
2465
4410
|
} finally {
|
|
@@ -2469,7 +4414,7 @@ var MessageProvider = class {
|
|
|
2469
4414
|
};
|
|
2470
4415
|
|
|
2471
4416
|
// src/server/infrastructure/metadata.provider.ts
|
|
2472
|
-
import { Logger as
|
|
4417
|
+
import { Logger as Logger16 } from "@nestjs/common";
|
|
2473
4418
|
import { Kvm } from "@nats-io/kv";
|
|
2474
4419
|
var MetadataProvider = class {
|
|
2475
4420
|
constructor(options, connection) {
|
|
@@ -2478,7 +4423,7 @@ var MetadataProvider = class {
|
|
|
2478
4423
|
this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
|
|
2479
4424
|
this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
|
|
2480
4425
|
}
|
|
2481
|
-
logger = new
|
|
4426
|
+
logger = new Logger16("Jetstream:Metadata");
|
|
2482
4427
|
bucketName;
|
|
2483
4428
|
replicas;
|
|
2484
4429
|
ttl;
|
|
@@ -2570,177 +4515,14 @@ var MetadataProvider = class {
|
|
|
2570
4515
|
}
|
|
2571
4516
|
};
|
|
2572
4517
|
|
|
2573
|
-
// src/server/routing/pattern-registry.ts
|
|
2574
|
-
import { Logger as Logger10 } from "@nestjs/common";
|
|
2575
|
-
var HANDLER_LABELS = {
|
|
2576
|
-
["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
|
|
2577
|
-
["ordered" /* Ordered */]: "ordered" /* Ordered */,
|
|
2578
|
-
["ev" /* Event */]: "event" /* Event */,
|
|
2579
|
-
["cmd" /* Command */]: "rpc" /* Rpc */
|
|
2580
|
-
};
|
|
2581
|
-
var PatternRegistry = class {
|
|
2582
|
-
constructor(options) {
|
|
2583
|
-
this.options = options;
|
|
2584
|
-
}
|
|
2585
|
-
logger = new Logger10("Jetstream:PatternRegistry");
|
|
2586
|
-
registry = /* @__PURE__ */ new Map();
|
|
2587
|
-
// Cached after registerHandlers() — the registry is immutable from that point
|
|
2588
|
-
cachedPatterns = null;
|
|
2589
|
-
_hasEvents = false;
|
|
2590
|
-
_hasCommands = false;
|
|
2591
|
-
_hasBroadcasts = false;
|
|
2592
|
-
_hasOrdered = false;
|
|
2593
|
-
_hasMetadata = false;
|
|
2594
|
-
/**
|
|
2595
|
-
* Register all handlers from the NestJS strategy.
|
|
2596
|
-
*
|
|
2597
|
-
* @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
|
|
2598
|
-
*/
|
|
2599
|
-
registerHandlers(handlers) {
|
|
2600
|
-
const serviceName = this.options.name;
|
|
2601
|
-
for (const [pattern, handler] of handlers) {
|
|
2602
|
-
const extras = handler.extras;
|
|
2603
|
-
const isEvent = handler.isEventHandler ?? false;
|
|
2604
|
-
const isBroadcast = !!extras?.broadcast;
|
|
2605
|
-
const isOrdered = !!extras?.ordered;
|
|
2606
|
-
const meta = extras?.meta;
|
|
2607
|
-
if (isBroadcast && isOrdered) {
|
|
2608
|
-
throw new Error(
|
|
2609
|
-
`Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
|
|
2610
|
-
);
|
|
2611
|
-
}
|
|
2612
|
-
let kind;
|
|
2613
|
-
if (isBroadcast) kind = "broadcast" /* Broadcast */;
|
|
2614
|
-
else if (isOrdered) kind = "ordered" /* Ordered */;
|
|
2615
|
-
else if (isEvent) kind = "ev" /* Event */;
|
|
2616
|
-
else kind = "cmd" /* Command */;
|
|
2617
|
-
const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
|
|
2618
|
-
this.registry.set(fullSubject, {
|
|
2619
|
-
handler,
|
|
2620
|
-
pattern,
|
|
2621
|
-
isEvent: isEvent && !isOrdered,
|
|
2622
|
-
isBroadcast,
|
|
2623
|
-
isOrdered,
|
|
2624
|
-
meta
|
|
2625
|
-
});
|
|
2626
|
-
this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
|
|
2627
|
-
}
|
|
2628
|
-
this.cachedPatterns = this.buildPatternsByKind();
|
|
2629
|
-
this._hasEvents = this.cachedPatterns.events.length > 0;
|
|
2630
|
-
this._hasCommands = this.cachedPatterns.commands.length > 0;
|
|
2631
|
-
this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
|
|
2632
|
-
this._hasOrdered = this.cachedPatterns.ordered.length > 0;
|
|
2633
|
-
this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
|
|
2634
|
-
this.logSummary();
|
|
2635
|
-
}
|
|
2636
|
-
/** Find handler for a full NATS subject. */
|
|
2637
|
-
getHandler(subject) {
|
|
2638
|
-
return this.registry.get(subject)?.handler ?? null;
|
|
2639
|
-
}
|
|
2640
|
-
/** Get all registered broadcast patterns (for consumer filter_subject setup). */
|
|
2641
|
-
getBroadcastPatterns() {
|
|
2642
|
-
return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
|
|
2643
|
-
}
|
|
2644
|
-
hasBroadcastHandlers() {
|
|
2645
|
-
return this._hasBroadcasts;
|
|
2646
|
-
}
|
|
2647
|
-
hasRpcHandlers() {
|
|
2648
|
-
return this._hasCommands;
|
|
2649
|
-
}
|
|
2650
|
-
hasEventHandlers() {
|
|
2651
|
-
return this._hasEvents;
|
|
2652
|
-
}
|
|
2653
|
-
hasOrderedHandlers() {
|
|
2654
|
-
return this._hasOrdered;
|
|
2655
|
-
}
|
|
2656
|
-
/** Get fully-qualified NATS subjects for ordered handlers. */
|
|
2657
|
-
getOrderedSubjects() {
|
|
2658
|
-
return this.getPatternsByKind().ordered.map(
|
|
2659
|
-
(p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
|
|
2660
|
-
);
|
|
2661
|
-
}
|
|
2662
|
-
/** Check if any registered handler has metadata. */
|
|
2663
|
-
hasMetadata() {
|
|
2664
|
-
return this._hasMetadata;
|
|
2665
|
-
}
|
|
2666
|
-
/**
|
|
2667
|
-
* Get handler metadata entries for KV publishing.
|
|
2668
|
-
*
|
|
2669
|
-
* Returns a map of KV key -> metadata object for all handlers that have `meta`.
|
|
2670
|
-
* Key format: `{serviceName}.{kind}.{pattern}`.
|
|
2671
|
-
*/
|
|
2672
|
-
getMetadataEntries() {
|
|
2673
|
-
const entries = /* @__PURE__ */ new Map();
|
|
2674
|
-
for (const entry of this.registry.values()) {
|
|
2675
|
-
if (!entry.meta) continue;
|
|
2676
|
-
const kind = this.resolveStreamKind(entry);
|
|
2677
|
-
const key = metadataKey(this.options.name, kind, entry.pattern);
|
|
2678
|
-
entries.set(key, entry.meta);
|
|
2679
|
-
}
|
|
2680
|
-
return entries;
|
|
2681
|
-
}
|
|
2682
|
-
/** Get patterns grouped by kind (cached after registration). */
|
|
2683
|
-
getPatternsByKind() {
|
|
2684
|
-
const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
|
|
2685
|
-
return {
|
|
2686
|
-
events: [...patterns.events],
|
|
2687
|
-
commands: [...patterns.commands],
|
|
2688
|
-
broadcasts: [...patterns.broadcasts],
|
|
2689
|
-
ordered: [...patterns.ordered]
|
|
2690
|
-
};
|
|
2691
|
-
}
|
|
2692
|
-
/** Normalize a full NATS subject back to the user-facing pattern. */
|
|
2693
|
-
normalizeSubject(subject) {
|
|
2694
|
-
const name = internalName(this.options.name);
|
|
2695
|
-
const prefixes = [
|
|
2696
|
-
`${name}.${"cmd" /* Command */}.`,
|
|
2697
|
-
`${name}.${"ev" /* Event */}.`,
|
|
2698
|
-
`${name}.${"ordered" /* Ordered */}.`,
|
|
2699
|
-
`${"broadcast" /* Broadcast */}.`
|
|
2700
|
-
];
|
|
2701
|
-
for (const prefix of prefixes) {
|
|
2702
|
-
if (subject.startsWith(prefix)) {
|
|
2703
|
-
return subject.slice(prefix.length);
|
|
2704
|
-
}
|
|
2705
|
-
}
|
|
2706
|
-
return subject;
|
|
2707
|
-
}
|
|
2708
|
-
buildPatternsByKind() {
|
|
2709
|
-
const events = [];
|
|
2710
|
-
const commands = [];
|
|
2711
|
-
const broadcasts = [];
|
|
2712
|
-
const ordered = [];
|
|
2713
|
-
for (const entry of this.registry.values()) {
|
|
2714
|
-
if (entry.isBroadcast) broadcasts.push(entry.pattern);
|
|
2715
|
-
else if (entry.isOrdered) ordered.push(entry.pattern);
|
|
2716
|
-
else if (entry.isEvent) events.push(entry.pattern);
|
|
2717
|
-
else commands.push(entry.pattern);
|
|
2718
|
-
}
|
|
2719
|
-
return { events, commands, broadcasts, ordered };
|
|
2720
|
-
}
|
|
2721
|
-
resolveStreamKind(entry) {
|
|
2722
|
-
if (entry.isBroadcast) return "broadcast" /* Broadcast */;
|
|
2723
|
-
if (entry.isOrdered) return "ordered" /* Ordered */;
|
|
2724
|
-
if (entry.isEvent) return "ev" /* Event */;
|
|
2725
|
-
return "cmd" /* Command */;
|
|
2726
|
-
}
|
|
2727
|
-
logSummary() {
|
|
2728
|
-
const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
|
|
2729
|
-
const parts = [
|
|
2730
|
-
`${commands.length} RPC`,
|
|
2731
|
-
`${events.length} events`,
|
|
2732
|
-
`${broadcasts.length} broadcasts`
|
|
2733
|
-
];
|
|
2734
|
-
if (ordered.length > 0) {
|
|
2735
|
-
parts.push(`${ordered.length} ordered`);
|
|
2736
|
-
}
|
|
2737
|
-
this.logger.log(`Registered handlers: ${parts.join(", ")}`);
|
|
2738
|
-
}
|
|
2739
|
-
};
|
|
2740
|
-
|
|
2741
4518
|
// src/server/routing/event.router.ts
|
|
2742
|
-
import { Logger as
|
|
4519
|
+
import { Logger as Logger17 } from "@nestjs/common";
|
|
2743
4520
|
import { headers as natsHeaders3 } from "@nats-io/transport-node";
|
|
4521
|
+
var eventConsumeKindFor = (kind) => {
|
|
4522
|
+
if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
|
|
4523
|
+
if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
|
|
4524
|
+
return "event" /* Event */;
|
|
4525
|
+
};
|
|
2744
4526
|
var EventRouter = class {
|
|
2745
4527
|
constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
|
|
2746
4528
|
this.messageProvider = messageProvider;
|
|
@@ -2752,9 +4534,22 @@ var EventRouter = class {
|
|
|
2752
4534
|
this.ackWaitMap = ackWaitMap;
|
|
2753
4535
|
this.connection = connection;
|
|
2754
4536
|
this.options = options;
|
|
4537
|
+
if (options) {
|
|
4538
|
+
const derived = deriveOtelAttrs(options);
|
|
4539
|
+
this.otel = derived.otel;
|
|
4540
|
+
this.serviceName = derived.serviceName;
|
|
4541
|
+
this.serverEndpoint = derived.serverEndpoint;
|
|
4542
|
+
} else {
|
|
4543
|
+
this.otel = resolveOtelOptions({ enabled: false });
|
|
4544
|
+
this.serviceName = "";
|
|
4545
|
+
this.serverEndpoint = null;
|
|
4546
|
+
}
|
|
2755
4547
|
}
|
|
2756
|
-
logger = new
|
|
4548
|
+
logger = new Logger17("Jetstream:EventRouter");
|
|
2757
4549
|
subscriptions = [];
|
|
4550
|
+
otel;
|
|
4551
|
+
serviceName;
|
|
4552
|
+
serverEndpoint;
|
|
2758
4553
|
/**
|
|
2759
4554
|
* Update the max_deliver thresholds from actual NATS consumer configs.
|
|
2760
4555
|
* Called after consumers are ensured so the DLQ map reflects reality.
|
|
@@ -2784,13 +4579,24 @@ var EventRouter = class {
|
|
|
2784
4579
|
const patternRegistry = this.patternRegistry;
|
|
2785
4580
|
const codec = this.codec;
|
|
2786
4581
|
const eventBus = this.eventBus;
|
|
2787
|
-
const
|
|
4582
|
+
const logger5 = this.logger;
|
|
2788
4583
|
const deadLetterConfig = this.deadLetterConfig;
|
|
4584
|
+
const otel = this.otel;
|
|
4585
|
+
const serviceName = this.serviceName;
|
|
4586
|
+
const serverEndpoint = this.serverEndpoint;
|
|
4587
|
+
const spanKind = eventConsumeKindFor(kind);
|
|
2789
4588
|
const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
|
|
2790
4589
|
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
2791
4590
|
const concurrency = this.getConcurrency(kind);
|
|
2792
4591
|
const hasDlqCheck = deadLetterConfig !== void 0;
|
|
2793
|
-
const
|
|
4592
|
+
const reportHandlerCompleted = (msg, startedAt, status) => {
|
|
4593
|
+
if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
|
|
4594
|
+
const declared = patternRegistry.resolveDeclared(msg.subject);
|
|
4595
|
+
const pattern = declared?.pattern ?? msg.subject;
|
|
4596
|
+
const declaredKind = declared?.kind ?? kind;
|
|
4597
|
+
const durationMs = performance.now() - startedAt;
|
|
4598
|
+
eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
|
|
4599
|
+
};
|
|
2794
4600
|
const isDeadLetter = (msg) => {
|
|
2795
4601
|
if (!hasDlqCheck) return false;
|
|
2796
4602
|
const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
|
|
@@ -2816,7 +4622,7 @@ var EventRouter = class {
|
|
|
2816
4622
|
const handler = patternRegistry.getHandler(subject);
|
|
2817
4623
|
if (!handler) {
|
|
2818
4624
|
msg.term(`No handler for event: ${subject}`);
|
|
2819
|
-
|
|
4625
|
+
logger5.error(`No handler for subject: ${subject}`);
|
|
2820
4626
|
return null;
|
|
2821
4627
|
}
|
|
2822
4628
|
let data;
|
|
@@ -2824,47 +4630,79 @@ var EventRouter = class {
|
|
|
2824
4630
|
data = codec.decode(msg.data);
|
|
2825
4631
|
} catch (err) {
|
|
2826
4632
|
msg.term("Decode error");
|
|
2827
|
-
|
|
4633
|
+
logger5.error(`Decode error for ${subject}:`, err);
|
|
2828
4634
|
return null;
|
|
2829
4635
|
}
|
|
2830
|
-
|
|
4636
|
+
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
2831
4637
|
return { handler, data };
|
|
2832
4638
|
} catch (err) {
|
|
2833
|
-
|
|
4639
|
+
logger5.error(`Unexpected error in ${kind} event router`, err);
|
|
2834
4640
|
try {
|
|
2835
4641
|
msg.term("Unexpected router error");
|
|
2836
4642
|
} catch (termErr) {
|
|
2837
|
-
|
|
4643
|
+
logger5.error(`Failed to terminate message ${subject}:`, termErr);
|
|
2838
4644
|
}
|
|
2839
4645
|
return null;
|
|
2840
4646
|
}
|
|
2841
4647
|
};
|
|
4648
|
+
const statusForContext = (ctx) => {
|
|
4649
|
+
if (ctx.shouldTerminate) return "terminated";
|
|
4650
|
+
if (ctx.shouldRetry) return "retried";
|
|
4651
|
+
return "success";
|
|
4652
|
+
};
|
|
2842
4653
|
const handleSafe = (msg) => {
|
|
2843
4654
|
const resolved = resolveEvent(msg);
|
|
2844
4655
|
if (resolved === null) return void 0;
|
|
2845
4656
|
const { handler, data } = resolved;
|
|
2846
4657
|
const ctx = new RpcContext([msg]);
|
|
2847
4658
|
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
4659
|
+
const startedAt = performance.now();
|
|
2848
4660
|
let pending;
|
|
2849
4661
|
try {
|
|
2850
|
-
pending =
|
|
4662
|
+
pending = withConsumeSpan(
|
|
4663
|
+
{
|
|
4664
|
+
subject: msg.subject,
|
|
4665
|
+
msg,
|
|
4666
|
+
info: msg.info,
|
|
4667
|
+
kind: spanKind,
|
|
4668
|
+
payloadBytes: msg.data.length,
|
|
4669
|
+
handlerMetadata: { pattern: msg.subject },
|
|
4670
|
+
serviceName,
|
|
4671
|
+
endpoint: serverEndpoint
|
|
4672
|
+
},
|
|
4673
|
+
otel,
|
|
4674
|
+
() => unwrapResult(handler(data, ctx))
|
|
4675
|
+
);
|
|
2851
4676
|
} catch (err) {
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
4677
|
+
eventBus.emit(
|
|
4678
|
+
"error" /* Error */,
|
|
4679
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
4680
|
+
`${kind}-handler:${msg.subject}`
|
|
4681
|
+
);
|
|
4682
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
4683
|
+
return settleFailure(msg, data, err).finally(() => {
|
|
4684
|
+
if (stopAckExtension !== null) stopAckExtension();
|
|
4685
|
+
});
|
|
2855
4686
|
}
|
|
2856
|
-
if (!
|
|
4687
|
+
if (!isPromiseLike2(pending)) {
|
|
2857
4688
|
settleSuccess(msg, ctx);
|
|
4689
|
+
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
2858
4690
|
if (stopAckExtension !== null) stopAckExtension();
|
|
2859
4691
|
return void 0;
|
|
2860
4692
|
}
|
|
2861
4693
|
return pending.then(
|
|
2862
4694
|
() => {
|
|
2863
4695
|
settleSuccess(msg, ctx);
|
|
4696
|
+
reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
|
|
2864
4697
|
if (stopAckExtension !== null) stopAckExtension();
|
|
2865
4698
|
},
|
|
2866
4699
|
async (err) => {
|
|
2867
|
-
|
|
4700
|
+
eventBus.emit(
|
|
4701
|
+
"error" /* Error */,
|
|
4702
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
4703
|
+
`${kind}-handler:${msg.subject}`
|
|
4704
|
+
);
|
|
4705
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
2868
4706
|
try {
|
|
2869
4707
|
await settleFailure(msg, data, err);
|
|
2870
4708
|
} finally {
|
|
@@ -2880,42 +4718,65 @@ var EventRouter = class {
|
|
|
2880
4718
|
try {
|
|
2881
4719
|
handler = patternRegistry.getHandler(subject);
|
|
2882
4720
|
if (!handler) {
|
|
2883
|
-
|
|
4721
|
+
logger5.error(`No handler for subject: ${subject}`);
|
|
2884
4722
|
return void 0;
|
|
2885
4723
|
}
|
|
2886
4724
|
try {
|
|
2887
4725
|
data = codec.decode(msg.data);
|
|
2888
4726
|
} catch (err) {
|
|
2889
|
-
|
|
4727
|
+
logger5.error(`Decode error for ${subject}:`, err);
|
|
2890
4728
|
return void 0;
|
|
2891
4729
|
}
|
|
2892
|
-
|
|
4730
|
+
eventBus.emitMessageRouted(subject, "event" /* Event */);
|
|
2893
4731
|
} catch (err) {
|
|
2894
|
-
|
|
4732
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
2895
4733
|
return void 0;
|
|
2896
4734
|
}
|
|
2897
4735
|
const ctx = new RpcContext([msg]);
|
|
2898
4736
|
const warnIfSettlementAttempted = () => {
|
|
2899
4737
|
if (ctx.shouldRetry || ctx.shouldTerminate) {
|
|
2900
|
-
|
|
4738
|
+
logger5.warn(
|
|
2901
4739
|
`retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
|
|
2902
4740
|
);
|
|
2903
4741
|
}
|
|
2904
4742
|
};
|
|
4743
|
+
const startedAt = performance.now();
|
|
2905
4744
|
let pending;
|
|
2906
4745
|
try {
|
|
2907
|
-
pending =
|
|
4746
|
+
pending = withConsumeSpan(
|
|
4747
|
+
{
|
|
4748
|
+
subject: msg.subject,
|
|
4749
|
+
msg,
|
|
4750
|
+
info: msg.info,
|
|
4751
|
+
kind: spanKind,
|
|
4752
|
+
payloadBytes: msg.data.length,
|
|
4753
|
+
handlerMetadata: { pattern: msg.subject },
|
|
4754
|
+
serviceName,
|
|
4755
|
+
endpoint: serverEndpoint
|
|
4756
|
+
},
|
|
4757
|
+
otel,
|
|
4758
|
+
() => unwrapResult(handler(data, ctx))
|
|
4759
|
+
);
|
|
2908
4760
|
} catch (err) {
|
|
2909
|
-
|
|
4761
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
4762
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
2910
4763
|
return void 0;
|
|
2911
4764
|
}
|
|
2912
|
-
if (!
|
|
4765
|
+
if (!isPromiseLike2(pending)) {
|
|
2913
4766
|
warnIfSettlementAttempted();
|
|
4767
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
2914
4768
|
return void 0;
|
|
2915
4769
|
}
|
|
2916
|
-
return pending.then(
|
|
2917
|
-
|
|
2918
|
-
|
|
4770
|
+
return pending.then(
|
|
4771
|
+
() => {
|
|
4772
|
+
warnIfSettlementAttempted();
|
|
4773
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
4774
|
+
},
|
|
4775
|
+
(err) => {
|
|
4776
|
+
logger5.error(`Ordered handler error (${subject}):`, err);
|
|
4777
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
4778
|
+
}
|
|
4779
|
+
);
|
|
2919
4780
|
};
|
|
2920
4781
|
const route = isOrdered ? handleOrderedSafe : handleSafe;
|
|
2921
4782
|
const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
|
|
@@ -2947,7 +4808,7 @@ var EventRouter = class {
|
|
|
2947
4808
|
backlog.push(msg);
|
|
2948
4809
|
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
2949
4810
|
backlogWarned = true;
|
|
2950
|
-
|
|
4811
|
+
logger5.warn(
|
|
2951
4812
|
`${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
2952
4813
|
);
|
|
2953
4814
|
}
|
|
@@ -2963,7 +4824,7 @@ var EventRouter = class {
|
|
|
2963
4824
|
}
|
|
2964
4825
|
},
|
|
2965
4826
|
error: (err) => {
|
|
2966
|
-
|
|
4827
|
+
logger5.error(`Stream error in ${kind} router`, err);
|
|
2967
4828
|
}
|
|
2968
4829
|
});
|
|
2969
4830
|
this.subscriptions.push(subscription);
|
|
@@ -2978,14 +4839,11 @@ var EventRouter = class {
|
|
|
2978
4839
|
if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
|
|
2979
4840
|
return void 0;
|
|
2980
4841
|
}
|
|
2981
|
-
/** Handle a dead letter: invoke callback, then term or nak based on result. */
|
|
2982
4842
|
/**
|
|
2983
|
-
*
|
|
2984
|
-
*
|
|
2985
|
-
*
|
|
2986
|
-
*
|
|
2987
|
-
* On success, terminates the message. On error, leaves it unacknowledged (nak)
|
|
2988
|
-
* so NATS can retry the delivery on the next cycle.
|
|
4843
|
+
* Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
|
|
4844
|
+
* success or `nak` on hook failure so NATS retries on the next delivery
|
|
4845
|
+
* cycle. Used when DLQ stream isn't configured, or when publishing to it
|
|
4846
|
+
* failed and we still have to surface the message somewhere observable.
|
|
2989
4847
|
*/
|
|
2990
4848
|
async fallbackToOnDeadLetterCallback(info, msg) {
|
|
2991
4849
|
if (!this.deadLetterConfig) {
|
|
@@ -3077,20 +4935,37 @@ var EventRouter = class {
|
|
|
3077
4935
|
streamSequence: msg.info.streamSequence,
|
|
3078
4936
|
timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
|
|
3079
4937
|
};
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
4938
|
+
await withDeadLetterSpan(
|
|
4939
|
+
{
|
|
4940
|
+
msg,
|
|
4941
|
+
// Pattern resolution mirrors event-routing: when a registered
|
|
4942
|
+
// pattern matches, surface it on the DLQ span so APM can filter
|
|
4943
|
+
// dead letters by handler without parsing the subject. Falls back
|
|
4944
|
+
// to the subject itself when no glob handler is in play.
|
|
4945
|
+
pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
|
|
4946
|
+
finalDeliveryCount: msg.info.deliveryCount,
|
|
4947
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
4948
|
+
serviceName: this.serviceName,
|
|
4949
|
+
endpoint: this.serverEndpoint
|
|
4950
|
+
},
|
|
4951
|
+
this.otel,
|
|
4952
|
+
async () => {
|
|
4953
|
+
this.eventBus.emit("deadLetter" /* DeadLetter */, info);
|
|
4954
|
+
if (!this.options?.dlq) {
|
|
4955
|
+
await this.fallbackToOnDeadLetterCallback(info, msg);
|
|
4956
|
+
} else {
|
|
4957
|
+
await this.publishToDlq(msg, info, error);
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
);
|
|
3086
4961
|
}
|
|
3087
4962
|
};
|
|
3088
4963
|
|
|
3089
4964
|
// src/server/routing/rpc.router.ts
|
|
3090
|
-
import { Logger as
|
|
4965
|
+
import { Logger as Logger18 } from "@nestjs/common";
|
|
3091
4966
|
import { headers } from "@nats-io/transport-node";
|
|
3092
4967
|
var RpcRouter = class {
|
|
3093
|
-
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
|
|
4968
|
+
constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
|
|
3094
4969
|
this.messageProvider = messageProvider;
|
|
3095
4970
|
this.patternRegistry = patternRegistry;
|
|
3096
4971
|
this.connection = connection;
|
|
@@ -3100,13 +4975,26 @@ var RpcRouter = class {
|
|
|
3100
4975
|
this.ackWaitMap = ackWaitMap;
|
|
3101
4976
|
this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
|
|
3102
4977
|
this.concurrency = rpcOptions?.concurrency;
|
|
4978
|
+
if (options) {
|
|
4979
|
+
const derived = deriveOtelAttrs(options);
|
|
4980
|
+
this.otel = derived.otel;
|
|
4981
|
+
this.serviceName = derived.serviceName;
|
|
4982
|
+
this.serverEndpoint = derived.serverEndpoint;
|
|
4983
|
+
} else {
|
|
4984
|
+
this.otel = resolveOtelOptions({ enabled: false });
|
|
4985
|
+
this.serviceName = "";
|
|
4986
|
+
this.serverEndpoint = null;
|
|
4987
|
+
}
|
|
3103
4988
|
}
|
|
3104
|
-
logger = new
|
|
4989
|
+
logger = new Logger18("Jetstream:RpcRouter");
|
|
3105
4990
|
timeout;
|
|
3106
4991
|
concurrency;
|
|
3107
4992
|
resolvedAckExtensionInterval;
|
|
3108
4993
|
subscription = null;
|
|
3109
4994
|
cachedNc = null;
|
|
4995
|
+
otel;
|
|
4996
|
+
serviceName;
|
|
4997
|
+
serverEndpoint;
|
|
3110
4998
|
/** Lazily resolve the ack extension interval (needs ackWaitMap populated at runtime). */
|
|
3111
4999
|
get ackExtensionInterval() {
|
|
3112
5000
|
if (this.resolvedAckExtensionInterval !== void 0) return this.resolvedAckExtensionInterval;
|
|
@@ -3123,21 +5011,37 @@ var RpcRouter = class {
|
|
|
3123
5011
|
const patternRegistry = this.patternRegistry;
|
|
3124
5012
|
const codec = this.codec;
|
|
3125
5013
|
const eventBus = this.eventBus;
|
|
3126
|
-
const
|
|
5014
|
+
const logger5 = this.logger;
|
|
3127
5015
|
const timeout = this.timeout;
|
|
3128
5016
|
const ackExtensionInterval = this.ackExtensionInterval;
|
|
3129
5017
|
const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
|
|
3130
5018
|
const maxActive = this.concurrency ?? Number.POSITIVE_INFINITY;
|
|
5019
|
+
const otel = this.otel;
|
|
5020
|
+
const serviceName = this.serviceName;
|
|
5021
|
+
const serverEndpoint = this.serverEndpoint;
|
|
3131
5022
|
const emitRpcTimeout = (subject, correlationId) => {
|
|
3132
5023
|
eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
|
|
3133
5024
|
};
|
|
5025
|
+
const reportHandlerCompleted = (msg, startedAt, status) => {
|
|
5026
|
+
if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
|
|
5027
|
+
const declared = patternRegistry.resolveDeclared(msg.subject);
|
|
5028
|
+
const pattern = declared?.pattern ?? msg.subject;
|
|
5029
|
+
const declaredKind = declared?.kind ?? "cmd" /* Command */;
|
|
5030
|
+
eventBus.emit(
|
|
5031
|
+
"handlerCompleted" /* HandlerCompleted */,
|
|
5032
|
+
pattern,
|
|
5033
|
+
declaredKind,
|
|
5034
|
+
performance.now() - startedAt,
|
|
5035
|
+
status
|
|
5036
|
+
);
|
|
5037
|
+
};
|
|
3134
5038
|
const publishReply = (replyTo, correlationId, payload) => {
|
|
3135
5039
|
try {
|
|
3136
5040
|
const hdrs = headers();
|
|
3137
5041
|
hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
|
|
3138
5042
|
nc.publish(replyTo, codec.encode(payload), { headers: hdrs });
|
|
3139
5043
|
} catch (publishErr) {
|
|
3140
|
-
|
|
5044
|
+
logger5.error(`Failed to publish RPC response`, publishErr);
|
|
3141
5045
|
}
|
|
3142
5046
|
};
|
|
3143
5047
|
const publishErrorReply = (replyTo, correlationId, subject, err) => {
|
|
@@ -3147,7 +5051,7 @@ var RpcRouter = class {
|
|
|
3147
5051
|
hdrs.set("x-error" /* Error */, "true");
|
|
3148
5052
|
nc.publish(replyTo, codec.encode(serializeError(err)), { headers: hdrs });
|
|
3149
5053
|
} catch (encodeErr) {
|
|
3150
|
-
|
|
5054
|
+
logger5.error(`Failed to encode RPC error for ${subject}`, encodeErr);
|
|
3151
5055
|
}
|
|
3152
5056
|
};
|
|
3153
5057
|
const resolveCommand = (msg) => {
|
|
@@ -3156,7 +5060,7 @@ var RpcRouter = class {
|
|
|
3156
5060
|
const handler = patternRegistry.getHandler(subject);
|
|
3157
5061
|
if (!handler) {
|
|
3158
5062
|
msg.term(`No handler for RPC: ${subject}`);
|
|
3159
|
-
|
|
5063
|
+
logger5.error(`No handler for RPC subject: ${subject}`);
|
|
3160
5064
|
return null;
|
|
3161
5065
|
}
|
|
3162
5066
|
const msgHeaders = msg.headers;
|
|
@@ -3164,7 +5068,7 @@ var RpcRouter = class {
|
|
|
3164
5068
|
const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
|
|
3165
5069
|
if (!replyTo || !correlationId) {
|
|
3166
5070
|
msg.term("Missing required headers (reply-to or correlation-id)");
|
|
3167
|
-
|
|
5071
|
+
logger5.error(`Missing headers for RPC: ${subject}`);
|
|
3168
5072
|
return null;
|
|
3169
5073
|
}
|
|
3170
5074
|
let data;
|
|
@@ -3172,17 +5076,17 @@ var RpcRouter = class {
|
|
|
3172
5076
|
data = codec.decode(msg.data);
|
|
3173
5077
|
} catch (err) {
|
|
3174
5078
|
msg.term("Decode error");
|
|
3175
|
-
|
|
5079
|
+
logger5.error(`Decode error for RPC ${subject}:`, err);
|
|
3176
5080
|
return null;
|
|
3177
5081
|
}
|
|
3178
5082
|
eventBus.emitMessageRouted(subject, "rpc" /* Rpc */);
|
|
3179
5083
|
return { handler, data, replyTo, correlationId };
|
|
3180
5084
|
} catch (err) {
|
|
3181
|
-
|
|
5085
|
+
logger5.error("Unexpected error in RPC router", err);
|
|
3182
5086
|
try {
|
|
3183
5087
|
msg.term("Unexpected router error");
|
|
3184
5088
|
} catch (termErr) {
|
|
3185
|
-
|
|
5089
|
+
logger5.error(`Failed to terminate RPC message ${subject}:`, termErr);
|
|
3186
5090
|
}
|
|
3187
5091
|
return null;
|
|
3188
5092
|
}
|
|
@@ -3194,20 +5098,45 @@ var RpcRouter = class {
|
|
|
3194
5098
|
const subject = msg.subject;
|
|
3195
5099
|
const ctx = new RpcContext([msg]);
|
|
3196
5100
|
const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
|
|
5101
|
+
const startedAt = performance.now();
|
|
5102
|
+
const reportHandlerError = (err) => {
|
|
5103
|
+
eventBus.emit(
|
|
5104
|
+
"error" /* Error */,
|
|
5105
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
5106
|
+
`rpc-handler:${subject}`
|
|
5107
|
+
);
|
|
5108
|
+
publishErrorReply(replyTo, correlationId, subject, err);
|
|
5109
|
+
msg.term(`Handler error: ${subject}`);
|
|
5110
|
+
};
|
|
5111
|
+
const abortController = new AbortController();
|
|
3197
5112
|
let pending;
|
|
3198
5113
|
try {
|
|
3199
|
-
pending =
|
|
5114
|
+
pending = withConsumeSpan(
|
|
5115
|
+
{
|
|
5116
|
+
subject,
|
|
5117
|
+
msg,
|
|
5118
|
+
info: msg.info,
|
|
5119
|
+
kind: "rpc" /* Rpc */,
|
|
5120
|
+
payloadBytes: msg.data.length,
|
|
5121
|
+
handlerMetadata: { pattern: subject },
|
|
5122
|
+
serviceName,
|
|
5123
|
+
endpoint: serverEndpoint
|
|
5124
|
+
},
|
|
5125
|
+
otel,
|
|
5126
|
+
() => unwrapResult(handler(data, ctx)),
|
|
5127
|
+
{ signal: abortController.signal, timeoutLabel: "rpc.handler.timeout" }
|
|
5128
|
+
);
|
|
3200
5129
|
} catch (err) {
|
|
3201
5130
|
if (stopAckExtension !== null) stopAckExtension();
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
msg.term(`Handler error: ${subject}`);
|
|
5131
|
+
reportHandlerError(err);
|
|
5132
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
3205
5133
|
return void 0;
|
|
3206
5134
|
}
|
|
3207
|
-
if (!
|
|
5135
|
+
if (!isPromiseLike2(pending)) {
|
|
3208
5136
|
if (stopAckExtension !== null) stopAckExtension();
|
|
3209
5137
|
msg.ack();
|
|
3210
5138
|
publishReply(replyTo, correlationId, pending);
|
|
5139
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
3211
5140
|
return void 0;
|
|
3212
5141
|
}
|
|
3213
5142
|
let settled = false;
|
|
@@ -3215,9 +5144,10 @@ var RpcRouter = class {
|
|
|
3215
5144
|
if (settled) return;
|
|
3216
5145
|
settled = true;
|
|
3217
5146
|
if (stopAckExtension !== null) stopAckExtension();
|
|
3218
|
-
|
|
5147
|
+
abortController.abort();
|
|
3219
5148
|
emitRpcTimeout(subject, correlationId);
|
|
3220
5149
|
msg.term("Handler timeout");
|
|
5150
|
+
reportHandlerCompleted(msg, startedAt, "terminated");
|
|
3221
5151
|
}, timeout);
|
|
3222
5152
|
return pending.then(
|
|
3223
5153
|
(result) => {
|
|
@@ -3227,15 +5157,15 @@ var RpcRouter = class {
|
|
|
3227
5157
|
if (stopAckExtension !== null) stopAckExtension();
|
|
3228
5158
|
msg.ack();
|
|
3229
5159
|
publishReply(replyTo, correlationId, result);
|
|
5160
|
+
reportHandlerCompleted(msg, startedAt, "success");
|
|
3230
5161
|
},
|
|
3231
5162
|
(err) => {
|
|
3232
5163
|
if (settled) return;
|
|
3233
5164
|
settled = true;
|
|
3234
5165
|
clearTimeout(timeoutId);
|
|
3235
5166
|
if (stopAckExtension !== null) stopAckExtension();
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
msg.term(`Handler error: ${subject}`);
|
|
5167
|
+
reportHandlerError(err);
|
|
5168
|
+
reportHandlerCompleted(msg, startedAt, "error");
|
|
3239
5169
|
}
|
|
3240
5170
|
);
|
|
3241
5171
|
};
|
|
@@ -3267,7 +5197,7 @@ var RpcRouter = class {
|
|
|
3267
5197
|
backlog.push(msg);
|
|
3268
5198
|
if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
|
|
3269
5199
|
backlogWarned = true;
|
|
3270
|
-
|
|
5200
|
+
logger5.warn(
|
|
3271
5201
|
`RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
|
|
3272
5202
|
);
|
|
3273
5203
|
}
|
|
@@ -3283,7 +5213,7 @@ var RpcRouter = class {
|
|
|
3283
5213
|
}
|
|
3284
5214
|
},
|
|
3285
5215
|
error: (err) => {
|
|
3286
|
-
|
|
5216
|
+
logger5.error("Stream error in RPC router", err);
|
|
3287
5217
|
}
|
|
3288
5218
|
});
|
|
3289
5219
|
}
|
|
@@ -3295,14 +5225,14 @@ var RpcRouter = class {
|
|
|
3295
5225
|
};
|
|
3296
5226
|
|
|
3297
5227
|
// src/shutdown/shutdown.manager.ts
|
|
3298
|
-
import { Logger as
|
|
5228
|
+
import { Logger as Logger19 } from "@nestjs/common";
|
|
3299
5229
|
var ShutdownManager = class {
|
|
3300
5230
|
constructor(connection, eventBus, timeout) {
|
|
3301
5231
|
this.connection = connection;
|
|
3302
5232
|
this.eventBus = eventBus;
|
|
3303
5233
|
this.timeout = timeout;
|
|
3304
5234
|
}
|
|
3305
|
-
logger = new
|
|
5235
|
+
logger = new Logger19("Jetstream:Shutdown");
|
|
3306
5236
|
shutdownPromise;
|
|
3307
5237
|
/**
|
|
3308
5238
|
* Execute the full shutdown sequence.
|
|
@@ -3342,9 +5272,6 @@ var JetstreamModule = class {
|
|
|
3342
5272
|
this.shutdownManager = shutdownManager;
|
|
3343
5273
|
this.strategy = strategy;
|
|
3344
5274
|
}
|
|
3345
|
-
// -------------------------------------------------------------------
|
|
3346
|
-
// forRoot — global module registration
|
|
3347
|
-
// -------------------------------------------------------------------
|
|
3348
5275
|
/**
|
|
3349
5276
|
* Register the JetStream transport globally.
|
|
3350
5277
|
*
|
|
@@ -3359,21 +5286,20 @@ var JetstreamModule = class {
|
|
|
3359
5286
|
return {
|
|
3360
5287
|
module: JetstreamModule,
|
|
3361
5288
|
global: true,
|
|
5289
|
+
imports: options.metrics ? [JetstreamMetricsModule.forFeature(options.metrics)] : [],
|
|
3362
5290
|
providers,
|
|
3363
5291
|
exports: [
|
|
3364
5292
|
JETSTREAM_CONNECTION,
|
|
3365
5293
|
JETSTREAM_CODEC,
|
|
3366
5294
|
JETSTREAM_EVENT_BUS,
|
|
3367
5295
|
JETSTREAM_OPTIONS,
|
|
5296
|
+
PatternRegistry,
|
|
3368
5297
|
ShutdownManager,
|
|
3369
5298
|
JetstreamStrategy,
|
|
3370
5299
|
JetstreamHealthIndicator
|
|
3371
5300
|
]
|
|
3372
5301
|
};
|
|
3373
5302
|
}
|
|
3374
|
-
// -------------------------------------------------------------------
|
|
3375
|
-
// forRootAsync — async global module registration
|
|
3376
|
-
// -------------------------------------------------------------------
|
|
3377
5303
|
/**
|
|
3378
5304
|
* Register the JetStream transport globally with async configuration.
|
|
3379
5305
|
*
|
|
@@ -3386,25 +5312,24 @@ var JetstreamModule = class {
|
|
|
3386
5312
|
static forRootAsync(asyncOptions) {
|
|
3387
5313
|
const asyncProviders = this.createAsyncOptionsProvider(asyncOptions);
|
|
3388
5314
|
const coreProviders = this.createCoreDependentProviders();
|
|
5315
|
+
const metricsImports = asyncOptions.metrics ? [JetstreamMetricsModule.forFeature(asyncOptions.metrics)] : [];
|
|
3389
5316
|
return {
|
|
3390
5317
|
module: JetstreamModule,
|
|
3391
5318
|
global: true,
|
|
3392
|
-
imports: asyncOptions.imports ?? [],
|
|
5319
|
+
imports: [...asyncOptions.imports ?? [], ...metricsImports],
|
|
3393
5320
|
providers: [...asyncProviders, ...coreProviders],
|
|
3394
5321
|
exports: [
|
|
3395
5322
|
JETSTREAM_CONNECTION,
|
|
3396
5323
|
JETSTREAM_CODEC,
|
|
3397
5324
|
JETSTREAM_EVENT_BUS,
|
|
3398
5325
|
JETSTREAM_OPTIONS,
|
|
5326
|
+
PatternRegistry,
|
|
3399
5327
|
ShutdownManager,
|
|
3400
5328
|
JetstreamStrategy,
|
|
3401
5329
|
JetstreamHealthIndicator
|
|
3402
5330
|
]
|
|
3403
5331
|
};
|
|
3404
5332
|
}
|
|
3405
|
-
// -------------------------------------------------------------------
|
|
3406
|
-
// forFeature — per-module client registration
|
|
3407
|
-
// -------------------------------------------------------------------
|
|
3408
5333
|
/**
|
|
3409
5334
|
* Register a lightweight client proxy for a target service.
|
|
3410
5335
|
*
|
|
@@ -3430,9 +5355,6 @@ var JetstreamModule = class {
|
|
|
3430
5355
|
exports: [clientToken]
|
|
3431
5356
|
};
|
|
3432
5357
|
}
|
|
3433
|
-
// -------------------------------------------------------------------
|
|
3434
|
-
// Provider factories
|
|
3435
|
-
// -------------------------------------------------------------------
|
|
3436
5358
|
static createCoreProviders(options) {
|
|
3437
5359
|
return [
|
|
3438
5360
|
{
|
|
@@ -3450,8 +5372,8 @@ var JetstreamModule = class {
|
|
|
3450
5372
|
provide: JETSTREAM_EVENT_BUS,
|
|
3451
5373
|
inject: [JETSTREAM_OPTIONS],
|
|
3452
5374
|
useFactory: (options) => {
|
|
3453
|
-
const
|
|
3454
|
-
return new EventBus(
|
|
5375
|
+
const logger5 = new Logger20("Jetstream:Module");
|
|
5376
|
+
return new EventBus(logger5, options.hooks);
|
|
3455
5377
|
}
|
|
3456
5378
|
},
|
|
3457
5379
|
// Codec — global encode/decode
|
|
@@ -3490,10 +5412,8 @@ var JetstreamModule = class {
|
|
|
3490
5412
|
);
|
|
3491
5413
|
}
|
|
3492
5414
|
},
|
|
3493
|
-
// ---------------------------------------------------------------
|
|
3494
5415
|
// Consumer infrastructure — only created when consumer !== false.
|
|
3495
5416
|
// Providers return null when consumer is disabled (publisher-only mode).
|
|
3496
|
-
// ---------------------------------------------------------------
|
|
3497
5417
|
// PatternRegistry — subject-to-handler mapping
|
|
3498
5418
|
{
|
|
3499
5419
|
provide: PatternRegistry,
|
|
@@ -3529,8 +5449,14 @@ var JetstreamModule = class {
|
|
|
3529
5449
|
// MessageProvider — pull-based message consumption
|
|
3530
5450
|
{
|
|
3531
5451
|
provide: MessageProvider,
|
|
3532
|
-
inject: [
|
|
3533
|
-
|
|
5452
|
+
inject: [
|
|
5453
|
+
JETSTREAM_OPTIONS,
|
|
5454
|
+
JETSTREAM_CONNECTION,
|
|
5455
|
+
JETSTREAM_EVENT_BUS,
|
|
5456
|
+
ConsumerProvider,
|
|
5457
|
+
StreamProvider
|
|
5458
|
+
],
|
|
5459
|
+
useFactory: (options, connection, eventBus, consumerProvider, streamProvider) => {
|
|
3534
5460
|
if (options.consumer === false) return null;
|
|
3535
5461
|
const consumeOptionsMap = /* @__PURE__ */ new Map();
|
|
3536
5462
|
if (options.events?.consume)
|
|
@@ -3540,10 +5466,22 @@ var JetstreamModule = class {
|
|
|
3540
5466
|
if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
|
|
3541
5467
|
consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
|
|
3542
5468
|
}
|
|
3543
|
-
const
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
5469
|
+
const derived = deriveOtelAttrs(options);
|
|
5470
|
+
const { otel, serverEndpoint: otelEndpoint, serviceName: otelServiceName } = derived;
|
|
5471
|
+
const consumerRecoveryFn = consumerProvider && streamProvider ? async (kind) => withSelfHealingSpan(
|
|
5472
|
+
otel,
|
|
5473
|
+
{
|
|
5474
|
+
serviceName: otelServiceName,
|
|
5475
|
+
endpoint: otelEndpoint,
|
|
5476
|
+
consumer: consumerProvider.getConsumerName(kind),
|
|
5477
|
+
stream: streamProvider.getStreamName(kind),
|
|
5478
|
+
reason: "consumer not found"
|
|
5479
|
+
},
|
|
5480
|
+
async () => {
|
|
5481
|
+
const jsm = await connection.getJetStreamManager();
|
|
5482
|
+
return consumerProvider.recoverConsumer(jsm, kind);
|
|
5483
|
+
}
|
|
5484
|
+
) : void 0;
|
|
3547
5485
|
return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
|
|
3548
5486
|
}
|
|
3549
5487
|
},
|
|
@@ -3614,7 +5552,8 @@ var JetstreamModule = class {
|
|
|
3614
5552
|
codec,
|
|
3615
5553
|
eventBus,
|
|
3616
5554
|
rpcOptions,
|
|
3617
|
-
ackWaitMap
|
|
5555
|
+
ackWaitMap,
|
|
5556
|
+
options
|
|
3618
5557
|
);
|
|
3619
5558
|
}
|
|
3620
5559
|
},
|
|
@@ -3717,9 +5656,6 @@ var JetstreamModule = class {
|
|
|
3717
5656
|
}
|
|
3718
5657
|
];
|
|
3719
5658
|
}
|
|
3720
|
-
// -------------------------------------------------------------------
|
|
3721
|
-
// Lifecycle hooks
|
|
3722
|
-
// -------------------------------------------------------------------
|
|
3723
5659
|
/**
|
|
3724
5660
|
* Gracefully shut down the transport on application termination.
|
|
3725
5661
|
*/
|
|
@@ -3731,13 +5667,14 @@ var JetstreamModule = class {
|
|
|
3731
5667
|
};
|
|
3732
5668
|
JetstreamModule = __decorateClass([
|
|
3733
5669
|
Global(),
|
|
3734
|
-
|
|
3735
|
-
__decorateParam(0,
|
|
3736
|
-
__decorateParam(0,
|
|
3737
|
-
__decorateParam(1,
|
|
3738
|
-
__decorateParam(1,
|
|
5670
|
+
Module2({}),
|
|
5671
|
+
__decorateParam(0, Optional2()),
|
|
5672
|
+
__decorateParam(0, Inject2(ShutdownManager)),
|
|
5673
|
+
__decorateParam(1, Optional2()),
|
|
5674
|
+
__decorateParam(1, Inject2(JetstreamStrategy))
|
|
3739
5675
|
], JetstreamModule);
|
|
3740
5676
|
export {
|
|
5677
|
+
ConsumeKind,
|
|
3741
5678
|
DEFAULT_BROADCAST_CONSUMER_CONFIG,
|
|
3742
5679
|
DEFAULT_BROADCAST_STREAM_CONFIG,
|
|
3743
5680
|
DEFAULT_COMMAND_CONSUMER_CONFIG,
|
|
@@ -3753,6 +5690,7 @@ export {
|
|
|
3753
5690
|
DEFAULT_ORDERED_STREAM_CONFIG,
|
|
3754
5691
|
DEFAULT_RPC_TIMEOUT,
|
|
3755
5692
|
DEFAULT_SHUTDOWN_TIMEOUT,
|
|
5693
|
+
DEFAULT_TRACES,
|
|
3756
5694
|
JETSTREAM_CODEC,
|
|
3757
5695
|
JETSTREAM_CONNECTION,
|
|
3758
5696
|
JETSTREAM_OPTIONS,
|
|
@@ -3764,15 +5702,18 @@ export {
|
|
|
3764
5702
|
JetstreamRecord,
|
|
3765
5703
|
JetstreamRecordBuilder,
|
|
3766
5704
|
JetstreamStrategy,
|
|
5705
|
+
JetstreamTrace,
|
|
3767
5706
|
JsonCodec,
|
|
3768
5707
|
MIN_METADATA_TTL,
|
|
3769
5708
|
MessageKind,
|
|
3770
5709
|
MsgpackCodec,
|
|
3771
5710
|
NatsErrorCode,
|
|
3772
5711
|
PatternPrefix,
|
|
5712
|
+
PublishKind,
|
|
3773
5713
|
RESERVED_HEADERS,
|
|
3774
5714
|
RpcContext,
|
|
3775
5715
|
StreamKind,
|
|
5716
|
+
TRACER_NAME,
|
|
3776
5717
|
TransportEvent,
|
|
3777
5718
|
buildBroadcastSubject,
|
|
3778
5719
|
buildSubject,
|