Skip to content

fix(ingestion): decode Kafka sample data per topic and exclude internal topics - #30897

Open
IceS2 wants to merge 5 commits into
mainfrom
fix/kafka-sample-data-and-default-filter
Open

fix(ingestion): decode Kafka sample data per topic and exclude internal topics#30897
IceS2 wants to merge 5 commits into
mainfrom
fix/kafka-sample-data-and-default-filter

Conversation

@IceS2

@IceS2 IceS2 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Three related defects found while exercising the Kafka connector end to end.

Sample data only ever worked for Avro

get_connection built a DeserializingConsumer with AvroDeserializer hard-wired as value.deserializer for every topic, regardless of its schema type. JSON and plain-text payloads failed the Confluent magic-byte check and were dropped:

_VALUE_DESERIALIZATION: Unexpected magic byte 123. This message was not produced with a Confluent Schema Registry serializer

The consumer now hands back raw bytes and decode_message selects the decoder from the topic's own messageSchema.schemaType — which it already had branches for. Confluent framing is stripped before a non-Avro payload is read, so a topic written with a JSON Schema serializer is readable too.

The decode is strict. An undecodable binary payload raises, and the caller already logs a warning and skips that message. That matters: with errors="replace" an Avro payload that cannot be deserialised is stored as mojibake, which is worse than storing nothing.

generateSampleData silently did nothing without a Schema Registry

The consumer was constructed only inside if connection.schemaRegistryURL:, so with no registry consumer_client was None, the poll was skipped, and an empty TopicSampleData was yielded — no warning, toggle apparently honoured. Decoupling the decoder from the consumer removes the registry from that path entirely.

Two smaller things fell out: admin_client_config was the same dict object as consumer_config, so consumer-only settings leaked into it; it is now a copy. And with a plain Consumer, poll() returns messages carrying .error() instead of raising, so that is now checked rather than treated as data.

AvroDeserializer was being constructed once per message; it is now cached per schema in a bounded LRU.

Kafka's internal topics were ingested as entities

get_topic_list returns everything the broker knows and topicFilterPattern had no default, so _schemas and __consumer_offsets became Topic entities, each emitting a schema-registry 404 per run. The connection schema already had the field titled "Default Topic Filter Pattern" — it just had no default, unlike ten database connectors that ship excludes: ["^information_schema$"].

Added to kafkaConnection.json and redpandaConnection.json:

"default": { "includes": [], "excludes": ["^__.*", "^_schemas$", "^_confluent.*"] }

Deliberately narrow — no blanket ^_.*, since a single leading underscore is legal in user topic names, and not connect-*, whose names are customer-configurable.

No migration, and no risk to existing services. Ingestion never reads the connection-level pattern; it reads sourceConfig.topicFilterPattern. The only consumer of the connection-level value is the UI, which seeds it into a new agent's form (IngestionConfigUtils.ts, isEditMode === false) and ignores it when editing. Existing agents keep their stored config, so nothing starts getting soft-deleted.

Verification

Live against a two-broker cluster with a Schema Registry, comparing before and after on the same seeded topics:

Topic Schema Before After
om_orders_avro Avro 10 10
analytics_clicks_avro Avro 10 10
om_customers_json JSON Schema 0 8
om_logs_noschema none 0 6

With the registry removed from the config, the JSON and plain-text topics still return sample data (previously nothing at all was collected), and the Avro topics return nothing plus a clear per-message warning rather than unreadable text.

Unit tests cover per-schema-type decoding, Confluent framing, the deserializer cache, strict decoding of binary payloads, and that a consumer is built with no registry configured while the admin client does not inherit consumer-only settings. tests/unit/source/messaging, tests/unit/sampler and tests/unit/topology are green.

Redpanda shares get_connection and CommonBrokerSource, so it is fixed by the same change.

…al topics

Sample data only ever worked for Avro. The consumer was built with
AvroDeserializer hard-wired as its value deserializer for every topic, so JSON
and plain-text payloads failed the Confluent magic-byte check and were dropped
with a warning. The consumer was also created only when a schema registry was
configured, which meant generateSampleData silently did nothing without one.

The consumer now hands back raw bytes and decode_message picks the decoder from
the topic's own schema type, which it already knew how to do. That removes the
registry from the consumer's construction path and makes every schema type work.
Confluent framing is stripped before a non-Avro payload is read, and the decode
is strict so an undecodable binary payload is skipped by the caller rather than
stored as mojibake. Avro deserializers are cached per schema in a bounded LRU,
since decode_message runs once per sampled message.

