feature(Streaming): Add Kafka live-stream wrapper and acceptance test - #29
Open
szemyd wants to merge 3 commits into
Open
feature(Streaming): Add Kafka live-stream wrapper and acceptance test#29szemyd wants to merge 3 commits into
szemyd wants to merge 3 commits into
Conversation
Thin wrapper over confluent-kafka for the live metric streams, plus an acceptance test covering external customer authentication and entitlement scoping. The wrapper builds its config from APERIODIC_KAFKA_* environment variables with no fallback defaults, enforces the <account-id>-<suffix> consumer group rule so callers cannot trip GROUP_AUTHORIZATION_FAILED, and bounds every network call. Redaction is the reason it exists. librdkafka embeds the broker address in almost every error and log line it produces, and writes them to stderr from C where Python-level capture cannot reach. This repository is public, so the wrapper routes librdkafka through a redacting logger and scrubs the endpoint, account id and API key out of its own errors, tracebacks and repr(). Scrubbing is structural as well as literal: a cluster advertises brokers under hostnames the client never configured, which neither literal matching nor GitHub's secret masking can catch. The acceptance test asserts access control rather than message flow — staging metric topics are currently empty, so a consume-based assertion would hang and read as an auth failure. It skips, never fails, when the environment variables are absent, so fork pull requests stay green. Also documents the streaming client for customers in the README, with a runnable example, and records the required CI secret names in CONTRIBUTING.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXodwmrwrhmaruyqBVgNS4
…p-free Two failures from the first CI run on this branch. tests/test_readme.py executes every README python block containing `api_key=`, so the direct-credentials streaming example needs its import inline. Also gives it real-looking placeholders instead of "...". Validate the consumer group id before requiring confluent-kafka, so a bad group id is reported as a bad group id whether or not the optional extra is installed. The matrix jobs do not install it, and were getting ImportError. Constructing a client from the README's placeholder endpoint also exposed a sharp edge in the redactor: a malformed endpoint such as "..." registered "." as a literal, and since redactors are process-global that would have turned every full stop in every scrubbed message into a placeholder. Literals now have to carry an alphanumeric character. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXodwmrwrhmaruyqBVgNS4
The foreign-account-id guard matched any 8 hex characters followed by a hyphen, which rejects ordinary suffixes built from a short commit sha — "a1b2c3d4-worker" is a plausible consumer group name, not someone else's account. Require the whole UUID shape instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXodwmrwrhmaruyqBVgNS4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thin wrapper over
confluent-kafkafor the live metric streams, plus an acceptance test covering the external customer authentication we just shipped.Written to be shareable: the wrapper is documented in the README with a runnable example, so it can go to a trial customer as-is rather than only serving CI.
Secrets to configure
In this repository's settings, before the acceptance job can do anything (it skips cleanly until then). Names only — values belong in the secret store:
APERIODIC_KAFKA_BOOTSTRAPhost:portAPERIODIC_KAFKA_ACCOUNT_IDAPERIODIC_KAFKA_API_KEYUse a dedicated throwaway test account, never a real customer's.
Optional repository variable (not a secret) — when set, the topic listing is checked exactly rather than loosely:
APERIODIC_KAFKA_ENTITLED_DATASETSohlcv,l1_liquidityNo router-side secret is needed — see the companion PR on
dream-faster/unravel-router.The wrapper —
src/aperiodic/streaming/APERIODIC_KAFKA_*with no fallback defaults. A hard-coded fallback in a public repo publishes the value it falls back to. A missing variable raisesStreamConfigErrornaming the variable, never its value.<account-id>-<suffix>consumer group rule. Callers passgroup_suffix=and the prefix is applied, so the most likely mistake — aGROUP_AUTHORIZATION_FAILEDfrom a hand-written group id — cannot be made.stream()holds one consumer open for continuous reading;consume()is the bounded convenience built on it. Repeatedconsume()calls would rejoin the group and rebalance each time, which matters for a customer following a topic.SASL_SSL/SCRAM-SHA-256, system CA store, verification explicitly on and nossl.ca.location.Redaction is the reason it exists
librdkafka embeds the broker address in nearly every error and log line it produces, and writes them to stderr from C, below the reach of Python-level capture. This is a public repo with world-readable CI logs.
Two layers, because one is not enough:
Also: librdkafka is routed through a redacting logger rather than stderr,
__repr__redacts, the config dict is never logged, and translated errors are re-raised withfrom None—from excwould reprint librdkafka's message, broker address and all, in the traceback.Verified against the real thing, not just stubs:
test_forced_connection_failure_leaks_nothingdrives librdkafka into a genuine connection failure against a closed local port and asserts the endpoint appears in no surface — exception, traceback, stdout, stderr (viacapfd, sincecapsyscannot see C-level writes) or log records. librdkafka does emit itsFAILrecords on that path, so the filter is doing real work.The redaction rules are deliberately eager, and one test (
test_useful_diagnostics_survive) holds the other side of that line so error messages stay useful. It caught a real over-match during development:ohlcv.binance-futures.m1was being read as an FQDN and scrubbed to[redacted-host]-futures.m1.The acceptance test —
tests/test_kafka_acceptance.pyAsserts access control, not message flow:
TOPIC_AUTHORIZATION_FAILED.SASL_AUTHENTICATION_FAILED/_AUTHENTICATION.There is no blocking consume assertion. Staging metric topics are currently empty, so one would hang and read as an auth failure. The one read that does happen is short and bounded, and treats "no messages" as a pass —
consume()distinguishes a quiet topic (returns[]) from being refused or never connecting (raises), so an empty topic cannot mask a broken connection.Skips, never fails, when the env vars are absent — that is what keeps fork PRs green.
No assertion embeds the endpoint, account id or API key: pytest prints the operands of a failing assertion, so those comparisons are reduced to a boolean first, and
ClusterInfocarries a broker count rather than broker names for exactly this reason.Workflow —
.github/workflows/kafka-acceptance.yamlTwo jobs.
unit-testsruns the redaction suite with no secrets and no network, and gatesacceptance, which carries the secrets.No
pull_requesttrigger, so fork PRs never reach it; the acceptance job is additionally pinned to this repository so a fork pushing to its ownmaincannot run it. Noset -x, noechoof any secret, and anadd-maskstep registers the values derived from secrets — the bare host without its port, and the account id — since GitHub masks only the literal secret.Verification
pytest tests/test_streaming.py— 50 passedpytest tests/test_kafka_acceptance.py— 7 skipped (no credentials locally, as intended)ruff check .— clean; full suite still collects at 143 testsconfluent-kafkais a new[streaming]extra, kept out of[tests]on purpose — it is a compiled wheel and the existing matrix spans Python versions it does not always ship for.Generated by Claude Code