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 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 helps services:
5
+ This package gives you one shared way to:
6
6
 
7
- - connect to Kafka with a small shared API
8
- - create and reuse producers, consumers, and admin clients safely
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
- - provision topics if they do not already exist
11
- - bootstrap from environment variables for both Docker-internal and host-external Kafka access
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 in this package:
38
+ For local package development:
20
39
 
21
40
  ```bash
22
41
  npm install
23
42
  npm run build
24
43
  ```
25
44
 
26
- ## Features
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
- - built on `kafkajs`
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
272
+ - topics are not auto-created by default
273
+ - deleting topics is allowed
274
+ - retention is 7 days by default
34
275
 
35
- ## Exports
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
- import { KafkaManager } from 'sentinel-kafka-manager'
328
+ await kafkaManager.provisionTopics([
329
+ {
330
+ topic: 'claims.created',
331
+ numPartitions: 6,
332
+ replicationFactor: 3,
333
+ },
334
+ ])
39
335
  ```
40
336
 
41
- ## Quick Start
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
- ### 1. Create a manager with explicit brokers
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
- ### 2. Publish a message
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
- ### 3. Provision topics
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
- ### 4. Reuse a connected consumer
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
- const consumer = await kafkaManager.getConsumer('claims-service-group')
684
+ process.on('SIGINT', async () => {
685
+ await kafkaManager.disconnect()
686
+ process.exit(0)
687
+ })
83
688
 
84
- await consumer.subscribe({
85
- topic: 'claims.created',
86
- fromBeginning: false,
689
+ process.on('SIGTERM', async () => {
690
+ await kafkaManager.disconnect()
691
+ process.exit(0)
87
692
  })
88
693
  ```
89
694
 
90
- ### 5. Disconnect on shutdown
695
+ ### Inspect health
91
696
 
92
697
  ```ts
93
- await kafkaManager.disconnect()
698
+ const health = kafkaManager.getHealthSnapshot()
699
+
700
+ console.log(health)
94
701
  ```
95
702
 
96
- ## Environment-Based Setup
703
+ ## Environment Setup
97
704
 
98
- `KafkaManager.fromEnv()` supports two styles:
705
+ This package supports two environment styles:
99
706
 
100
- 1. explicit broker list with `KAFKA_BROKERS`
101
- 2. derived broker list from your Kafka runtime env naming
707
+ 1. one explicit broker list with `KAFKA_BROKERS`
708
+ 2. derived brokers from your existing Kafka runtime variables
102
709
 
103
- ### Option A: Explicit broker list
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
- ```ts
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
- ### Option B: Derived from your current Kafka cluster env
728
+ ## Option 2: Use your current Kafka cluster variables
729
+
730
+ This matches the Kafka server setup you shared.
118
731
 
119
- This package supports the env structure you shared for your KRaft cluster.
732
+ ### For host machine apps
120
733
 
121
- #### External mode
734
+ Use this when your Node.js service runs on the host machine.
122
735
 
123
- Use this when your app runs on the host machine or outside the Kafka Docker network.
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
- #### Internal mode
763
+ ### For Dockerized apps on the Kafka network
149
764
 
150
- Use this when your app runs inside Docker on the same Kafka network.
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
- ## Supported Environment Variables
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
- ### Minimal variables
798
+ Based on your running Docker containers, your Kafka cluster is exposed like this:
174
799
 
175
- - `KAFKA_BROKERS`
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`
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
- ### `fromEnv()` options
805
+ If your Node.js service runs on the host machine, use:
183
806
 
184
- You can customize the env lookup behavior:
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
- ## Advanced Connection Options
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 = new KafkaManager({
825
+ const kafkaManager = KafkaManager.fromEnv({
206
826
  clientId: 'claims-service',
207
- brokers: ['localhost:29092'],
208
- ssl: {
209
- rejectUnauthorized: true,
210
- },
827
+ mode: 'internal',
211
828
  })
212
829
  ```
213
830
 
214
- ### SASL
831
+ This package is designed to work with that exact broker layout.
832
+
833
+ ## Full Service Example
215
834
 
216
835
  ```ts
217
- const kafkaManager = new KafkaManager({
218
- clientId: 'claims-service',
219
- brokers: ['localhost:29092'],
220
- sasl: {
221
- mechanism: 'plain',
222
- username: 'kafka-user',
223
- password: 'secret',
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
- ### Retry and timeout tuning
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?: { mechanism, username, password }`
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
- Creates a manager by reading broker information from environment variables.
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?: { mechanism, username, password }`
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 the provided group id.
287
-
288
- ### `provisionTopics(topics)`
956
+ Returns a shared connected Kafka consumer for that `groupId`.
289
957
 
290
- Creates missing topics using the Kafka admin client.
958
+ ### `publish(options)`
291
959
 
292
- Topic shape:
960
+ Publishes one JSON message.
293
961
 
294
962
  ```ts
295
- {
296
- topic: string
297
- numPartitions?: number
298
- replicationFactor?: number
299
- }
963
+ await kafkaManager.publish({
964
+ topic: 'claims.created',
965
+ key: 'claim-1001',
966
+ payload: { claimId: 'claim-1001' },
967
+ })
300
968
  ```
301
969
 
302
- ### `publish(options)`
970
+ ### `publishEnvelope(options)`
303
971
 
304
- Publishes one JSON-encoded message.
972
+ Publishes a standard metadata envelope for traceable event-driven systems.
305
973
 
306
- Message shape:
974
+ ### `provisionTopics(topics)`
307
975
 
308
- ```ts
309
- {
310
- topic: string
311
- payload: T
312
- key?: string
313
- headers?: IHeaders
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 managed producer, admin client, and any connected consumers.
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
- ## Example With Your Kafka Cluster
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
- ### Host-based app
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 { KafkaManager } from 'sentinel-kafka-manager'
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-api',
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
- await kafkaManager.publish({
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
- key: 'claim-1001',
336
- payload: {
337
- claimId: 'claim-1001',
338
- source: 'claims-api',
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
- ### Dockerized app on the same network
1112
+ ### SASL example
344
1113
 
345
1114
  ```ts
346
- import { KafkaManager } from 'sentinel-kafka-manager'
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
- const kafkaManager = KafkaManager.fromEnv({
349
- clientId: 'claims-worker',
350
- mode: 'internal',
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
- ## Publishing
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
- Build before publishing:
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 the package:
1171
+ Publish:
363
1172
 
364
1173
  ```bash
365
1174
  npm publish
366
1175
  ```
367
1176
 
368
- This package already sets `"publishConfig": { "access": "public" }`, so you do not need to pass `--access public` every time.
369
-
370
- ### Publish Troubleshooting
1177
+ If npm rejects the publish:
371
1178
 
372
- #### `403` with 2FA or token message
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
- If npm says two-factor authentication or a granular token with bypass 2FA is required, authenticate with an account that can publish or use a publish-capable npm token.
375
-
376
- Useful checks:
1182
+ Useful check:
377
1183
 
378
1184
  ```bash
379
1185
  npm whoami
380
1186
  ```
381
1187
 
382
- #### `404` or package name unavailable
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
- If npm says the package could not be published, confirm that the package name is available to your npm account and not already owned by someone else.
1194
+ ## Links
385
1195
 
386
- ## Notes
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
- - connect clients only to brokers, not KRaft controller nodes
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`