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 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 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
+ ## 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
- - 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
66
+ This package is now suitable as a shared internal Kafka module for SaaS microservices.
34
67
 
35
- ## Exports
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
- ## Quick Start
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 { KafkaManager } from 'sentinel-kafka-manager'
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
- ### 2. Publish a message
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
- ### 3. Provision topics
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
- ### 4. Reuse a connected consumer
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
- const consumer = await kafkaManager.getConsumer('claims-service-group')
398
+ process.on('SIGINT', async () => {
399
+ await kafkaManager.disconnect()
400
+ process.exit(0)
401
+ })
83
402
 
84
- await consumer.subscribe({
85
- topic: 'claims.created',
86
- fromBeginning: false,
403
+ process.on('SIGTERM', async () => {
404
+ await kafkaManager.disconnect()
405
+ process.exit(0)
87
406
  })
88
407
  ```
89
408
 
90
- ### 5. Disconnect on shutdown
409
+ ### Inspect health
91
410
 
92
411
  ```ts
93
- await kafkaManager.disconnect()
412
+ const health = kafkaManager.getHealthSnapshot()
413
+
414
+ console.log(health)
94
415
  ```
95
416
 
96
- ## Environment-Based Setup
417
+ ## Environment Setup
97
418
 
98
- `KafkaManager.fromEnv()` supports two styles:
419
+ This package supports two environment styles:
99
420
 
100
- 1. explicit broker list with `KAFKA_BROKERS`
101
- 2. derived broker list from your Kafka runtime env naming
421
+ 1. one explicit broker list with `KAFKA_BROKERS`
422
+ 2. derived brokers from your existing Kafka runtime variables
102
423
 
103
- ### Option A: Explicit broker list
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
- ```ts
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
- ### Option B: Derived from your current Kafka cluster env
442
+ ## Option 2: Use your current Kafka cluster variables
118
443
 
119
- This package supports the env structure you shared for your KRaft cluster.
444
+ This matches the Kafka server setup you shared.
120
445
 
121
- #### External mode
446
+ ### For host machine apps
122
447
 
123
- Use this when your app runs on the host machine or outside the Kafka Docker network.
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
- #### Internal mode
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
- Use this when your app runs inside Docker on the same Kafka network.
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
- ## Supported Environment Variables
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
- ### Minimal variables
510
+ ## Your Current Running Kafka Cluster
174
511
 
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`
512
+ Based on your running Docker containers, your Kafka cluster is exposed like this:
181
513
 
182
- ### `fromEnv()` options
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
- You can customize the env lookup behavior:
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
- ## Advanced Connection Options
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 = new KafkaManager({
539
+ const kafkaManager = KafkaManager.fromEnv({
206
540
  clientId: 'claims-service',
207
- brokers: ['localhost:29092'],
208
- ssl: {
209
- rejectUnauthorized: true,
210
- },
541
+ mode: 'internal',
211
542
  })
212
543
  ```
213
544
 
214
- ### SASL
545
+ This package is designed to work with that exact broker layout.
546
+
547
+ ## Full Service Example
215
548
 
216
549
  ```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
- },
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
- ### Retry and timeout tuning
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?: { mechanism, username, password }`
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
- Creates a manager by reading broker information from environment variables.
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?: { mechanism, username, password }`
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 the provided group id.
287
-
288
- ### `provisionTopics(topics)`
670
+ Returns a shared connected Kafka consumer for that `groupId`.
289
671
 
290
- Creates missing topics using the Kafka admin client.
672
+ ### `publish(options)`
291
673
 
292
- Topic shape:
674
+ Publishes one JSON message.
293
675
 
294
676
  ```ts
295
- {
296
- topic: string
297
- numPartitions?: number
298
- replicationFactor?: number
299
- }
677
+ await kafkaManager.publish({
678
+ topic: 'claims.created',
679
+ key: 'claim-1001',
680
+ payload: { claimId: 'claim-1001' },
681
+ })
300
682
  ```
301
683
 
302
- ### `publish(options)`
684
+ ### `publishEnvelope(options)`
303
685
 
304
- Publishes one JSON-encoded message.
686
+ Publishes a standard metadata envelope for traceable event-driven systems.
305
687
 
306
- Message shape:
688
+ ### `provisionTopics(topics)`
307
689
 
308
- ```ts
309
- {
310
- topic: string
311
- payload: T
312
- key?: string
313
- headers?: IHeaders
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 managed producer, admin client, and any connected consumers.
707
+ Disconnects the producer, admin client, and any connected consumers.
320
708
 
321
- ## Example With Your Kafka Cluster
709
+ ## Error Handling
322
710
 
323
- ### Host-based app
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 { KafkaManager } from 'sentinel-kafka-manager'
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-api',
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
- await kafkaManager.publish({
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
- key: 'claim-1001',
336
- payload: {
337
- claimId: 'claim-1001',
338
- source: 'claims-api',
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
- ### Dockerized app on the same network
826
+ ### SASL example
344
827
 
345
828
  ```ts
346
- import { KafkaManager } from 'sentinel-kafka-manager'
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
- const kafkaManager = KafkaManager.fromEnv({
349
- clientId: 'claims-worker',
350
- mode: 'internal',
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
- ## Publishing
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
- Build before publishing:
877
+ ## Publishing This Package
878
+
879
+ Build:
357
880
 
358
881
  ```bash
359
882
  npm run build
360
883
  ```
361
884
 
362
- Publish the package:
885
+ Publish:
363
886
 
364
887
  ```bash
365
888
  npm publish
366
889
  ```
367
890
 
368
- This package already sets `"publishConfig": { "access": "public" }`, so you do not need to pass `--access public` every time.
369
-
370
- ### Publish Troubleshooting
371
-
372
- #### `403` with 2FA or token message
891
+ If npm rejects the publish:
373
892
 
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.
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 checks:
896
+ Useful check:
377
897
 
378
898
  ```bash
379
899
  npm whoami
380
900
  ```
381
901
 
382
- #### `404` or package name unavailable
902
+ ## Notes
383
903
 
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.
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
- ## Notes
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
- - 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'`
918
+ If you have any feedback, please reach out to us at `dilipabc@gmail.com`