Verified against a two-broker cluster with a registry: all four decodable topics
now return sample data where only the two Avro ones did before. Without a
registry, JSON and text topics return data and Avro topics return nothing with a
clear per-message warning.

Also give topicFilterPattern a default that excludes Kafka's own internal topics.
Without it _schemas and __consumer_offsets are ingested as Topic entities and
each emits a schema-registry 404 per run. Ingestion never reads the
connection-level pattern; the UI seeds it into new agents only, so existing
agents keep their stored config and nothing is soft-deleted.
Copilot AI review requested due to automatic review settings August 3, 2026 20:10
@IceS2
IceS2 requested a review from a team as a code owner August 3, 2026 20:10
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added Ingestion safe to test Add this label to run secure Github workflows on PRs labels Aug 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes Kafka/Redpanda ingestion sample-data decoding by switching to raw-byte consumption and decoding per-topic schema type, while also preventing Kafka internal topics from being default-ingested via a new UI-seeded default topicFilterPattern.

Changes:

  • Update Kafka/Redpanda connection creation to always build a raw-byte Consumer (no hard-wired Avro deserializer) and isolate admin vs consumer configs.
  • Implement strict per-schema-type decoding in CommonBrokerSource, including Confluent framing stripping for non-Avro payloads and a bounded Avro deserializer cache.
  • Add default topicFilterPattern exclusions for Kafka internal topics to Kafka/Redpanda connection schemas, with unit tests for decoding and connection construction.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
openmetadata-spec/src/main/resources/json/schema/entity/services/connections/messaging/redpandaConnection.json Adds default topic filter exclusions to avoid internal topics being seeded into new configs.
openmetadata-spec/src/main/resources/json/schema/entity/services/connections/messaging/kafkaConnection.json Same default topic filter exclusions for Kafka connections.
ingestion/tests/unit/source/messaging/test_common_broker_source.py Adds unit tests for strict decoding behavior, Confluent framing stripping, and Avro deserializer caching.
ingestion/tests/unit/source/messaging/kafka/test_connection.py Adds unit tests ensuring a consumer is created without Schema Registry and admin config doesn’t inherit consumer-only settings.
ingestion/src/metadata/ingestion/source/messaging/kafka/connection.py Switches from DeserializingConsumer to raw Consumer, decoupling sample collection from Schema Registry and separating admin config.
ingestion/src/metadata/ingestion/source/messaging/common_broker_source.py Adds framing stripping + strict decoding, Avro deserializer LRU cache, and adjusts polling/error handling.

Comment thread ingestion/tests/unit/source/messaging/test_common_broker_source.py
…r-only payload

Review feedback.

strip_confluent_framing required the payload to be longer than the header, so a
message consisting of framing plus an empty body kept its header and then failed
to decode. It now strips at exactly the header length.

The poll loop warned on every message carrying an error, including _PARTITION_EOF.
That marker never appears under the default config, but consumerConfig accepts
arbitrary librdkafka keys, so a user who sets enable.partition.eof would get one
spurious warning per partition per topic. EOF now ends that partition quietly and
polling continues, since other partitions may still have data. Verified against a
two-broker cluster with enable.partition.eof on: no warnings, and sample data
still collected on every topic.

Also corrected a test docstring that described the Avro path while asserting on
the non-Avro one.
Copilot AI review requested due to automatic review settings August 3, 2026 21:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

ingestion/src/metadata/ingestion/source/messaging/common_broker_source.py:328

  • decode_message strips the Confluent framing for all non-Avro records (including SchemaType.Other). That can violate the intended “strict decode” behavior: a binary payload that would fail UTF-8 decoding (and be skipped) may become decodable after the first 5 bytes are removed, causing corrupted sample data to be stored. Stripping should be limited to schema types that actually use Confluent framing (e.g. JSON Schema).
        # Strict: a binary payload we cannot type (Avro with no registry configured)
        # must be skipped by the caller, not stored as mojibake.
        return strip_confluent_framing(bytes(record)).decode("utf-8")

