kafka-crab-js 3.1.0 → 4.0.0-beta.3
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 +605 -634
- package/assets/consumer-benchmark-snapshot.svg +189 -0
- package/dist/base-kafka-stream-readable-BokUK06Z.d.ts +69 -0
- package/dist/base-kafka-stream-readable-I1xda63L.d.cts +69 -0
- package/dist/channels-CYQ4ni8K.d.cts +133 -0
- package/dist/channels-QBobKRC2.d.ts +133 -0
- package/dist/diagnostics/channels.cjs +85 -0
- package/dist/diagnostics/channels.cjs.map +1 -0
- package/dist/diagnostics/channels.d.cts +2 -0
- package/dist/diagnostics/channels.d.ts +2 -138
- package/dist/diagnostics/channels.js +72 -0
- package/dist/diagnostics/channels.js.map +1 -0
- package/dist/diagnostics/index.cjs +21 -0
- package/dist/diagnostics/index.d.cts +3 -0
- package/dist/diagnostics/index.d.ts +3 -7
- package/dist/diagnostics/index.js +3 -0
- package/dist/diagnostics/instrumentation.cjs +334 -0
- package/dist/diagnostics/instrumentation.cjs.map +1 -0
- package/dist/diagnostics/instrumentation.d.cts +2 -0
- package/dist/diagnostics/instrumentation.d.ts +2 -31
- package/dist/diagnostics/instrumentation.js +329 -0
- package/dist/diagnostics/instrumentation.js.map +1 -0
- package/dist/index.cjs +47 -701
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -8
- package/dist/index.js +9 -662
- package/dist/instrumentation-BiIPAe1B.d.cts +37 -0
- package/dist/instrumentation-CwoJPHwZ.d.ts +37 -0
- package/dist/kafka-client.cjs +249 -0
- package/dist/kafka-client.cjs.map +1 -0
- package/dist/kafka-client.d.cts +107 -0
- package/dist/kafka-client.d.ts +98 -52
- package/dist/kafka-client.js +248 -0
- package/dist/kafka-client.js.map +1 -0
- package/dist/streams/base-kafka-stream-readable.cjs +109 -0
- package/dist/streams/base-kafka-stream-readable.cjs.map +1 -0
- package/dist/streams/base-kafka-stream-readable.d.cts +2 -0
- package/dist/streams/base-kafka-stream-readable.d.ts +2 -60
- package/dist/streams/base-kafka-stream-readable.js +108 -0
- package/dist/streams/base-kafka-stream-readable.js.map +1 -0
- package/dist/streams/kafka-batch-stream-readable.cjs +106 -0
- package/dist/streams/kafka-batch-stream-readable.cjs.map +1 -0
- package/dist/streams/kafka-batch-stream-readable.d.cts +46 -0
- package/dist/streams/kafka-batch-stream-readable.d.ts +40 -25
- package/dist/streams/kafka-batch-stream-readable.js +105 -0
- package/dist/streams/kafka-batch-stream-readable.js.map +1 -0
- package/dist/streams/kafka-stream-readable.cjs +63 -0
- package/dist/streams/kafka-stream-readable.cjs.map +1 -0
- package/dist/streams/kafka-stream-readable.d.cts +27 -0
- package/dist/streams/kafka-stream-readable.d.ts +22 -7
- package/dist/streams/kafka-stream-readable.js +62 -0
- package/dist/streams/kafka-stream-readable.js.map +1 -0
- package/js-binding.cjs +52 -52
- package/js-binding.d.ts +28 -0
- package/js-binding.js +52 -52
- package/package.json +54 -52
- package/dist/index.cjs.map +0 -7
- package/dist/index.js.map +0 -7
package/README.md
CHANGED
|
@@ -1,838 +1,809 @@
|
|
|
1
|
-
|
|
1
|
+
# Kafka Crab JS
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
A lightweight, flexible, and reliable Kafka client for JavaScript/TypeScript. It is built using Rust and librdkafka, providing a high-performance and feature-rich Kafka client.
|
|
3
|
+
A high-performance Kafka client for Node.js and TypeScript, implemented with Rust, NAPI-RS, librdkafka, and a small
|
|
4
|
+
JavaScript API layer.
|
|
6
5
|
|
|
7
6
|
[](https://www.npmjs.com/package/kafka-crab-js)
|
|
8
7
|
[](https://opensource.org/licenses/MIT)
|
|
9
|
-
</div>
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
## Highlights
|
|
10
|
+
|
|
11
|
+
- Native librdkafka client exposed through a TypeScript-friendly API.
|
|
12
|
+
- Producer, direct consumer, Node.js `Readable` stream, and native Web Stream consumer APIs.
|
|
13
|
+
- High-throughput batch receive APIs for workloads that can process more than one message at a time.
|
|
14
|
+
- Manual commit helpers that commit `message.offset + 1` correctly.
|
|
15
|
+
- Optional diagnostics-channel instrumentation for OpenTelemetry through `kafka-crab-js-otel`.
|
|
16
|
+
- Advanced librdkafka settings are forwarded through `configuration` without forcing a custom allowlist.
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
- Node.js `>= 22`.
|
|
21
|
+
- A Kafka broker reachable from the Node.js process.
|
|
22
|
+
- No separate librdkafka install is required for the published binaries.
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
## Table Of Contents
|
|
25
|
+
|
|
26
|
+
1. [Installation](#installation)
|
|
27
|
+
2. [Quick Start](#quick-start)
|
|
28
|
+
3. [Choosing A Consumer API](#choosing-a-consumer-api)
|
|
29
|
+
4. [Producer API](#producer-api)
|
|
30
|
+
5. [Consumer API](#consumer-api)
|
|
31
|
+
6. [Stream Consumers](#stream-consumers)
|
|
32
|
+
7. [Configuration](#configuration)
|
|
33
|
+
8. [OpenTelemetry](#opentelemetry)
|
|
34
|
+
9. [Performance Benchmarks](#performance-benchmarks)
|
|
35
|
+
10. [Migration Notes](#migration-notes)
|
|
36
|
+
11. [Troubleshooting](#troubleshooting)
|
|
37
|
+
12. [Development](#development)
|
|
38
|
+
13. [License](#license)
|
|
16
39
|
|
|
17
|
-
|
|
40
|
+
## Installation
|
|
18
41
|
|
|
19
|
-
|
|
42
|
+
```bash
|
|
43
|
+
npm install kafka-crab-js
|
|
44
|
+
```
|
|
20
45
|
|
|
21
|
-
|
|
46
|
+
```bash
|
|
47
|
+
pnpm add kafka-crab-js
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
yarn add kafka-crab-js
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
22
55
|
|
|
23
|
-
|
|
24
|
-
|
|
56
|
+
### Produce Messages
|
|
57
|
+
|
|
58
|
+
```ts
|
|
25
59
|
import { KafkaClient } from 'kafka-crab-js'
|
|
26
60
|
|
|
27
61
|
const client = new KafkaClient({
|
|
28
62
|
brokers: 'localhost:9092',
|
|
29
|
-
clientId: '
|
|
30
|
-
|
|
31
|
-
serviceName: 'my-service',
|
|
32
|
-
metrics: { enabled: true },
|
|
33
|
-
},
|
|
63
|
+
clientId: 'orders-api',
|
|
64
|
+
securityProtocol: 'Plaintext',
|
|
34
65
|
})
|
|
35
|
-
```
|
|
36
66
|
|
|
37
|
-
|
|
38
|
-
```javascript
|
|
39
|
-
import { KafkaClient } from 'kafka-crab-js'
|
|
40
|
-
import { enableOtelInstrumentation, endSpan } from 'kafka-crab-js-otel'
|
|
67
|
+
const producer = client.createProducer()
|
|
41
68
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
const metadata = await producer.send({
|
|
70
|
+
topic: 'orders',
|
|
71
|
+
messages: [
|
|
72
|
+
{
|
|
73
|
+
key: Buffer.from('order-123'),
|
|
74
|
+
payload: Buffer.from(JSON.stringify({ id: 'order-123', status: 'created' })),
|
|
75
|
+
headers: {
|
|
76
|
+
'content-type': Buffer.from('application/json'),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
46
80
|
})
|
|
47
81
|
|
|
48
|
-
|
|
82
|
+
console.log(metadata)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Consume One Message At A Time
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { KafkaClient } from 'kafka-crab-js'
|
|
89
|
+
|
|
49
90
|
const client = new KafkaClient({
|
|
50
91
|
brokers: 'localhost:9092',
|
|
51
|
-
clientId: '
|
|
52
|
-
|
|
92
|
+
clientId: 'orders-worker',
|
|
93
|
+
securityProtocol: 'Plaintext',
|
|
53
94
|
})
|
|
54
95
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
96
|
+
const consumer = client.createConsumer({
|
|
97
|
+
groupId: 'orders-worker',
|
|
98
|
+
enableAutoCommit: false,
|
|
99
|
+
configuration: {
|
|
100
|
+
'auto.offset.reset': 'earliest',
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
await consumer.subscribe('orders')
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
while (true) {
|
|
108
|
+
const message = await consumer.recv()
|
|
109
|
+
if (!message) {
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const value = JSON.parse(message.payload.toString('utf8'))
|
|
114
|
+
console.log({ value, topic: message.topic, partition: message.partition, offset: message.offset })
|
|
115
|
+
|
|
116
|
+
await consumer.commitMessage(message, 'Sync')
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
await consumer.disconnect()
|
|
120
|
+
}
|
|
59
121
|
```
|
|
60
122
|
|
|
61
|
-
|
|
123
|
+
### Consume In Batches
|
|
62
124
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
125
|
+
```ts
|
|
126
|
+
const consumer = client.createConsumer({
|
|
127
|
+
groupId: 'orders-batch-worker',
|
|
128
|
+
enableAutoCommit: false,
|
|
129
|
+
configuration: {
|
|
130
|
+
'auto.offset.reset': 'earliest',
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
await consumer.subscribe('orders')
|
|
69
135
|
|
|
70
|
-
|
|
136
|
+
try {
|
|
137
|
+
while (true) {
|
|
138
|
+
const batch = await consumer.recvBatch(500, 50)
|
|
139
|
+
if (batch.length === 0) {
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
71
142
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
143
|
+
for (const message of batch) {
|
|
144
|
+
await processOrder(message.payload)
|
|
145
|
+
await consumer.commitMessage(message, 'Async')
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
await consumer.disconnect()
|
|
150
|
+
}
|
|
151
|
+
```
|
|
76
152
|
|
|
77
|
-
|
|
78
|
-
- Uses Node.js `diagnostics_channel` for observability (zero overhead when not subscribed)
|
|
79
|
-
- Cleaner separation of concerns between core and observability
|
|
80
|
-
- Smaller bundle size for users who don't need OTEL
|
|
153
|
+
## Choosing A Consumer API
|
|
81
154
|
|
|
82
|
-
|
|
155
|
+
| API | Emits | Best For |
|
|
156
|
+
| ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ |
|
|
157
|
+
| `consumer.recv()` | `Message \| null` | Simple sequential workers and explicit control. |
|
|
158
|
+
| `consumer.recvBatch()` | `Message[]` | High-throughput workers that can process chunks. |
|
|
159
|
+
| `consumer.recvStream()` | Web `ReadableStream<Message>` | Pull-based Web Stream workflows. |
|
|
160
|
+
| `consumer.recvBatchStream()` | Web `ReadableStream<Message[]>` | Native batch streaming with explicit consumer control. |
|
|
161
|
+
| `client.createStreamConsumer()` | Node.js `Readable` emitting `Message` | Existing Node stream pipelines. |
|
|
162
|
+
| `client.createWebStreamConsumer()` | Web `ReadableStream<Message>` or `ReadableStream<Message[]>` | v4 high-throughput stream consumers. |
|
|
83
163
|
|
|
84
|
-
|
|
164
|
+
Two details matter for choosing correctly:
|
|
85
165
|
|
|
86
|
-
|
|
87
|
-
|
|
166
|
+
- `createStreamConsumer()` is a Node.js `Readable` compatibility API. Even when `batchSize > 1`, it emits individual
|
|
167
|
+
`Message` objects and uses batching internally to reduce native boundary crossings.
|
|
168
|
+
- `createWebStreamConsumer()` returns a discriminated object. In serial mode it emits `Message`; in batch mode it emits
|
|
169
|
+
`Message[]`. Use this API when your code can process whole batches directly.
|
|
88
170
|
|
|
89
|
-
|
|
171
|
+
## Producer API
|
|
90
172
|
|
|
91
|
-
|
|
92
|
-
- New convenience method that accepts a message and commit mode directly
|
|
93
|
-
- Automatically handles `offset + 1` increment internally - no more manual offset arithmetic
|
|
94
|
-
- Available on both `KafkaConsumer` and stream consumers
|
|
95
|
-
- **Before** (v2.0.0):
|
|
96
|
-
```javascript
|
|
97
|
-
const message = await consumer.recv();
|
|
98
|
-
await consumer.commit(message.topic, message.partition, message.offset + 1, 'Sync');
|
|
99
|
-
```
|
|
100
|
-
- **After** (v2.1.0):
|
|
101
|
-
```javascript
|
|
102
|
-
const message = await consumer.recv();
|
|
103
|
-
await consumer.commitMessage(message, 'Sync');
|
|
104
|
-
```
|
|
173
|
+
Create a producer from a `KafkaClient`:
|
|
105
174
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
175
|
+
```ts
|
|
176
|
+
const producer = client.createProducer({
|
|
177
|
+
autoFlush: true,
|
|
178
|
+
queueTimeout: 5000,
|
|
179
|
+
configuration: {
|
|
180
|
+
'compression.type': 'lz4',
|
|
181
|
+
acks: 'all',
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
```
|
|
111
185
|
|
|
112
|
-
|
|
113
|
-
- Updated to Node.js 24 support
|
|
114
|
-
- GitHub Actions updated to v6
|
|
115
|
-
- Improved caching with actions/cache v4
|
|
186
|
+
Send one or more messages:
|
|
116
187
|
|
|
117
|
-
|
|
188
|
+
```ts
|
|
189
|
+
const records = await producer.send({
|
|
190
|
+
topic: 'orders',
|
|
191
|
+
messages: [
|
|
192
|
+
{ key: Buffer.from('1'), payload: Buffer.from('first') },
|
|
193
|
+
{ key: Buffer.from('2'), payload: Buffer.from('second') },
|
|
194
|
+
],
|
|
195
|
+
})
|
|
196
|
+
```
|
|
118
197
|
|
|
119
|
-
|
|
120
|
-
|
|
198
|
+
When `autoFlush` is enabled, `send()` waits for delivery confirmation and returns `RecordMetadata[]`. When `autoFlush`
|
|
199
|
+
is disabled, `send()` buffers messages and `flush()` sends pending messages:
|
|
121
200
|
|
|
122
|
-
|
|
201
|
+
```ts
|
|
202
|
+
const producer = client.createProducer({ autoFlush: false })
|
|
123
203
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
204
|
+
await producer.send({
|
|
205
|
+
topic: 'orders',
|
|
206
|
+
messages: [{ payload: Buffer.from('buffered') }],
|
|
207
|
+
})
|
|
127
208
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
209
|
+
const records = await producer.flush()
|
|
210
|
+
console.log(records)
|
|
211
|
+
```
|
|
131
212
|
|
|
132
|
-
|
|
133
|
-
- **BREAKING**: The `consumer.commit()` method is now async and must be awaited
|
|
213
|
+
`producer.inFlightCount()` returns the number of messages sent but not yet acknowledged.
|
|
134
214
|
|
|
135
|
-
|
|
215
|
+
## Consumer API
|
|
136
216
|
|
|
137
|
-
|
|
217
|
+
### Subscribe
|
|
138
218
|
|
|
139
|
-
|
|
219
|
+
The simplest subscription accepts a topic string:
|
|
140
220
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
- 🎯 Type-safe interfaces (TypeScript support)
|
|
145
|
-
- ⚡ Async/await support
|
|
146
|
-
- 🛠️ Configurable consumer and producer options
|
|
147
|
-
- 📊 Stream processing support with configurable stream options
|
|
148
|
-
- 📦 Message batching capabilities
|
|
149
|
-
- 🔍 Comprehensive error handling
|
|
150
|
-
- 📈 Performance benchmarking suite
|
|
151
|
-
- 🔧 Flexible configuration system supporting complex data types
|
|
221
|
+
```ts
|
|
222
|
+
await consumer.subscribe('orders')
|
|
223
|
+
```
|
|
152
224
|
|
|
153
|
-
|
|
225
|
+
Use `TopicPartitionConfig[]` when you need offsets, partition-specific offsets, or topic creation:
|
|
154
226
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
227
|
+
```ts
|
|
228
|
+
await consumer.subscribe([
|
|
229
|
+
{
|
|
230
|
+
topic: 'orders',
|
|
231
|
+
createTopic: true,
|
|
232
|
+
numPartitions: 3,
|
|
233
|
+
replicas: 1,
|
|
234
|
+
allOffsets: { position: 'Beginning' },
|
|
235
|
+
},
|
|
236
|
+
])
|
|
237
|
+
```
|
|
166
238
|
|
|
167
|
-
|
|
239
|
+
For partition-specific offsets:
|
|
168
240
|
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
241
|
+
```ts
|
|
242
|
+
await consumer.subscribe([
|
|
243
|
+
{
|
|
244
|
+
topic: 'orders',
|
|
245
|
+
partitionOffset: [
|
|
246
|
+
{
|
|
247
|
+
partition: 0,
|
|
248
|
+
offset: { offset: 42 },
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
])
|
|
175
253
|
```
|
|
176
254
|
|
|
177
|
-
|
|
255
|
+
### Receive
|
|
178
256
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
```javascript
|
|
182
|
-
import { KafkaClient } from 'kafka-crab-js';
|
|
183
|
-
async function run() {
|
|
184
|
-
const kafkaClient = new KafkaClient({
|
|
185
|
-
brokers: 'localhost:29092',
|
|
186
|
-
clientId: 'foo-client',
|
|
187
|
-
logLevel: 'debug',
|
|
188
|
-
brokerAddressFamily: 'v4',
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// Create consumer with topic creation control
|
|
192
|
-
const consumer = kafkaClient.createConsumer({
|
|
193
|
-
groupId: 'foo-group',
|
|
194
|
-
configuration: {
|
|
195
|
-
'auto.offset.reset': 'earliest',
|
|
196
|
-
'enable.auto.commit': false, // Use manual commit for better control
|
|
197
|
-
}
|
|
198
|
-
});
|
|
257
|
+
`recv()` waits for one message and returns `null` when the consumer is disconnected:
|
|
199
258
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
numPartitions: 3
|
|
205
|
-
}]);
|
|
206
|
-
|
|
207
|
-
const message = await consumer.recv();
|
|
208
|
-
const { payload, partition, offset, topic } = message;
|
|
209
|
-
console.log({
|
|
210
|
-
topic,
|
|
211
|
-
partition,
|
|
212
|
-
offset,
|
|
213
|
-
value: payload.toString()
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Manual commit - two options:
|
|
217
|
-
// Option 1 (v2.1.0+): Simplified with commitMessage
|
|
218
|
-
await consumer.commitMessage(message, 'Sync');
|
|
219
|
-
|
|
220
|
-
// Option 2: Traditional commit with manual offset increment
|
|
221
|
-
// await consumer.commit(topic, partition, offset + 1, 'Sync');
|
|
222
|
-
|
|
223
|
-
consumer.unsubscribe();
|
|
259
|
+
```ts
|
|
260
|
+
const message = await consumer.recv()
|
|
261
|
+
if (message) {
|
|
262
|
+
await handleMessage(message)
|
|
224
263
|
}
|
|
264
|
+
```
|
|
225
265
|
|
|
226
|
-
|
|
266
|
+
`recvBatch(size, timeoutMs)` returns up to `size` messages:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const messages = await consumer.recvBatch(1000, 100)
|
|
227
270
|
```
|
|
228
271
|
|
|
229
|
-
|
|
272
|
+
An empty array means the timeout elapsed without a full batch. It does not mean the consumer is closed.
|
|
230
273
|
|
|
231
|
-
|
|
232
|
-
import { KafkaClient } from 'kafka-crab-js';
|
|
274
|
+
### Commit
|
|
233
275
|
|
|
234
|
-
|
|
235
|
-
brokers: 'localhost:29092',
|
|
236
|
-
clientId: 'my-client-id',
|
|
237
|
-
logLevel: 'info',
|
|
238
|
-
brokerAddressFamily: 'v4',
|
|
239
|
-
});
|
|
276
|
+
Use `commitMessage()` for normal message processing. It commits `message.offset + 1`, which is the offset Kafka expects:
|
|
240
277
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
configuration: {
|
|
244
|
-
'message.timeout.ms': 5000, // Now supports number values
|
|
245
|
-
'batch.size': 16384,
|
|
246
|
-
'compression.type': 'snappy'
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
const message = {
|
|
251
|
-
id: 1,
|
|
252
|
-
name: "Sample Message",
|
|
253
|
-
timestamp: new Date().toISOString()
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const result = await producer.send({
|
|
257
|
-
topic: 'my-topic',
|
|
258
|
-
messages: [{
|
|
259
|
-
payload: Buffer.from(JSON.stringify(message))
|
|
260
|
-
}]
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
const errors = result.map(r => r.error).filter(Boolean);
|
|
264
|
-
if (errors.length > 0) {
|
|
265
|
-
console.error('Error sending message:', errors);
|
|
266
|
-
} else {
|
|
267
|
-
console.log('Message sent. Offset:', result);
|
|
268
|
-
}
|
|
278
|
+
```ts
|
|
279
|
+
await consumer.commitMessage(message, 'Sync')
|
|
269
280
|
```
|
|
270
281
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
### Enhanced Stream Consumer Example
|
|
282
|
+
Use `commit()` when you already calculated the offset:
|
|
274
283
|
|
|
275
|
-
```
|
|
276
|
-
|
|
284
|
+
```ts
|
|
285
|
+
await consumer.commit(message.topic, message.partition, message.offset + 1, 'Async')
|
|
286
|
+
```
|
|
277
287
|
|
|
278
|
-
|
|
279
|
-
brokers: 'localhost:29092',
|
|
280
|
-
clientId: 'my-client-id',
|
|
281
|
-
logLevel: 'info',
|
|
282
|
-
brokerAddressFamily: 'v4',
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// Stream consumer with custom ReadableOptions (v2.0.0+)
|
|
286
|
-
const kafkaStream = kafkaClient.createStreamConsumer({
|
|
287
|
-
groupId: `my-group-id`,
|
|
288
|
-
enableAutoCommit: true,
|
|
289
|
-
}, {
|
|
290
|
-
objectMode: true, // Default in v2.0.0+
|
|
291
|
-
highWaterMark: 1024,
|
|
292
|
-
encoding: null
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
await kafkaStream.subscribe([
|
|
296
|
-
{ topic: 'foo', createTopic: true },
|
|
297
|
-
{ topic: 'bar', createTopic: true }
|
|
298
|
-
]);
|
|
299
|
-
|
|
300
|
-
kafkaStream.on('data', (message) => {
|
|
301
|
-
console.log('>>> Message received:', {
|
|
302
|
-
payload: message.payload.toString(),
|
|
303
|
-
offset: message.offset,
|
|
304
|
-
partition: message.partition,
|
|
305
|
-
topic: message.topic
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
if (message.offset > 10) {
|
|
309
|
-
kafkaStream.destroy();
|
|
310
|
-
}
|
|
311
|
-
});
|
|
288
|
+
Commit modes are string literal values:
|
|
312
289
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.log('Stream ended');
|
|
316
|
-
});
|
|
290
|
+
```ts
|
|
291
|
+
type CommitMode = 'Sync' | 'Async'
|
|
317
292
|
```
|
|
318
293
|
|
|
319
|
-
|
|
294
|
+
### Pause, Resume, Seek, And Assignment
|
|
320
295
|
|
|
321
|
-
|
|
296
|
+
```ts
|
|
297
|
+
consumer.pause()
|
|
298
|
+
consumer.resume()
|
|
322
299
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
brokers: 'localhost:29092',
|
|
326
|
-
clientId: 'my-client-id',
|
|
327
|
-
brokerAddressFamily: 'v4',
|
|
328
|
-
});
|
|
300
|
+
consumer.seek('orders', 0, { position: 'Beginning' })
|
|
301
|
+
consumer.seek('orders', 0, { offset: 42 })
|
|
329
302
|
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
303
|
+
const assignment = consumer.assignment()
|
|
304
|
+
const subscription = consumer.getSubscription()
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Consumer events are available through `onEvents()`:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
consumer.onEvents((error, event) => {
|
|
311
|
+
if (error) {
|
|
312
|
+
console.error(error)
|
|
313
|
+
return
|
|
337
314
|
}
|
|
338
|
-
});
|
|
339
315
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}))
|
|
346
|
-
}));
|
|
316
|
+
console.log(event.name, event.payload)
|
|
317
|
+
})
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Cleanup
|
|
347
321
|
|
|
322
|
+
Always disconnect direct consumers:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
348
325
|
try {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
console.log('Batch sent. Offset:', result);
|
|
354
|
-
console.assert(result.length === 100);
|
|
355
|
-
} catch (error) {
|
|
356
|
-
console.error('Batch error:', error);
|
|
326
|
+
await consumer.subscribe('orders')
|
|
327
|
+
// consume...
|
|
328
|
+
} finally {
|
|
329
|
+
await consumer.disconnect()
|
|
357
330
|
}
|
|
358
331
|
```
|
|
359
332
|
|
|
360
|
-
|
|
333
|
+
## Stream Consumers
|
|
361
334
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
335
|
+
### Node.js Readable Stream
|
|
336
|
+
|
|
337
|
+
Use `createStreamConsumer()` when integrating with Node.js stream tooling. It emits `Message` objects.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
const stream = client.createStreamConsumer({
|
|
341
|
+
groupId: 'orders-stream',
|
|
342
|
+
enableAutoCommit: false,
|
|
343
|
+
batchSize: 256,
|
|
344
|
+
batchTimeout: 50,
|
|
345
|
+
configuration: {
|
|
346
|
+
'auto.offset.reset': 'earliest',
|
|
347
|
+
},
|
|
348
|
+
})
|
|
371
349
|
|
|
350
|
+
await stream.subscribe('orders')
|
|
351
|
+
|
|
352
|
+
stream.on('data', async (message) => {
|
|
372
353
|
try {
|
|
373
|
-
await
|
|
374
|
-
|
|
375
|
-
messages: [{
|
|
376
|
-
key: 'user-123',
|
|
377
|
-
payload: Buffer.from(JSON.stringify({
|
|
378
|
-
userId: 123,
|
|
379
|
-
action: 'update'
|
|
380
|
-
})),
|
|
381
|
-
headers: {
|
|
382
|
-
'correlation-id': 'txn-123',
|
|
383
|
-
'source': 'user-service'
|
|
384
|
-
}
|
|
385
|
-
}]
|
|
386
|
-
});
|
|
354
|
+
await processOrder(message.payload)
|
|
355
|
+
await stream.commitMessage(message, 'Async')
|
|
387
356
|
} catch (error) {
|
|
388
|
-
|
|
357
|
+
stream.destroy(error instanceof Error ? error : new Error(String(error)))
|
|
389
358
|
}
|
|
390
|
-
}
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
stream.on('error', (error) => {
|
|
362
|
+
console.error('stream error', error)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
stream.on('close', () => {
|
|
366
|
+
console.log('stream closed')
|
|
367
|
+
})
|
|
391
368
|
```
|
|
392
369
|
|
|
393
|
-
|
|
370
|
+
Destroy stream consumers instead of calling `disconnect()` directly. The stream cleanup path cancels the source reader,
|
|
371
|
+
unsubscribes, and disconnects the native consumer:
|
|
394
372
|
|
|
395
|
-
```
|
|
396
|
-
|
|
373
|
+
```ts
|
|
374
|
+
stream.destroy()
|
|
375
|
+
```
|
|
397
376
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
377
|
+
### Native Web Stream
|
|
378
|
+
|
|
379
|
+
Use `createWebStreamConsumer()` when you want a Web Stream and clear serial/batch typing.
|
|
380
|
+
|
|
381
|
+
Serial mode is selected when `batchSize` is omitted, `0`, or `1`:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
const webConsumer = client.createWebStreamConsumer({
|
|
385
|
+
groupId: 'orders-web-serial',
|
|
386
|
+
serialPrefetchSize: 64,
|
|
387
|
+
serialPrefetchTimeout: 1,
|
|
403
388
|
configuration: {
|
|
404
389
|
'auto.offset.reset': 'earliest',
|
|
405
|
-
'session.timeout.ms': 30000,
|
|
406
|
-
'heartbeat.interval.ms': 10000
|
|
407
390
|
},
|
|
408
391
|
})
|
|
409
392
|
|
|
410
|
-
|
|
411
|
-
* Creates and configures a new Kafka stream consumer
|
|
412
|
-
*/
|
|
413
|
-
async function createConsumer() {
|
|
414
|
-
const kafkaStream = kafkaClient.createStreamConsumer({
|
|
415
|
-
groupId: 'reconnect-test',
|
|
416
|
-
enableAutoCommit: true,
|
|
417
|
-
}, {
|
|
418
|
-
highWaterMark: 100,
|
|
419
|
-
objectMode: true
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
await kafkaStream.subscribe([
|
|
423
|
-
{ topic: 'foo', createTopic: true },
|
|
424
|
-
{ topic: 'bar', createTopic: true },
|
|
425
|
-
])
|
|
426
|
-
return kafkaStream
|
|
427
|
-
}
|
|
393
|
+
await webConsumer.consumer.subscribe('orders')
|
|
428
394
|
|
|
429
|
-
|
|
430
|
-
* Starts a Kafka consumer with auto-restart capability
|
|
431
|
-
*/
|
|
432
|
-
async function startConsumer() {
|
|
433
|
-
let counter = 0
|
|
434
|
-
let retryCount = 0
|
|
435
|
-
const MAX_RETRIES = 5
|
|
436
|
-
const RETRY_DELAY = 5000 // 5 seconds
|
|
437
|
-
|
|
438
|
-
async function handleRetry() {
|
|
439
|
-
if (retryCount < MAX_RETRIES) {
|
|
440
|
-
retryCount++
|
|
441
|
-
console.log(
|
|
442
|
-
`Attempting to restart consumer (attempt ${retryCount}/${MAX_RETRIES}) in ${RETRY_DELAY / 1000} seconds...`,
|
|
443
|
-
)
|
|
444
|
-
setTimeout(setupConsumerWithRetry, RETRY_DELAY)
|
|
445
|
-
} else {
|
|
446
|
-
console.error(`Maximum retry attempts (${MAX_RETRIES}) reached. Stopping consumer.`)
|
|
447
|
-
process.exit(1)
|
|
448
|
-
}
|
|
449
|
-
}
|
|
395
|
+
const reader = webConsumer.stream.getReader()
|
|
450
396
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
console.log('Starting consumer')
|
|
457
|
-
|
|
458
|
-
kafkaStream.on('data', (message) => {
|
|
459
|
-
counter++
|
|
460
|
-
console.log('>>> Message received:', {
|
|
461
|
-
counter,
|
|
462
|
-
payload: message.payload.toString(),
|
|
463
|
-
offset: message.offset,
|
|
464
|
-
partition: message.partition,
|
|
465
|
-
topic: message.topic,
|
|
466
|
-
})
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
kafkaStream.on('error', async (error) => {
|
|
470
|
-
console.error('Stream error:', error)
|
|
471
|
-
handleRetry()
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
kafkaStream.on('close', () => {
|
|
475
|
-
console.log('Stream ended')
|
|
476
|
-
try {
|
|
477
|
-
kafkaStream.unsubscribe()
|
|
478
|
-
} catch (unsubError) {
|
|
479
|
-
console.error('Error unsubscribing:', unsubError)
|
|
480
|
-
}
|
|
481
|
-
})
|
|
482
|
-
} catch (error) {
|
|
483
|
-
console.error('Error setting up consumer:', error)
|
|
484
|
-
handleRetry()
|
|
485
|
-
}
|
|
397
|
+
try {
|
|
398
|
+
const { value: message, done } = await reader.read()
|
|
399
|
+
if (!done && message) {
|
|
400
|
+
await processOrder(message.payload)
|
|
401
|
+
await webConsumer.consumer.commitMessage(message, 'Async')
|
|
486
402
|
}
|
|
487
|
-
|
|
488
|
-
await
|
|
403
|
+
} finally {
|
|
404
|
+
await reader.cancel()
|
|
405
|
+
await webConsumer.consumer.disconnect()
|
|
489
406
|
}
|
|
490
|
-
|
|
491
|
-
await startConsumer()
|
|
492
407
|
```
|
|
493
408
|
|
|
494
|
-
|
|
409
|
+
Batch mode is selected with `batchSize > 1` and emits `Message[]` chunks:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
const webConsumer = client.createWebStreamConsumer({
|
|
413
|
+
groupId: 'orders-web-batch',
|
|
414
|
+
batchSize: 1024,
|
|
415
|
+
batchTimeout: 10,
|
|
416
|
+
configuration: {
|
|
417
|
+
'auto.offset.reset': 'earliest',
|
|
418
|
+
},
|
|
419
|
+
})
|
|
495
420
|
|
|
496
|
-
|
|
421
|
+
if (webConsumer.mode === 'batch') {
|
|
422
|
+
await webConsumer.consumer.subscribe('orders')
|
|
423
|
+
const reader = webConsumer.stream.getReader()
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const { value: batch, done } = await reader.read()
|
|
427
|
+
if (!done && batch) {
|
|
428
|
+
for (const message of batch) {
|
|
429
|
+
await processOrder(message.payload)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} finally {
|
|
433
|
+
await reader.cancel()
|
|
434
|
+
await webConsumer.consumer.disconnect()
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
```
|
|
497
438
|
|
|
498
439
|
## Configuration
|
|
499
440
|
|
|
500
|
-
###
|
|
441
|
+
### KafkaClient
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
const client = new KafkaClient({
|
|
445
|
+
brokers: 'localhost:9092',
|
|
446
|
+
clientId: 'orders-service',
|
|
447
|
+
securityProtocol: 'Plaintext',
|
|
448
|
+
logLevel: 'info',
|
|
449
|
+
brokerAddressFamily: 'v4',
|
|
450
|
+
diagnostics: true,
|
|
451
|
+
configuration: {
|
|
452
|
+
'socket.keepalive.enable': true,
|
|
453
|
+
},
|
|
454
|
+
})
|
|
455
|
+
```
|
|
501
456
|
|
|
502
|
-
|
|
|
503
|
-
|
|
|
504
|
-
| `brokers`
|
|
505
|
-
| `clientId`
|
|
506
|
-
| `securityProtocol`
|
|
507
|
-
| `logLevel`
|
|
508
|
-
| `brokerAddressFamily` | `string`
|
|
509
|
-
| `
|
|
510
|
-
| `
|
|
457
|
+
| Option | Type | Default | Description |
|
|
458
|
+
| --------------------- | ------------------------------------------------------ | --------- | ------------------------------------------------------------------------- |
|
|
459
|
+
| `brokers` | `string` | required | Comma-separated broker list, for example `localhost:9092,localhost:9093`. |
|
|
460
|
+
| `clientId` | `string` | `rdkafka` | Client identifier sent to Kafka. |
|
|
461
|
+
| `securityProtocol` | `'Plaintext' \| 'Ssl' \| 'SaslPlaintext' \| 'SaslSsl'` | | Security protocol. |
|
|
462
|
+
| `logLevel` | `string` | | librdkafka log level. |
|
|
463
|
+
| `brokerAddressFamily` | `string` | | Address family hint such as `v4`. |
|
|
464
|
+
| `diagnostics` | `boolean` | `true` | Enables diagnostic-channel events used by `kafka-crab-js-otel`. |
|
|
465
|
+
| `configuration` | `Record<string, any>` | | Additional librdkafka client settings. |
|
|
511
466
|
|
|
512
467
|
### ConsumerConfiguration
|
|
513
468
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
469
|
+
```ts
|
|
470
|
+
const consumer = client.createConsumer({
|
|
471
|
+
groupId: 'orders-worker',
|
|
472
|
+
enableAutoCommit: false,
|
|
473
|
+
fetchMetadataTimeout: 5000,
|
|
474
|
+
configuration: {
|
|
475
|
+
'auto.offset.reset': 'earliest',
|
|
476
|
+
'enable.auto.commit': false,
|
|
477
|
+
'fetch.min.bytes': 1,
|
|
478
|
+
},
|
|
479
|
+
})
|
|
480
|
+
```
|
|
521
481
|
|
|
522
|
-
|
|
482
|
+
| Option | Type | Description |
|
|
483
|
+
| ---------------------- | --------------------- | ------------------------------------------ |
|
|
484
|
+
| `groupId` | `string` | Kafka consumer group id. |
|
|
485
|
+
| `enableAutoCommit` | `boolean` | Convenience flag for auto commit behavior. |
|
|
486
|
+
| `fetchMetadataTimeout` | `number` | Metadata fetch timeout in milliseconds. |
|
|
487
|
+
| `configuration` | `Record<string, any>` | Additional librdkafka consumer settings. |
|
|
523
488
|
|
|
524
|
-
|
|
489
|
+
### ProducerConfiguration
|
|
525
490
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
491
|
+
```ts
|
|
492
|
+
const producer = client.createProducer({
|
|
493
|
+
autoFlush: true,
|
|
494
|
+
queueTimeout: 5000,
|
|
495
|
+
configuration: {
|
|
496
|
+
'compression.type': 'lz4',
|
|
497
|
+
},
|
|
498
|
+
})
|
|
499
|
+
```
|
|
530
500
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
501
|
+
| Option | Type | Description |
|
|
502
|
+
| --------------- | --------------------- | ------------------------------------------------------- |
|
|
503
|
+
| `autoFlush` | `boolean` | When enabled, `send()` waits for delivery confirmation. |
|
|
504
|
+
| `queueTimeout` | `number` | Queue timeout in milliseconds. |
|
|
505
|
+
| `configuration` | `Record<string, any>` | Additional librdkafka producer settings. |
|
|
535
506
|
|
|
536
|
-
|
|
537
|
-
const message = await consumer.recv();
|
|
538
|
-
await consumer.commit(message.topic, message.partition, message.offset + 1, 'Sync');
|
|
539
|
-
```
|
|
507
|
+
### TopicPartitionConfig
|
|
540
508
|
|
|
541
|
-
|
|
509
|
+
| Option | Type | Description |
|
|
510
|
+
| ----------------- | ------------------- | ------------------------------------------------ |
|
|
511
|
+
| `topic` | `string` | Topic name. |
|
|
512
|
+
| `allOffsets` | `OffsetModel` | Offset model applied to all assigned partitions. |
|
|
513
|
+
| `partitionOffset` | `PartitionOffset[]` | Partition-specific offset model. |
|
|
514
|
+
| `createTopic` | `boolean` | Create the topic before subscribing. |
|
|
515
|
+
| `numPartitions` | `number` | Partition count when creating a topic. |
|
|
516
|
+
| `replicas` | `number` | Replica count when creating a topic. |
|
|
542
517
|
|
|
543
|
-
|
|
518
|
+
Offset models:
|
|
544
519
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
520
|
+
```ts
|
|
521
|
+
{
|
|
522
|
+
position: 'Beginning'
|
|
523
|
+
}
|
|
524
|
+
{
|
|
525
|
+
position: 'End'
|
|
526
|
+
}
|
|
527
|
+
{
|
|
528
|
+
position: 'Stored'
|
|
529
|
+
}
|
|
530
|
+
{
|
|
531
|
+
position: 'Invalid'
|
|
532
|
+
}
|
|
533
|
+
{
|
|
534
|
+
offset: 42
|
|
535
|
+
}
|
|
536
|
+
```
|
|
550
537
|
|
|
551
|
-
###
|
|
538
|
+
### Type-Only Literals
|
|
539
|
+
|
|
540
|
+
In v4, these names are TypeScript-only exports:
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
import type { CommitMode, KafkaEventName, PartitionPosition, SecurityProtocol } from 'kafka-crab-js'
|
|
544
|
+
```
|
|
552
545
|
|
|
553
|
-
|
|
554
|
-
| --- | --- | --- | --- |
|
|
555
|
-
| `topic` | `string` || Topic name |
|
|
556
|
-
| `allOffsets` | `OffsetModel` || Offset configuration for all partitions |
|
|
557
|
-
| `partitionOffset` | `Array<PartitionOffset>` || Per-partition offset configuration |
|
|
558
|
-
| `createTopic` | `boolean` | `false` | **v2.0.0+**: Create topic if it doesn't exist |
|
|
559
|
-
| `numPartitions` | `number` | `1` | **v2.0.0+**: Number of partitions when creating topic |
|
|
560
|
-
| `replicas` | `number` | `1` | **v2.0.0+**: Number of replicas when creating topic |
|
|
546
|
+
Use literal values at runtime:
|
|
561
547
|
|
|
562
|
-
|
|
548
|
+
```ts
|
|
549
|
+
const commitMode: CommitMode = 'Sync'
|
|
550
|
+
const securityProtocol: SecurityProtocol = 'Plaintext'
|
|
551
|
+
```
|
|
563
552
|
|
|
564
|
-
|
|
553
|
+
## OpenTelemetry
|
|
565
554
|
|
|
566
|
-
|
|
555
|
+
OpenTelemetry support lives in the separate `kafka-crab-js-otel` package. The core package emits diagnostic-channel
|
|
556
|
+
events when `diagnostics !== false`; the OTEL package subscribes to those events.
|
|
567
557
|
|
|
568
|
-
|
|
558
|
+
Install the instrumentation package and OpenTelemetry dependencies:
|
|
569
559
|
|
|
570
560
|
```bash
|
|
571
|
-
npm install kafka-crab-js-otel @opentelemetry/api
|
|
561
|
+
npm install kafka-crab-js-otel @opentelemetry/api @opentelemetry/sdk-node
|
|
572
562
|
```
|
|
573
563
|
|
|
574
|
-
|
|
564
|
+
Enable instrumentation before creating `KafkaClient`:
|
|
575
565
|
|
|
576
|
-
```
|
|
566
|
+
```ts
|
|
577
567
|
import { KafkaClient } from 'kafka-crab-js'
|
|
578
568
|
import { enableOtelInstrumentation, endSpan } from 'kafka-crab-js-otel'
|
|
579
569
|
|
|
580
|
-
// Enable instrumentation before creating client
|
|
581
|
-
// Note: serviceName is configured via OTEL SDK Resource
|
|
582
570
|
enableOtelInstrumentation({
|
|
583
571
|
metrics: { enabled: true },
|
|
584
572
|
})
|
|
585
573
|
|
|
586
|
-
// Create client with diagnostics enabled
|
|
587
574
|
const client = new KafkaClient({
|
|
588
575
|
brokers: 'localhost:9092',
|
|
589
|
-
clientId: '
|
|
576
|
+
clientId: 'orders-worker',
|
|
590
577
|
diagnostics: true,
|
|
591
578
|
})
|
|
592
579
|
|
|
593
|
-
|
|
580
|
+
const consumer = client.createConsumer({ groupId: 'orders-worker' })
|
|
581
|
+
await consumer.subscribe('orders')
|
|
582
|
+
|
|
594
583
|
const message = await consumer.recv()
|
|
595
|
-
|
|
596
|
-
|
|
584
|
+
if (message) {
|
|
585
|
+
try {
|
|
586
|
+
await processOrder(message.payload)
|
|
587
|
+
} finally {
|
|
588
|
+
endSpan(message)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
597
591
|
```
|
|
598
592
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
#### Examples
|
|
602
|
-
|
|
603
|
-
See comprehensive examples in the `example/` directory:
|
|
604
|
-
- `example/otel-tracing-example.mjs` - Complete tracing setup with Jaeger
|
|
605
|
-
- `example/otel-metrics-example.mjs` - Metrics collection with Prometheus
|
|
606
|
-
- `example/README.md` - Full documentation for all examples
|
|
593
|
+
See the [OpenTelemetry package README](../otel/README.md) for full tracing and metrics setup.
|
|
607
594
|
|
|
608
595
|
## Performance Benchmarks
|
|
609
596
|
|
|
610
|
-
|
|
597
|
+
The repository includes a benchmark suite that compares `kafka-crab-js` with KafkaJS and `@platformatic/kafka`.
|
|
611
598
|
|
|
612
|
-
|
|
599
|
+
From the repository root:
|
|
613
600
|
|
|
614
601
|
```bash
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
602
|
+
cd packages/benchmark
|
|
603
|
+
|
|
604
|
+
# Set up benchmark data. Requires Kafka running locally.
|
|
605
|
+
vp install
|
|
606
|
+
vp run setup:consumer
|
|
618
607
|
|
|
619
|
-
# Run
|
|
620
|
-
|
|
608
|
+
# Run the default isolated-process memory benchmark.
|
|
609
|
+
vp run benchmark
|
|
621
610
|
```
|
|
622
611
|
|
|
623
|
-
|
|
612
|
+
The default benchmark runs one child Node.js process per selected scenario. This avoids carrying V8 heap pages, native
|
|
613
|
+
allocator arenas, sockets, librdkafka state, and Kafka metadata from one client into the next client's measurement.
|
|
624
614
|
|
|
625
|
-
|
|
615
|
+
### Consumer Benchmark Snapshot
|
|
626
616
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
║ node-rdkafka (evented) │ 84115 │ 24922.67 op/sec │ ± 74.82 % │ + 2887.91 % ║
|
|
633
|
-
║ kafka-crab-js (serial) │ 50000 │ 43213.86 op/sec │ ± 3.46 % │ + 73.39 % ║
|
|
634
|
-
║ node-rdkafka (stream) │ 50000 │ 49805.32 op/sec │ ± 27.10 % │ + 15.25 % ║
|
|
635
|
-
╟────────────────────────┼─────────┼──────────────────┼───────────┼──────────────────────────╢
|
|
636
|
-
║ Fastest test │ Samples │ Result │ Tolerance │ Difference with previous ║
|
|
637
|
-
╟────────────────────────┼─────────┼──────────────────┼───────────┼──────────────────────────╢
|
|
638
|
-
║ kafka-crab-js (batch) │ 50000 │ 205985.31 op/sec │ ± 16.53 % │ + 313.58 % ║
|
|
639
|
-
╚════════════════════════╧═════════╧══════════════════╧═══════════╧══════════════════════════╝
|
|
617
|
+
The captured run below is equivalent to:
|
|
618
|
+
|
|
619
|
+
```bash
|
|
620
|
+
cd packages/benchmark
|
|
621
|
+
BENCHMARK_BATCH_SIZE=300000 BENCHMARK_BATCH_TIMEOUT_MS=2 vp run benchmark
|
|
640
622
|
```
|
|
641
623
|
|
|
642
|
-
|
|
643
|
-
- **kafka-crab-js (serial)**: Single message processing - **43,214 ops/sec**
|
|
644
|
-
- **kafka-crab-js (batch)**: Batch message processing - **205,985 ops/sec** (fastest)
|
|
645
|
-
- **node-rdkafka (evented)**: Event-based processing - **24,923 ops/sec**
|
|
646
|
-
- **node-rdkafka (stream)**: Stream-based processing - **49,805 ops/sec**
|
|
647
|
-
- **kafkajs**: Official KafkaJS client - **834 ops/sec**
|
|
624
|
+
Batch scenarios were normalized to the comparable effective batch size of `16384`.
|
|
648
625
|
|
|
649
|
-
|
|
650
|
-
- **52x faster than kafkajs** in serial mode, **247x faster in batch mode**
|
|
651
|
-
- **High throughput**: Batch processing provides 4.8x performance improvement over serial mode
|
|
652
|
-
- **Low latency**: Optimized for both single and batch message processing
|
|
653
|
-
- **Memory efficient**: Lock-free data structures minimize memory overhead
|
|
654
|
-
- **Concurrent processing**: Zero-contention concurrent operations
|
|
626
|
+

|
|
655
627
|
|
|
656
|
-
|
|
628
|
+
#### Throughput
|
|
657
629
|
|
|
658
|
-
|
|
630
|
+
| Rank | Scenario | Result | Relative |
|
|
631
|
+
| ---: | ----------------------------------- | --------------------: | -------: |
|
|
632
|
+
| 1 | `kafka-crab-js v4 (stream, batch)` | `1,089,284.97 op/sec` | `100.0%` |
|
|
633
|
+
| 2 | `@platformatic/kafka` | `732,207.27 op/sec` | `67.2%` |
|
|
634
|
+
| 3 | `KafkaJS (eachBatch)` | `676,576.98 op/sec` | `62.1%` |
|
|
635
|
+
| 4 | `kafka-crab-js v4 (stream, serial)` | `544,658.33 op/sec` | `50.0%` |
|
|
636
|
+
| 5 | `KafkaJS (eachMessage, concurrent)` | `509,082.43 op/sec` | `46.7%` |
|
|
637
|
+
| 6 | `KafkaJS (eachMessage)` | `505,815.57 op/sec` | `46.4%` |
|
|
659
638
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
639
|
+
`kafka-crab-js v4 (stream, batch)` is the fastest scenario in this run. It is about `49%` faster than
|
|
640
|
+
`@platformatic/kafka` and about `61%` faster than `KafkaJS (eachBatch)`. Its tolerance is higher than the other top
|
|
641
|
+
rows (`+/- 9.15%`), so the exact gap should be treated as a benchmark snapshot rather than a universal constant.
|
|
663
642
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
```
|
|
643
|
+
For message-by-message consumption, `kafka-crab-js v4 (stream, serial)` is close to KafkaJS throughput while using much
|
|
644
|
+
less lifecycle memory. In this run it is about `7.7%` faster than `KafkaJS (eachMessage)` and about `7.0%` faster than
|
|
645
|
+
the concurrent KafkaJS `eachMessage` scenario.
|
|
668
646
|
|
|
669
|
-
|
|
647
|
+
#### Memory And GC
|
|
670
648
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
-
|
|
674
|
-
-
|
|
649
|
+
| Scenario | RSS delta | Peak heap | GC time | GC share | Notes |
|
|
650
|
+
| ----------------------------------- | ----------: | ---------: | ---------: | -------: | ----------------------------------------------- |
|
|
651
|
+
| `kafka-crab-js v4 (stream, serial)` | `72.3 MiB` | `11.7 MiB` | `36.75 ms` | `4.00%` | Best memory efficiency. |
|
|
652
|
+
| `kafka-crab-js v4 (stream, batch)` | `167.0 MiB` | `35.9 MiB` | `30.60 ms` | `6.67%` | Best throughput and lowest GC time. |
|
|
653
|
+
| `KafkaJS (eachBatch)` | `193.1 MiB` | `70.0 MiB` | `63.03 ms` | `8.53%` | Strong batch baseline. |
|
|
654
|
+
| `@platformatic/kafka` | `217.6 MiB` | `93.7 MiB` | `73.62 ms` | `10.78%` | Fastest non-crab competitor. |
|
|
655
|
+
| `KafkaJS (eachMessage)` | `181.5 MiB` | `77.0 MiB` | `79.11 ms` | `8.00%` | Similar throughput to v4 serial, higher memory. |
|
|
656
|
+
| `KafkaJS (eachMessage, concurrent)` | `188.2 MiB` | `75.6 MiB` | `77.17 ms` | `7.86%` | Concurrency did not improve this workload. |
|
|
675
657
|
|
|
676
|
-
|
|
677
|
-
-
|
|
678
|
-
- Configure appropriate batch sizes and compression settings
|
|
679
|
-
- Monitor and tune consumer group performance
|
|
680
|
-
- Leverage the benchmark suite to optimize your specific use case
|
|
658
|
+
Lifecycle memory includes module loading, client creation, subscription, consumption, cleanup, and retained RSS after
|
|
659
|
+
cleanup. GC metrics use the narrower first-to-last-message window used for throughput.
|
|
681
660
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const config = {
|
|
686
|
-
'batch.size': 16384, // number
|
|
687
|
-
'compression.type': 'snappy', // string
|
|
688
|
-
'enable.idempotence': true, // boolean
|
|
689
|
-
'retries': 5 // number
|
|
690
|
-
}
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
### Message Processing
|
|
694
|
-
- Validate message formats before processing
|
|
695
|
-
- Implement proper serialization/deserialization
|
|
696
|
-
- Handle message ordering when required
|
|
697
|
-
- Use topic creation options for better topic management
|
|
661
|
+
The most important memory result is efficiency, not just raw RSS. `kafka-crab-js v4 (stream, serial)` delivered
|
|
662
|
+
`7529 op/sec/MiB`, the best score in the run. `kafka-crab-js v4 (stream, batch)` delivered `6521 op/sec/MiB`, ahead of
|
|
663
|
+
`KafkaJS (eachBatch)` at `3504 op/sec/MiB` and `@platformatic/kafka` at `3365 op/sec/MiB`.
|
|
698
664
|
|
|
699
|
-
###
|
|
700
|
-
- Configure appropriate `ReadableOptions` for your use case
|
|
701
|
-
- Use `objectMode: true` for structured message processing
|
|
702
|
-
- Set appropriate `highWaterMark` based on memory constraints
|
|
665
|
+
### Benchmark Interpretation
|
|
703
666
|
|
|
704
|
-
|
|
667
|
+
- Use `kafka-crab-js v4 (stream, batch)` when raw throughput is the priority.
|
|
668
|
+
- Use `kafka-crab-js v4 (stream, serial)` when memory efficiency and low heap pressure matter most.
|
|
669
|
+
- Compare batch scenarios with batch scenarios and message scenarios with message scenarios.
|
|
670
|
+
- `@platformatic/kafka` is the strongest non-crab throughput competitor in this run, but it used more RSS, heap, and GC
|
|
671
|
+
time than the v4 batch scenario.
|
|
672
|
+
- KafkaJS `eachMessage` concurrency did not help this workload.
|
|
705
673
|
|
|
706
|
-
|
|
707
|
-
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
708
|
-
3. Run the benchmark suite to ensure performance isn't degraded
|
|
709
|
-
4. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
710
|
-
5. Push to the branch (`git push origin feature/amazing-feature`)
|
|
711
|
-
6. Open a Pull Request
|
|
674
|
+
### Benchmark Configuration
|
|
712
675
|
|
|
713
|
-
|
|
676
|
+
Common knobs:
|
|
714
677
|
|
|
715
678
|
```bash
|
|
716
|
-
|
|
717
|
-
|
|
679
|
+
BENCHMARK_ITERATIONS=100000
|
|
680
|
+
BENCHMARK_RUNS=5
|
|
681
|
+
BENCHMARK_BATCH_SIZE=4096
|
|
682
|
+
BENCHMARK_BATCH_TIMEOUT_MS=2
|
|
683
|
+
BENCHMARK_MAX_BYTES=2048
|
|
684
|
+
BENCHMARK_MEMORY=1
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
See the [benchmark README](../benchmark/README.md) for the full benchmark methodology and environment variables.
|
|
718
688
|
|
|
719
|
-
|
|
720
|
-
pnpm typecheck
|
|
689
|
+
## Migration Notes
|
|
721
690
|
|
|
722
|
-
|
|
723
|
-
pnpm lint
|
|
691
|
+
### v4 Type-Only Runtime Exports
|
|
724
692
|
|
|
725
|
-
|
|
726
|
-
|
|
693
|
+
`CommitMode`, `KafkaEventName`, `PartitionPosition`, and `SecurityProtocol` are no longer runtime exports from
|
|
694
|
+
`kafka-crab-js`.
|
|
727
695
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
696
|
+
Before:
|
|
697
|
+
|
|
698
|
+
```ts
|
|
699
|
+
import { CommitMode, SecurityProtocol } from 'kafka-crab-js'
|
|
731
700
|
```
|
|
732
701
|
|
|
733
|
-
|
|
702
|
+
After:
|
|
734
703
|
|
|
735
|
-
|
|
704
|
+
```ts
|
|
705
|
+
import type { CommitMode, SecurityProtocol } from 'kafka-crab-js'
|
|
736
706
|
|
|
737
|
-
|
|
707
|
+
const commitMode: CommitMode = 'Sync'
|
|
708
|
+
const securityProtocol: SecurityProtocol = 'Plaintext'
|
|
709
|
+
```
|
|
738
710
|
|
|
739
|
-
|
|
740
|
-
- **Consumer & stream coverage** – Standard consumers, batch consumers, and `createStreamConsumer` streams emit spans that include consumer group, topic, partition, offset, and batch size semantics.
|
|
741
|
-
- **Hook-friendly spans** – Both `messageHook` and `producerHook` callbacks run inside the active span context, simplifying attribute decoration or error handling.
|
|
742
|
-
- **Zero overhead when disabled** – Uses Node.js `diagnostics_channel` for near-zero cost when OTEL is not active.
|
|
711
|
+
### v3 OpenTelemetry Split
|
|
743
712
|
|
|
744
|
-
|
|
713
|
+
OpenTelemetry instrumentation moved from the core package to `kafka-crab-js-otel` in v3. This keeps the core package
|
|
714
|
+
small and makes tracing/metrics opt-in.
|
|
745
715
|
|
|
746
|
-
|
|
716
|
+
## Troubleshooting
|
|
747
717
|
|
|
748
|
-
|
|
749
|
-
- For batch consumers, call `endSpan(batch)` when you're done processing the batch.
|
|
750
|
-
- The `endSpan()` helper is exported from `kafka-crab-js-otel`.
|
|
718
|
+
### The Consumer Does Not Receive Existing Messages
|
|
751
719
|
|
|
752
|
-
|
|
720
|
+
Use an offset reset policy and a fresh consumer group:
|
|
753
721
|
|
|
754
|
-
|
|
722
|
+
```ts
|
|
723
|
+
const consumer = client.createConsumer({
|
|
724
|
+
groupId: `orders-worker-${Date.now()}`,
|
|
725
|
+
configuration: {
|
|
726
|
+
'auto.offset.reset': 'earliest',
|
|
727
|
+
},
|
|
728
|
+
})
|
|
729
|
+
```
|
|
755
730
|
|
|
756
|
-
|
|
731
|
+
Or subscribe with an explicit starting position:
|
|
757
732
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
733
|
+
```ts
|
|
734
|
+
await consumer.subscribe([{ topic: 'orders', allOffsets: { position: 'Beginning' } }])
|
|
735
|
+
```
|
|
761
736
|
|
|
762
|
-
###
|
|
737
|
+
### The Process Hangs On Shutdown
|
|
763
738
|
|
|
764
|
-
|
|
739
|
+
Direct consumers should call `disconnect()`:
|
|
765
740
|
|
|
766
741
|
```ts
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
|
|
770
|
-
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
771
|
-
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'
|
|
772
|
-
import { context } from '@opentelemetry/api'
|
|
773
|
-
|
|
774
|
-
// 1. Set up OpenTelemetry SDK
|
|
775
|
-
const provider = new NodeTracerProvider()
|
|
776
|
-
provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
|
|
777
|
-
provider.register()
|
|
778
|
-
context.setGlobalContextManager(new AsyncHooksContextManager().enable())
|
|
779
|
-
|
|
780
|
-
// 2. Enable kafka-crab-js instrumentation BEFORE creating client
|
|
781
|
-
enableOtelInstrumentation({
|
|
782
|
-
captureMessagePayload: true,
|
|
783
|
-
captureMessageHeaders: true,
|
|
784
|
-
producerHook: span => span.setAttribute('messaging.client.kind', 'producer'),
|
|
785
|
-
})
|
|
742
|
+
await consumer.disconnect()
|
|
743
|
+
```
|
|
786
744
|
|
|
787
|
-
|
|
788
|
-
const client = new KafkaClient({
|
|
789
|
-
brokers: 'localhost:29092',
|
|
790
|
-
clientId: 'orders-api',
|
|
791
|
-
diagnostics: true, // Required for OTEL to receive events
|
|
792
|
-
})
|
|
745
|
+
Node stream consumers should call `destroy()`:
|
|
793
746
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
topic: 'orders',
|
|
798
|
-
messages: [{
|
|
799
|
-
payload: Buffer.from(JSON.stringify({ orderId: '123' })),
|
|
800
|
-
headers: { 'custom-header': 'foo' },
|
|
801
|
-
}],
|
|
802
|
-
})
|
|
747
|
+
```ts
|
|
748
|
+
stream.destroy()
|
|
749
|
+
```
|
|
803
750
|
|
|
804
|
-
|
|
805
|
-
const consumer = client.createStreamConsumer({
|
|
806
|
-
groupId: 'orders-consumer',
|
|
807
|
-
enableAutoCommit: false,
|
|
808
|
-
})
|
|
751
|
+
Web stream consumers should cancel the reader and disconnect the underlying consumer:
|
|
809
752
|
|
|
810
|
-
|
|
753
|
+
```ts
|
|
754
|
+
await reader.cancel()
|
|
755
|
+
await webConsumer.consumer.disconnect()
|
|
756
|
+
```
|
|
811
757
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
758
|
+
### Localhost Resolves To IPv6 But Kafka Listens On IPv4
|
|
759
|
+
|
|
760
|
+
Set `brokerAddressFamily: 'v4'`:
|
|
761
|
+
|
|
762
|
+
```ts
|
|
763
|
+
const client = new KafkaClient({
|
|
764
|
+
brokers: 'localhost:9092',
|
|
765
|
+
brokerAddressFamily: 'v4',
|
|
819
766
|
})
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### Need Advanced Kafka Settings
|
|
820
770
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
771
|
+
Pass librdkafka settings through `configuration`:
|
|
772
|
+
|
|
773
|
+
```ts
|
|
774
|
+
const client = new KafkaClient({
|
|
775
|
+
brokers: 'localhost:9092',
|
|
776
|
+
configuration: {
|
|
777
|
+
'socket.keepalive.enable': true,
|
|
778
|
+
'metadata.max.age.ms': 300000,
|
|
779
|
+
},
|
|
780
|
+
})
|
|
824
781
|
```
|
|
825
782
|
|
|
826
|
-
|
|
783
|
+
Consumer and producer configuration objects also accept their own `configuration` maps.
|
|
827
784
|
|
|
828
|
-
##
|
|
785
|
+
## Development
|
|
829
786
|
|
|
830
|
-
|
|
787
|
+
From the repository root:
|
|
831
788
|
|
|
832
|
-
|
|
789
|
+
```bash
|
|
790
|
+
vp install
|
|
791
|
+
vp run build
|
|
792
|
+
vp run test
|
|
793
|
+
vp run test:integration
|
|
794
|
+
vp check
|
|
795
|
+
```
|
|
833
796
|
|
|
834
|
-
|
|
797
|
+
Useful package-local commands:
|
|
835
798
|
|
|
836
|
-
|
|
799
|
+
```bash
|
|
800
|
+
vp run --filter kafka-crab-js build
|
|
801
|
+
vp run --filter kafka-crab-js test
|
|
802
|
+
vp run --filter kafka-crab-js test:integration
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
Integration tests require Kafka at `localhost:9092` unless `KAFKA_BROKERS` is set.
|
|
806
|
+
|
|
807
|
+
## License
|
|
837
808
|
|
|
838
|
-
|
|
809
|
+
MIT
|