@palmetto/pubsub 2.1.3 → 2.2.1

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 CHANGED
@@ -61,6 +61,10 @@ yarn add @palmetto/pubsub zod
61
61
  };
62
62
 
63
63
  await publisher.publish(schemaConfig, message);
64
+
65
+ // publish a batch of messages quicker than sending the one at a time:
66
+ const messages: Model[] = [...];
67
+ await publisher.publish(schemaConfig, messages);
64
68
  ```
65
69
 
66
70
  1. Create subscriber and subscriber to a message
@@ -108,6 +112,15 @@ yarn add @palmetto/pubsub zod
108
112
  await subscriber.startSubscribe(schemaConfig, onMessage);
109
113
  ```
110
114
 
115
+ 1. Register with dd-trace for improved trace logs
116
+
117
+ ```ts
118
+ import tracer from "dd-trace";
119
+ import { registerDdTrace } from "@palmetto/pubsub";
120
+
121
+ registerDdTrace(tracer);
122
+ ```
123
+
111
124
  ### Message logging
112
125
 
113
126
  For each message you can indicate how to log messages when they are published or handled by a subscriber. Use the `messageLogLevel` property of the queue configuration object.
@@ -8,7 +8,7 @@ export declare class BullMqPublisher implements PublisherProvider {
8
8
  constructor(connection: bullmq.ConnectionOptions, logger: Logger);
9
9
  readonly transport: string;
10
10
  private getQueue;
11
- publish(config: BullMqQueueConfiguration, message: string): Promise<void>;
11
+ publish(config: BullMqQueueConfiguration, messages: string[]): Promise<void>;
12
12
  init(config: BullMqQueueConfiguration): Promise<void>;
13
13
  close(): Promise<void>;
14
14
  enrichPublishedMesssageLog(config: BullMqQueueConfiguration): Record<string, unknown>;
@@ -36,18 +36,20 @@ class BullMqPublisher {
36
36
  return queue;
37
37
  });
38
38
  }
39
- publish(config, message) {
39
+ publish(config, messages) {
40
40
  return __awaiter(this, void 0, void 0, function* () {
41
- var _a, _b;
42
41
  const queue = yield this.getQueue(config);
43
- (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Publishing message to ${queue.name} - ${message}`);
44
- yield queue.add(config.job || connection_js_1.BULLMQ_DEFAULTJOB, message, {
45
- attempts: config.retries,
46
- backoff: {
47
- type: "fixed",
48
- delay: config.retryDelay || 30000,
49
- },
50
- });
42
+ yield Promise.all(messages.map((message) => __awaiter(this, void 0, void 0, function* () {
43
+ var _a, _b;
44
+ (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Publishing message to ${queue.name} - ${message}`);
45
+ yield queue.add(config.job || connection_js_1.BULLMQ_DEFAULTJOB, message, {
46
+ attempts: config.retries,
47
+ backoff: {
48
+ type: "fixed",
49
+ delay: config.retryDelay || 30000,
50
+ },
51
+ });
52
+ })));
51
53
  });
52
54
  }
