@jetit/publisher 5.0.0 → 5.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetit/publisher",
3
- "version": "5.0.0",
3
+ "version": "5.1.1",
4
4
  "type": "commonjs",
5
5
  "dependencies": {
6
6
  "@jetit/id": "^0.0.12",
@@ -21,6 +21,11 @@ export declare class PrometheusAdapter {
21
21
  private subscribeErrorCount;
22
22
  private individualQueueDepth;
23
23
  private duplicateEventCount;
24
+ private messageRatePublish;
25
+ private messageRateSubscribe;
26
+ private processingTimeHistogram;
27
+ private redisCommandLatency;
28
+ private consumerLag;
24
29
  /**
25
30
  *
26
31
  * @param streams [Publisher]
@@ -75,6 +75,36 @@ class PrometheusAdapter {
75
75
  labelNames: ['queue_name'],
76
76
  registers: [this.registry],
77
77
  });
78
+ this.messageRatePublish = new this.promClient.Gauge({
79
+ name: 'message_rate_publish',
80
+ help: 'Number of messages published per event type',
81
+ labelNames: ['event_type'],
82
+ registers: [this.registry],
83
+ });
84
+ this.messageRateSubscribe = new this.promClient.Gauge({
85
+ name: 'message_rate_subscribe',
86
+ help: 'Number of messages subscribed per event type',
87
+ labelNames: ['event_type'],
88
+ registers: [this.registry],
89
+ });
90
+ this.processingTimeHistogram = new this.promClient.Histogram({
91
+ name: 'processing_time_histogram',
92
+ help: 'Distribution of event processing times',
93
+ buckets: [0, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000],
94
+ registers: [this.registry],
95
+ });
96
+ this.redisCommandLatency = new this.promClient.Gauge({
97
+ name: 'redis_command_latency',
98
+ help: 'Latency of Redis commands',
99
+ labelNames: ['command'],
100
+ registers: [this.registry],
101
+ });
102
+ this.consumerLag = new this.promClient.Gauge({
103
+ name: 'consumer_lag',
104
+ help: 'Lag of consumers in milliseconds',
105
+ labelNames: ['consumer_group'],
106
+ registers: [this.registry],
107
+ });
78
108
  this.registry.registerMetric(this.queueDepth);
79
109
  this.registry.registerMetric(this.dlqSize);
80
110
  this.registry.registerMetric(this.dlqRate);
@@ -87,6 +117,11 @@ class PrometheusAdapter {
87
117
  this.registry.registerMetric(this.subscribeErrorCount);
88
118
  this.registry.registerMetric(this.individualQueueDepth);
89
119
  this.registry.registerMetric(this.duplicateEventCount);
120
+ this.registry.registerMetric(this.messageRatePublish);
121
+ this.registry.registerMetric(this.messageRateSubscribe);
122
+ this.registry.registerMetric(this.processingTimeHistogram);
123
+ this.registry.registerMetric(this.redisCommandLatency);
124
+ this.registry.registerMetric(this.consumerLag);
90
125
  }
91
126
  async updateMetrics() {
92
127
  const metrics = await this.streams.getLatestMetrics();
@@ -111,6 +146,24 @@ class PrometheusAdapter {
111
146
  this.individualQueueDepth.set({ queue_name: queueName }, depth);
112
147
  });
113
148
  }
149
+ Object.entries(metrics.messageRatePublish).forEach(([eventType, rate]) => {
150
+ this.messageRatePublish.set({ event_type: eventType }, rate);
151
+ });
152
+ Object.entries(metrics.messageRateSubscribe).forEach(([eventType, rate]) => {
153
+ this.messageRateSubscribe.set({ event_type: eventType }, rate);
154
+ });
155
+ Object.entries(metrics.processingTimeDistribution).forEach(([bucket, count]) => {
156
+ const [lower, upper] = bucket.split('-').map(Number);
157
+ for (let i = 0; i < count; i++) {
158
+ this.processingTimeHistogram.observe(upper || lower);
159
+ }
160
+ });
161
+ Object.entries(metrics.redisCommandLatencies).forEach(([command, latency]) => {
162
+ this.redisCommandLatency.set({ command }, latency);
163
+ });
164
+ Object.entries(metrics.consumerLag).forEach(([consumerGroup, lag]) => {
165
+ this.consumerLag.set({ consumer_group: consumerGroup }, lag);
166
+ });
114
167
  }
115
168
  /**
116
169
  * @param app This needs to be an instance of express or fastify something that supports the express api
@@ -10,6 +10,11 @@ export declare class MetricsCollector {
10
10
  constructor(config: IMetricsCollectorConfig, dlq: DeadLetterQueue);
11
11
  addMetrics(metrics: IPerformanceMetrics): void;
12
12
  private aggregateAndStoreMetrics;
13
+ private aggregateMessageRates;
14
+ private aggregateHistogram;
15
+ private aggregateRedisCommandLatencies;
16
+ private aggregateConsumerLag;
17
+ private calculateAverages;
13
18
  private getQueueDepth;
14
19
  private storeMetrics;
15
20
  getMetrics(startTime: number, endTime: number): Promise<IAggregatedMetrics[]>;
@@ -32,6 +32,11 @@ class MetricsCollector {
32
32
  dlqRate: 0,
33
33
  operationCount: this.metrics.length,
34
34
  duplicateEventsCount: 0,
35
+ messageRatePublish: {},
36
+ messageRateSubscribe: {},
37
+ processingTimeDistribution: {},
38
+ redisCommandLatencies: {},
39
+ consumerLag: {},
35
40
  };
36
41
  for (const metric of this.metrics) {
37
42
  aggregated.totalTime += metric.totalTime;
@@ -41,6 +46,11 @@ class MetricsCollector {
41
46
  aggregated.publishErrorCount += metric.publishErrorCount;
42
47
  aggregated.subscribeErrorCount += metric.subscribeErrorCount;
43
48
  aggregated.duplicateEventsCount += metric.duplicateEventsCount;
49
+ this.aggregateMessageRates(aggregated.messageRatePublish, metric.messageRatePublish);
50
+ this.aggregateMessageRates(aggregated.messageRateSubscribe, metric.messageRateSubscribe);
51
+ this.aggregateHistogram(aggregated.processingTimeDistribution, metric.processingTimeDistribution);
52
+ this.aggregateRedisCommandLatencies(aggregated.redisCommandLatencies, metric.redisCommandLatencies);
53
+ this.aggregateConsumerLag(aggregated.consumerLag, metric.consumerLag);
44
54
  }
45
55
  // Calculate averages for time-based metrics
46
56
  aggregated.totalTime /= aggregated.operationCount;
@@ -49,21 +59,65 @@ class MetricsCollector {
49
59
  const dlqStats = await this.dlq.getDLQStats();
50
60
  aggregated.dlqSize = dlqStats.size;
51
61
  aggregated.dlqRate = dlqStats.additionRate;
62
+ this.calculateAverages(aggregated.redisCommandLatencies);
63
+ this.calculateAverages(aggregated.consumerLag);
52
64
  await this.storeMetrics(aggregated);
53
65
  this.metrics = [];
54
66
  }
67
+ aggregateMessageRates(target, source) {
68
+ for (const [eventType, count] of Object.entries(source)) {
69
+ if (!target[eventType])
70
+ target[eventType] = 0;
71
+ target[eventType] += count;
72
+ }
73
+ }
74
+ aggregateHistogram(target, source) {
75
+ for (const [bucket, count] of Object.entries(source)) {
76
+ if (!target[bucket])
77
+ target[bucket] = 0;
78
+ target[bucket] += count;
79
+ }
80
+ }
81
+ aggregateRedisCommandLatencies(target, source) {
82
+ for (const [command, { total, count }] of Object.entries(source)) {
83
+ if (!target[command]) {
84
+ target[command] = { total: 0, count: 0 };
85
+ }
86
+ target[command].total += total;
87
+ target[command].count += count;
88
+ }
89
+ }
90
+ aggregateConsumerLag(target, source) {
91
+ for (const [consumerGroup, { total, count }] of Object.entries(source)) {
92
+ if (!target[consumerGroup]) {
93
+ target[consumerGroup] = { total: 0, count: 0 };
94
+ }
95
+ target[consumerGroup].total += total;
96
+ target[consumerGroup].count += count;
97
+ }
98
+ }
99
+ calculateAverages(data) {
100
+ for (const key in data) {
101
+ if (data[key].count > 0) {
102
+ data[key].total = data[key].total / data[key].count;
103
+ }
104
+ else {
105
+ data[key].total = 0;
106
+ }
107
+ }
108
+ }
55
109
  async getQueueDepth() {
56
110
  let totalDepth = 0;
57
111
  const individualDepths = {};
58
112
  let cursor = '0';
59
113
  do {
60
- const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH', '*:cg-*', 'COUNT', 100);
114
+ const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH', `ack:*:cg-*`, 'COUNT', 100);
61
115
  cursor = nextCursor;
62
116
  if (keys.length > 0) {
63
117
  const pipeline = this.redisClient.pipeline();
64
118
  keys.forEach((key) => {
65
- pipeline.xlen(key);
66
- pipeline.zcard(`ack:${key}`);
119
+ pipeline.xlen(key.slice(4));
120
+ pipeline.zcard(key);
67
121
  });
68
122
  const results = await pipeline.exec();
69
123
  if (!results) {
@@ -11,5 +11,10 @@ export declare class MetricsTracker {
11
11
  incrementDuplicateEvent(): void;
12
12
  incrementErrorCount(type: 'publish' | 'subscribe'): void;
13
13
  getMetrics(): IPerformanceMetrics;
14
+ incrementMessageRate(type: 'publish' | 'subscribe', eventType: string): void;
15
+ addProcessingTime(time: number): void;
16
+ private getBucket;
17
+ addRedisCommandLatency(command: string, time: number): void;
18
+ setConsumerLag(consumerGroup: string, lag: number): void;
14
19
  reset(): void;
15
20
  }
@@ -12,6 +12,11 @@ class MetricsTracker {
12
12
  publishErrorCount: 0,
13
13
  subscribeErrorCount: 0,
14
14
  duplicateEventsCount: 0,
15
+ messageRatePublish: {},
16
+ messageRateSubscribe: {},
17
+ processingTimeDistribution: {},
18
+ redisCommandLatencies: {},
19
+ consumerLag: {},
15
20
  };
16
21
  this.startTime = (0, node_process_1.hrtime)();
17
22
  }
@@ -43,6 +48,43 @@ class MetricsTracker {
43
48
  this.metrics.totalTime = totalSeconds * 1000 + totalNanoseconds / 1e6;
44
49
  return { ...this.metrics };
45
50
  }
51
+ incrementMessageRate(type, eventType) {
52
+ const key = `messageRate${type.charAt(0).toUpperCase() + type.slice(1)}`;
53
+ if (!this.metrics[key][eventType]) {
54
+ this.metrics[key][eventType] = 0;
55
+ }
56
+ this.metrics[key][eventType]++;
57
+ }
58
+ addProcessingTime(time) {
59
+ const bucket = this.getBucket(time);
60
+ if (!this.metrics.processingTimeDistribution[bucket]) {
61
+ this.metrics.processingTimeDistribution[bucket] = 0;
62
+ }
63
+ this.metrics.processingTimeDistribution[bucket]++;
64
+ }
65
+ getBucket(time) {
66
+ const buckets = [0, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000];
67
+ for (let i = 0; i < buckets.length; i++) {
68
+ if (time <= buckets[i]) {
69
+ return i === 0 ? `0-${buckets[i]}` : `${buckets[i - 1] + 1}-${buckets[i]}`;
70
+ }
71
+ }
72
+ return `>${buckets[buckets.length - 1]}`;
73
+ }
74
+ addRedisCommandLatency(command, time) {
75
+ if (!this.metrics.redisCommandLatencies[command]) {
76
+ this.metrics.redisCommandLatencies[command] = { total: 0, count: 0 };
77
+ }
78
+ this.metrics.redisCommandLatencies[command].total += time;
79
+ this.metrics.redisCommandLatencies[command].count++;
80
+ }
81
+ setConsumerLag(consumerGroup, lag) {
82
+ if (!this.metrics.consumerLag[consumerGroup]) {
83
+ this.metrics.consumerLag[consumerGroup] = { total: 0, count: 0 };
84
+ }
85
+ this.metrics.consumerLag[consumerGroup].total += lag;
86
+ this.metrics.consumerLag[consumerGroup].count++;
87
+ }
46
88
  reset() {
47
89
  this.startTime = (0, node_process_1.hrtime)();
48
90
  this.metrics = {
@@ -53,6 +95,11 @@ class MetricsTracker {
53
95
  publishErrorCount: 0,
54
96
  subscribeErrorCount: 0,
55
97
  duplicateEventsCount: 0,
98
+ messageRatePublish: {},
99
+ messageRateSubscribe: {},
100
+ processingTimeDistribution: {},
101
+ redisCommandLatencies: {},
102
+ consumerLag: {},
56
103
  };
57
104
  }
58
105
  }
@@ -19,7 +19,7 @@ export interface IMetricsCollectorConfig {
19
19
  collectionInterval: number;
20
20
  retentionPeriod: number;
21
21
  }
22
- export type TQueryableMetrics = Omit<IAggregatedMetrics, 'timestamp' | 'individualQueueDepts'>;
22
+ export type TQueryableMetrics = Omit<IAggregatedMetrics, 'timestamp' | 'individualQueueDepts' | 'messageRatePublish' | 'messageRateSubscribe' | 'processingTimeDistribution' | 'redisCommandLatencies' | 'consumerLag'>;
23
23
  export interface IQueueDepths {
24
24
  total: number;
25
25
  individual: {
@@ -34,4 +34,25 @@ export interface IPerformanceMetrics {
34
34
  publishErrorCount: number;
35
35
  subscribeErrorCount: number;
36
36
  duplicateEventsCount: number;
37
+ messageRatePublish: {
38
+ [eventType: string]: number;
39
+ };
40
+ messageRateSubscribe: {
41
+ [eventType: string]: number;
42
+ };
43
+ processingTimeDistribution: {
44
+ [bucket: string]: number;
45
+ };
46
+ redisCommandLatencies: {
47
+ [command: string]: {
48
+ total: number;
49
+ count: number;
50
+ };
51
+ };
52
+ consumerLag: {
53
+ [consumerGroup: string]: {
54
+ total: number;
55
+ count: number;
56
+ };
57
+ };
37
58
  }
@@ -15,7 +15,9 @@ class ContentBasedDeduplication {
15
15
  return hmac.update(JSON.stringify(hashableData)).digest('hex');
16
16
  }
17
17
  async isDuplicate(event, consumerGroupName) {
18
- const eventHash = this.calculateEventHash(event.data);
18
+ if (event.data === null)
19
+ return false;
20
+ const eventHash = this.calculateEventHash(event);
19
21
  const key = `processed:${consumerGroupName}:${eventHash}`;
20
22
  const result = await this.redisClient.set(key, '1', 'EX', this.ttl, 'NX');
21
23
  const isDuplicate = result === null;
@@ -59,7 +59,7 @@ class ScheduledProcessor {
59
59
  // Publish the event to each consumer group's stream
60
60
  const streamName = `${eventData.eventName}:${consumerGroup}`;
61
61
  const generatedKey = await this.redisPublisher
62
- .xadd(streamName, '*', 'data', JSON.stringify(eventData))
62
+ .xadd(streamName, key, 'data', JSON.stringify(eventData))
63
63
  .catch((e) => logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Publishing event ${eventData.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(eventData)}, ${e} `));
64
64
  if (key === '*')
65
65
  key = generatedKey ?? key;
@@ -53,7 +53,7 @@ class Streams {
53
53
  errorThresholdPercentage: 50,
54
54
  openStateDuration: 30000,
55
55
  halfOpenStateMaxAttempts: 10,
56
- maxStoredEvents: 5000,
56
+ maxStoredEvents: 10000,
57
57
  },
58
58
  };
59
59
  /** Initialise Config properties */
@@ -149,6 +149,7 @@ class Streams {
149
149
  const generatedKey = await this.redisPublisher.xadd(streamName, key, 'data', JSON.stringify(data));
150
150
  tracker.endRedisOperation();
151
151
  tracker.incrementEventCount();
152
+ tracker.incrementMessageRate('publish', data.eventName);
152
153
  if (this.metricsCollector) {
153
154
  this.metricsCollector.addMetrics(tracker.getMetrics());
154
155
  }
@@ -256,7 +257,13 @@ class Streams {
256
257
  this.eventsListened.push(eventName);
257
258
  try {
258
259
  // Check if the consumer group already exists
259
- const groupInfo = (await this.redisGroups.xinfo('GROUPS', streamName));
260
+ let groupInfo = [];
261
+ try {
262
+ groupInfo = (await this.redisGroups.xinfo('GROUPS', streamName));
263
+ }
264
+ catch (e) {
265
+ // Do nothing
266
+ }
260
267
  let groupExists = false;
261
268
  for (const group of groupInfo) {
262
269
  if (group[1] === this.consumerGroupName) {
@@ -276,7 +283,13 @@ class Streams {
276
283
  logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Group creation failed with error ${e.message} for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
277
284
  }
278
285
  // Check if the consumer already exists in the group
279
- const consumers = (await this.redisGroups.xinfo('CONSUMERS', streamName, this.consumerGroupName));
286
+ let consumers = [];
287
+ try {
288
+ consumers = (await this.redisGroups.xinfo('CONSUMERS', streamName, this.consumerGroupName));
289
+ }
290
+ catch (e) {
291
+ // Do nothing
292
+ }
280
293
  let consumerExists = false;
281
294
  for (const consumer of consumers) {
282
295
  if (consumer[1] === this.instanceId) {
@@ -361,6 +374,10 @@ class Streams {
361
374
  else {
362
375
  const messages = (await redisClient.xreadgroup('GROUP', this.consumerGroupName, this.instanceId, 'COUNT', 1, 'STREAMS', streamName, '>'));
363
376
  if (messages && messages.length) {
377
+ if (messageId === '0') {
378
+ messageId = messages[0][1][0][0];
379
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Reprocessing unprocessed message with id: ${messageId}`);
380
+ }
364
381
  eventData = JSON.parse(messages[0][1][0][1][1]);
365
382
  }
366
383
  }
@@ -412,6 +429,11 @@ class Streams {
412
429
  };
413
430
  await this.dlq.addToDLQ(dlqEvent);
414
431
  }
432
+ tracker.incrementMessageRate('subscribe', eventData.eventName);
433
+ const processingTime = Date.now() - eventData.createdAt;
434
+ tracker.addProcessingTime(processingTime);
435
+ const lag = Date.now() - eventData.createdAt;
436
+ tracker.setConsumerLag(this.consumerGroupName, lag);
415
437
  }
416
438
  else {
417
439
  logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Message ${messageId} not found for ${streamName}`);