ingestion/src/metadata/ingestion/source/messaging/kafka/connection.py:104

  • get_connection now always constructs a confluent_kafka.Consumer (even when sample data is disabled). However, the only consumer_client.close() in the codebase is guarded by generate_sample_data in CommonBrokerSource.close(), so in non-sample-data runs this consumer will never be closed. This expands an existing lifecycle gap and can leak sockets/background threads over long-running ingestion processes. Consider registering consumer_client.close() in the connection lifecycle (e.g., via BaseConnection._on_close(...) in KafkaConnection._get_client) or lazily creating the consumer only when sample data is enabled.
    consumer_config["bootstrap.servers"] = connection.bootstrapServers
    consumer_config.setdefault("group.id", "openmetadata-consumer")
    consumer_config.setdefault("auto.offset.reset", "largest")
    consumer_config["enable.auto.commit"] = False
    consumer_client = Consumer(consumer_config)

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit 962b7c0e942c78761b485a1efe9701090476aaa6 in Playwright run 30858614338, attempt 1.

✅ 667 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 3 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 44m 25s

⏱️ Max setup 3m 32s · max shard execution 18m 28s · max shard-job elapsed before upload 22m 29s · reporting 6s

🌐 214.34 requests/attempt · 2.67 app boots/UI scenario · 10.74% common-shard skew

Optimization targets still in progress:

  • Browser traffic was 214.34 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.67 per UI scenario (1860 boots / 697 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 142 0 0 0 0 0
✅ Shard chromium-02 175 0 0 3 0 0
✅ Shard chromium-03 144 0 0 0 0 0
✅ Shard chromium-04 147 0 0 0 0 0
✅ Shard ingestion-01 25 0 0 0 0 0
✅ Shard ingestion-02 34 0 0 0 0 0

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Copilot AI review requested due to automatic review settings August 3, 2026 22:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

IceS2 added 2 commits August 4, 2026 00:24
…ients

Constructing a KafkaSource runs get_connection for real, so this test created a
librdkafka AdminClient that background-dialled localhost:9092. Building the
consumer unconditionally, as this branch now does, made it two clients rather
than one.

Patching both constructors keeps the test on the SSL wiring it actually asserts.
This was the only place in tests/unit that built a real messaging client.
…:open-metadata/OpenMetadata into fix/kafka-sample-data-and-default-filter
Copilot AI review requested due to automatic review settings August 3, 2026 22:25
@gitar-bot

gitar-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 1 resolved / 1 findings

Decouples sample data decoding from the schema registry to support JSON and plain-text payloads, caches Avro deserializers, and excludes internal Kafka topics. Addresses the strip_confluent_framing finding.

✅ 1 resolved
Edge Case: strip_confluent_framing may corrupt unframed payloads starting with NUL

📄 ingestion/src/metadata/ingestion/source/messaging/common_broker_source.py:64-68 📄 ingestion/src/metadata/ingestion/source/messaging/common_broker_source.py:326
strip_confluent_framing only checks that the first byte is 0x00 and length > 5 before dropping 5 bytes. A non-Confluent JSON/text payload that legitimately begins with a NUL byte (or any binary-ish payload the caller still tries to utf-8 decode) would have its first 5 bytes silently removed, producing corrupted sample data rather than a clean skip. Confluent framing also encodes a schema id; if feasible, additionally require the payload to be non-empty after stripping and consider validating that the schema-id bytes are plausible, or accept this as a rare, low-impact heuristic given real JSON/text virtually never starts with 0x00.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar | Powered by Gitar — free for open source

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

ingestion/src/metadata/ingestion/source/messaging/kafka/connection.py:104

  • get_connection now always instantiates a confluent_kafka.Consumer. However MessagingServiceSource.close() only calls self._connection.close() (BaseConnection), and KafkaConnection does not register any _on_close teardowns for the underlying KafkaClient. This means the Consumer can remain unclosed (background threads/sockets) for the lifetime of the process, and the impact is now unconditional even when sample data is disabled.

Consider registering a teardown in KafkaConnection._get_client() (e.g., self._on_close(client.consumer_client.close)) or otherwise ensuring the Consumer is created only when needed and always closed via BaseConnection’s lifecycle.

    consumer_config["bootstrap.servers"] = connection.bootstrapServers
    consumer_config.setdefault("group.id", "openmetadata-consumer")
    consumer_config.setdefault("auto.offset.reset", "largest")
    consumer_config["enable.auto.commit"] = False
    consumer_client = Consumer(consumer_config)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ingestion safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants