Skip to main content

Kafka Connector

The Kafka connector enables EventFlux to consume records from Kafka topics and publish processed results back to Kafka. It supports JSON, CSV, and bytes formats, consumer groups with configurable offset semantics, and SASL/SSL security.

Built on librdkafka (via the rdkafka crate), the connector inherits its battle-tested networking: automatic reconnection, internal retries, and producer-side batching all come from librdkafka's background threads — no extra runtime inside EventFlux.

Prerequisites

Building with Kafka Support

The Kafka connector is feature-gated. Build EventFlux with the kafka feature (Docker images already include all connectors). Compiling librdkafka requires cmake and a C toolchain:

cargo build --release --features kafka
# or: cargo build --release --features connectors-all

Starting Kafka

The easiest way to run a single-node broker is Docker (KRaft mode, no ZooKeeper):

docker run -d --name kafka -p 9092:9092 apache/kafka:3.9.1

Creating Topics

EventFlux validates at startup that configured topics exist (fail-fast):

docker exec kafka /opt/kafka/bin/kafka-topics.sh --create \
--bootstrap-server localhost:9092 --topic trades-in

Kafka Source

The Kafka source joins a consumer group and delivers record payloads into the pipeline.

SQL Syntax

CREATE STREAM TradeInput (
symbol STRING,
price DOUBLE,
volume INT
) WITH (
type = 'source',
extension = 'kafka',
format = 'json',
"kafka.bootstrap.servers" = 'localhost:9092',
"kafka.topic" = 'trades-in',
"kafka.group.id" = 'eventflux-trades',
"kafka.enable.auto.commit" = 'false'
);

Source Configuration Options

OptionRequiredDefaultDescription
kafka.bootstrap.serversYes-Comma-separated broker list
kafka.topicYes-Topic(s) to consume, comma-separated for multiple
kafka.group.idNoeventflux-<topic>Consumer group id (deterministic default, so offsets survive restarts)
kafka.auto.offset.resetNolatestStart position without committed offsets: earliest or latest
kafka.enable.auto.commitNotruefalse = commit after pipeline delivery (at-least-once)
kafka.poll.timeout.msNo100Poll timeout; bounds shutdown latency
kafka.security.protocolNoplaintextplaintext, ssl, sasl_plaintext, sasl_ssl
kafka.sasl.mechanismNo-PLAIN, SCRAM-SHA-256, SCRAM-SHA-512
kafka.sasl.username / kafka.sasl.passwordNo-SASL credentials
kafka.ssl.ca.locationNo-CA certificate path
kafka.rdkafka.<prop>No-Escape hatch: passed verbatim to librdkafka (e.g. kafka.rdkafka.fetch.min.bytes)
error.strategyNo-Error handling: drop, retry, dlq, fail

Delivery Semantics

ConfigurationGuarantee
kafka.enable.auto.commit = true (default)~at-most-once: librdkafka commits periodically, possibly before processing completes
kafka.enable.auto.commit = falseat-least-once: a record's offset is committed only after the pipeline accepted it (or the error strategy disposed of it via drop/DLQ). Crash before commit → redelivery on restart

With the fail error strategy, an unprocessable record is not committed, so it is redelivered after restart.

Records with empty payloads (tombstones) are skipped.

Kafka Sink

The Kafka sink publishes each event as its own record (one event = one message).

SQL Syntax

CREATE STREAM TradeOutput (
symbol STRING,
price DOUBLE,
price_category STRING
) WITH (
type = 'sink',
extension = 'kafka',
format = 'json',
"kafka.bootstrap.servers" = 'localhost:9092',
"kafka.topic" = 'trades-out',
"kafka.acks" = 'all',
"kafka.compression" = 'lz4'
);

Sink Configuration Options

OptionRequiredDefaultDescription
kafka.bootstrap.serversYes-Comma-separated broker list
kafka.topicYes-Target topic
kafka.keyNo-Static record key (keyless records use the default partitioner)
kafka.acksNoallDurability vs latency: 0, 1, all
kafka.compressionNononenone, gzip, snappy, lz4, zstd
kafka.linger.msNo5Producer batching delay
kafka.batch.sizeNolibrdkafka defaultMax batch bytes
kafka.enable.idempotenceNofalseNo duplicates on producer retry (requires kafka.acks = 'all')
kafka.message.timeout.msNo30000Total time to deliver a record before it's reported failed
kafka.delivery.syncNofalsetrue: publish() waits for delivery confirmation per record (strict, slow)
kafka.flush.timeout.msNo30000Bounds the shutdown flush, sync-mode flushes, and queue-full backpressure
kafka.queue.full.strategyNoblockLocal queue full: block (backpressure) or error (fail fast)
kafka.rdkafka.<prop>No-Escape hatch: passed verbatim to librdkafka

