sentinel-kafka-manager 1.0.0 → 1.0.2
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 +960 -142
- 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,454 @@
|
|
|
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
|
+
## Create A Kafka Server With This Repo
|
|
52
|
+
|
|
53
|
+
This repository already includes a full local Kafka server setup in [docker-compose.yml](/var/www/html/matrix-project/sentinel-kafka-manager/docker-compose.yml) and sample environment values in [.env.local.example](/var/www/html/matrix-project/sentinel-kafka-manager/.env.local.example).
|
|
54
|
+
|
|
55
|
+
The stack includes:
|
|
56
|
+
|
|
57
|
+
- 3 KRaft controllers
|
|
58
|
+
- 3 Kafka brokers
|
|
59
|
+
- 1 Kafka UI instance
|
|
60
|
+
|
|
61
|
+
### 1. Create your `.env`
|
|
62
|
+
|
|
63
|
+
Copy the example file:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cp .env.local.example .env
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If you want a standard localhost setup, the example values are already suitable.
|
|
70
|
+
|
|
71
|
+
### 2. Start the Kafka cluster
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
docker compose --env-file .env up -d
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This creates:
|
|
78
|
+
|
|
79
|
+
- controller quorum on port `9093` inside Docker
|
|
80
|
+
- internal broker traffic on port `9092` inside Docker
|
|
81
|
+
- host-accessible broker ports `29092`, `39092`, and `49092`
|
|
82
|
+
- Kafka UI on `127.0.0.1:5001`
|
|
83
|
+
|
|
84
|
+
### 3. Stop the Kafka cluster
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
docker compose --env-file .env down
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
To also remove persisted Kafka data volumes:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
docker compose --env-file .env down -v
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 4. Check container status
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
docker compose --env-file .env ps
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 5. Current broker endpoints
|
|
103
|
+
|
|
104
|
+
For applications running on your host machine:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
localhost:29092
|
|
108
|
+
localhost:39092
|
|
109
|
+
localhost:49092
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For applications running inside Docker on the same Compose network:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
broker-1:9092
|
|
116
|
+
broker-2:9092
|
|
117
|
+
broker-3:9092
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Important:
|
|
121
|
+
|
|
122
|
+
- application clients must connect to brokers, not controllers
|
|
123
|
+
- controllers are internal cluster metadata nodes only
|
|
124
|
+
- `mode: 'external'` is for host-based apps
|
|
125
|
+
- `mode: 'internal'` is for Dockerized apps on the same network
|
|
126
|
+
|
|
127
|
+
## Kafka UI Usage
|
|
128
|
+
|
|
129
|
+
The Docker stack also starts Kafka UI for cluster inspection.
|
|
130
|
+
|
|
131
|
+
### Open the UI
|
|
132
|
+
|
|
133
|
+
Visit:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
http://127.0.0.1:5001
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Login credentials
|
|
140
|
+
|
|
141
|
+
By default, the UI uses the credentials from `.env`:
|
|
142
|
+
|
|
143
|
+
```env
|
|
144
|
+
KAFKA_UI_USERNAME=admin
|
|
145
|
+
KAFKA_UI_PASSWORD=Admin@007
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### What the UI connects to
|
|
149
|
+
|
|
150
|
+
Kafka UI is preconfigured in `docker-compose.yml` to use all three brokers:
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
broker-1:9092,broker-2:9092,broker-3:9092
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The cluster name shown in the UI comes from:
|
|
157
|
+
|
|
158
|
+
```env
|
|
159
|
+
KAFKA_UI_CLUSTER_NAME=Project Omni Enterprise Kafka
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### What you can do in the UI
|
|
163
|
+
|
|
164
|
+
- view brokers and cluster health
|
|
165
|
+
- inspect topics and partitions
|
|
166
|
+
- browse messages
|
|
167
|
+
- inspect consumer groups
|
|
168
|
+
- check offsets and lag
|
|
169
|
+
|
|
170
|
+
### Read-only mode
|
|
171
|
+
|
|
172
|
+
The example `.env` sets:
|
|
173
|
+
|
|
174
|
+
```env
|
|
175
|
+
KAFKA_UI_READONLY=true
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
That means the UI is intended for safe inspection only.
|
|
179
|
+
|
|
180
|
+
If you want to create topics or make changes from the UI, change it to:
|
|
181
|
+
|
|
182
|
+
```env
|
|
183
|
+
KAFKA_UI_READONLY=false
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Then restart the stack:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
docker compose --env-file .env up -d
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Typical UI flow
|
|
193
|
+
|
|
194
|
+
1. Start the Docker stack.
|
|
195
|
+
2. Open `http://127.0.0.1:5001`.
|
|
196
|
+
3. Log in with `KAFKA_UI_USERNAME` and `KAFKA_UI_PASSWORD`.
|
|
197
|
+
4. Open the configured cluster.
|
|
198
|
+
5. Go to Topics, Consumer Groups, or Brokers as needed.
|
|
199
|
+
|
|
200
|
+
## Docker Compose And `.env` Reference
|
|
201
|
+
|
|
202
|
+
This project does not use a separate `Dockerfile` for Kafka. The Kafka server is created from `docker-compose.yml` and environment values from `.env`.
|
|
203
|
+
|
|
204
|
+
### Core Kafka image and cluster identity
|
|
205
|
+
|
|
206
|
+
```env
|
|
207
|
+
KAFKA_IMAGE=confluentinc/cp-kafka:7.6.6
|
|
208
|
+
KAFKA_CLUSTER_ID=MkU3OEVBNTcwNTJENDM2Qk
|
|
209
|
+
KAFKA_RESTART_POLICY=unless-stopped
|
|
210
|
+
KAFKA_STOP_GRACE_PERIOD=60s
|
|
211
|
+
KAFKA_NETWORK_NAME=kafka-network
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- `KAFKA_IMAGE`: Kafka container image used by controllers and brokers
|
|
215
|
+
- `KAFKA_CLUSTER_ID`: shared KRaft cluster id for all nodes
|
|
216
|
+
- `KAFKA_NETWORK_NAME`: Docker network name used by the full stack
|
|
217
|
+
|
|
218
|
+
### Controller quorum settings
|
|
219
|
+
|
|
220
|
+
```env
|
|
221
|
+
KAFKA_CONTROLLER_PORT=9093
|
|
222
|
+
KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER
|
|
223
|
+
KAFKA_CONTROLLER_QUORUM_VOTERS=1@controller-1:9093,2@controller-2:9093,3@controller-3:9093
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- controllers run only inside Docker
|
|
227
|
+
- apps should never use these controller addresses as Kafka client brokers
|
|
228
|
+
|
|
229
|
+
### Broker listener settings
|
|
230
|
+
|
|
231
|
+
```env
|
|
232
|
+
KAFKA_BROKER_BIND_ADDRESS=0.0.0.0
|
|
233
|
+
KAFKA_EXTERNAL_HOST=localhost
|
|
234
|
+
KAFKA_BROKER_INTERNAL_PORT=9092
|
|
235
|
+
KAFKA_BROKER_EXTERNAL_CONTAINER_PORT=19092
|
|
236
|
+
KAFKA_BROKER_1_EXTERNAL_PORT=29092
|
|
237
|
+
KAFKA_BROKER_2_EXTERNAL_PORT=39092
|
|
238
|
+
KAFKA_BROKER_3_EXTERNAL_PORT=49092
|
|
239
|
+
KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL
|
|
240
|
+
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- `KAFKA_EXTERNAL_HOST=localhost` exposes brokers to your host machine
|
|
244
|
+
- `KAFKA_BROKER_1_EXTERNAL_PORT`, `KAFKA_BROKER_2_EXTERNAL_PORT`, and `KAFKA_BROKER_3_EXTERNAL_PORT` are the ports your local apps should use
|
|
245
|
+
- `KAFKA_BROKER_INTERNAL_PORT=9092` is used by containers on the Docker network
|
|
246
|
+
|
|
247
|
+
### Replication and durability defaults
|
|
248
|
+
|
|
249
|
+
```env
|
|
250
|
+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=3
|
|
251
|
+
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
|
|
252
|
+
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=2
|
|
253
|
+
KAFKA_DEFAULT_REPLICATION_FACTOR=3
|
|
254
|
+
KAFKA_MIN_INSYNC_REPLICAS=2
|
|
255
|
+
KAFKA_NUM_PARTITIONS=6
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
These values are designed for the included three-broker cluster.
|
|
259
|
+
|
|
260
|
+
### Broker behavior
|
|
261
|
+
|
|
262
|
+
```env
|
|
263
|
+
KAFKA_AUTO_CREATE_TOPICS_ENABLE=false
|
|
264
|
+
KAFKA_DELETE_TOPIC_ENABLE=true
|
|
265
|
+
KAFKA_LOG_RETENTION_HOURS=168
|
|
266
|
+
KAFKA_LOG_SEGMENT_BYTES=1073741824
|
|
267
|
+
KAFKA_MESSAGE_MAX_BYTES=10485880
|
|
268
|
+
KAFKA_REPLICA_FETCH_MAX_BYTES=10485880
|
|
269
|
+
KAFKA_SOCKET_REQUEST_MAX_BYTES=104857600
|
|
270
|
+
```
|
|
27
271
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
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
|
|
272
|
+
- topics are not auto-created by default
|
|
273
|
+
- deleting topics is allowed
|
|
274
|
+
- retention is 7 days by default
|
|
34
275
|
|
|
35
|
-
|
|
276
|
+
### Storage and safety limits
|
|
277
|
+
|
|
278
|
+
```env
|
|
279
|
+
KAFKA_LOG_DIRS=/var/lib/kafka/data
|
|
280
|
+
KAFKA_ULIMIT_NOFILE_SOFT=65536
|
|
281
|
+
KAFKA_ULIMIT_NOFILE_HARD=65536
|
|
282
|
+
KAFKA_LOG_MAX_SIZE=100m
|
|
283
|
+
KAFKA_LOG_MAX_FILE=5
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Kafka UI settings
|
|
287
|
+
|
|
288
|
+
```env
|
|
289
|
+
KAFKA_UI_IMAGE=provectuslabs/kafka-ui:latest
|
|
290
|
+
KAFKA_UI_CONTAINER_NAME=kafka-ui
|
|
291
|
+
KAFKA_UI_HOSTNAME=kafka-ui
|
|
292
|
+
KAFKA_UI_RESTART_POLICY=unless-stopped
|
|
293
|
+
KAFKA_UI_BIND_ADDRESS=127.0.0.1
|
|
294
|
+
KAFKA_UI_PORT=5001
|
|
295
|
+
KAFKA_UI_DYNAMIC_CONFIG_ENABLED=false
|
|
296
|
+
KAFKA_UI_CLUSTER_NAME=Project Omni Enterprise Kafka
|
|
297
|
+
KAFKA_UI_READONLY=true
|
|
298
|
+
KAFKA_UI_AUTH_TYPE=LOGIN_FORM
|
|
299
|
+
KAFKA_UI_USERNAME=admin
|
|
300
|
+
KAFKA_UI_PASSWORD=Admin@007
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
- `KAFKA_UI_BIND_ADDRESS=127.0.0.1` keeps the UI local-only
|
|
304
|
+
- `KAFKA_UI_PORT=5001` publishes the UI on your machine
|
|
305
|
+
- `KAFKA_UI_READONLY=true` prevents write actions from the UI
|
|
306
|
+
|
|
307
|
+
### Healthcheck settings
|
|
308
|
+
|
|
309
|
+
```env
|
|
310
|
+
KAFKA_HEALTHCHECK_INTERVAL=10s
|
|
311
|
+
KAFKA_HEALTHCHECK_TIMEOUT=5s
|
|
312
|
+
KAFKA_HEALTHCHECK_RETRIES=15
|
|
313
|
+
KAFKA_HEALTHCHECK_START_PERIOD=30s
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Creating Topics
|
|
317
|
+
|
|
318
|
+
Because `KAFKA_AUTO_CREATE_TOPICS_ENABLE=false`, topics should be created deliberately.
|
|
319
|
+
|
|
320
|
+
You have two good options:
|
|
321
|
+
|
|
322
|
+
- create topics from your service with `kafkaManager.provisionTopics()`
|
|
323
|
+
- allow UI-based creation by setting `KAFKA_UI_READONLY=false`
|
|
324
|
+
|
|
325
|
+
Example with this package:
|
|
36
326
|
|
|
37
327
|
```ts
|
|
38
|
-
|
|
328
|
+
await kafkaManager.provisionTopics([
|
|
329
|
+
{
|
|
330
|
+
topic: 'claims.created',
|
|
331
|
+
numPartitions: 6,
|
|
332
|
+
replicationFactor: 3,
|
|
333
|
+
},
|
|
334
|
+
])
|
|
39
335
|
```
|
|
40
336
|
|
|
41
|
-
##
|
|
337
|
+
## Enterprise Features
|
|
338
|
+
|
|
339
|
+
This version now includes a stronger production baseline:
|
|
340
|
+
|
|
341
|
+
- config validation for client id, brokers, and broker discovery inputs
|
|
342
|
+
- shared producer, admin, and consumer lifecycle
|
|
343
|
+
- logger and metrics hooks
|
|
344
|
+
- standard message envelope publishing
|
|
345
|
+
- managed consumer runner with dead-letter topic support
|
|
346
|
+
- health snapshot reporting
|
|
347
|
+
- safer startup behavior for concurrent connections
|
|
348
|
+
- stable error codes and structured exception details
|
|
349
|
+
|
|
350
|
+
## Recommended Usage Level
|
|
42
351
|
|
|
43
|
-
|
|
352
|
+
This package is now suitable as a shared internal Kafka module for SaaS microservices.
|
|
353
|
+
|
|
354
|
+
Recommended use:
|
|
355
|
+
|
|
356
|
+
- internal platform package for Node.js services
|
|
357
|
+
- event publishing with standard envelopes
|
|
358
|
+
- managed consumers with DLQ handling
|
|
359
|
+
- consistent service-layer success and error responses
|
|
360
|
+
|
|
361
|
+
Still recommended outside the package:
|
|
362
|
+
|
|
363
|
+
- automated unit and integration tests in the consuming service or platform repo
|
|
364
|
+
- schema validation for business payloads when required by your organization
|
|
365
|
+
- organization-specific tracing, alerting, and compliance rules
|
|
366
|
+
|
|
367
|
+
## Basic Usage
|
|
368
|
+
|
|
369
|
+
### 1. Import the package
|
|
44
370
|
|
|
45
371
|
```ts
|
|
46
372
|
import { KafkaManager } from 'sentinel-kafka-manager'
|
|
373
|
+
```
|
|
47
374
|
|
|
375
|
+
If you want to catch package-specific errors explicitly:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import {
|
|
379
|
+
KafkaErrorCode,
|
|
380
|
+
KafkaManager,
|
|
381
|
+
KafkaManagerError,
|
|
382
|
+
OperationResponse,
|
|
383
|
+
} from 'sentinel-kafka-manager'
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### 2. Create a manager
|
|
387
|
+
|
|
388
|
+
You can create it in two ways:
|
|
389
|
+
|
|
390
|
+
1. pass brokers directly
|
|
391
|
+
2. build brokers from environment variables
|
|
392
|
+
|
|
393
|
+
### Direct broker example
|
|
394
|
+
|
|
395
|
+
```ts
|
|
48
396
|
const kafkaManager = new KafkaManager({
|
|
49
397
|
clientId: 'claims-service',
|
|
50
398
|
brokers: ['localhost:29092', 'localhost:39092', 'localhost:49092'],
|
|
51
399
|
})
|
|
52
400
|
```
|
|
53
401
|
|
|
54
|
-
###
|
|
402
|
+
### Environment-based example
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
const kafkaManager = KafkaManager.fromEnv({
|
|
406
|
+
clientId: 'claims-service',
|
|
407
|
+
mode: 'external',
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Recommended Project Pattern
|
|
412
|
+
|
|
413
|
+
In a real service, create one shared manager instance and reuse it.
|
|
414
|
+
|
|
415
|
+
Example:
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
import { KafkaManager } from 'sentinel-kafka-manager'
|
|
419
|
+
|
|
420
|
+
export const kafkaManager = KafkaManager.fromEnv({
|
|
421
|
+
clientId: 'claims-service',
|
|
422
|
+
mode: 'external',
|
|
423
|
+
})
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Then import that shared instance anywhere you need Kafka access.
|
|
427
|
+
|
|
428
|
+
You can also attach enterprise hooks:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
import { KafkaManager } from 'sentinel-kafka-manager'
|
|
432
|
+
|
|
433
|
+
export const kafkaManager = KafkaManager.fromEnv({
|
|
434
|
+
clientId: 'claims-service',
|
|
435
|
+
mode: 'external',
|
|
436
|
+
logger: {
|
|
437
|
+
debug: (message, meta) => console.debug(message, meta),
|
|
438
|
+
info: (message, meta) => console.info(message, meta),
|
|
439
|
+
warn: (message, meta) => console.warn(message, meta),
|
|
440
|
+
error: (message, meta) => console.error(message, meta),
|
|
441
|
+
},
|
|
442
|
+
metrics: {
|
|
443
|
+
emit: (event) => {
|
|
444
|
+
console.log('METRIC', event)
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
})
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## How To Use In Your Service
|
|
451
|
+
|
|
452
|
+
### Publish a message
|
|
55
453
|
|
|
56
454
|
```ts
|
|
57
455
|
await kafkaManager.publish({
|
|
@@ -60,11 +458,206 @@ await kafkaManager.publish({
|
|
|
60
458
|
payload: {
|
|
61
459
|
claimId: 'claim-1001',
|
|
62
460
|
status: 'created',
|
|
461
|
+
source: 'claims-service',
|
|
462
|
+
},
|
|
463
|
+
})
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
`publish()` automatically:
|
|
467
|
+
|
|
468
|
+
- gets a shared producer
|
|
469
|
+
- connects it once
|
|
470
|
+
- serializes `payload` with `JSON.stringify()`
|
|
471
|
+
|
|
472
|
+
### Publish an enterprise event envelope
|
|
473
|
+
|
|
474
|
+
Use this when you want traceable event metadata such as `eventType`, `version`, `traceId`, or `tenantId`.
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
await kafkaManager.publishEnvelope({
|
|
478
|
+
topic: 'claims.created',
|
|
479
|
+
key: 'claim-1001',
|
|
480
|
+
envelope: {
|
|
481
|
+
eventId: 'evt-claim-1001',
|
|
482
|
+
eventType: 'claims.created',
|
|
483
|
+
version: '1.0.0',
|
|
484
|
+
timestamp: new Date().toISOString(),
|
|
485
|
+
source: 'claims-service',
|
|
486
|
+
traceId: 'trace-123',
|
|
487
|
+
tenantId: 'tenant-abc',
|
|
488
|
+
correlationId: 'corr-001',
|
|
489
|
+
payload: {
|
|
490
|
+
claimId: 'claim-1001',
|
|
491
|
+
status: 'created',
|
|
492
|
+
},
|
|
63
493
|
},
|
|
64
494
|
})
|
|
65
495
|
```
|
|
66
496
|
|
|
67
|
-
###
|
|
497
|
+
### Create or reuse a producer manually
|
|
498
|
+
|
|
499
|
+
If you need direct producer access:
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
const producer = await kafkaManager.getProducer()
|
|
503
|
+
|
|
504
|
+
await producer.send({
|
|
505
|
+
topic: 'claims.created',
|
|
506
|
+
messages: [
|
|
507
|
+
{
|
|
508
|
+
key: 'claim-1001',
|
|
509
|
+
value: JSON.stringify({ claimId: 'claim-1001' }),
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
})
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Create or reuse a consumer
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
const consumer = await kafkaManager.getConsumer('claims-service-group')
|
|
519
|
+
|
|
520
|
+
await consumer.subscribe({
|
|
521
|
+
topic: 'claims.created',
|
|
522
|
+
fromBeginning: false,
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
await consumer.run({
|
|
526
|
+
eachMessage: async ({ topic, partition, message }) => {
|
|
527
|
+
const rawValue = message.value?.toString() ?? '{}'
|
|
528
|
+
const payload = JSON.parse(rawValue)
|
|
529
|
+
|
|
530
|
+
console.log({
|
|
531
|
+
topic,
|
|
532
|
+
partition,
|
|
533
|
+
key: message.key?.toString(),
|
|
534
|
+
payload,
|
|
535
|
+
})
|
|
536
|
+
},
|
|
537
|
+
})
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
`getConsumer(groupId)` returns one shared connected consumer per group id.
|
|
541
|
+
|
|
542
|
+
### Run a managed consumer with DLQ support
|
|
543
|
+
|
|
544
|
+
For SaaS-style services, this is the recommended consumer pattern.
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
await kafkaManager.runConsumer({
|
|
548
|
+
groupId: 'claims-service-group',
|
|
549
|
+
topic: 'claims.created',
|
|
550
|
+
dlqTopic: 'claims.created.dlq',
|
|
551
|
+
onMessage: async ({ message, headers, key }) => {
|
|
552
|
+
console.log('Processing:', {
|
|
553
|
+
key,
|
|
554
|
+
traceId: headers['x-trace-id'],
|
|
555
|
+
payload: message,
|
|
556
|
+
})
|
|
557
|
+
},
|
|
558
|
+
onError: async (error, context) => {
|
|
559
|
+
console.error('Consumer error:', {
|
|
560
|
+
error: error.message,
|
|
561
|
+
topic: context.topic,
|
|
562
|
+
offset: context.offset,
|
|
563
|
+
})
|
|
564
|
+
},
|
|
565
|
+
})
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
If processing fails:
|
|
569
|
+
|
|
570
|
+
- the error is logged
|
|
571
|
+
- a metric hook can receive the failure event
|
|
572
|
+
- the message can be published to a dead-letter topic when `dlqTopic` is set
|
|
573
|
+
|
|
574
|
+
### Catch structured errors
|
|
575
|
+
|
|
576
|
+
All package-thrown operational errors now use `KafkaManagerError`.
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
import {
|
|
580
|
+
KafkaErrorCode,
|
|
581
|
+
KafkaManagerError,
|
|
582
|
+
} from 'sentinel-kafka-manager'
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
await kafkaManager.publish({
|
|
586
|
+
topic: 'claims.created',
|
|
587
|
+
payload: { claimId: 'claim-1001' },
|
|
588
|
+
})
|
|
589
|
+
} catch (error) {
|
|
590
|
+
if (error instanceof KafkaManagerError) {
|
|
591
|
+
console.error('Kafka error', {
|
|
592
|
+
code: error.code,
|
|
593
|
+
message: error.message,
|
|
594
|
+
details: error.details,
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
if (error.code === KafkaErrorCode.MESSAGE_PUBLISH_FAILED) {
|
|
598
|
+
// retry, alert, or degrade gracefully
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
throw error
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Use common success and error response types
|
|
607
|
+
|
|
608
|
+
If your service wraps Kafka operations in API or service-layer responses, you can use the shared response contracts from this package.
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
import {
|
|
612
|
+
ErrorResponse,
|
|
613
|
+
OperationResponse,
|
|
614
|
+
SuccessResponse,
|
|
615
|
+
} from 'sentinel-kafka-manager'
|
|
616
|
+
|
|
617
|
+
type PublishClaimCreatedResponse = OperationResponse<{
|
|
618
|
+
topic: string
|
|
619
|
+
key: string
|
|
620
|
+
}>
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Success example:
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
const response: SuccessResponse<{ topic: string; key: string }> = {
|
|
627
|
+
success: true,
|
|
628
|
+
message: 'Kafka message published successfully.',
|
|
629
|
+
data: {
|
|
630
|
+
topic: 'claims.created',
|
|
631
|
+
key: 'claim-1001',
|
|
632
|
+
},
|
|
633
|
+
meta: {
|
|
634
|
+
timestamp: new Date().toISOString(),
|
|
635
|
+
traceId: 'trace-123',
|
|
636
|
+
},
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Error example:
|
|
641
|
+
|
|
642
|
+
```ts
|
|
643
|
+
const response: ErrorResponse = {
|
|
644
|
+
success: false,
|
|
645
|
+
message: 'Failed to publish Kafka message.',
|
|
646
|
+
error: {
|
|
647
|
+
code: KafkaErrorCode.MESSAGE_PUBLISH_FAILED,
|
|
648
|
+
details: {
|
|
649
|
+
topic: 'claims.created',
|
|
650
|
+
key: 'claim-1001',
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
meta: {
|
|
654
|
+
timestamp: new Date().toISOString(),
|
|
655
|
+
retryable: true,
|
|
656
|
+
},
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Provision topics
|
|
68
661
|
|
|
69
662
|
```ts
|
|
70
663
|
await kafkaManager.provisionTopics([
|
|
@@ -73,54 +666,74 @@ await kafkaManager.provisionTopics([
|
|
|
73
666
|
numPartitions: 6,
|
|
74
667
|
replicationFactor: 3,
|
|
75
668
|
},
|
|
669
|
+
{
|
|
670
|
+
topic: 'claims.updated',
|
|
671
|
+
numPartitions: 6,
|
|
672
|
+
replicationFactor: 3,
|
|
673
|
+
},
|
|
76
674
|
])
|
|
77
675
|
```
|
|
78
676
|
|
|
79
|
-
|
|
677
|
+
This is useful during service startup when your topics should exist before producers or consumers begin work.
|
|
678
|
+
|
|
679
|
+
### Disconnect on shutdown
|
|
680
|
+
|
|
681
|
+
Always close Kafka clients during application shutdown.
|
|
80
682
|
|
|
81
683
|
```ts
|
|
82
|
-
|
|
684
|
+
process.on('SIGINT', async () => {
|
|
685
|
+
await kafkaManager.disconnect()
|
|
686
|
+
process.exit(0)
|
|
687
|
+
})
|
|
83
688
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
689
|
+
process.on('SIGTERM', async () => {
|
|
690
|
+
await kafkaManager.disconnect()
|
|
691
|
+
process.exit(0)
|
|
87
692
|
})
|
|
88
693
|
```
|
|
89
694
|
|
|
90
|
-
###
|
|
695
|
+
### Inspect health
|
|
91
696
|
|
|
92
697
|
```ts
|
|
93
|
-
|
|
698
|
+
const health = kafkaManager.getHealthSnapshot()
|
|
699
|
+
|
|
700
|
+
console.log(health)
|
|
94
701
|
```
|
|
95
702
|
|
|
96
|
-
## Environment
|
|
703
|
+
## Environment Setup
|
|
97
704
|
|
|
98
|
-
|
|
705
|
+
This package supports two environment styles:
|
|
99
706
|
|
|
100
|
-
1. explicit broker list with `KAFKA_BROKERS`
|
|
101
|
-
2. derived
|
|
707
|
+
1. one explicit broker list with `KAFKA_BROKERS`
|
|
708
|
+
2. derived brokers from your existing Kafka runtime variables
|
|
102
709
|
|
|
103
|
-
|
|
710
|
+
## Option 1: Use `KAFKA_BROKERS`
|
|
711
|
+
|
|
712
|
+
This is the simplest option.
|
|
713
|
+
|
|
714
|
+
### `.env`
|
|
104
715
|
|
|
105
716
|
```env
|
|
106
717
|
KAFKA_BROKERS=localhost:29092,localhost:39092,localhost:49092
|
|
107
718
|
```
|
|
108
719
|
|
|
109
|
-
|
|
110
|
-
import { KafkaManager } from 'sentinel-kafka-manager'
|
|
720
|
+
### Code
|
|
111
721
|
|
|
722
|
+
```ts
|
|
112
723
|
const kafkaManager = KafkaManager.fromEnv({
|
|
113
724
|
clientId: 'claims-service',
|
|
114
725
|
})
|
|
115
726
|
```
|
|
116
727
|
|
|
117
|
-
|
|
728
|
+
## Option 2: Use your current Kafka cluster variables
|
|
729
|
+
|
|
730
|
+
This matches the Kafka server setup you shared.
|
|
118
731
|
|
|
119
|
-
|
|
732
|
+
### For host machine apps
|
|
120
733
|
|
|
121
|
-
|
|
734
|
+
Use this when your Node.js service runs on the host machine.
|
|
122
735
|
|
|
123
|
-
|
|
736
|
+
### `.env`
|
|
124
737
|
|
|
125
738
|
```env
|
|
126
739
|
KAFKA_EXTERNAL_HOST=localhost
|
|
@@ -130,6 +743,8 @@ KAFKA_BROKER_3_EXTERNAL_PORT=49092
|
|
|
130
743
|
KAFKA_BROKER_INTERNAL_PORT=9092
|
|
131
744
|
```
|
|
132
745
|
|
|
746
|
+
### Code
|
|
747
|
+
|
|
133
748
|
```ts
|
|
134
749
|
const kafkaManager = KafkaManager.fromEnv({
|
|
135
750
|
clientId: 'claims-service',
|
|
@@ -145,14 +760,18 @@ localhost:39092
|
|
|
145
760
|
localhost:49092
|
|
146
761
|
```
|
|
147
762
|
|
|
148
|
-
|
|
763
|
+
### For Dockerized apps on the Kafka network
|
|
149
764
|
|
|
150
|
-
Use this when your
|
|
765
|
+
Use this when your Node.js service runs inside Docker on the same Kafka network.
|
|
766
|
+
|
|
767
|
+
### `.env`
|
|
151
768
|
|
|
152
769
|
```env
|
|
153
770
|
KAFKA_BROKER_INTERNAL_PORT=9092
|
|
154
771
|
```
|
|
155
772
|
|
|
773
|
+
### Code
|
|
774
|
+
|
|
156
775
|
```ts
|
|
157
776
|
const kafkaManager = KafkaManager.fromEnv({
|
|
158
777
|
clientId: 'claims-service',
|
|
@@ -168,97 +787,144 @@ broker-2:9092
|
|
|
168
787
|
broker-3:9092
|
|
169
788
|
```
|
|
170
789
|
|
|
171
|
-
##
|
|
790
|
+
## Which Mode Should You Use?
|
|
791
|
+
|
|
792
|
+
- Use `mode: 'external'` when your service uses `localhost` broker ports like `29092`, `39092`, and `49092`
|
|
793
|
+
- Use `mode: 'internal'` when your service is inside Docker and should reach brokers like `broker-1:9092`
|
|
794
|
+
- Use `KAFKA_BROKERS` when you want the simplest and most explicit configuration
|
|
795
|
+
|
|
796
|
+
## Your Current Running Kafka Cluster
|
|
172
797
|
|
|
173
|
-
|
|
798
|
+
Based on your running Docker containers, your Kafka cluster is exposed like this:
|
|
174
799
|
|
|
175
|
-
- `
|
|
176
|
-
- `
|
|
177
|
-
- `
|
|
178
|
-
- `
|
|
179
|
-
- `KAFKA_BROKER_3_EXTERNAL_PORT`
|
|
180
|
-
- `KAFKA_BROKER_INTERNAL_PORT`
|
|
800
|
+
- `broker-1` -> host port `29092`
|
|
801
|
+
- `broker-2` -> host port `39092`
|
|
802
|
+
- `broker-3` -> host port `49092`
|
|
803
|
+
- `controller-1`, `controller-2`, and `controller-3` are controller-only nodes and should not be used by application clients
|
|
181
804
|
|
|
182
|
-
|
|
805
|
+
If your Node.js service runs on the host machine, use:
|
|
183
806
|
|
|
184
|
-
|
|
807
|
+
```env
|
|
808
|
+
KAFKA_EXTERNAL_HOST=localhost
|
|
809
|
+
KAFKA_BROKER_1_EXTERNAL_PORT=29092
|
|
810
|
+
KAFKA_BROKER_2_EXTERNAL_PORT=39092
|
|
811
|
+
KAFKA_BROKER_3_EXTERNAL_PORT=49092
|
|
812
|
+
KAFKA_BROKER_INTERNAL_PORT=9092
|
|
813
|
+
```
|
|
185
814
|
|
|
186
815
|
```ts
|
|
187
816
|
const kafkaManager = KafkaManager.fromEnv({
|
|
188
817
|
clientId: 'claims-service',
|
|
189
818
|
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
819
|
})
|
|
198
820
|
```
|
|
199
821
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
### SSL
|
|
822
|
+
If your Node.js service runs inside Docker on the same Kafka network, use:
|
|
203
823
|
|
|
204
824
|
```ts
|
|
205
|
-
const kafkaManager =
|
|
825
|
+
const kafkaManager = KafkaManager.fromEnv({
|
|
206
826
|
clientId: 'claims-service',
|
|
207
|
-
|
|
208
|
-
ssl: {
|
|
209
|
-
rejectUnauthorized: true,
|
|
210
|
-
},
|
|
827
|
+
mode: 'internal',
|
|
211
828
|
})
|
|
212
829
|
```
|
|
213
830
|
|
|
214
|
-
|
|
831
|
+
This package is designed to work with that exact broker layout.
|
|
832
|
+
|
|
833
|
+
## Full Service Example
|
|
215
834
|
|
|
216
835
|
```ts
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
836
|
+
import { KafkaManager } from 'sentinel-kafka-manager'
|
|
837
|
+
|
|
838
|
+
const kafkaManager = KafkaManager.fromEnv({
|
|
839
|
+
clientId: 'claims-api',
|
|
840
|
+
mode: 'external',
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
async function bootstrap(): Promise<void> {
|
|
844
|
+
await kafkaManager.provisionTopics([
|
|
845
|
+
{
|
|
846
|
+
topic: 'claims.created',
|
|
847
|
+
numPartitions: 6,
|
|
848
|
+
replicationFactor: 3,
|
|
849
|
+
},
|
|
850
|
+
])
|
|
851
|
+
|
|
852
|
+
await kafkaManager.publish({
|
|
853
|
+
topic: 'claims.created',
|
|
854
|
+
key: 'claim-1001',
|
|
855
|
+
payload: {
|
|
856
|
+
claimId: 'claim-1001',
|
|
857
|
+
source: 'claims-api',
|
|
858
|
+
},
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
await kafkaManager.runConsumer({
|
|
862
|
+
groupId: 'claims-api-group',
|
|
863
|
+
topic: 'claims.created',
|
|
864
|
+
dlqTopic: 'claims.created.dlq',
|
|
865
|
+
onMessage: async ({ message, headers }) => {
|
|
866
|
+
console.log('Received event:', {
|
|
867
|
+
traceId: headers['x-trace-id'],
|
|
868
|
+
payload: message,
|
|
869
|
+
})
|
|
870
|
+
},
|
|
871
|
+
})
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
bootstrap().catch(async (error) => {
|
|
875
|
+
console.error('Kafka bootstrap failed:', error)
|
|
876
|
+
await kafkaManager.disconnect()
|
|
877
|
+
process.exit(1)
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
process.on('SIGINT', async () => {
|
|
881
|
+
await kafkaManager.disconnect()
|
|
882
|
+
process.exit(0)
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
process.on('SIGTERM', async () => {
|
|
886
|
+
await kafkaManager.disconnect()
|
|
887
|
+
process.exit(0)
|
|
225
888
|
})
|
|
226
889
|
```
|
|
227
890
|
|
|
228
|
-
|
|
891
|
+
## API Summary
|
|
892
|
+
|
|
893
|
+
### `new KafkaManager(options)`
|
|
894
|
+
|
|
895
|
+
Use this when you already know your brokers.
|
|
229
896
|
|
|
230
897
|
```ts
|
|
231
898
|
const kafkaManager = new KafkaManager({
|
|
232
899
|
clientId: 'claims-service',
|
|
233
900
|
brokers: ['localhost:29092'],
|
|
234
|
-
connectionTimeout: 5000,
|
|
235
|
-
requestTimeout: 30000,
|
|
236
|
-
retry: {
|
|
237
|
-
initialRetryTime: 300,
|
|
238
|
-
retries: 10,
|
|
239
|
-
},
|
|
240
901
|
})
|
|
241
902
|
```
|
|
242
903
|
|
|
243
|
-
## API
|
|
244
|
-
|
|
245
|
-
### `new KafkaManager(options)`
|
|
246
|
-
|
|
247
|
-
Creates a manager from an explicit broker list.
|
|
248
|
-
|
|
249
904
|
Options:
|
|
250
905
|
|
|
251
906
|
- `clientId: string`
|
|
252
907
|
- `brokers: string[]`
|
|
253
908
|
- `ssl?: boolean | { rejectUnauthorized?: boolean }`
|
|
254
|
-
- `sasl?:
|
|
909
|
+
- `sasl?: SASLOptions`
|
|
255
910
|
- `connectionTimeout?: number`
|
|
256
911
|
- `requestTimeout?: number`
|
|
257
912
|
- `retry?: { initialRetryTime?: number; retries?: number }`
|
|
913
|
+
- `logger?: KafkaLogger`
|
|
914
|
+
- `metrics?: KafkaMetrics`
|
|
915
|
+
- `producerConfig?: ProducerConfig`
|
|
916
|
+
- `consumerDefaults?: Omit<ConsumerConfig, 'groupId'>`
|
|
258
917
|
|
|
259
918
|
### `KafkaManager.fromEnv(options)`
|
|
260
919
|
|
|
261
|
-
|
|
920
|
+
Use this when you want brokers to come from environment variables.
|
|
921
|
+
|
|
922
|
+
```ts
|
|
923
|
+
const kafkaManager = KafkaManager.fromEnv({
|
|
924
|
+
clientId: 'claims-service',
|
|
925
|
+
mode: 'external',
|
|
926
|
+
})
|
|
927
|
+
```
|
|
262
928
|
|
|
263
929
|
Options:
|
|
264
930
|
|
|
@@ -272,10 +938,14 @@ Options:
|
|
|
272
938
|
- `externalPortEnvKeySuffix?: string`
|
|
273
939
|
- `internalBrokerHostPrefix?: string`
|
|
274
940
|
- `ssl?: boolean | { rejectUnauthorized?: boolean }`
|
|
275
|
-
- `sasl?:
|
|
941
|
+
- `sasl?: SASLOptions`
|
|
276
942
|
- `connectionTimeout?: number`
|
|
277
943
|
- `requestTimeout?: number`
|
|
278
944
|
- `retry?: { initialRetryTime?: number; retries?: number }`
|
|
945
|
+
- `logger?: KafkaLogger`
|
|
946
|
+
- `metrics?: KafkaMetrics`
|
|
947
|
+
- `producerConfig?: ProducerConfig`
|
|
948
|
+
- `consumerDefaults?: Omit<ConsumerConfig, 'groupId'>`
|
|
279
949
|
|
|
280
950
|
### `getProducer()`
|
|
281
951
|
|
|
@@ -283,109 +953,257 @@ Returns a shared connected Kafka producer.
|
|
|
283
953
|
|
|
284
954
|
### `getConsumer(groupId)`
|
|
285
955
|
|
|
286
|
-
Returns a shared connected Kafka consumer for
|
|
287
|
-
|
|
288
|
-
### `provisionTopics(topics)`
|
|
956
|
+
Returns a shared connected Kafka consumer for that `groupId`.
|
|
289
957
|
|
|
290
|
-
|
|
958
|
+
### `publish(options)`
|
|
291
959
|
|
|
292
|
-
|
|
960
|
+
Publishes one JSON message.
|
|
293
961
|
|
|
294
962
|
```ts
|
|
295
|
-
{
|
|
296
|
-
topic:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
963
|
+
await kafkaManager.publish({
|
|
964
|
+
topic: 'claims.created',
|
|
965
|
+
key: 'claim-1001',
|
|
966
|
+
payload: { claimId: 'claim-1001' },
|
|
967
|
+
})
|
|
300
968
|
```
|
|
301
969
|
|
|
302
|
-
### `
|
|
970
|
+
### `publishEnvelope(options)`
|
|
303
971
|
|
|
304
|
-
Publishes
|
|
972
|
+
Publishes a standard metadata envelope for traceable event-driven systems.
|
|
305
973
|
|
|
306
|
-
|
|
974
|
+
### `provisionTopics(topics)`
|
|
307
975
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
976
|
+
Creates missing topics.
|
|
977
|
+
|
|
978
|
+
### `runConsumer(options)`
|
|
979
|
+
|
|
980
|
+
Runs a managed consumer with:
|
|
981
|
+
|
|
982
|
+
- a typed message parser
|
|
983
|
+
- application callback handling
|
|
984
|
+
- optional DLQ publishing
|
|
985
|
+
- optional error callback
|
|
986
|
+
|
|
987
|
+
### `getHealthSnapshot()`
|
|
988
|
+
|
|
989
|
+
Returns a lightweight connection-health snapshot.
|
|
316
990
|
|
|
317
991
|
### `disconnect()`
|
|
318
992
|
|
|
319
|
-
Disconnects the
|
|
993
|
+
Disconnects the producer, admin client, and any connected consumers.
|
|
994
|
+
|
|
995
|
+
## Error Handling
|
|
996
|
+
|
|
997
|
+
This package now throws structured errors with stable codes for production handling.
|
|
998
|
+
|
|
999
|
+
### Error class
|
|
1000
|
+
|
|
1001
|
+
- `KafkaManagerError`
|
|
1002
|
+
|
|
1003
|
+
### Common response types
|
|
1004
|
+
|
|
1005
|
+
- `SuccessResponse<T>`
|
|
1006
|
+
- `ErrorResponse`
|
|
1007
|
+
- `OperationResponse<T>`
|
|
1008
|
+
|
|
1009
|
+
### Error fields
|
|
320
1010
|
|
|
321
|
-
|
|
1011
|
+
- `code`: stable machine-readable error code
|
|
1012
|
+
- `message`: human-readable explanation
|
|
1013
|
+
- `details`: structured metadata for logs or monitoring
|
|
1014
|
+
- `cause`: original thrown error when available
|
|
322
1015
|
|
|
323
|
-
###
|
|
1016
|
+
### Common error codes
|
|
1017
|
+
|
|
1018
|
+
- `INVALID_CLIENT_ID`
|
|
1019
|
+
- `INVALID_BROKERS`
|
|
1020
|
+
- `INVALID_GROUP_ID`
|
|
1021
|
+
- `INVALID_TOPIC`
|
|
1022
|
+
- `INVALID_ENVELOPE`
|
|
1023
|
+
- `INVALID_BROKER_COUNT`
|
|
1024
|
+
- `MISSING_ENVIRONMENT_VARIABLE`
|
|
1025
|
+
- `PRODUCER_CONNECTION_FAILED`
|
|
1026
|
+
- `CONSUMER_CONNECTION_FAILED`
|
|
1027
|
+
- `ADMIN_CONNECTION_FAILED`
|
|
1028
|
+
- `MESSAGE_PUBLISH_FAILED`
|
|
1029
|
+
- `TOPIC_PROVISION_FAILED`
|
|
1030
|
+
- `CONSUMER_RUN_FAILED`
|
|
1031
|
+
- `CONSUMER_HANDLER_FAILED`
|
|
1032
|
+
- `DLQ_PUBLISH_FAILED`
|
|
1033
|
+
- `MESSAGE_PARSE_FAILED`
|
|
1034
|
+
- `DISCONNECT_FAILED`
|
|
1035
|
+
|
|
1036
|
+
### Example
|
|
324
1037
|
|
|
325
1038
|
```ts
|
|
326
|
-
import {
|
|
1039
|
+
import { KafkaManagerError } from 'sentinel-kafka-manager'
|
|
1040
|
+
|
|
1041
|
+
try {
|
|
1042
|
+
await kafkaManager.runConsumer({
|
|
1043
|
+
groupId: 'claims-service-group',
|
|
1044
|
+
topic: 'claims.created',
|
|
1045
|
+
onMessage: async ({ message }) => {
|
|
1046
|
+
console.log(message)
|
|
1047
|
+
},
|
|
1048
|
+
})
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
if (error instanceof KafkaManagerError) {
|
|
1051
|
+
console.error(error.code, error.message, error.details)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
throw error
|
|
1055
|
+
}
|
|
1056
|
+
```
|
|
327
1057
|
|
|
1058
|
+
## Advanced Examples
|
|
1059
|
+
|
|
1060
|
+
### Logger and metrics example
|
|
1061
|
+
|
|
1062
|
+
```ts
|
|
328
1063
|
const kafkaManager = KafkaManager.fromEnv({
|
|
329
|
-
clientId: 'claims-
|
|
1064
|
+
clientId: 'claims-service',
|
|
330
1065
|
mode: 'external',
|
|
1066
|
+
logger: {
|
|
1067
|
+
debug: (message, meta) => console.debug(message, meta),
|
|
1068
|
+
info: (message, meta) => console.info(message, meta),
|
|
1069
|
+
warn: (message, meta) => console.warn(message, meta),
|
|
1070
|
+
error: (message, meta) => console.error(message, meta),
|
|
1071
|
+
},
|
|
1072
|
+
metrics: {
|
|
1073
|
+
emit: (event) => {
|
|
1074
|
+
console.log('METRIC', event.name, event.tags, event.meta)
|
|
1075
|
+
},
|
|
1076
|
+
},
|
|
331
1077
|
})
|
|
1078
|
+
```
|
|
332
1079
|
|
|
333
|
-
|
|
1080
|
+
### SSL example
|
|
1081
|
+
|
|
1082
|
+
```ts
|
|
1083
|
+
const kafkaManager = new KafkaManager({
|
|
1084
|
+
clientId: 'claims-service',
|
|
1085
|
+
brokers: ['localhost:29092'],
|
|
1086
|
+
ssl: {
|
|
1087
|
+
rejectUnauthorized: true,
|
|
1088
|
+
},
|
|
1089
|
+
})
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
### Managed consumer with custom parser
|
|
1093
|
+
|
|
1094
|
+
```ts
|
|
1095
|
+
await kafkaManager.runConsumer({
|
|
1096
|
+
groupId: 'claims-service-group',
|
|
334
1097
|
topic: 'claims.created',
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
1098
|
+
dlqTopic: 'claims.created.dlq',
|
|
1099
|
+
parser: (payload) => {
|
|
1100
|
+
const rawValue = payload.message.value?.toString() ?? '{}'
|
|
1101
|
+
return JSON.parse(rawValue) as {
|
|
1102
|
+
claimId: string
|
|
1103
|
+
status: string
|
|
1104
|
+
}
|
|
1105
|
+
},
|
|
1106
|
+
onMessage: async ({ message }) => {
|
|
1107
|
+
console.log(message.claimId, message.status)
|
|
339
1108
|
},
|
|
340
1109
|
})
|
|
341
1110
|
```
|
|
342
1111
|
|
|
343
|
-
###
|
|
1112
|
+
### SASL example
|
|
344
1113
|
|
|
345
1114
|
```ts
|
|
346
|
-
|
|
1115
|
+
const kafkaManager = new KafkaManager({
|
|
1116
|
+
clientId: 'claims-service',
|
|
1117
|
+
brokers: ['localhost:29092'],
|
|
1118
|
+
sasl: {
|
|
1119
|
+
mechanism: 'plain',
|
|
1120
|
+
username: 'kafka-user',
|
|
1121
|
+
password: 'secret',
|
|
1122
|
+
},
|
|
1123
|
+
})
|
|
1124
|
+
```
|
|
347
1125
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
1126
|
+
### Retry and timeout example
|
|
1127
|
+
|
|
1128
|
+
```ts
|
|
1129
|
+
const kafkaManager = new KafkaManager({
|
|
1130
|
+
clientId: 'claims-service',
|
|
1131
|
+
brokers: ['localhost:29092'],
|
|
1132
|
+
connectionTimeout: 5000,
|
|
1133
|
+
requestTimeout: 30000,
|
|
1134
|
+
retry: {
|
|
1135
|
+
initialRetryTime: 300,
|
|
1136
|
+
retries: 10,
|
|
1137
|
+
},
|
|
351
1138
|
})
|
|
352
1139
|
```
|
|
353
1140
|
|
|
354
|
-
##
|
|
1141
|
+
## Common Mistakes
|
|
1142
|
+
|
|
1143
|
+
- Do not connect application clients to KRaft controller nodes. Connect only to brokers.
|
|
1144
|
+
- Do not use `mode: 'external'` inside Docker unless you truly want host-published ports.
|
|
1145
|
+
- Do not create a new `KafkaManager` for every request. Reuse one shared instance.
|
|
1146
|
+
- Do not forget `disconnect()` during shutdown.
|
|
1147
|
+
- Do not assume topics will exist unless your platform creates them or your service provisions them.
|
|
1148
|
+
- Do not ignore failed consumer messages in production. Use `runConsumer()` with a `dlqTopic`.
|
|
1149
|
+
- Do not publish business-critical events without metadata. Prefer `publishEnvelope()` for traceability.
|
|
1150
|
+
- Do not depend on raw driver error strings in application logic. Use `KafkaManagerError.code`.
|
|
1151
|
+
|
|
1152
|
+
## Recommended SaaS Pattern
|
|
1153
|
+
|
|
1154
|
+
For most SaaS services, the cleanest pattern is:
|
|
355
1155
|
|
|
356
|
-
|
|
1156
|
+
1. Create one shared `KafkaManager` during service bootstrap.
|
|
1157
|
+
2. Use `publishEnvelope()` for business events.
|
|
1158
|
+
3. Use `runConsumer()` instead of raw `consumer.run()` for managed handling.
|
|
1159
|
+
4. Configure a `dlqTopic` for important consumers.
|
|
1160
|
+
5. Catch `KafkaManagerError` and branch on `error.code`.
|
|
1161
|
+
6. Wrap service outcomes in `OperationResponse<T>` where useful.
|
|
1162
|
+
|
|
1163
|
+
## Publishing This Package
|
|
1164
|
+
|
|
1165
|
+
Build:
|
|
357
1166
|
|
|
358
1167
|
```bash
|
|
359
1168
|
npm run build
|
|
360
1169
|
```
|
|
361
1170
|
|
|
362
|
-
Publish
|
|
1171
|
+
Publish:
|
|
363
1172
|
|
|
364
1173
|
```bash
|
|
365
1174
|
npm publish
|
|
366
1175
|
```
|
|
367
1176
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
### Publish Troubleshooting
|
|
1177
|
+
If npm rejects the publish:
|
|
371
1178
|
|
|
372
|
-
|
|
1179
|
+
- `403` usually means authentication, 2FA, or token permission issues
|
|
1180
|
+
- `404` usually means the package name is unavailable or not publishable by your account
|
|
373
1181
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
Useful checks:
|
|
1182
|
+
Useful check:
|
|
377
1183
|
|
|
378
1184
|
```bash
|
|
379
1185
|
npm whoami
|
|
380
1186
|
```
|
|
381
1187
|
|
|
382
|
-
|
|
1188
|
+
## Notes
|
|
1189
|
+
|
|
1190
|
+
- Your current Kafka cluster works fine with PLAINTEXT, so SSL and SASL are optional unless your environment changes
|
|
1191
|
+
- This package is designed for both host-based services and Dockerized services
|
|
1192
|
+
- If you want the least surprise, use `KAFKA_BROKERS`
|
|
383
1193
|
|
|
384
|
-
|
|
1194
|
+
## Links
|
|
385
1195
|
|
|
386
|
-
|
|
1196
|
+
- npm deployment guide: [NPM_DEPLOYMENT.md](/var/www/html/matrix-project/sentinel-kafka-manager/NPM_DEPLOYMENT.md)
|
|
1197
|
+
- Example folder: [examples/service-module-usage/README.md](/var/www/html/matrix-project/sentinel-kafka-manager/examples/service-module-usage/README.md)
|
|
1198
|
+
- Service module guide: [SERVICE_MODULE_USAGE.md](/var/www/html/matrix-project/sentinel-kafka-manager/SERVICE_MODULE_USAGE.md)
|
|
1199
|
+
- GitHub: https://github.com/dilipshaw2024/sentinel-kafka-manager
|
|
1200
|
+
- npm: https://www.npmjs.com/package/sentinel-kafka-manager
|
|
1201
|
+
- LinkedIn: https://www.linkedin.com/in/dilip-shaw-2740769/
|
|
1202
|
+
|
|
1203
|
+
## About Me
|
|
1204
|
+
|
|
1205
|
+
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.
|
|
1206
|
+
|
|
1207
|
+
## Feedback
|
|
387
1208
|
|
|
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'`
|
|
1209
|
+
If you have any feedback, please reach out to us at `dilipabc@gmail.com`
|