sentinel-kafka-manager 1.0.0 → 1.0.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 +670 -143
- package/dist/index.d.mts +207 -3
- package/dist/index.d.ts +207 -3
- package/dist/index.js +721 -58
- package/dist/index.mjs +718 -57
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -2,56 +2,168 @@
|
|
|
2
2
|
|
|
3
3
|
Reusable Kafka manager for Node.js microservices built on top of `kafkajs`.
|
|
4
4
|
|
|
5
|
-
This package
|
|
5
|
+
This package gives you one shared way to:
|
|
6
6
|
|
|
7
|
-
- connect to Kafka
|
|
8
|
-
-
|
|
7
|
+
- connect to Kafka
|
|
8
|
+
- reuse a single producer safely
|
|
9
|
+
- reuse consumers by `groupId`
|
|
10
|
+
- provision topics if they do not exist
|
|
9
11
|
- publish JSON messages
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
+
- build broker lists from environment variables
|
|
13
|
+
- attach logging and metrics hooks
|
|
14
|
+
- publish standard event envelopes
|
|
15
|
+
- run consumers with DLQ support
|
|
16
|
+
- inspect connection health
|
|
17
|
+
|
|
18
|
+
## What This Package Solves
|
|
19
|
+
|
|
20
|
+
In most services, Kafka setup gets repeated:
|
|
21
|
+
|
|
22
|
+
- build broker arrays
|
|
23
|
+
- create producers
|
|
24
|
+
- create consumers
|
|
25
|
+
- manage connect and disconnect
|
|
26
|
+
- handle Docker-internal vs host-external broker addresses
|
|
27
|
+
|
|
28
|
+
`sentinel-kafka-manager` wraps those common tasks in one small class: `KafkaManager`.
|
|
12
29
|
|
|
13
30
|
## Installation
|
|
14
31
|
|
|
32
|
+
Install from npm:
|
|
33
|
+
|
|
15
34
|
```bash
|
|
16
35
|
npm install sentinel-kafka-manager
|
|
17
36
|
```
|
|
18
37
|
|
|
19
|
-
For local development
|
|
38
|
+
For local package development:
|
|
20
39
|
|
|
21
40
|
```bash
|
|
22
41
|
npm install
|
|
23
42
|
npm run build
|
|
24
43
|
```
|
|
25
44
|
|
|
26
|
-
##
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- Node.js service
|
|
48
|
+
- Kafka cluster reachable from the service
|
|
49
|
+
- `kafkajs` is included automatically as a dependency of this package
|
|
50
|
+
|
|
51
|
+
## Enterprise Features
|
|
52
|
+
|
|
53
|
+
This version now includes a stronger production baseline:
|
|
54
|
+
|
|
55
|
+
- config validation for client id, brokers, and broker discovery inputs
|
|
56
|
+
- shared producer, admin, and consumer lifecycle
|
|
57
|
+
- logger and metrics hooks
|
|
58
|
+
- standard message envelope publishing
|
|
59
|
+
- managed consumer runner with dead-letter topic support
|
|
60
|
+
- health snapshot reporting
|
|
61
|
+
- safer startup behavior for concurrent connections
|
|
62
|
+
- stable error codes and structured exception details
|
|
63
|
+
|
|
64
|
+
## Recommended Usage Level
|
|
27
65
|
|
|
28
|
-
|
|
29
|
-
- supports direct broker lists or env-driven broker discovery
|
|
30
|
-
- supports internal Docker-network broker routing
|
|
31
|
-
- supports external localhost or host-based broker routing
|
|
32
|
-
- supports optional SSL, SASL, retry, and timeout settings
|
|
33
|
-
- avoids duplicate producer, consumer, and admin connections during concurrent startup
|
|
66
|
+
This package is now suitable as a shared internal Kafka module for SaaS microservices.
|
|
34
67
|
|
|
35
|
-
|
|
68
|
+
Recommended use:
|
|
69
|
+
|
|
70
|
+
- internal platform package for Node.js services
|
|
71
|
+
- event publishing with standard envelopes
|
|
72
|
+
- managed consumers with DLQ handling
|
|
73
|
+
- consistent service-layer success and error responses
|
|
74
|
+
|
|
75
|
+
Still recommended outside the package:
|
|
76
|
+
|
|
77
|
+
- automated unit and integration tests in the consuming service or platform repo
|
|
78
|
+
- schema validation for business payloads when required by your organization
|
|
79
|
+
- organization-specific tracing, alerting, and compliance rules
|
|
80
|
+
|
|
81
|
+
## Basic Usage
|
|
82
|
+
|
|
83
|
+
### 1. Import the package
|
|
36
84
|
|
|
37
85
|
```ts
|
|
38
86
|
import { KafkaManager } from 'sentinel-kafka-manager'
|
|
39
87
|
```
|
|
40
88
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
### 1. Create a manager with explicit brokers
|
|
89
|
+
If you want to catch package-specific errors explicitly:
|
|
44
90
|
|
|
45
91
|
```ts
|
|
46
|
-
import {
|
|
92
|
+
import {
|
|
93
|
+
KafkaErrorCode,
|
|
94
|
+
KafkaManager,
|
|
95
|
+
KafkaManagerError,
|
|
96
|
+
OperationResponse,
|
|
97
|
+
} from 'sentinel-kafka-manager'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 2. Create a manager
|
|
101
|
+
|
|
102
|
+
You can create it in two ways:
|
|
47
103
|
|
|
104
|
+
1. pass brokers directly
|
|
105
|
+
2. build brokers from environment variables
|
|
106
|
+
|
|
107
|
+
### Direct broker example
|
|
108
|
+
|
|
109
|
+
```ts
|
|
48
110
|
const kafkaManager = new KafkaManager({
|
|
49
111
|
clientId: 'claims-service',
|
|
50
112
|
brokers: ['localhost:29092', 'localhost:39092', 'localhost:49092'],
|
|
51
113
|
})
|
|
52
114
|
```
|
|
53
115
|
|
|
54
|
-
###
|
|
116
|
+
### Environment-based example
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const kafkaManager = KafkaManager.fromEnv({
|
|
120
|
+
clientId: 'claims-service',
|
|
121
|
+
mode: 'external',
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Recommended Project Pattern
|
|
126
|
+
|
|
127
|
+
In a real service, create one shared manager instance and reuse it.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { KafkaManager } from 'sentinel-kafka-manager'
|
|
133
|
+
|
|
134
|
+
export const kafkaManager = KafkaManager.fromEnv({
|
|
135
|
+
clientId: 'claims-service',
|
|
136
|
+
mode: 'external',
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Then import that shared instance anywhere you need Kafka access.
|
|
141
|
+
|
|
142
|
+
You can also attach enterprise hooks:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { KafkaManager } from 'sentinel-kafka-manager'
|
|
146
|
+
|
|
147
|
+
export const kafkaManager = KafkaManager.fromEnv({
|
|
148
|
+
clientId: 'claims-service',
|
|
149
|
+
mode: 'external',
|
|
150
|
+
logger: {
|
|
151
|
+
debug: (message, meta) => console.debug(message, meta),
|
|
152
|
+
info: (message, meta) => console.info(message, meta),
|
|
153
|
+
warn: (message, meta) => console.warn(message, meta),
|
|
154
|
+
error: (message, meta) => console.error(message, meta),
|
|
155
|
+
},
|
|
156
|
+
metrics: {
|
|
157
|
+
emit: (event) => {
|
|
158
|
+
console.log('METRIC', event)
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## How To Use In Your Service
|
|
165
|
+
|
|
166
|
+
### Publish a message
|
|
55
167
|
|
|
56
168
|
```ts
|
|
57
169
|
await kafkaManager.publish({
|
|
@@ -60,11 +172,206 @@ await kafkaManager.publish({
|
|
|
60
172
|
payload: {
|
|
61
173
|
claimId: 'claim-1001',
|
|
62
174
|
status: 'created',
|
|
175
|
+
source: 'claims-service',
|
|
63
176
|
},
|
|
64
177
|
})
|
|
65
178
|
```
|
|
66
179
|
|
|
67
|
-
|
|
180
|
+
`publish()` automatically:
|
|
181
|
+
|
|
182
|
+
- gets a shared producer
|
|
183
|
+
- connects it once
|
|
184
|
+
- serializes `payload` with `JSON.stringify()`
|
|
185
|
+
|
|
186
|
+
### Publish an enterprise event envelope
|
|
187
|
+
|
|
188
|
+
Use this when you want traceable event metadata such as `eventType`, `version`, `traceId`, or `tenantId`.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
await kafkaManager.publishEnvelope({
|
|
192
|
+
topic: 'claims.created',
|
|
193
|
+
key: 'claim-1001',
|
|
194
|
+
envelope: {
|
|
195
|
+
eventId: 'evt-claim-1001',
|
|
196
|
+
eventType: 'claims.created',
|
|
197
|
+
version: '1.0.0',
|
|
198
|
+
timestamp: new Date().toISOString(),
|
|
199
|
+
source: 'claims-service',
|
|
200
|
+
traceId: 'trace-123',
|
|
201
|
+
tenantId: 'tenant-abc',
|
|
202
|
+
correlationId: 'corr-001',
|
|
203
|
+
payload: {
|
|
204
|
+
claimId: 'claim-1001',
|
|
205
|
+
status: 'created',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Create or reuse a producer manually
|
|
212
|
+
|
|
213
|
+
If you need direct producer access:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
const producer = await kafkaManager.getProducer()
|
|
217
|
+
|
|
218
|
+
await producer.send({
|
|
219
|
+
topic: 'claims.created',
|
|
220
|
+
messages: [
|
|
221
|
+
{
|
|
222
|
+
key: 'claim-1001',
|
|
223
|
+
value: JSON.stringify({ claimId: 'claim-1001' }),
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Create or reuse a consumer
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
const consumer = await kafkaManager.getConsumer('claims-service-group')
|
|
233
|
+
|
|
234
|
+
await consumer.subscribe({
|
|
235
|
+
topic: 'claims.created',
|
|
236
|
+
fromBeginning: false,
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
await consumer.run({
|
|
240
|
+
eachMessage: async ({ topic, partition, message }) => {
|
|
241
|
+
const rawValue = message.value?.toString() ?? '{}'
|
|
242
|
+
const payload = JSON.parse(rawValue)
|
|
243
|
+
|
|
244
|
+
console.log({
|
|
245
|
+
topic,
|
|
246
|
+
partition,
|
|
247
|
+
key: message.key?.toString(),
|
|
248
|
+
payload,
|
|
249
|
+
})
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`getConsumer(groupId)` returns one shared connected consumer per group id.
|
|
255
|
+
|
|
256
|
+
### Run a managed consumer with DLQ support
|
|
257
|
+
|
|
258
|
+
For SaaS-style services, this is the recommended consumer pattern.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
await kafkaManager.runConsumer({
|
|
262
|
+
groupId: 'claims-service-group',
|
|
263
|
+
topic: 'claims.created',
|
|
264
|
+
dlqTopic: 'claims.created.dlq',
|
|
265
|
+
onMessage: async ({ message, headers, key }) => {
|
|
266
|
+
console.log('Processing:', {
|
|
267
|
+
key,
|
|
268
|
+
traceId: headers['x-trace-id'],
|
|
269
|
+
payload: message,
|
|
270
|
+
})
|
|
271
|
+
},
|
|
272
|
+
onError: async (error, context) => {
|
|
273
|
+
console.error('Consumer error:', {
|
|
274
|
+
error: error.message,
|
|
275
|
+
topic: context.topic,
|
|
276
|
+
offset: context.offset,
|
|
277
|
+
})
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
If processing fails:
|
|
283
|
+
|
|
284
|
+
- the error is logged
|
|
285
|
+
- a metric hook can receive the failure event
|
|
286
|
+
- the message can be published to a dead-letter topic when `dlqTopic` is set
|
|
287
|
+
|
|
288
|
+
### Catch structured errors
|
|
289
|
+
|
|
290
|
+
All package-thrown operational errors now use `KafkaManagerError`.
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import {
|
|
294
|
+
KafkaErrorCode,
|
|
295
|
+
KafkaManagerError,
|
|
296
|
+
} from 'sentinel-kafka-manager'
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
await kafkaManager.publish({
|
|
300
|
+
topic: 'claims.created',
|
|
301
|
+
payload: { claimId: 'claim-1001' },
|
|
302
|
+
})
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error instanceof KafkaManagerError) {
|
|
305
|
+
console.error('Kafka error', {
|
|
306
|
+
code: error.code,
|
|
307
|
+
message: error.message,
|
|
308
|
+
details: error.details,
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
if (error.code === KafkaErrorCode.MESSAGE_PUBLISH_FAILED) {
|
|
312
|
+
// retry, alert, or degrade gracefully
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
throw error
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Use common success and error response types
|
|
321
|
+
|
|
322
|
+
If your service wraps Kafka operations in API or service-layer responses, you can use the shared response contracts from this package.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import {
|
|
326
|
+
ErrorResponse,
|
|
327
|
+
OperationResponse,
|
|
328
|
+
SuccessResponse,
|
|
329
|
+
} from 'sentinel-kafka-manager'
|
|
330
|
+
|
|
331
|
+
type PublishClaimCreatedResponse = OperationResponse<{
|
|
332
|
+
topic: string
|
|
333
|
+
key: string
|
|
334
|
+
}>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Success example:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
const response: SuccessResponse<{ topic: string; key: string }> = {
|
|
341
|
+
success: true,
|
|
342
|
+
message: 'Kafka message published successfully.',
|
|
343
|
+
data: {
|
|
344
|
+
topic: 'claims.created',
|
|
345
|
+
key: 'claim-1001',
|
|
346
|
+
},
|
|
347
|
+
meta: {
|
|
348
|
+
timestamp: new Date().toISOString(),
|
|
349
|
+
traceId: 'trace-123',
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Error example:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
const response: ErrorResponse = {
|
|
358
|
+
success: false,
|
|
359
|
+
message: 'Failed to publish Kafka message.',
|
|
360
|
+
error: {
|
|
361
|
+
code: KafkaErrorCode.MESSAGE_PUBLISH_FAILED,
|
|
362
|
+
details: {
|
|
363
|
+
topic: 'claims.created',
|
|
364
|
+
key: 'claim-1001',
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
meta: {
|
|
368
|
+
timestamp: new Date().toISOString(),
|
|
369
|
+
retryable: true,
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Provision topics
|
|
68
375
|
|
|
69
376
|
```ts
|
|
70
377
|
await kafkaManager.provisionTopics([
|
|
@@ -73,54 +380,74 @@ await kafkaManager.provisionTopics([
|
|
|
73
380
|
numPartitions: 6,
|
|
74
381
|
replicationFactor: 3,
|
|
75
382
|
},
|
|
383
|
+
{
|
|
384
|
+
topic: 'claims.updated',
|
|
385
|
+
numPartitions: 6,
|
|
386
|
+
replicationFactor: 3,
|
|
387
|
+
},
|
|
76
388
|
])
|
|
77
389
|
```
|
|
78
390
|
|
|
79
|
-
|
|
391
|
+
This is useful during service startup when your topics should exist before producers or consumers begin work.
|
|
392
|
+
|
|
393
|
+
### Disconnect on shutdown
|
|
394
|
+
|
|
395
|
+
Always close Kafka clients during application shutdown.
|
|
80
396
|
|
|
81
397
|
```ts
|
|
82
|
-
|
|
398
|
+
process.on('SIGINT', async () => {
|
|
399
|
+
await kafkaManager.disconnect()
|
|
400
|
+
process.exit(0)
|
|
401
|
+
})
|
|
83
402
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
403
|
+
process.on('SIGTERM', async () => {
|
|
404
|
+
await kafkaManager.disconnect()
|
|
405
|
+
process.exit(0)
|
|
87
406
|
})
|
|
88
407
|
```
|
|
89
408
|
|
|
90
|
-
###
|
|
409
|
+
### Inspect health
|
|
91
410
|
|
|
92
411
|
```ts
|
|
93
|
-
|
|
412
|
+
const health = kafkaManager.getHealthSnapshot()
|
|
413
|
+
|
|
414
|
+
console.log(health)
|
|
94
415
|
```
|
|
95
416
|
|
|
96
|
-
## Environment
|
|
417
|
+
## Environment Setup
|
|
97
418
|
|
|
98
|
-
|
|
419
|
+
This package supports two environment styles:
|
|
99
420
|
|
|
100
|
-
1. explicit broker list with `KAFKA_BROKERS`
|
|
101
|
-
2. derived
|
|
421
|
+
1. one explicit broker list with `KAFKA_BROKERS`
|
|
422
|
+
2. derived brokers from your existing Kafka runtime variables
|
|
102
423
|
|
|
103
|
-
|
|
424
|
+
## Option 1: Use `KAFKA_BROKERS`
|
|
425
|
+
|
|
426
|
+
This is the simplest option.
|
|
427
|
+
|
|
428
|
+
### `.env`
|
|
104
429
|
|
|
105
430
|
```env
|
|
106
431
|
KAFKA_BROKERS=localhost:29092,localhost:39092,localhost:49092
|
|
107
432
|
```
|
|
108
433
|
|
|
109
|
-
|
|
110
|
-
import { KafkaManager } from 'sentinel-kafka-manager'
|
|
434
|
+
### Code
|
|
111
435
|
|
|
436
|
+
```ts
|
|
112
437
|
const kafkaManager = KafkaManager.fromEnv({
|
|
113
438
|
clientId: 'claims-service',
|
|
114
439
|
})
|
|
115
440
|
```
|
|
116
441
|
|
|
117
|
-
|
|
442
|
+
## Option 2: Use your current Kafka cluster variables
|
|
118
443
|
|
|
119
|
-
This
|
|
444
|
+
This matches the Kafka server setup you shared.
|
|
120
445
|
|
|
121
|
-
|
|
446
|
+
### For host machine apps
|
|
122
447
|
|
|
123
|
-
Use this when your
|
|
448
|
+
Use this when your Node.js service runs on the host machine.
|
|
449
|
+
|
|
450
|
+
### `.env`
|
|
124
451
|
|
|
125
452
|
```env
|
|
126
453
|
KAFKA_EXTERNAL_HOST=localhost
|
|
@@ -130,6 +457,8 @@ KAFKA_BROKER_3_EXTERNAL_PORT=49092
|
|
|
130
457
|
KAFKA_BROKER_INTERNAL_PORT=9092
|
|
131
458
|
```
|
|
132
459
|
|
|
460
|
+
### Code
|
|
461
|
+
|
|
133
462
|
```ts
|
|
134
463
|
const kafkaManager = KafkaManager.fromEnv({
|
|
135
464
|
clientId: 'claims-service',
|
|
@@ -145,14 +474,18 @@ localhost:39092
|
|
|
145
474
|
localhost:49092
|
|
146
475
|
```
|
|
147
476
|
|
|
148
|
-
|
|
477
|
+
### For Dockerized apps on the Kafka network
|
|
478
|
+
|
|
479
|
+
Use this when your Node.js service runs inside Docker on the same Kafka network.
|
|
149
480
|
|
|
150
|
-
|
|
481
|
+
### `.env`
|
|
151
482
|
|
|
152
483
|
```env
|
|
153
484
|
KAFKA_BROKER_INTERNAL_PORT=9092
|
|
154
485
|
```
|
|
155
486
|
|
|
487
|
+
### Code
|
|
488
|
+
|
|
156
489
|
```ts
|
|
157
490
|
const kafkaManager = KafkaManager.fromEnv({
|
|
158
491
|
clientId: 'claims-service',
|
|
@@ -168,97 +501,144 @@ broker-2:9092
|
|
|
168
501
|
broker-3:9092
|
|
169
502
|
```
|
|
170
503
|
|
|
171
|
-
##
|
|
504
|
+
## Which Mode Should You Use?
|
|
505
|
+
|
|
506
|
+
- Use `mode: 'external'` when your service uses `localhost` broker ports like `29092`, `39092`, and `49092`
|
|
507
|
+
- Use `mode: 'internal'` when your service is inside Docker and should reach brokers like `broker-1:9092`
|
|
508
|
+
- Use `KAFKA_BROKERS` when you want the simplest and most explicit configuration
|
|
172
509
|
|
|
173
|
-
|
|
510
|
+
## Your Current Running Kafka Cluster
|
|
174
511
|
|
|
175
|
-
|
|
176
|
-
- `KAFKA_EXTERNAL_HOST`
|
|
177
|
-
- `KAFKA_BROKER_1_EXTERNAL_PORT`
|
|
178
|
-
- `KAFKA_BROKER_2_EXTERNAL_PORT`
|
|
179
|
-
- `KAFKA_BROKER_3_EXTERNAL_PORT`
|
|
180
|
-
- `KAFKA_BROKER_INTERNAL_PORT`
|
|
512
|
+
Based on your running Docker containers, your Kafka cluster is exposed like this:
|
|
181
513
|
|
|
182
|
-
|
|
514
|
+
- `broker-1` -> host port `29092`
|
|
515
|
+
- `broker-2` -> host port `39092`
|
|
516
|
+
- `broker-3` -> host port `49092`
|
|
517
|
+
- `controller-1`, `controller-2`, and `controller-3` are controller-only nodes and should not be used by application clients
|
|
183
518
|
|
|
184
|
-
|
|
519
|
+
If your Node.js service runs on the host machine, use:
|
|
520
|
+
|
|
521
|
+
```env
|
|
522
|
+
KAFKA_EXTERNAL_HOST=localhost
|
|
523
|
+
KAFKA_BROKER_1_EXTERNAL_PORT=29092
|
|
524
|
+
KAFKA_BROKER_2_EXTERNAL_PORT=39092
|
|
525
|
+
KAFKA_BROKER_3_EXTERNAL_PORT=49092
|
|
526
|
+
KAFKA_BROKER_INTERNAL_PORT=9092
|
|
527
|
+
```
|
|
185
528
|
|
|
186
529
|
```ts
|
|
187
530
|
const kafkaManager = KafkaManager.fromEnv({
|
|
188
531
|
clientId: 'claims-service',
|
|
189
532
|
mode: 'external',
|
|
190
|
-
brokerCount: 3,
|
|
191
|
-
envKey: 'KAFKA_BROKERS',
|
|
192
|
-
externalHostEnvKey: 'KAFKA_EXTERNAL_HOST',
|
|
193
|
-
internalPortEnvKey: 'KAFKA_BROKER_INTERNAL_PORT',
|
|
194
|
-
externalPortEnvKeyPrefix: 'KAFKA_BROKER_',
|
|
195
|
-
externalPortEnvKeySuffix: '_EXTERNAL_PORT',
|
|
196
|
-
internalBrokerHostPrefix: 'broker-',
|
|
197
533
|
})
|
|
198
534
|
```
|
|
199
535
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
### SSL
|
|
536
|
+
If your Node.js service runs inside Docker on the same Kafka network, use:
|
|
203
537
|
|
|
204
538
|
```ts
|
|
205
|
-
const kafkaManager =
|
|
539
|
+
const kafkaManager = KafkaManager.fromEnv({
|
|
206
540
|
clientId: 'claims-service',
|
|
207
|
-
|
|
208
|
-
ssl: {
|
|
209
|
-
rejectUnauthorized: true,
|
|
210
|
-
},
|
|
541
|
+
mode: 'internal',
|
|
211
542
|
})
|
|
212
543
|
```
|
|
213
544
|
|
|
214
|
-
|
|
545
|
+
This package is designed to work with that exact broker layout.
|
|
546
|
+
|
|
547
|
+
## Full Service Example
|
|
215
548
|
|
|
216
549
|
```ts
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
550
|
+
import { KafkaManager } from 'sentinel-kafka-manager'
|
|
551
|
+
|
|
552
|
+
const kafkaManager = KafkaManager.fromEnv({
|
|
553
|
+
clientId: 'claims-api',
|
|
554
|
+
mode: 'external',
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
async function bootstrap(): Promise<void> {
|
|
558
|
+
await kafkaManager.provisionTopics([
|
|
559
|
+
{
|
|
560
|
+
topic: 'claims.created',
|
|
561
|
+
numPartitions: 6,
|
|
562
|
+
replicationFactor: 3,
|
|
563
|
+
},
|
|
564
|
+
])
|
|
565
|
+
|
|
566
|
+
await kafkaManager.publish({
|
|
567
|
+
topic: 'claims.created',
|
|
568
|
+
key: 'claim-1001',
|
|
569
|
+
payload: {
|
|
570
|
+
claimId: 'claim-1001',
|
|
571
|
+
source: 'claims-api',
|
|
572
|
+
},
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
await kafkaManager.runConsumer({
|
|
576
|
+
groupId: 'claims-api-group',
|
|
577
|
+
topic: 'claims.created',
|
|
578
|
+
dlqTopic: 'claims.created.dlq',
|
|
579
|
+
onMessage: async ({ message, headers }) => {
|
|
580
|
+
console.log('Received event:', {
|
|
581
|
+
traceId: headers['x-trace-id'],
|
|
582
|
+
payload: message,
|
|
583
|
+
})
|
|
584
|
+
},
|
|
585
|
+
})
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
bootstrap().catch(async (error) => {
|
|
589
|
+
console.error('Kafka bootstrap failed:', error)
|
|
590
|
+
await kafkaManager.disconnect()
|
|
591
|
+
process.exit(1)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
process.on('SIGINT', async () => {
|
|
595
|
+
await kafkaManager.disconnect()
|
|
596
|
+
process.exit(0)
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
process.on('SIGTERM', async () => {
|
|
600
|
+
await kafkaManager.disconnect()
|
|
601
|
+
process.exit(0)
|
|
225
602
|
})
|
|
226
603
|
```
|
|
227
604
|
|
|
228
|
-
|
|
605
|
+
## API Summary
|
|
606
|
+
|
|
607
|
+
### `new KafkaManager(options)`
|
|
608
|
+
|
|
609
|
+
Use this when you already know your brokers.
|
|
229
610
|
|
|
230
611
|
```ts
|
|
231
612
|
const kafkaManager = new KafkaManager({
|
|
232
613
|
clientId: 'claims-service',
|
|
233
614
|
brokers: ['localhost:29092'],
|
|
234
|
-
connectionTimeout: 5000,
|
|
235
|
-
requestTimeout: 30000,
|
|
236
|
-
retry: {
|
|
237
|
-
initialRetryTime: 300,
|
|
238
|
-
retries: 10,
|
|
239
|
-
},
|
|
240
615
|
})
|
|
241
616
|
```
|
|
242
617
|
|
|
243
|
-
## API
|
|
244
|
-
|
|
245
|
-
### `new KafkaManager(options)`
|
|
246
|
-
|
|
247
|
-
Creates a manager from an explicit broker list.
|
|
248
|
-
|
|
249
618
|
Options:
|
|
250
619
|
|
|
251
620
|
- `clientId: string`
|
|
252
621
|
- `brokers: string[]`
|
|
253
622
|
- `ssl?: boolean | { rejectUnauthorized?: boolean }`
|
|
254
|
-
- `sasl?:
|
|
623
|
+
- `sasl?: SASLOptions`
|
|
255
624
|
- `connectionTimeout?: number`
|
|
256
625
|
- `requestTimeout?: number`
|
|
257
626
|
- `retry?: { initialRetryTime?: number; retries?: number }`
|
|
627
|
+
- `logger?: KafkaLogger`
|
|
628
|
+
- `metrics?: KafkaMetrics`
|
|
629
|
+
- `producerConfig?: ProducerConfig`
|
|
630
|
+
- `consumerDefaults?: Omit<ConsumerConfig, 'groupId'>`
|
|
258
631
|
|
|
259
632
|
### `KafkaManager.fromEnv(options)`
|
|
260
633
|
|
|
261
|
-
|
|
634
|
+
Use this when you want brokers to come from environment variables.
|
|
635
|
+
|
|
636
|
+
```ts
|
|
637
|
+
const kafkaManager = KafkaManager.fromEnv({
|
|
638
|
+
clientId: 'claims-service',
|
|
639
|
+
mode: 'external',
|
|
640
|
+
})
|
|
641
|
+
```
|
|
262
642
|
|
|
263
643
|
Options:
|
|
264
644
|
|
|
@@ -272,10 +652,14 @@ Options:
|
|
|
272
652
|
- `externalPortEnvKeySuffix?: string`
|
|
273
653
|
- `internalBrokerHostPrefix?: string`
|
|
274
654
|
- `ssl?: boolean | { rejectUnauthorized?: boolean }`
|
|
275
|
-
- `sasl?:
|
|
655
|
+
- `sasl?: SASLOptions`
|
|
276
656
|
- `connectionTimeout?: number`
|
|
277
657
|
- `requestTimeout?: number`
|
|
278
658
|
- `retry?: { initialRetryTime?: number; retries?: number }`
|
|
659
|
+
- `logger?: KafkaLogger`
|
|
660
|
+
- `metrics?: KafkaMetrics`
|
|
661
|
+
- `producerConfig?: ProducerConfig`
|
|
662
|
+
- `consumerDefaults?: Omit<ConsumerConfig, 'groupId'>`
|
|
279
663
|
|
|
280
664
|
### `getProducer()`
|
|
281
665
|
|
|
@@ -283,109 +667,252 @@ Returns a shared connected Kafka producer.
|
|
|
283
667
|
|
|
284
668
|
### `getConsumer(groupId)`
|
|
285
669
|
|
|
286
|
-
Returns a shared connected Kafka consumer for
|
|
287
|
-
|
|
288
|
-
### `provisionTopics(topics)`
|
|
670
|
+
Returns a shared connected Kafka consumer for that `groupId`.
|
|
289
671
|
|
|
290
|
-
|
|
672
|
+
### `publish(options)`
|
|
291
673
|
|
|
292
|
-
|
|
674
|
+
Publishes one JSON message.
|
|
293
675
|
|
|
294
676
|
```ts
|
|
295
|
-
{
|
|
296
|
-
topic:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
677
|
+
await kafkaManager.publish({
|
|
678
|
+
topic: 'claims.created',
|
|
679
|
+
key: 'claim-1001',
|
|
680
|
+
payload: { claimId: 'claim-1001' },
|
|
681
|
+
})
|
|
300
682
|
```
|
|
301
683
|
|
|
302
|
-
### `
|
|
684
|
+
### `publishEnvelope(options)`
|
|
303
685
|
|
|
304
|
-
Publishes
|
|
686
|
+
Publishes a standard metadata envelope for traceable event-driven systems.
|
|
305
687
|
|
|
306
|
-
|
|
688
|
+
### `provisionTopics(topics)`
|
|
307
689
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
690
|
+
Creates missing topics.
|
|
691
|
+
|
|
692
|
+
### `runConsumer(options)`
|
|
693
|
+
|
|
694
|
+
Runs a managed consumer with:
|
|
695
|
+
|
|
696
|
+
- a typed message parser
|
|
697
|
+
- application callback handling
|
|
698
|
+
- optional DLQ publishing
|
|
699
|
+
- optional error callback
|
|
700
|
+
|
|
701
|
+
### `getHealthSnapshot()`
|
|
702
|
+
|
|
703
|
+
Returns a lightweight connection-health snapshot.
|
|
316
704
|
|
|
317
705
|
### `disconnect()`
|
|
318
706
|
|
|
319
|
-
Disconnects the
|
|
707
|
+
Disconnects the producer, admin client, and any connected consumers.
|
|
320
708
|
|
|
321
|
-
##
|
|
709
|
+
## Error Handling
|
|
322
710
|
|
|
323
|
-
|
|
711
|
+
This package now throws structured errors with stable codes for production handling.
|
|
712
|
+
|
|
713
|
+
### Error class
|
|
714
|
+
|
|
715
|
+
- `KafkaManagerError`
|
|
716
|
+
|
|
717
|
+
### Common response types
|
|
718
|
+
|
|
719
|
+
- `SuccessResponse<T>`
|
|
720
|
+
- `ErrorResponse`
|
|
721
|
+
- `OperationResponse<T>`
|
|
722
|
+
|
|
723
|
+
### Error fields
|
|
724
|
+
|
|
725
|
+
- `code`: stable machine-readable error code
|
|
726
|
+
- `message`: human-readable explanation
|
|
727
|
+
- `details`: structured metadata for logs or monitoring
|
|
728
|
+
- `cause`: original thrown error when available
|
|
729
|
+
|
|
730
|
+
### Common error codes
|
|
731
|
+
|
|
732
|
+
- `INVALID_CLIENT_ID`
|
|
733
|
+
- `INVALID_BROKERS`
|
|
734
|
+
- `INVALID_GROUP_ID`
|
|
735
|
+
- `INVALID_TOPIC`
|
|
736
|
+
- `INVALID_ENVELOPE`
|
|
737
|
+
- `INVALID_BROKER_COUNT`
|
|
738
|
+
- `MISSING_ENVIRONMENT_VARIABLE`
|
|
739
|
+
- `PRODUCER_CONNECTION_FAILED`
|
|
740
|
+
- `CONSUMER_CONNECTION_FAILED`
|
|
741
|
+
- `ADMIN_CONNECTION_FAILED`
|
|
742
|
+
- `MESSAGE_PUBLISH_FAILED`
|
|
743
|
+
- `TOPIC_PROVISION_FAILED`
|
|
744
|
+
- `CONSUMER_RUN_FAILED`
|
|
745
|
+
- `CONSUMER_HANDLER_FAILED`
|
|
746
|
+
- `DLQ_PUBLISH_FAILED`
|
|
747
|
+
- `MESSAGE_PARSE_FAILED`
|
|
748
|
+
- `DISCONNECT_FAILED`
|
|
749
|
+
|
|
750
|
+
### Example
|
|
324
751
|
|
|
325
752
|
```ts
|
|
326
|
-
import {
|
|
753
|
+
import { KafkaManagerError } from 'sentinel-kafka-manager'
|
|
327
754
|
|
|
755
|
+
try {
|
|
756
|
+
await kafkaManager.runConsumer({
|
|
757
|
+
groupId: 'claims-service-group',
|
|
758
|
+
topic: 'claims.created',
|
|
759
|
+
onMessage: async ({ message }) => {
|
|
760
|
+
console.log(message)
|
|
761
|
+
},
|
|
762
|
+
})
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (error instanceof KafkaManagerError) {
|
|
765
|
+
console.error(error.code, error.message, error.details)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
throw error
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
## Advanced Examples
|
|
773
|
+
|
|
774
|
+
### Logger and metrics example
|
|
775
|
+
|
|
776
|
+
```ts
|
|
328
777
|
const kafkaManager = KafkaManager.fromEnv({
|
|
329
|
-
clientId: 'claims-
|
|
778
|
+
clientId: 'claims-service',
|
|
330
779
|
mode: 'external',
|
|
780
|
+
logger: {
|
|
781
|
+
debug: (message, meta) => console.debug(message, meta),
|
|
782
|
+
info: (message, meta) => console.info(message, meta),
|
|
783
|
+
warn: (message, meta) => console.warn(message, meta),
|
|
784
|
+
error: (message, meta) => console.error(message, meta),
|
|
785
|
+
},
|
|
786
|
+
metrics: {
|
|
787
|
+
emit: (event) => {
|
|
788
|
+
console.log('METRIC', event.name, event.tags, event.meta)
|
|
789
|
+
},
|
|
790
|
+
},
|
|
331
791
|
})
|
|
792
|
+
```
|
|
332
793
|
|
|
333
|
-
|
|
794
|
+
### SSL example
|
|
795
|
+
|
|
796
|
+
```ts
|
|
797
|
+
const kafkaManager = new KafkaManager({
|
|
798
|
+
clientId: 'claims-service',
|
|
799
|
+
brokers: ['localhost:29092'],
|
|
800
|
+
ssl: {
|
|
801
|
+
rejectUnauthorized: true,
|
|
802
|
+
},
|
|
803
|
+
})
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### Managed consumer with custom parser
|
|
807
|
+
|
|
808
|
+
```ts
|
|
809
|
+
await kafkaManager.runConsumer({
|
|
810
|
+
groupId: 'claims-service-group',
|
|
334
811
|
topic: 'claims.created',
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
812
|
+
dlqTopic: 'claims.created.dlq',
|
|
813
|
+
parser: (payload) => {
|
|
814
|
+
const rawValue = payload.message.value?.toString() ?? '{}'
|
|
815
|
+
return JSON.parse(rawValue) as {
|
|
816
|
+
claimId: string
|
|
817
|
+
status: string
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
onMessage: async ({ message }) => {
|
|
821
|
+
console.log(message.claimId, message.status)
|
|
339
822
|
},
|
|
340
823
|
})
|
|
341
824
|
```
|
|
342
825
|
|
|
343
|
-
###
|
|
826
|
+
### SASL example
|
|
344
827
|
|
|
345
828
|
```ts
|
|
346
|
-
|
|
829
|
+
const kafkaManager = new KafkaManager({
|
|
830
|
+
clientId: 'claims-service',
|
|
831
|
+
brokers: ['localhost:29092'],
|
|
832
|
+
sasl: {
|
|
833
|
+
mechanism: 'plain',
|
|
834
|
+
username: 'kafka-user',
|
|
835
|
+
password: 'secret',
|
|
836
|
+
},
|
|
837
|
+
})
|
|
838
|
+
```
|
|
347
839
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
840
|
+
### Retry and timeout example
|
|
841
|
+
|
|
842
|
+
```ts
|
|
843
|
+
const kafkaManager = new KafkaManager({
|
|
844
|
+
clientId: 'claims-service',
|
|
845
|
+
brokers: ['localhost:29092'],
|
|
846
|
+
connectionTimeout: 5000,
|
|
847
|
+
requestTimeout: 30000,
|
|
848
|
+
retry: {
|
|
849
|
+
initialRetryTime: 300,
|
|
850
|
+
retries: 10,
|
|
851
|
+
},
|
|
351
852
|
})
|
|
352
853
|
```
|
|
353
854
|
|
|
354
|
-
##
|
|
855
|
+
## Common Mistakes
|
|
856
|
+
|
|
857
|
+
- Do not connect application clients to KRaft controller nodes. Connect only to brokers.
|
|
858
|
+
- Do not use `mode: 'external'` inside Docker unless you truly want host-published ports.
|
|
859
|
+
- Do not create a new `KafkaManager` for every request. Reuse one shared instance.
|
|
860
|
+
- Do not forget `disconnect()` during shutdown.
|
|
861
|
+
- Do not assume topics will exist unless your platform creates them or your service provisions them.
|
|
862
|
+
- Do not ignore failed consumer messages in production. Use `runConsumer()` with a `dlqTopic`.
|
|
863
|
+
- Do not publish business-critical events without metadata. Prefer `publishEnvelope()` for traceability.
|
|
864
|
+
- Do not depend on raw driver error strings in application logic. Use `KafkaManagerError.code`.
|
|
865
|
+
|
|
866
|
+
## Recommended SaaS Pattern
|
|
867
|
+
|
|
868
|
+
For most SaaS services, the cleanest pattern is:
|
|
869
|
+
|
|
870
|
+
1. Create one shared `KafkaManager` during service bootstrap.
|
|
871
|
+
2. Use `publishEnvelope()` for business events.
|
|
872
|
+
3. Use `runConsumer()` instead of raw `consumer.run()` for managed handling.
|
|
873
|
+
4. Configure a `dlqTopic` for important consumers.
|
|
874
|
+
5. Catch `KafkaManagerError` and branch on `error.code`.
|
|
875
|
+
6. Wrap service outcomes in `OperationResponse<T>` where useful.
|
|
355
876
|
|
|
356
|
-
|
|
877
|
+
## Publishing This Package
|
|
878
|
+
|
|
879
|
+
Build:
|
|
357
880
|
|
|
358
881
|
```bash
|
|
359
882
|
npm run build
|
|
360
883
|
```
|
|
361
884
|
|
|
362
|
-
Publish
|
|
885
|
+
Publish:
|
|
363
886
|
|
|
364
887
|
```bash
|
|
365
888
|
npm publish
|
|
366
889
|
```
|
|
367
890
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
### Publish Troubleshooting
|
|
371
|
-
|
|
372
|
-
#### `403` with 2FA or token message
|
|
891
|
+
If npm rejects the publish:
|
|
373
892
|
|
|
374
|
-
|
|
893
|
+
- `403` usually means authentication, 2FA, or token permission issues
|
|
894
|
+
- `404` usually means the package name is unavailable or not publishable by your account
|
|
375
895
|
|
|
376
|
-
Useful
|
|
896
|
+
Useful check:
|
|
377
897
|
|
|
378
898
|
```bash
|
|
379
899
|
npm whoami
|
|
380
900
|
```
|
|
381
901
|
|
|
382
|
-
|
|
902
|
+
## Notes
|
|
383
903
|
|
|
384
|
-
|
|
904
|
+
- Your current Kafka cluster works fine with PLAINTEXT, so SSL and SASL are optional unless your environment changes
|
|
905
|
+
- This package is designed for both host-based services and Dockerized services
|
|
906
|
+
- If you want the least surprise, use `KAFKA_BROKERS`
|
|
385
907
|
|
|
386
|
-
##
|
|
908
|
+
## Links
|
|
909
|
+
|
|
910
|
+
- LinkedIn: https://www.linkedin.com/in/dilip-shaw-2740769/
|
|
911
|
+
|
|
912
|
+
## About Me
|
|
913
|
+
|
|
914
|
+
I'm a full stack developer. Experienced developer with over 20 years of expertise in crafting scalable web applications. Proficient in frontend technologies such as Angular and React, alongside extensive experience in WordPress, Drupal, and backend frameworks like Node.js. With a proven track record of delivering high-quality solutions to meet client needs and drive business objectives, I bring a versatile skill set and a commitment to excellence to every project.
|
|
915
|
+
|
|
916
|
+
## Feedback
|
|
387
917
|
|
|
388
|
-
|
|
389
|
-
- for your current cluster, PLAINTEXT is enough and SSL/SASL is optional
|
|
390
|
-
- if your services use `localhost`, use `mode: 'external'`
|
|
391
|
-
- if your services run in Docker on the Kafka network, use `mode: 'internal'`
|
|
918
|
+
If you have any feedback, please reach out to us at `dilipabc@gmail.com`
|