Delivery Semantics

  • Default (async): publish() enqueues into librdkafka's local queue; delivery happens in the background with internal retries until kafka.message.timeout.ms. Failures are counted and logged. This keeps pipeline throughput decoupled from broker round-trips.
  • Sync (kafka.delivery.sync = true): each publish waits for its delivery report — strict confirmation for low-throughput, high-assurance pipelines.
  • Shutdown: in-flight records are flushed (bounded by kafka.flush.timeout.ms); lifetime produced/failed counters are logged.
  • kafka.acks = all + kafka.enable.idempotence = true gives at-least-once without producer-side duplicates.

Complete End-to-End Example

See examples/kafka.eventflux for a runnable pipeline (Kafka → filter + CASE → Kafka), including broker setup and console producer/consumer commands for testing.

Error Handling

The source integrates with EventFlux's error handling strategies (WITH clause options on the source stream):

OptionDefaultDescription
error.strategydropWhat to do when the pipeline rejects a record: drop, retry, dlq, fail
error.retry.max-attempts3Max retries before falling back to drop
error.retry.backoffexponentialBackoff strategy: exponential, linear, or fixed
error.retry.initial-delay100msFirst retry delay (duration string)
error.retry.max-delay30sRetry delay ceiling (duration string)
error.log-levelwarnLog level for handled errors
error.dlq.stream-Dead-letter stream name (required when strategy is dlq)
error.dlq.fallback-strategylogWhat to do when DLQ delivery itself fails

Strategy semantics and their offset interaction:

  • drop: skip the failing record (offset committed)
  • retry: retry delivery with exponential backoff
  • dlq: route the raw payload to a dead-letter stream (offset committed)
  • fail: stop the source; the record is not committed and is redelivered after restart

More Configuration Examples

At-least-once source with retry and a dead-letter queue

CREATE STREAM Orders (order_id STRING, amount DOUBLE) WITH (
type = 'source',
extension = 'kafka',
format = 'json',
"kafka.bootstrap.servers" = 'localhost:9092',
"kafka.topic" = 'orders',
"kafka.group.id" = 'eventflux-orders',
"kafka.auto.offset.reset" = 'earliest',
"kafka.enable.auto.commit" = 'false', -- at-least-once
"error.strategy" = 'retry',
"error.retry.max-attempts" = '5',
"error.retry.initial-delay" = '200ms',
"error.retry.max-delay" = '5s',
"error.retry.backoff" = 'exponential'
);

Secured cluster (SASL/SSL)

kafka.sasl.* options require a sasl_* security protocol. Use environment variable substitution for secrets rather than inlining them:

CREATE STREAM SecureInput (id STRING, value DOUBLE) WITH (
type = 'source',
extension = 'kafka',
format = 'json',
"kafka.bootstrap.servers" = 'broker1:9093,broker2:9093',
"kafka.topic" = 'secure-events',
"kafka.security.protocol" = 'sasl_ssl',
"kafka.sasl.mechanism" = 'SCRAM-SHA-256',
"kafka.sasl.username" = '${KAFKA_USERNAME}',
"kafka.sasl.password" = '${KAFKA_PASSWORD}',
"kafka.ssl.ca.location" = '/etc/ssl/certs/kafka-ca.pem'
);

High-throughput sink with idempotence and librdkafka tuning

kafka.enable.idempotence requires kafka.acks = 'all'. Any librdkafka setting without a first-class option goes through the kafka.rdkafka.* passthrough:

CREATE STREAM MetricsOut (name STRING, value DOUBLE) WITH (
type = 'sink',
extension = 'kafka',
format = 'json',
"kafka.bootstrap.servers" = 'localhost:9092',
"kafka.topic" = 'metrics',
"kafka.acks" = 'all',
"kafka.enable.idempotence" = 'true',
"kafka.compression" = 'zstd',
"kafka.linger.ms" = '50',
"kafka.batch.size" = '131072',
"kafka.rdkafka.queue.buffering.max.messages" = '500000'
);

Strict per-record confirmation sink

For low-throughput pipelines where every publish must be confirmed before the next event is processed:

CREATE STREAM AuditLog (user_id STRING, action STRING) WITH (
type = 'sink',
extension = 'kafka',
format = 'json',
"kafka.bootstrap.servers" = 'localhost:9092',
"kafka.topic" = 'audit-log',
"kafka.key" = 'audit',
"kafka.delivery.sync" = 'true',
"kafka.flush.timeout.ms" = '10000',
"kafka.queue.full.strategy" = 'error'
);

Connection Validation

Both source and sink validate at application startup that brokers are reachable and topics exist (10s timeout). The application does not start otherwise:

Configuration error: Kafka topic 'trades-in' does not exist (brokers: 'localhost:9092')

Not Yet Supported

  • Exactly-once semantics (Kafka transactions) — needs checkpoint-coordinated commits
  • Per-event record keys / topic routing (static kafka.key only)
  • Kafka headers ↔ event metadata
  • Avro / Schema Registry (no Avro mapper yet)