@platformatic/kafka Documentation

repository·main·Indexed 19 days ago

https://github.com/platformatic/kafka

A high-performance, type-safe, pure TypeScript/JavaScript client for Apache Kafka designed for modern ECMAScript environments. It features flexible consumption patterns via streams or events, pluggable serialization, and lazy auto-connection. The documentation includes detailed migration guides from KafkaJS, covering architectural differences in Producer, Consumer, and Admin clients, as well as integration with Node.js diagnostics_channel for tracing.

Tokens
54.4K
Snippets
106
Records
204
Agent score
64%

What's inside @platformatic/kafka

  1. Configure Confluent Schema Registry (Experimental)

    main

    The library provides experimental support for Confluent Schema Registry with AVRO, Protobuf, and JSON Schema. By passing a ConfluentSchemaRegistry instance to the Producer or Consumer constructor, serialization and deserialization are handled automatically using schema IDs provided in message metadata.

    Warning: This API is experimental, does not follow semver, and may change in minor or patch releases.

    import { Producer, Consumer } from '@platformatic/kafka'
    import { ConfluentSchemaRegistry } from '@platformatic/kafka/registries'
    
    // Create a schema registry instance
    const registry = new ConfluentSchemaRegistry({
      url: 'http://localhost:8081',
      auth: {
        username: 'user',
        password: 'password'
      }
    })
    
    // Producer with schema registry
    const producer = new Producer({
      clientId: 'schema-producer',
      bootstrapBrokers: ['localhost:9092'],
      registry // Automatic serialization with schemas
    })
    
    // Send messages with schema IDs
    await producer.send({
      messages: [
        {
          topic: 'users',
          value: { id: 1, name: 'Alice' },
          metadata: {
            schemas: {
              value: 100 // Schema ID in the registry
            }
          }
        }
      ]
    })
    
    // Consumer with schema registry
    const consumer = new Consumer({
      groupId: 'schema-consumers',
      clientId: 'schema-consumer',
      bootstrapBrokers: ['localhost:9092'],
      registry // Automatic deserialization with schemas
    })
    
    const stream = await consumer.consume({
      topics: ['users']
    })
    
    // Messages are automatically deserialized
    for await (const message of stream) {
      console.log('User:', message.value) // Typed object
    }
  2. Customize Serialisation and Deserialisation

    main

    You can provide custom serializers and deserializers for different parts of a Kafka message: key, value, headerKey, and headerValue. If no serializers are provided, the client defaults to no-operation (identity) functions, meaning all parts must be Buffers.

    To customize, pass an object containing the desired functions to the serializers option (for Producer) or the deserializers option (for Consumer).

    import {
      Consumer,
      jsonDeserializer,
      jsonSerializer,
      ProduceAcks,
      Producer,
      stringDeserializer,
      stringSerializer
    } from '@platformatic/kafka'
    
    type Strings = string[]
    
    const producer = new Producer({
      clientId: 'my-producer',
      bootstrapBrokers: ['localhost:9092'],
      serializers: {
        key: stringSerializer,
        value: jsonSerializer<Strings>
      }
    })
    
    const consumer = new Consumer({
      groupId: 'my-consumer-group',
      clientId: 'my-consumer',
      bootstrapBrokers: ['localhost:9092'],
      deserializers: {
        key: stringDeserializer,
        value: jsonDeserializer<Strings>
      },
      maxWaitTime: 1000,
      autocommit: 100
    })
    
    // Produce some messages
    let i = 0
    const timer = setTimeout(() => {
      producer.send({
        messages: [{ topic: 'temp', key: `key-${i++}`, value: ['first', 'second'] }],
        acks: ProduceAcks.LEADER
      })
    
      if (i < 3) {
        timer.refresh()
      }
    }, 1000)
    
    const stream = await consumer.consume({ topics: ['temp'] })
    
    for await (const message of stream) {
      console.log(message)
    
      if (message.key === 'key-2') {
        break
      }
    }
    
    await stream.close()
    await consumer.close()
    await producer.close()
  3. How to use Node.js Diagnostic Channels for instrumentation

    main

    Platformatic Kafka provides instrumentation via the Node.js Diagnostic Channel API. You can subscribe to specific tracing channels to monitor operations, performance, and errors across different Kafka components (Admin, Producer, Consumer, etc.).

    Execution Model

    Tracing follows a specific lifecycle. For asynchronous operations, the sequence of events is:

    1. channel.start.publish(context)
    2. channel.asyncStart.publish(context)
    3. channel.asyncEnd.publish(context)

    If an error occurs, the error event is published before the asyncStart and asyncEnd events.

    const { diagnosticChannel } = require('node:diagnostics_channel');
    
    // Example of subscribing to a channel
    diagnosticChannel('plt:kafka:producer:sends').subscribe(({ context }) => {
      console.log('Producer send operation:', context.operationId, context.result);
    });
  4. Manage regression baselines and artifacts

    main

    Baselines

    Baselines can be stored in two ways:

    1. Locally: Using the REGRESSION_BASELINE_DIR variable.
    2. Remotely: Using REGRESSION_BASELINE_URL. Remote storage uses GET and PUT requests to <REGRESSION_BASELINE_URL>/<REGRESSION_LANE>.json. Use REGRESSION_BASELINE_TOKEN for authentication.

    Artifacts

    Generated JSON artifacts are stored in regression/artifacts. Performance artifacts include:

    • Aggregate median duration and throughput
    • Raw per-sample runs
    • Bytes/sec (when available)
    • Resource samples for RSS, heap, CPU, and event loop delay
  5. Manage Connection Lifecycle

    main

    Connections in @platformatic/kafka are established lazily on the first operation (e.g., send() or consume()). You do not need to call connect(). Use close() to clean up resources when finished.

    // No connect() needed - connections are established lazily on first operation
    // ... use producer ...
    await producer.close()
  6. Implement exactly-once read-process-write patterns

    main

    To achieve exactly-once semantics when consuming and producing, follow these rules:

    1. Consumer Configuration: Set autocommit to false (or omit it) and use isolationLevel: FetchIsolationLevels.READ_COMMITTED when consuming to ensure you only read messages from committed transactions.
    2. Transaction Integration: Use transaction.addConsumer(consumer) to register the consumer group with the transaction.
    3. Offset Management: Do not use message.commit(). Instead, use transaction.addOffset(message) to include the consumer offset in the atomic transaction. The offsets are committed only when transaction.commit() is called.
    const transaction = await producer.beginTransaction()
    
    try {
      // Add the consumer to the transaction
      await transaction.addConsumer(consumer)
    
      // Process messages from the consumer
      stream.on('data', async message => {
        // Process the message and produce results
        await transaction.send({
          messages: [{ topic: 'output-topic', value: processedValue }]
        })
    
        // Add the message offset to the transaction
        await transaction.addOffset(message)
      })
    
      // Commit both the produced messages and consumer offsets
      await transaction.commit()
    } catch (error) {
      await transaction.abort()
    }
  7. Understand the Base client class

    main
    The Base class is the foundation for all specialized Kafka clients in this library, including Producer, Consumer, and Admin. While you typically interact with the specialized subclasses, the Base class defines the core connection logic, configuration, and event system shared across all clients. Use the Base class directly only if you specifically need to manage cluster metadata without performing producer, consumer, or admin operations.
  8. Tune Consumer memory usage with highWaterMark

    main

    The consumer uses a high watermark for its streams to improve throughput. The default value is 1024, which is significantly higher than the Node.js default of 16. This can lead to high memory usage if message objects are large (e.g., 1024 objects of 1MB each = 1GB RAM per stream).

    You can tune this by setting the highWaterMark option in the Consumer constructor.

  9. Enable rack-aware fetching

    main

    By setting the clientRack option, the consumer sends the rack ID in the Fetch request (rack_id). The consumer will then honor any preferred_read_replica returned by the broker. Subsequent fetches for that partition will route to the preferred replica until the lease expires, metadata changes, or the preferred fetch fails.

    To benefit from this, your Kafka cluster must support KIP-392 (Apache Kafka 2.4+) and have a replica selector configured (e.g., replica.selector.class=org.apache.kafka.common.replica.RackAwareReplicaSelector).

  10. Consumer overview and capabilities

    main

    The Consumer is a client used to consume messages from Kafka topics. It supports consumer groups and transactional message isolation. The Consumer inherits from the Base client.

    Note on Types: The complete TypeScript type of the Consumer instance is dynamically determined by the deserializers option provided during instantiation.

  11. Prevent missed heartbeats and rebalances during message processing

    main

    Kafka requires regular heartbeats to maintain consumer group membership. If heartbeats are missed, the consumer is considered dead, triggering a group rebalance that halts consumption for all members.

    Common causes and solutions:

    • Long fetch durations: If maxWaitTime exceeds sessionTimeout, the fetch API might block heartbeats. @platformatic/kafka mitigates this by using separate fetch connections per MessagesStream.
    • Complex async processing: Long-running async tasks in your message handler can block the driver. @platformatic/kafka prevents this by decoupling message fetching from processing.
    • CPU-intensive operations: Blocking the Node.js event loop prevents I/O (including heartbeats). Solution: Offload heavy computations to Node.js Worker Threads.
  12. Use the Admin client for Kafka administrative operations

    main

    The Admin client is used to perform administrative tasks on a Kafka cluster, such as managing topics, consumer groups, and client quotas. It inherits all configuration options from the Base client, allowing you to specify connection details like brokers, TLS, and SASL settings.

    // The Admin client inherits from Base, so it uses the same connection options
    const admin = new Admin({ brokers: ['localhost:9092'] });