Pritam Kumar created KAFKA-20797:
------------------------------------
Summary: KIP-1365: Transform Observability and Skipped Record
Handling for Kafka Connect
Key: KAFKA-20797
URL: https://issues.apache.org/jira/browse/KAFKA-20797
Project: Kafka
Issue Type: Improvement
Components: connect, kip
Reporter: Pritam Kumar
Kafka Connect's Single Message Transform (SMT) framework allows users to build
chains of transformations that process records between the connector and Kafka.
Transforms can modify records, route them to different topics, or *drop them
entirely* by returning {{null}} (a filter operation). When combined with the
error tolerance framework ({{{}errors.tolerance=all{}}}), records can also be
diverted to a Dead Letter Queue (DLQ) before reaching the connector.
Today, the transform layer has two significant gaps: *no operational
observability* and {*}no mechanism for sink connectors to learn about records
they never received{*}. This KIP addresses both with two complementary
enhancements.
h3. Problem 1: No Per-Transform Operational Metrics
Kafka Connect provides *no operational metrics* at the transform layer. The
existing {{connector-transform-metrics}} group only exposes static metadata:
||Existing Metric||Type||Description||
|{{transform-class}}|Gauge (string)|The class name of the transformation|
|{{transform-version}}|Gauge (string)|The version of the transformation|
This creates several operational pain points:
# *No visibility into filtering* — When using the {{Filter}} transform (or any
transform that drops records by returning {{{}null{}}}), operators have no way
to know how many records are being filtered. The only workaround is subtracting
task-level {{source-record-poll-total}} from {{source-record-write-total}} (or
{{sink-record-read-total}} from {{{}sink-record-send-total{}}}), which provides
only an aggregate across the entire transform chain.
# *No per-transform throughput* — In a chain of 3-5 transforms, operators
cannot determine the throughput contribution or bottleneck of each individual
transform. If a transform has a bug that silently drops records, it goes
undetected.
# *No error attribution* — While {{task-error-metrics}} tracks aggregate
errors, operators cannot determine which transform in a chain is causing
failures without correlating with debug-level logs.
# *Capacity planning gap* — Without per-transform record counts, operators
cannot make informed decisions about whether a transform is cost-effective
(e.g., "is this filter dropping 90% of records, or 1%?").
The consumer group protocol and other Kafka subsystems have rich per-component
metrics. The transform layer is a notable gap.
h3. Problem 2: Offset Gap for Sink Connectors When Records Are Skipped
The {{SinkTask.preCommit()}} API allows sink connectors to control which
offsets are committed to Kafka. Many production connectors override this method
to implement exactly-once or at-least-once delivery guarantees with external
systems. These connectors track offsets based on records they receive in
{{put()}} and only return offsets from {{preCommit()}} after durably writing to
the external system.
When transforms or error tolerance drop records before {{{}put(){}}}, these
connectors have *no visibility* into the dropped records. The result:
# *Offset gap* — The consumer position advances past the dropped records, but
the sink's committed offset does not include them.
# *Reprocessing after rebalance* — On rebalance, the consumer seeks to the
last committed offset and re-fetches the dropped records. The transforms drop
them again, and the cycle repeats.
# *Infinite reprocessing loop* — If all records for a partition are dropped,
the sink returns an empty map from {{{}preCommit(){}}}, the framework skips
commit entirely, and no progress is ever made for that partition.
This problem affects widely-used connectors in production:
||Connector||Offset Strategy||Impact||
|*S3 Sink*|Returns offsets only after flushing to S3 (rotation interval /
record count)|Filtered records create offset gaps. Rebalance reprocesses all
records since last S3 flush|
|IcebergSinkConnector|Workers commit offset only after coordinator
signal|Trailing filtered records never committed. Gap grows with each poll
cycle|
|*Any connector* using {{preCommit()}} returning {{{}}}|Full offset opt-out|If
all records filtered, partition offset permanently frozen|
h3. How the Two Problems Relate
The two problems form a *diagnose → fix* pair:
|Per-Transform Metrics (Part A) Skipped Record Notification (Part B)
───────────────────────────── ────────────────────────────────────── DIAGNOSE:
"Which transform is FIX: "The sink can now track offsets dropping records, and
how many?" for records it never received." Operator sees transform-record-
Connector overrides onRecordSkipped() filtered-total climbing on to incorporate
skipped offsets into filterNull transform at 632/sec its preCommit() return
value │ │ └─── Together, they close the loop ──────┘|
Without Part A, operators cannot diagnose *where* records are being dropped.
Without Part B, connectors cannot *act* on records they never received.
Together, they provide complete observability and correctness for the transform
pipeline.
The use of SMTs and Predicates in production has grown significantly since
their introduction. Common patterns that trigger these problems:
* *{{Filter}}* *SMT* — Drop records not matching criteria (e.g., filter by
record type, tenant, region)
* *{{RegexRouter}}* *+* *{{Predicate}}* — Route and filter based on content
* *{{errors.tolerance=all}}* — Tolerate conversion errors, send to DLQ
* *{{HeaderFilter}}* *+* *{{Predicate}}* — Drop records missing required
headers
* *Multi-tenant pipelines* — Filter per-tenant records across shared topics
h4. *Success Criteria*
||Area||Criterion||
|*Observability*|Operators can query per-transform record-in, record-out, and
record-filtered counts via JMX|
|*Observability*|Operators can compute per-transform filter rates for alerting
and dashboards|
|*Correctness*|Sink connectors that override {{preCommit()}} can track offsets
for all consumed records, including those dropped before {{put()}}|
|*Safety*|Zero risk of data loss — the framework never overrides the sink's
offset decisions|
|*Compatibility*|Full backward compatibility — existing connectors work
identically without code changes|
|*Performance*|Zero performance regression for connectors with no transforms
configured|
|*Universality*|Metrics are available for all transforms (built-in and custom)
without any transform code changes|
|*Simplicity*|Connectors need minimal code changes to incorporate skipped
record awareness|
--
This message was sent by Atlassian Jira
(v8.20.10#820010)