Skip to main content

Kafka input (your own topics)

Some estates already have every log they care about on a broker. Asking them to run a forwarder that re-publishes those records over HTTP is asking them to run a second pipeline to feed the first one. The ingest service can consume your topics directly.

Mint a token per tenant​

A Kafka record carries no authenticated principal. A tenant read from the payload or from a record header would be set by whoever produced the record, so the tenant comes from a minted ingest token and from nothing else. One source serves one tenant.

curl -sS -X POST "$AISOC_URL/api/v1/inbox/tokens" \
-H "Authorization: Bearer $AISOC_API_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"template_id": "generic-json", "label": "kafka-customer-a"}'

Use whichever template matches the shape on the topic. generic-json is the usual answer; splunk-hec and cef-syslog are there if the topic carries those shapes. The source's template_id must match the one the token was minted with — a mismatch is a permanent failure that names the fix rather than retrying.

Declare the sources​

One JSON array, not numbered environment variables: a deployment reading three tenants' topics needs three of everything, and numbered variables are how a fourth source silently gets the third one's token.

AISOC_KAFKA_INPUT_SOURCES='[
{
"name": "customer-a-security",
"brokers": "broker-1.example.internal:9092,broker-2.example.internal:9092",
"topic": "security.events",
"group_id": "aisoc-ingest-customer-a",
"token": "tok_live_...",
"template_id": "generic-json"
}
]'
FieldRequiredNotes
nameyesWhat readiness, the logs and the metrics call this source
brokersyesComma-separated bootstrap list
topicyesYour topic
group_idnoDefaults to aisoc-ingest-<name>, per source. Two sources sharing a group id on different topics is legal in Kafka and almost never what was meant
tokenyesThe minted ingest token; this is where the tenant comes from
template_idyesMust match the token's template
from_beginningnoDefault false. Replaying a week of a production topic into the detection pipeline is not a thing to do by accident
max_bytesnoFetch cap, default 10 MiB

A malformed array is a hard error at startup that names the problem. It does not start with the input silently off.

TLS is resolved the same way as every other Kafka client in this service; see Secrets. A reader that cannot be built securely in a protected environment is not built at all.

What arrives​

Each record becomes one OCSF event through the template you named. A record that is not JSON is carried as a log line rather than refused — a plain-text topic is a legitimate source, and refusing it would be a consumer that looks attached and drops everything.

Kafka's own metadata rides along as kafka_topic, kafka_partition, kafka_offset, kafka_key and kafka_header_*, so a detection rule can clause on where a record came from. Headers are carried for context and are explicitly not read for a tenant.

What happens when something breaks​

This is a long-lived consumer, and the question that matters is whether the loop can tell a condition that will never resolve from one that might. Three classes, not two:

ClassExampleWhat happens
TransientA broker restarting, a rebalance, a leader electionCapped exponential backoff. A rebalance happens on every deploy; stopping on it would be an outage
PermanentTopicAuthorizationFailed, SASLAuthenticationFailed, an invalid group idThe loop stops and logs the operator action at error level. Retrying a misconfiguration is not progress, it is churn you have to filter out of the logs
PoisonOne record that cannot be decodedSkipped and committed. The subscription is demonstrably working; a poison record that is not committed is re-read forever

A publish failure is the one case that does not commit. Kafka is the only input in this service that can withhold an acknowledgement, so an undelivered record stays undelivered rather than being lost.

A transient failure that outlives two minutes stops being reported as transient and is reported as not resolving — the point at which the operator's next move stops being "wait" and becomes "go and look".

Readiness​

/readyz names every source as kafka_input:<name> with its state, and says something useful when healthy too: a quiet topic is a legitimate state, so the detail carries what it is attached to and how much it has seen. A consumer detached from its topic behind a 200 that never mentions it is indistinguishable from an idle one.

Metrics​

SeriesMeaning
aisoc_ingest_kafka_input_records_total{source}Records accepted
aisoc_ingest_kafka_input_attached{topic}1 while consuming
aisoc_ingest_kafka_input_errors_total{topic,class}By failure class
aisoc_ingest_kafka_input_not_resolving{topic}1 when a fault has outlived a transient explanation

aisoc_ingest_kafka_input_not_resolving is the alertable one.