@jetit/publisher 4.1.1 → 5.0.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 +249 -171
- package/package.json +3 -2
- package/src/lib/monitoring/adapters/prom.d.ts +37 -0
- package/src/lib/monitoring/adapters/prom.js +126 -0
- package/src/lib/monitoring/collector.d.ts +17 -0
- package/src/lib/monitoring/collector.js +109 -0
- package/src/lib/monitoring/tracker.d.ts +15 -0
- package/src/lib/monitoring/tracker.js +59 -0
- package/src/lib/monitoring/types.d.ts +37 -0
- package/src/lib/monitoring/types.js +2 -0
- package/src/lib/performance/circuit_breaker.d.ts +29 -0
- package/src/lib/performance/circuit_breaker.js +103 -0
- package/src/lib/publisher.d.ts +3 -0
- package/src/lib/publisher.js +6 -1
- package/src/lib/redis/batch.d.ts +6 -0
- package/src/lib/redis/batch.js +79 -0
- package/src/lib/redis/dlq.d.ts +18 -0
- package/src/lib/redis/dlq.js +133 -0
- package/src/lib/redis/duplication.d.ts +10 -0
- package/src/lib/redis/duplication.js +28 -0
- package/src/lib/redis/streams.d.ts +61 -3
- package/src/lib/redis/streams.js +385 -77
- package/src/lib/redis/types.d.ts +64 -0
package/README.md
CHANGED
|
@@ -1,203 +1,281 @@
|
|
|
1
|
-
# publisher
|
|
2
|
-
|
|
3
|
-
publisher is a library for implementing an event-driven architecture using Redis PUB/SUB and Redis Streams. It provides a
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const subscribedEvents = await getAllEventsForInstance(connection, instanceId);
|
|
36
|
-
console.log(`Subscribed Events : ${JSON.stringify(subscribedEvents)}`);
|
|
37
|
-
await clearSubscribedEvents(connection, consumerGroupName, instanceId, subscribedEvents);
|
|
38
|
-
await deleteConsumerGroupNameForInstance(connection, instanceId);
|
|
39
|
-
await deleteAllEventsFroInstance(connection, instanceId);
|
|
40
|
-
}
|
|
1
|
+
# @jetit/publisher
|
|
2
|
+
|
|
3
|
+
`@jetit/publisher` is a robust and feature-rich library for implementing an event-driven architecture using Redis PUB/SUB and Redis Streams. It provides a scalable mechanism for publishing and consuming events in real-time, with support for advanced features such as message deduplication, consumer group management, scheduled event publishing, and more.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [@jetit/publisher](#jetitpublisher)
|
|
8
|
+
- [Table of Contents](#table-of-contents)
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Key Features](#key-features)
|
|
11
|
+
- [Usage](#usage)
|
|
12
|
+
- [Basic Example](#basic-example)
|
|
13
|
+
- [Configuration](#configuration)
|
|
14
|
+
- [Publishing Events](#publishing-events)
|
|
15
|
+
- [Subscribing to Events](#subscribing-to-events)
|
|
16
|
+
- [Scheduled Publishing](#scheduled-publishing)
|
|
17
|
+
- [Batch Publishing](#batch-publishing)
|
|
18
|
+
- [Dead Letter Queue (DLQ)](#dead-letter-queue-dlq)
|
|
19
|
+
- [Event Filtering](#event-filtering)
|
|
20
|
+
- [Performance Monitoring](#performance-monitoring)
|
|
21
|
+
- [Prometheus Integration](#prometheus-integration)
|
|
22
|
+
- [Advanced Features](#advanced-features)
|
|
23
|
+
- [Content-Based Deduplication](#content-based-deduplication)
|
|
24
|
+
- [Multiple Event Subscriptions](#multiple-event-subscriptions)
|
|
25
|
+
- [Circuit Breaker](#circuit-breaker)
|
|
26
|
+
- [Performance Optimizations](#performance-optimizations)
|
|
27
|
+
- [Cleanup and Graceful Shutdown](#cleanup-and-graceful-shutdown)
|
|
28
|
+
- [Troubleshooting](#troubleshooting)
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @jetit/publisher
|
|
34
|
+
```
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
*
|
|
44
|
-
* @param {ioredis.Redis|ioredis.Cluster} connection
|
|
45
|
-
* @param {string} consumerGroupName
|
|
46
|
-
* @param {string} instanceId
|
|
47
|
-
* @param {Array<string>} events
|
|
48
|
-
*/
|
|
49
|
-
async function clearSubscribedEvents(connection, consumerGroupName, instanceId, events) {
|
|
50
|
-
return Promise.all(
|
|
51
|
-
events.map(async (eventName) => {
|
|
52
|
-
console.log(`${eventName} is being cleared in publisher`);
|
|
53
|
-
const streamName = `${eventName}:${consumerGroupName}`;
|
|
54
|
-
console.log(`${streamName} is being removed.`);
|
|
55
|
-
await connection.srem(`${eventName}`, consumerGroupName);
|
|
56
|
-
console.log(`${eventName} is removed from ${consumerGroupName}`);
|
|
57
|
-
// Releasing all claims based on info from: https://redis.io/commands/xgroup-delconsumer/
|
|
58
|
-
await releaseAllClaims(connection, streamName, consumerGroupName, instanceId);
|
|
59
|
-
console.log(`${eventName} removes all claims`);
|
|
60
|
-
await connection.xgroup(`DELCONSUMER`, streamName, consumerGroupName, instanceId);
|
|
61
|
-
console.log(`${eventName} is deleted as a consumer from ${consumerGroupName}, ${instanceId}`);
|
|
62
|
-
})
|
|
63
|
-
);
|
|
64
|
-
}
|
|
36
|
+
## Key Features
|
|
65
37
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*/
|
|
79
|
-
const pendingMessages = await connection.xpending(streamName, consumerGroupName, `-`, `+`, 10000, instanceId);
|
|
80
|
-
|
|
81
|
-
if (pendingMessages && pendingMessages.length > 0) {
|
|
82
|
-
console.log(`${pendingMessages.length} messages to clean up.`);
|
|
83
|
-
const transaction = connection.multi({ pipeline: true });
|
|
84
|
-
const tempConsumerId = `${consumerGroupName}-temp`;
|
|
85
|
-
for (const [messageId] of pendingMessages) {
|
|
86
|
-
transaction.xclaim(streamName, consumerGroupName, tempConsumerId, 10, messageId);
|
|
87
|
-
}
|
|
88
|
-
await transaction.exec();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
38
|
+
- Real-time event publishing and subscribing
|
|
39
|
+
- Configurable Streams class for flexible usage
|
|
40
|
+
- Improved error handling and reliability
|
|
41
|
+
- Performance tracking with Redis time and operation time metrics
|
|
42
|
+
- Dead Letter Queue (DLQ) for handling subscription failures
|
|
43
|
+
- Event filtering for specialized subscriptions
|
|
44
|
+
- Support for multiple event subscriptions from the same service
|
|
45
|
+
- Batch publishing (regular and scheduled)
|
|
46
|
+
- Basic monitoring with Prometheus export support
|
|
47
|
+
- Content-based one-time guarantee (0-1 semantics support)
|
|
48
|
+
- Optimized cleanup processes for improved performance
|
|
49
|
+
- Circuit Breaker pattern for fault tolerance
|
|
91
50
|
|
|
92
|
-
|
|
93
|
-
*
|
|
94
|
-
* @param {string} serviceName
|
|
95
|
-
* @param {string} instanceUniqueId
|
|
96
|
-
*/
|
|
97
|
-
function getInstanceId(serviceName, instanceUniqueId) {
|
|
98
|
-
const instanceId = `${serviceName}:${instanceUniqueId}`;
|
|
99
|
-
console.log(`Generated Instance ID : ${instanceId}`);
|
|
100
|
-
return instanceId;
|
|
101
|
-
}
|
|
51
|
+
## Usage
|
|
102
52
|
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
* @param {ioredis.Redis|ioredis.Cluster} serviceName
|
|
106
|
-
* @param {string} instanceId
|
|
107
|
-
* @returns {Promise<string>} consumer group name
|
|
108
|
-
*/
|
|
109
|
-
async function getConsumerGroupName(connection, instanceId) {
|
|
110
|
-
const key = `instance:${instanceId}:consumerGroupName`;
|
|
111
|
-
console.log(`Get consumer group name called for key : ${key}`);
|
|
112
|
-
return await connection.get(key);
|
|
113
|
-
}
|
|
53
|
+
### Basic Example
|
|
114
54
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
* @param {ioredis.Redis|ioredis.Cluster} connection
|
|
118
|
-
* @param {string} instanceId
|
|
119
|
-
* @returns
|
|
120
|
-
*/
|
|
121
|
-
async function deleteConsumerGroupNameForInstance(connection, instanceId) {
|
|
122
|
-
return await connection.del(`instance:${instanceId}:consumerGroupName`);
|
|
123
|
-
}
|
|
55
|
+
```typescript
|
|
56
|
+
import { Publisher, EventData } from '@jetit/publisher';
|
|
124
57
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
* @param {ioredis.Redis|ioredis.Cluster} connection
|
|
128
|
-
* @param {string} instanceId
|
|
129
|
-
* @returns {Promise<Array<events>>} subscribed events for this instance
|
|
130
|
-
*/
|
|
131
|
-
async function getAllEventsForInstance(connection, instanceId) {
|
|
132
|
-
const key = `instance:${instanceId}:subscribedEvents`;
|
|
133
|
-
console.log(`Get consumer group events : ${key}`);
|
|
134
|
-
return (await connection.sscan(key, 0))[1];
|
|
135
|
-
}
|
|
58
|
+
// Create an instance of the publisher
|
|
59
|
+
const publisher = new Publisher('MyService');
|
|
136
60
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
async function deleteAllEventsFroInstance(connection, instanceId) {
|
|
143
|
-
return await connection.del(`instance:${instanceId}:subscribedEvents`);
|
|
144
|
-
}
|
|
61
|
+
// Publish an event
|
|
62
|
+
const eventData: EventData<{ message: string }> = {
|
|
63
|
+
eventName: 'my-event',
|
|
64
|
+
data: { message: 'Hello, world!' }
|
|
65
|
+
};
|
|
145
66
|
|
|
146
|
-
|
|
147
|
-
return new Promise((res, _) => setTimeout(() => res(), seconds * 1000));
|
|
148
|
-
}
|
|
67
|
+
await publisher.publish(eventData);
|
|
149
68
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
69
|
+
// Subscribe to an event
|
|
70
|
+
publisher.listen('my-event').subscribe(event => {
|
|
71
|
+
console.log(`Received event: ${event.eventName}`, event.data);
|
|
72
|
+
});
|
|
73
|
+
```
|
|
153
74
|
|
|
154
|
-
|
|
155
|
-
.then(() => process.exit(0))
|
|
156
|
-
.catch((e) => {
|
|
157
|
-
console.error(e);
|
|
158
|
-
process.exit(1);
|
|
159
|
-
});
|
|
75
|
+
### Configuration
|
|
160
76
|
|
|
77
|
+
The `Publisher` class can be configured with various options, including Circuit Breaker and Backpressure handling:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { Publisher, IStreamsConfig } from '@jetit/publisher';
|
|
81
|
+
|
|
82
|
+
const config: Partial<IStreamsConfig> = {
|
|
83
|
+
cleanUpInterval: 3600000, // 1 hour
|
|
84
|
+
maxRetries: 5,
|
|
85
|
+
initialRetryDelay: 1000,
|
|
86
|
+
immediatePublishThreshold: 500,
|
|
87
|
+
unprocessedMessageThreshold: 25,
|
|
88
|
+
acknowledgedMessageCleanupInterval: 3600000, // 1 hour
|
|
89
|
+
dlqEventThreshold: 2000,
|
|
90
|
+
filterKeepAlive: 86400000, // 24 hours
|
|
91
|
+
duplicationCheckWindow: 86400, // 24 hours
|
|
92
|
+
circuitBreaker: {
|
|
93
|
+
enabled: true,
|
|
94
|
+
errorThreshold: 50,
|
|
95
|
+
errorThresholdPercentage: 50,
|
|
96
|
+
openStateDuration: 30000, // 30s
|
|
97
|
+
halfOpenStateMaxAttempts: 10,
|
|
98
|
+
maxStoredEvents: 5000,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const publisher = new Publisher('MyService', config);
|
|
161
103
|
```
|
|
162
104
|
|
|
163
|
-
|
|
105
|
+
### Publishing Events
|
|
164
106
|
|
|
165
107
|
```typescript
|
|
166
|
-
|
|
108
|
+
const eventData = {
|
|
109
|
+
eventName: 'user-registered',
|
|
110
|
+
data: { userId: '123', email: 'user@example.com' }
|
|
111
|
+
};
|
|
167
112
|
|
|
168
|
-
|
|
169
|
-
|
|
113
|
+
await publisher.publish(eventData);
|
|
114
|
+
```
|
|
170
115
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
116
|
+
### Subscribing to Events
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
publisher.listen('user-registered').subscribe(event => {
|
|
120
|
+
console.log('New user registered:', event.data);
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Scheduled Publishing
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const futureDate = new Date(Date.now() + 60000); // 1 minute from now
|
|
128
|
+
await publisher.scheduledPublish(futureDate, eventData);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Batch Publishing
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { publishBatch } from '@jetit/publisher';
|
|
135
|
+
|
|
136
|
+
const events = [
|
|
137
|
+
{ eventName: 'event1', data: { /* ... */ } },
|
|
138
|
+
{ eventName: 'event2', data: { /* ... */ } },
|
|
139
|
+
// ...
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const result = await publishBatch(publisher, events, { batchSize: 100, delayBetweenBatches: 1000 });
|
|
143
|
+
console.log('Batch publish result:', result);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Dead Letter Queue (DLQ)
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// Retry an event from DLQ
|
|
150
|
+
const success = await publisher.retryFromDLQ('eventId');
|
|
151
|
+
|
|
152
|
+
// Get DLQ stats
|
|
153
|
+
const stats = await publisher.getDLQStats();
|
|
154
|
+
console.log('DLQ stats:', stats);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Event Filtering
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const options = {
|
|
161
|
+
eventFilter: (event) => event.data.userId === '123',
|
|
162
|
+
filterKeepAlive: 3600000 // 1 hour
|
|
175
163
|
};
|
|
176
164
|
|
|
177
|
-
|
|
165
|
+
publisher.listen('user-action', options).subscribe(event => {
|
|
166
|
+
console.log('Filtered user action:', event);
|
|
167
|
+
});
|
|
168
|
+
```
|
|
178
169
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
170
|
+
### Performance Monitoring
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// Get metrics for a specific time range
|
|
174
|
+
const metrics = await publisher.getMetrics(startTime, endTime);
|
|
175
|
+
console.log('Performance metrics:', metrics);
|
|
176
|
+
|
|
177
|
+
// Get latest metrics
|
|
178
|
+
const latestMetrics = await publisher.getLatestMetrics();
|
|
179
|
+
console.log('Latest metrics:', latestMetrics);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Prometheus Integration
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { PrometheusAdapter } from '@jetit/publisher';
|
|
186
|
+
import promClient from 'prom-client';
|
|
187
|
+
import express from 'express';
|
|
188
|
+
|
|
189
|
+
const app = express();
|
|
190
|
+
const prometheusAdapter = new PrometheusAdapter(publisher, promClient);
|
|
191
|
+
|
|
192
|
+
prometheusAdapter.setupEndpoint(app, '/metrics');
|
|
193
|
+
|
|
194
|
+
app.listen(3000, () => {
|
|
195
|
+
console.log('Metrics server listening on port 3000');
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Advanced Features
|
|
200
|
+
|
|
201
|
+
### Content-Based Deduplication
|
|
202
|
+
|
|
203
|
+
The library supports content-based deduplication to ensure that each unique event is processed only once:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const options = {
|
|
207
|
+
publishOnceGuarantee: true
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
publisher.listen('important-event', options).subscribe(event => {
|
|
211
|
+
console.log('Guaranteed unique event:', event);
|
|
182
212
|
});
|
|
183
213
|
```
|
|
184
214
|
|
|
185
|
-
|
|
215
|
+
### Multiple Event Subscriptions
|
|
186
216
|
|
|
187
|
-
|
|
217
|
+
You can subscribe to multiple events from the same service:
|
|
188
218
|
|
|
189
|
-
|
|
219
|
+
```typescript
|
|
220
|
+
const subscription1 = publisher.listen('event1').subscribe(/* ... */);
|
|
221
|
+
const subscription2 = publisher.listen('event2').subscribe(/* ... */);
|
|
222
|
+
```
|
|
190
223
|
|
|
191
|
-
|
|
224
|
+
### Circuit Breaker
|
|
192
225
|
|
|
193
|
-
|
|
226
|
+
The Circuit Breaker pattern is implemented to prevent cascading failures in a distributed system. It helps to gracefully handle failures and allows the system to recover without overwhelming failed services.
|
|
194
227
|
|
|
195
|
-
|
|
228
|
+
Configuration options:
|
|
196
229
|
|
|
197
|
-
|
|
230
|
+
- `enabled`: Enable or disable the Circuit Breaker.
|
|
231
|
+
- `errorThreshold`: Number of errors before opening the circuit.
|
|
232
|
+
- `errorThresholdPercentage`: Percentage of errors to total calls before opening the circuit.
|
|
233
|
+
- `timeWindow`: Time window for error rate calculation (in milliseconds).
|
|
234
|
+
- `openStateDuration`: Duration to keep the circuit open before moving to half-open state (in milliseconds).
|
|
235
|
+
- `halfOpenStateMaxAttempts`: Maximum number of attempts allowed in half-open state.
|
|
236
|
+
|
|
237
|
+
The Circuit Breaker has three states:
|
|
238
|
+
|
|
239
|
+
1. Closed: Normal operation, calls pass through.
|
|
240
|
+
2. Open: Calls are immediately rejected without reaching the service.
|
|
241
|
+
3. Half-Open: A limited number of calls are allowed to test if the service has recovered.
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
## Performance Optimizations
|
|
245
|
+
|
|
246
|
+
- Batched `xdel` operations for improved cleanup performance
|
|
247
|
+
- Configurable cleanup intervals and thresholds
|
|
248
|
+
- Efficient event filtering at the subscription level
|
|
249
|
+
- Retry logic with exponential backoff for failed operations
|
|
250
|
+
- Circuit Breaker to prevent overwhelming failed services
|
|
251
|
+
- Dead Letter Queue (DLQ) for handling subscription failures
|
|
252
|
+
|
|
253
|
+
## Cleanup and Graceful Shutdown
|
|
254
|
+
|
|
255
|
+
To ensure proper cleanup of resources, implement a graceful shutdown:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
process.on('SIGTERM', shutdown);
|
|
259
|
+
process.on('SIGINT', shutdown);
|
|
260
|
+
|
|
261
|
+
async function shutdown() {
|
|
262
|
+
console.log('Graceful shutdown initiated.');
|
|
263
|
+
try {
|
|
264
|
+
await publisher.close();
|
|
265
|
+
console.log('Resources and connections successfully closed.');
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error('Error during graceful shutdown:', error);
|
|
268
|
+
}
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
```
|
|
198
272
|
|
|
199
|
-
|
|
273
|
+
## Troubleshooting
|
|
200
274
|
|
|
201
|
-
|
|
275
|
+
If you encounter issues:
|
|
202
276
|
|
|
203
|
-
|
|
277
|
+
1. Check the Redis connection settings
|
|
278
|
+
2. Verify that consumer groups are correctly created
|
|
279
|
+
3. Monitor the DLQ for failed events
|
|
280
|
+
4. Review the performance metrics for any anomalies
|
|
281
|
+
5. Check the logs for detailed error messages
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetit/publisher",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@jetit/id": "^0.0.12",
|
|
7
|
+
"events": "3.3.0",
|
|
7
8
|
"ioredis": "^5.3.0",
|
|
8
9
|
"rxjs": "^7.8.0",
|
|
9
|
-
"tslib": "
|
|
10
|
+
"tslib": "2.5.0"
|
|
10
11
|
},
|
|
11
12
|
"main": "./src/index.js"
|
|
12
13
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One of the horribly typed code I have written. But I dont see
|
|
3
|
+
* another option without importing other libraries in. So this
|
|
4
|
+
* works well when the right stuff is passed in for the promClient
|
|
5
|
+
* and the web server.
|
|
6
|
+
*/
|
|
7
|
+
import { Streams as Publisher } from '../../redis/streams';
|
|
8
|
+
export declare class PrometheusAdapter {
|
|
9
|
+
private streams;
|
|
10
|
+
private promClient;
|
|
11
|
+
private registry;
|
|
12
|
+
private queueDepth;
|
|
13
|
+
private dlqSize;
|
|
14
|
+
private dlqRate;
|
|
15
|
+
private operationCount;
|
|
16
|
+
private totalTime;
|
|
17
|
+
private redisOperationTime;
|
|
18
|
+
private processingTime;
|
|
19
|
+
private eventCount;
|
|
20
|
+
private publishErrorCount;
|
|
21
|
+
private subscribeErrorCount;
|
|
22
|
+
private individualQueueDepth;
|
|
23
|
+
private duplicateEventCount;
|
|
24
|
+
/**
|
|
25
|
+
*
|
|
26
|
+
* @param streams [Publisher]
|
|
27
|
+
* @param promClient [Prom Client] This needs to be an instance of prom-client
|
|
28
|
+
*/
|
|
29
|
+
constructor(streams: Publisher, promClient: any);
|
|
30
|
+
private initializeMetrics;
|
|
31
|
+
updateMetrics(): Promise<void>;
|
|
32
|
+
private updatePrometheusMetrics;
|
|
33
|
+
/**
|
|
34
|
+
* @param app This needs to be an instance of express or fastify something that supports the express api
|
|
35
|
+
*/
|
|
36
|
+
setupEndpoint(app: any, endPoint?: string): void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PrometheusAdapter = void 0;
|
|
4
|
+
class PrometheusAdapter {
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* @param streams [Publisher]
|
|
8
|
+
* @param promClient [Prom Client] This needs to be an instance of prom-client
|
|
9
|
+
*/
|
|
10
|
+
constructor(streams, promClient) {
|
|
11
|
+
this.streams = streams;
|
|
12
|
+
this.promClient = promClient;
|
|
13
|
+
this.registry = new this.promClient.Registry({ collectDefaultMetrics: { timeout: 60000 } });
|
|
14
|
+
this.initializeMetrics();
|
|
15
|
+
}
|
|
16
|
+
initializeMetrics() {
|
|
17
|
+
this.queueDepth = new this.promClient.Gauge({
|
|
18
|
+
name: 'queue_depth',
|
|
19
|
+
help: 'Total number of messages in all queues',
|
|
20
|
+
registers: [this.registry],
|
|
21
|
+
});
|
|
22
|
+
this.dlqSize = new this.promClient.Gauge({
|
|
23
|
+
name: 'dlq_size',
|
|
24
|
+
help: 'Number of messages in the Dead Letter Queue',
|
|
25
|
+
registers: [this.registry],
|
|
26
|
+
});
|
|
27
|
+
this.duplicateEventCount = new this.promClient.Counter({
|
|
28
|
+
name: 'duplicate_event_count',
|
|
29
|
+
help: 'Number of duplicate events detected',
|
|
30
|
+
registers: [this.registry],
|
|
31
|
+
});
|
|
32
|
+
this.dlqRate = new this.promClient.Gauge({
|
|
33
|
+
name: 'dlq_rate',
|
|
34
|
+
help: 'Rate of messages being added to the Dead Letter Queue',
|
|
35
|
+
registers: [this.registry],
|
|
36
|
+
});
|
|
37
|
+
this.operationCount = new this.promClient.Counter({
|
|
38
|
+
name: 'operation_count',
|
|
39
|
+
help: 'Total number of operations performed',
|
|
40
|
+
registers: [this.registry],
|
|
41
|
+
});
|
|
42
|
+
this.totalTime = new this.promClient.Gauge({
|
|
43
|
+
name: 'total_time',
|
|
44
|
+
help: 'Total processing time in milliseconds',
|
|
45
|
+
registers: [this.registry],
|
|
46
|
+
});
|
|
47
|
+
this.redisOperationTime = new this.promClient.Gauge({
|
|
48
|
+
name: 'redis_operation_time',
|
|
49
|
+
help: 'Time spent on Redis operations in milliseconds',
|
|
50
|
+
registers: [this.registry],
|
|
51
|
+
});
|
|
52
|
+
this.processingTime = new this.promClient.Gauge({
|
|
53
|
+
name: 'processing_time',
|
|
54
|
+
help: 'Time spent on processing events in milliseconds',
|
|
55
|
+
registers: [this.registry],
|
|
56
|
+
});
|
|
57
|
+
this.eventCount = new this.promClient.Counter({
|
|
58
|
+
name: 'event_count',
|
|
59
|
+
help: 'Total number of events processed',
|
|
60
|
+
registers: [this.registry],
|
|
61
|
+
});
|
|
62
|
+
this.publishErrorCount = new this.promClient.Counter({
|
|
63
|
+
name: 'publish_error_count',
|
|
64
|
+
help: 'Number of errors encountered during publish operations',
|
|
65
|
+
registers: [this.registry],
|
|
66
|
+
});
|
|
67
|
+
this.subscribeErrorCount = new this.promClient.Counter({
|
|
68
|
+
name: 'subscribe_error_count',
|
|
69
|
+
help: 'Number of errors encountered during subscribe operations',
|
|
70
|
+
registers: [this.registry],
|
|
71
|
+
});
|
|
72
|
+
this.individualQueueDepth = new this.promClient.Gauge({
|
|
73
|
+
name: 'individual_queue_depth',
|
|
74
|
+
help: 'Number of messages in each individual queue',
|
|
75
|
+
labelNames: ['queue_name'],
|
|
76
|
+
registers: [this.registry],
|
|
77
|
+
});
|
|
78
|
+
this.registry.registerMetric(this.queueDepth);
|
|
79
|
+
this.registry.registerMetric(this.dlqSize);
|
|
80
|
+
this.registry.registerMetric(this.dlqRate);
|
|
81
|
+
this.registry.registerMetric(this.operationCount);
|
|
82
|
+
this.registry.registerMetric(this.totalTime);
|
|
83
|
+
this.registry.registerMetric(this.redisOperationTime);
|
|
84
|
+
this.registry.registerMetric(this.processingTime);
|
|
85
|
+
this.registry.registerMetric(this.eventCount);
|
|
86
|
+
this.registry.registerMetric(this.publishErrorCount);
|
|
87
|
+
this.registry.registerMetric(this.subscribeErrorCount);
|
|
88
|
+
this.registry.registerMetric(this.individualQueueDepth);
|
|
89
|
+
this.registry.registerMetric(this.duplicateEventCount);
|
|
90
|
+
}
|
|
91
|
+
async updateMetrics() {
|
|
92
|
+
const metrics = await this.streams.getLatestMetrics();
|
|
93
|
+
if (metrics) {
|
|
94
|
+
this.updatePrometheusMetrics(metrics);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
updatePrometheusMetrics(metrics) {
|
|
98
|
+
this.queueDepth.set(metrics.queueDepth);
|
|
99
|
+
this.dlqSize.set(metrics.dlqSize);
|
|
100
|
+
this.dlqRate.set(metrics.dlqRate);
|
|
101
|
+
this.operationCount.inc(metrics.operationCount);
|
|
102
|
+
this.totalTime.set(metrics.totalTime);
|
|
103
|
+
this.redisOperationTime.set(metrics.redisOperationTime);
|
|
104
|
+
this.processingTime.set(metrics.processingTime);
|
|
105
|
+
this.eventCount.inc(metrics.eventCount);
|
|
106
|
+
this.publishErrorCount.inc(metrics.publishErrorCount);
|
|
107
|
+
this.subscribeErrorCount.inc(metrics.subscribeErrorCount);
|
|
108
|
+
this.duplicateEventCount.inc(metrics.duplicateEventsCount);
|
|
109
|
+
if (metrics.individualQueueDepts) {
|
|
110
|
+
Object.entries(metrics.individualQueueDepts).forEach(([queueName, depth]) => {
|
|
111
|
+
this.individualQueueDepth.set({ queue_name: queueName }, depth);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* @param app This needs to be an instance of express or fastify something that supports the express api
|
|
117
|
+
*/
|
|
118
|
+
setupEndpoint(app, endPoint = '/metrics') {
|
|
119
|
+
app.get(endPoint, async (_, res) => {
|
|
120
|
+
await this.updateMetrics();
|
|
121
|
+
res.set('Content-Type', this.registry.contentType);
|
|
122
|
+
res.send(await this.registry.metrics());
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.PrometheusAdapter = PrometheusAdapter;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DeadLetterQueue } from '../redis/dlq';
|
|
2
|
+
import { IAggregatedMetrics, IMetricsCollectorConfig, IPerformanceMetrics } from './types';
|
|
3
|
+
export declare class MetricsCollector {
|
|
4
|
+
private redisClient;
|
|
5
|
+
private collectionInterval;
|
|
6
|
+
private retentionPeriod;
|
|
7
|
+
private dlq;
|
|
8
|
+
private metrics;
|
|
9
|
+
private metricsKey;
|
|
10
|
+
constructor(config: IMetricsCollectorConfig, dlq: DeadLetterQueue);
|
|
11
|
+
addMetrics(metrics: IPerformanceMetrics): void;
|
|
12
|
+
private aggregateAndStoreMetrics;
|
|
13
|
+
private getQueueDepth;
|
|
14
|
+
private storeMetrics;
|
|
15
|
+
getMetrics(startTime: number, endTime: number): Promise<IAggregatedMetrics[]>;
|
|
16
|
+
getLatestMetrics(): Promise<IAggregatedMetrics | null>;
|
|
17
|
+
}
|