53
55
  init(config) {
@@ -6,7 +6,7 @@ export declare class BullMqPubSubProvider implements PubSubProvider {
6
6
  private readonly subscriber;
7
7
  constructor(connection: ConnectionOptions, logger: Logger);
8
8
  readonly transport: string;
9
- publish(config: BullMqQueueConfiguration, message: string): Promise<void>;
9
+ publish(config: BullMqQueueConfiguration, messages: string[]): Promise<void>;
10
10
  startSubscribe(config: BullMqQueueConfiguration, onMessage: (s: string, context: MessageContext) => Promise<MessageResult> | MessageResult): Promise<StopSubscribe>;
11
11
  close(): Promise<void>;
12
12
  init(config: BullMqQueueConfiguration): Promise<void>;
@@ -19,8 +19,8 @@ class BullMqPubSubProvider {
19
19
  this.publisher = new publisher_js_1.BullMqPublisher(connection, logger);
20
20
  this.subscriber = new subscriber_js_1.BullMqSubscriber(connection, logger);
21
21
  }
22
- publish(config, message) {
23
- return this.publisher.publish(config, message);
22
+ publish(config, messages) {
23
+ return this.publisher.publish(config, messages);
24
24
  }
25
25
  startSubscribe(config, onMessage) {
26
26
  return this.subscriber.startSubscribe(config, onMessage);
@@ -0,0 +1,61 @@
1
+ interface Span {
2
+ /**
3
+ * Sets the end timestamp and finalizes Span state.
4
+ *
5
+ * With the exception of calls to Span.context() (which are always allowed),
6
+ * finish() must be the last call made to any span instance, and to do
7
+ * otherwise leads to undefined behavior.
8
+ */
9
+ finish(): void;
10
+ /**
11
+ * Adds a single tag to the span. See `addTags()` for details.
12
+ *
13
+ * @param {string} key
14
+ * @param {any} value
15
+ */
16
+ setTag(key: string, value: unknown): this;
17
+ }
18
+ interface SpanOptions {
19
+ /**
20
+ * a parent SpanContext (or Span, for convenience) that the newly-started
21
+ * span will be the child of (per REFERENCE_CHILD_OF). If specified,
22
+ * `references` must be unspecified.
23
+ */
24
+ childOf: Span | null;
25
+ /**
26
+ * set of key-value pairs which will be set as tags on the newly created
27
+ * Span. Ownership of the object is passed to the created span for
28
+ * efficiency reasons (the caller should not modify this object after
29
+ * calling startSpan).
30
+ */
31
+ tags: {
32
+ [key: string]: unknown;
33
+ };
34
+ }
35
+ /**
36
+ * The Datadog Scope Manager. This is used for context propagation.
37
+ */
38
+ interface Scope {
39
+ /**
40
+ * Get the current active span or null if there is none.
41
+ *
42
+ * @returns {Span} The active span.
43
+ */
44
+ active(): Span | null;
45
+ }
46
+ interface Tracer {
47
+ /**
48
+ * Starts and returns a new Span representing a logical unit of work.
49
+ * @param {string} name The name of the operation.
50
+ * @param {SpanOptions} [options] Options for the newly created span.
51
+ * @returns {Span} A new Span object.
52
+ */
53
+ startSpan(name: string, options: SpanOptions): Span;
54
+ /**
55
+ * Returns a reference to the current scope.
56
+ */
57
+ scope(): Scope;
58
+ }
59
+ export declare function getTracer(): Tracer;
60
+ export declare function registerDdTrace(tracer: Tracer): void;
61
+ export {};
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTracer = getTracer;
4
+ exports.registerDdTrace = registerDdTrace;
5
+ class NoOpSpan {
6
+ finish() {
7
+ // no-op
8
+ }
9
+ setTag(_key, _value) {
10
+ return this;
11
+ }
12
+ }
13
+ class NoOpScope {
14
+ active() {
15
+ return null;
16
+ }
17
+ }
18
+ class NoOpTracer {
19
+ constructor() {
20
+ this.noOpSpan = new NoOpSpan();
21
+ this.noOpScope = new NoOpScope();
22
+ }
23
+ startSpan(_name, _options) {
24
+ return this.noOpSpan;
25
+ }
26
+ scope() {
27
+ return this.noOpScope;
28
+ }
29
+ }
30
+ let tracerInstance = new NoOpTracer();
31
+ function getTracer() {
32
+ return tracerInstance;
33
+ }
34
+ function registerDdTrace(tracer) {
35
+ tracerInstance = tracer;
36
+ }
package/dist/errors.d.ts CHANGED
@@ -10,6 +10,8 @@ export declare class MissingPubSubProviderError extends Error {
10
10
  static isMissingPubSubProviderError(err: unknown): err is MissingPubSubProviderError;
11
11
  }
12
12
  export declare class PublishError extends Error {
13
+ readonly failedMessages: string[];
14
+ constructor(message: string, failedMessages: string[]);
13
15
  static isPublishError(err: unknown): err is PublishError;
14
16
  }
15
17
  export declare class SchemaValidationError extends Error {
package/dist/errors.js CHANGED
@@ -24,6 +24,10 @@ class MissingPubSubProviderError extends Error {
24
24
  }
25
25
  exports.MissingPubSubProviderError = MissingPubSubProviderError;
26
26
  class PublishError extends Error {
27
+ constructor(message, failedMessages) {
28
+ super(message);
29
+ this.failedMessages = failedMessages;
30
+ }
27
31
  static isPublishError(err) {
28
32
  return err instanceof PublishError;
29
33
  }
@@ -23,7 +23,7 @@ export type BaseMessage = z.infer<typeof IdMetaSchema>;
23
23
  export interface PublisherProvider {
24
24
  transport: string;
25
25
  init(config: PubSubConfiguration): Promise<void> | void;
26
- publish(config: PubSubConfiguration, message: string): Promise<void> | void;
26
+ publish(config: PubSubConfiguration, messages: string[]): Promise<void> | void;
27
27
  close(): Promise<void> | void;
28
28
  enrichPublishedMesssageLog(config: PubSubConfiguration): Record<string, unknown>;
29
29
  }
package/dist/main.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./dd-trace.wrapper.js";
1
2
  export * from "./errors.js";
2
3
  export * from "./interfaces.js";
3
4
  export * from "./publisher.js";
package/dist/main.js CHANGED
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./dd-trace.wrapper.js"), exports);
17
18
  __exportStar(require("./errors.js"), exports);
18
19
  __exportStar(require("./interfaces.js"), exports);
19
20
  __exportStar(require("./publisher.js"), exports);
@@ -7,7 +7,7 @@ export declare class Publisher {
7
7
  addProvider(provider: PublisherProvider): void;
8
8
  removeProvider(providerOrTransport: PublisherProvider | string): boolean;
9
9
  init(config: PubSubConfiguration): Promise<void>;
10
- publish(config: PubSubConfiguration, message: BaseMessage): Promise<void>;
10
+ publish(config: PubSubConfiguration, message: BaseMessage | BaseMessage[]): Promise<void>;
11
11
  close(): Promise<void>;
12
12
  private getProvider;
13
13
  private assertSchema;
package/dist/publisher.js CHANGED
@@ -15,6 +15,7 @@ const uuid_1 = require("uuid");
15
15
  const crypto_1 = require("crypto");
16
16
  const errors_js_1 = require("./errors.js");
17
17
  const message_logger_js_1 = require("./message-logger.js");
18
+ const dd_trace_wrapper_js_1 = require("./dd-trace.wrapper.js");
18
19
  class Publisher {
19
20
  constructor(logger, providers) {
20
21
  var _a, _b;
@@ -53,43 +54,71 @@ class Publisher {
53
54
  }
54
55
  publish(config, message) {
55
56
  return __awaiter(this, void 0, void 0, function* () {
56
- const provider = this.getProvider(config);
57
- if (!message.id) {
58
- message.id = (0, uuid_1.v4)();
57
+ const tracer = (0, dd_trace_wrapper_js_1.getTracer)();
58
+ const span = tracer.startSpan("pubsub.publish", {
59
+ childOf: tracer.scope().active(),
60
+ tags: {
61
+ "pubsub.transport": config.transport,
62
+ "pubsub.name": config.name,
63
+ },
64
+ });
65
+ try {
66
+ const provider = this.getProvider(config);
67
+ const { schema, schemaId } = this.assertSchema(config);
68
+ if (!Array.isArray(message)) {
69
+ message = [message];
70
+ }
71
+ const jsons = message.map((msg) => {
72
+ if (!msg.id) {
73
+ msg.id = (0, uuid_1.v4)();
74
+ }
75
+ if (!msg.meta) {
76
+ msg.meta = {
77
+ createdAt: new Date().toISOString(),
78
+ publishedBy: "",
79
+ schemaId: schemaId,
80
+ };
81
+ }
82
+ else {
83
+ msg.meta.schemaId = schemaId;
84
+ }
85
+ const check = schema.safeEncode(msg);
86
+ if (!check.success) {
87
+ (0, message_logger_js_1.logMessage)({
88
+ note: "Publish message failed schema validation",
89
+ message: message,
90
+ level: "error",
91
+ logger: this.logger,
92
+ extra: Object.assign({ transport: provider.transport, name: config.name }, provider.enrichPublishedMesssageLog(config)),
93
+ });
94
+ throw new errors_js_1.SchemaValidationError(`Schema did not accept the published message: ${check.error.message}`);
95
+ }
96
+ const json = JSON.stringify(check.data);
97
+ return { json, data: check.data };
98
+ });
99
+ const start = (0, message_logger_js_1.startTiming)();
100
+ yield provider.publish(config, jsons.map((j) => j.json));
101
+ const duration = (0, message_logger_js_1.getDuration)(start);
102
+ jsons.forEach((msg) => {
103
+ (0, message_logger_js_1.logMessage)({
104
+ note: "Published message",
105
+ message: msg.data,
106
+ level: config.messageLogLevel,
107
+ logger: this.logger,
108
+ extra: Object.assign({ transport: provider.transport, name: config.name, durationMs: duration }, provider.enrichPublishedMesssageLog(config)),
109
+ });
110
+ });
111
+ span.setTag("pubsub.duration_ms", duration);
112
+ span.setTag("pubsub.message_count", message.length);
113
+ span.setTag("pubsub.success", true);
59
114
  }
60
- const { schema, schemaId } = this.assertSchema(config);
61
- if (!message.meta) {
62
- message.meta = {
63
- createdAt: new Date().toISOString(),
64
- publishedBy: "",
65
- schemaId: schemaId,
66
- };
115
+ catch (err) {
116
+ span.setTag("pubsub.success", false);
117
+ throw err;
67
118
  }
68
- else {
69
- message.meta.schemaId = schemaId;
119
+ finally {
120
+ span.finish();
70
121
  }
71
- const check = schema.safeEncode(message);
72
- if (!check.success) {
73
- (0, message_logger_js_1.logMessage)({
74
- note: "Publish message failed schema validation",
75
- message: message,
76
- level: "error",
77
- logger: this.logger,
78
- extra: Object.assign({ transport: provider.transport, name: config.name }, provider.enrichPublishedMesssageLog(config)),
79
- });
80
- throw new errors_js_1.SchemaValidationError(`Schema did not accept the published message: ${check.error.message}`);
81
- }
82
- const json = JSON.stringify(check.data);
83
- const start = (0, message_logger_js_1.startTiming)();
84
- yield provider.publish(config, json);
85
- const duration = (0, message_logger_js_1.getDuration)(start);
86
- (0, message_logger_js_1.logMessage)({
87
- note: "Published message",
88
- message: check.data,
89
- level: config.messageLogLevel,
90
- logger: this.logger,
91
- extra: Object.assign({ transport: provider.transport, name: config.name, durationMs: duration }, provider.enrichPublishedMesssageLog(config)),
92
- });
93
122
  });
94
123
  }
95
124
  close() {
@@ -108,10 +137,12 @@ class Publisher {
108
137
  return provider;
109
138
  }
110
139
  assertSchema(config) {
140
+ var _a, _b;
111
141
  const { schema } = config;
112
142
  let hash = this.hashes.get(schema);
113
143
  if (!hash) {
114
- const jsonSchema = JSON.stringify(zod_1.z.toJSONSchema(schema, { io: "input" }), null, 3);
144
+ (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Calculating schema id for ${config.name}`);
145
+ const jsonSchema = JSON.stringify(zod_1.z.toJSONSchema(schema, { io: "input" }), null, 2);
115
146
  hash = (0, crypto_1.createHash)("sha256").update(jsonSchema).digest("hex");
116
147
  this.hashes.set(schema, hash);
117
148
  }
@@ -24,9 +24,10 @@ export declare class RabbitMqPublisher implements PublisherProvider {
24
24
  * @param message The JSON message to send
25
25
  * @returns A promise that is completed when the message is published
26
26
  */
27
- publish(config: RabbitQueueExchangeConfiguration, message: string): Promise<void>;
27
+ publish(config: RabbitQueueExchangeConfiguration, messages: string[]): Promise<void>;
28
28
  enrichPublishedMesssageLog(config: RabbitQueueExchangeConfiguration): Record<string, unknown>;
29
29
  private publishToExchange;
30
30
  private publishToQueue;
31
+ private sendMessages;
31
32
  close(): Promise<void> | undefined;
32
33
  }
@@ -78,14 +78,14 @@ class RabbitMqPublisher {
78
78
  * @param message The JSON message to send
79
79
  * @returns A promise that is completed when the message is published
80
80
  */
81
- publish(config, message) {
81
+ publish(config, messages) {
82
82
  return __awaiter(this, void 0, void 0, function* () {
83
83
  const channel = yield this.getChannel(config);
84
84
  if (config.publishToSpecificQueue) {
85
- yield this.publishToQueue(channel, config.publishToSpecificQueue, message);
85
+ yield this.publishToQueue(channel, config.publishToSpecificQueue, messages);
86
86
  }
87
87
  else {
88
- yield this.publishToExchange(channel, config, message);
88
+ yield this.publishToExchange(channel, config, messages);
89
89
  }
90
90
  });
91
91
  }
@@ -100,28 +100,31 @@ class RabbitMqPublisher {
100
100
  routingKey: (0, config_js_1.getRoutingKey)(config),
101
101
  };
102
102
  }
103
- publishToExchange(channel, config, message) {
103
+ publishToExchange(channel, config, messages) {
104
104
  return __awaiter(this, void 0, void 0, function* () {
105
105
  const exchangeName = (0, config_js_1.getExchangeName)(config);
106
- const ok = yield channel.publish(exchangeName, (0, config_js_1.getRoutingKey)(config), Buffer.from(message, "utf8"), {
107
- contentType: "application/json",
108
- timestamp: new Date().valueOf(),
109
- persistent: true,
110
- });
111
- if (!ok) {
112
- throw new errors_js_1.PublishError(`RabbitMq publish to ${exchangeName} failed`);
113
- }
106
+ const routingKey = (0, config_js_1.getRoutingKey)(config);
107
+ return this.sendMessages(messages, (buf, options) => channel.publish(exchangeName, routingKey, buf, options), "publish to exchange", exchangeName);
114
108
  });
115
109
  }
116
- publishToQueue(channel, queue, message) {
110
+ publishToQueue(channel, queue, messages) {
111
+ return this.sendMessages(messages, (buf, options) => channel.sendToQueue(queue, buf, options), "send to queue", queue);
112
+ }
113
+ sendMessages(messages, sender, action, name) {
117
114
  return __awaiter(this, void 0, void 0, function* () {
118
- const ok = yield channel.sendToQueue(queue, Buffer.from(message, "utf8"), {
115
+ const options = {
119
116
  contentType: "application/json",
120
117
  timestamp: new Date().valueOf(),
121
118
  persistent: true,
122
- });
123
- if (!ok) {
124
- throw new errors_js_1.PublishError(`RabbitMq publish to queue:${queue} failed`);
119
+ };
120
+ const oks = yield Promise.all(messages.map((message) => sender(Buffer.from(message, "utf8"), options)));
121
+ const failedMessages = oks
122
+ .map((ok, index) => ({ ok, index }))
123
+ .filter(({ ok }) => !ok)
124
+ .map(({ index }) => messages[index]);
125
+ if (failedMessages.length > 0) {
126
+ const allOrSome = failedMessages.length === messages.length ? "all" : "some";
127
+ throw new errors_js_1.PublishError(`RabbitMq ${action} ${name} failed for ${allOrSome} messages`, failedMessages);
125
128
  }
126
129
  });
127
130
  }
@@ -9,7 +9,7 @@ export declare class RabbitMqPubSubProvider implements PubSubProvider {
9
9
  private readonly subscriber;
10
10
  constructor(connection: RabbitMqConnection, logger: Logger);
11
11
  readonly transport: string;
12
- publish(config: RabbitQueueExchangeConfiguration, message: string): Promise<void>;
12
+ publish(config: RabbitQueueExchangeConfiguration, messages: string[]): Promise<void>;
13
13
  startSubscribe(config: RabbitQueueExchangeConfiguration, onMessage: (s: string, context: MessageContext) => Promise<MessageResult> | MessageResult): StopSubscribe;
14
14
  close(): Promise<void>;
15
15
  init(config: RabbitQueueExchangeConfiguration): Promise<void>;
@@ -22,8 +22,8 @@ class RabbitMqPubSubProvider {
22
22
  this.publisher = new publisher_js_1.RabbitMqPublisher(connection, logger);
23
23
  this.subscriber = new subscriber_js_1.RabbitMqSubscriber(connection, logger);
24
24
  }
25
- publish(config, message) {
26
- return this.publisher.publish(config, message);
25
+ publish(config, messages) {
26
+ return this.publisher.publish(config, messages);
27
27
  }
28
28
  startSubscribe(config, onMessage) {
29
29
  return this.subscriber.startSubscribe(config, onMessage);
@@ -15,6 +15,7 @@ const errors_js_1 = require("./errors.js");
15
15
  const events_1 = require("events");
16
16
  const message_logger_js_1 = require("./message-logger.js");
17
17
  const create_log_error_payload_js_1 = require("./create-log-error-payload.js");
18
+ const dd_trace_wrapper_js_1 = require("./dd-trace.wrapper.js");
18
19
  class SubscribedMessage {
19
20
  constructor(stop) {
20
21
  this.stop = stop;
@@ -56,6 +57,14 @@ class Subscriber {
56
57
  throw new errors_js_1.MissingPubSubProviderError(`No provider configured for ${config.transport}`);
57
58
  }
58
59
  const transform = (jsonStr, context) => __awaiter(this, void 0, void 0, function* () {
60
+ const tracer = (0, dd_trace_wrapper_js_1.getTracer)();
61
+ const span = tracer.startSpan("pubsub.handleMessage", {
62
+ childOf: tracer.scope().active(),
63
+ tags: {
64
+ "pubsub.transport": config.transport,
65
+ "pubsub.name": config.name,
66
+ },
67
+ });
59
68
  const start = (0, message_logger_js_1.startTiming)();
60
69
  const jsonObject = JSON.parse(jsonStr);
61
70
  const r = schema.safeDecode(jsonObject);
@@ -77,6 +86,8 @@ class Subscriber {
77
86
  };
78
87
  this.events.emit("schemaError", handledEventContext);
79
88
  this.events.emit("messageHandled", handledEventContext);
89
+ span.setTag("pubsub.success", false);
90
+ span.finish();
80
91
  return interfaces_js_1.MessageResult.Fail;
81
92
  }
82
93
  try {
@@ -98,6 +109,9 @@ class Subscriber {
98
109
  result }, provider.enrichHandledMesssageLog(config)),
99
110
  });
100
111
  this.events.emit("messageHandled", eventContext);
112
+ span.setTag("pubsub.duration_ms", durationMs);
113
+ span.setTag("pubsub.success", true);
114
+ span.finish();
101
115
  return result;
102
116
  }
103
117
  catch (err) {
@@ -117,6 +131,8 @@ class Subscriber {
117
131
  err,
118
132
  };
119
133
  this.events.emit("messageHandled", eventContext);
134
+ span.setTag("pubsub.success", false);
135
+ span.finish();
120
136
  throw err;
121
137
  }
122
138
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palmetto/pubsub",
3
- "version": "2.1.3",
3
+ "version": "2.2.1",
4
4
  "main": "./dist/main.js",
5
5
  "scripts": {
6
6
  "lint": "yarn run -T eslint --fix ./src",