What it does
- Consumes JSON, schemaful Connect records, or Schema Registry-framed Avro/JSON Schema/Protobuf feature events from one or more Kafka topics
- Converts each event into a Feast push payload, preserving the event timestamp
- Pushes features to Feast over HTTP — online, offline, or both
- Batches efficiently and retries transient failures
- Integrates with Kafka Connect operational semantics: offsets, dead-letter queue, JMX metrics, structured logging
Architecture
Components
Requirements
- JDK 21
- Apache Kafka Connect 4.x+ (compiled against
connect-api4.3) - Feast Feature Server reachable over HTTP
Build & test
./gradlew test # unit tests
./gradlew integrationTest # integration tests (WireMock; Kafka via Testcontainers if Docker present)
./gradlew connectPluginZip # build the deployable plugin ZIP
The wrapper is pinned to Gradle 8.14.4 (Kotlin 2.4.0, JVM target 21) for reproducibility.
Packaging & install
./gradlew connectPluginZip produces:
build/distributions/feast-kafka-connect-sink-<version>.zip
└── feast-kafka-connect-sink-<version>/
├── lib/feast-kafka-connect-sink-<version>.jar # the connector
├── lib/jackson-*.jar, kotlin-stdlib-*.jar, ... # bundled runtime deps
├── README.md
└── feast-sink-connector.json
connect-api and slf4j-api are intentionally not bundled — the
Connect worker provides them. Unzip the folder into a directory on your worker's plugin.path and
restart the worker.
Configuration
Register via the Connect REST API (see feast-sink-connector.json):
curl -s -X POST -H 'Content-Type: application/json' \
--data @config/feast-sink-connector.json http://localhost:8083/connectors
Connector settings
| Key | Type | Default | Description |
|---|---|---|---|
feast.url |
string | — (required) | Feast Feature Server base URL; the connector POSTs to {feast.url}/push. |
feast.push.source.name |
string | "" |
Default push source. Required unless every topic is covered by the mapping. |
feast.push.mode |
string | online |
online, offline, or online_and_offline. |
topic.to.push.source.mapping |
string | "" |
Per-topic overrides: topic1:source1,topic2:source2. |
auth.type |
string | none |
none or bearer. |
auth.bearer.token |
password | — | Required when auth.type=bearer; sent as Authorization: Bearer …. |
request.timeout.ms |
int | 30000 |
Per-request HTTP timeout. |
batch.size |
int | 500 |
Max rows per push request, per push source. |
linger.ms |
long | 1000 |
Max time a partial batch waits before flushing. |
max.retries |
int | 5 |
Max retries for a transient push failure. |
retry.backoff.ms |
long | 1000 |
Base backoff; exponential with full jitter. |
event.timestamp.field |
string | event_timestamp |
Event field holding the ISO-8601 timestamp. |
entity.key.fields |
list | [] |
Entity/join-key columns that must be present and non-null. |
schema.registry.url |
string | "" |
Optional Confluent-compatible Schema Registry URL. When set, ByteArray record values are
decoded using the Confluent wire format. Supported schema types: AVRO, JSON,
PROTOBUF.
|
schema.registry.request.timeout.ms |
int | 10000 |
Per-request Schema Registry timeout. |
schema.registry.cache.size |
int | 1000 |
Maximum number of schemas cached by schema id. |
dlq.context.headers.enable |
boolean | true |
Attach __feast_* context headers to DLQ records. |
Recommended standard Connect settings
value.converter=org.apache.kafka.connect.json.JsonConverter
value.converter.schemas.enable=false
key.converter=org.apache.kafka.connect.storage.StringConverter
errors.tolerance=all
errors.deadletterqueue.topic.name=dlq
errors.deadletterqueue.context.headers.enable=true
Schema Registry formats
There are two supported deployment models for Schema Registry formats:
# Preferred when the worker already has Confluent converters installed.
value.converter=io.confluent.connect.avro.AvroConverter
value.converter.schema.registry.url=http://schema-registry:8081
For JSON Schema, use the matching converter:
value.converter=io.confluent.connect.json.JsonSchemaConverter
value.converter.schema.registry.url=http://schema-registry:8081
For Protobuf, use the matching converter:
value.converter=io.confluent.connect.protobuf.ProtobufConverter
value.converter.schema.registry.url=http://schema-registry:8081
With Confluent converters, Kafka Connect passes schemaful Struct values to the connector; no
connector Schema Registry settings are needed.
# In-connector Schema Registry decoding path.
value.converter=org.apache.kafka.connect.converters.ByteArrayConverter
schema.registry.url=http://schema-registry:8081
schema.registry.request.timeout.ms=10000
schema.registry.cache.size=1000
With ByteArrayConverter, the connector expects Confluent Schema Registry wire-format values
(magic byte + schema id + payload). AVRO payloads are decoded with the writer schema.
JSON payloads are parsed as UTF-8 JSON objects. PROTOBUF payloads are decoded with
Confluent message indexes and the registered .proto descriptor. Unknown schema IDs and malformed
payloads are treated as record-level conversion failures and routed to the DLQ. Registry timeouts, I/O
failures, and HTTP 5xx responses are treated as operational failures and pause consumption with
RetriableException.
Feast push payload
An event:
{
"trip_id": "yellow_2024_01_000001",
"event_timestamp": "2024-01-01T00:13:42Z",
"pickup_location_id": 161,
"fare_amount": 18.4
}
becomes a column-oriented push request (one POST carries a whole batch):
POST {feast.url}/push
{
"push_source_name": "taxi_trips_push_source",
"df": {
"trip_id": ["yellow_2024_01_000001", "..."],
"event_timestamp": ["2024-01-01T00:13:42Z", "..."],
"pickup_location_id": [161, "..."],
"fare_amount": [18.4, "..."]
},
"to": "online_and_offline"
}
Operational semantics
-
Batching & flush: records are buffered per push source and flushed when a source reaches
batch.size, when its oldest record has waitedlinger.ms, or at thepreCommitdurability barrier.batch.sizeis a hard per-request maximum (large polls are chunked). - Ordering: buffers are FIFO and flushed in order; a task never reorders a partition's records.
-
Delivery: at-least-once.
preCommitonly lets offsets advance once buffered records have been pushed (or routed to the DLQ), so no record is lost. -
Backpressure: while Feast is unavailable, the task retries with backoff; if a batch still
cannot be delivered, it is returned to the buffer and the task throws
RetriableExceptionto pause consumption until Feast recovers — bounding in-memory buffering. - Idempotency: online pushes are upserts (retry-safe). Offline pushes may duplicate on retry; dedup downstream by entity + event timestamp.
Error handling & DLQ
| Failure | Examples | Behaviour |
|---|---|---|
| Malformed record | bad JSON; missing/invalid timestamp; missing/null entity key | Reported to the Connect DLQ (ErrantRecordReporter, KIP-610) and skipped. |
| Transient Feast failure | I/O, timeout, HTTP 5xx, 429 | Retried (max.retries, exponential backoff + jitter); then RetriableException for redelivery. |
| Permanent Feast failure | HTTP 4xx (e.g. 400/422) | Whole batch routed to the DLQ with context; task stays alive. |
When dlq.context.headers.enable=true, DLQ records carry:
__feast_error_stage (CONVERT/VALIDATE/PUSH),
__feast_error_class, __feast_error_message, __feast_push_source.
Metrics
A JMX MBean is registered per task under
com.arthenica.kafka.feastconnect.sink:type=task,connector=<name>,task=<id> with
attributes: RecordsReceived, RecordsPushed, RecordsMalformed,
BatchesPushed, PushRetries, PushFailures,
PushLatencyAvgMs, PushLatencyMaxMs, BufferedRecords,
LastPushTimestamp.
License
feast-kafka-connect-sink is licensed under the terms of the Apache License version 2.0. See LICENSE for more details.