Send request logs to Kafka with the Kafka Log Policy
Create a global Kafka Log Policy that points at your Kafka brokers and a log topic. The Policy publishes a log entry to the topic for every request, whatever the response status.
Prerequisites
AI Gateway running
This is a Konnect tutorial and requires a Konnect personal access token.
-
Create a new personal access token by opening the Konnect PAT page and selecting Generate Token.
-
Export your token to an environment variable:
export KONNECT_TOKEN='YOUR_KONNECT_PAT' -
Run the AI Gateway quickstart script to automatically provision a control plane and data plane in Kong Konnect, and configure your environment:
curl -Ls https://get.konghq.com/ai | bash -s -- -k $KONNECT_TOKEN
This sets up a AI Gateway control plane named ai-quickstart, provisions a local data plane, and prints out the following environment variables export:
export AI_GATEWAY_ID=your-gateway-id
export KONNECT_TOKEN=$KONNECT_TOKEN
export KONNECT_CONTROL_PLANE_NAME=ai-quickstart
export KONNECT_CONTROL_PLANE_URL=https://us.api.konghq.com
export KONNECT_PROXY_URL='http://localhost:8000'Copy and paste these into your terminal to configure your session.
kongctl v1.21.0+
This tutorial uses kongctl to manage Konnect resources programmatically. We recommend keeping kongctl up to date with the latest version (1.21.0).
- Install kongctl from developer.konghq.com/kongctl.
-
Verify the installation:
kongctl version
kafkactl
This tutorial uses kafkactl to create topics and interact with them. Install it:
For other installation methods, see the kafkactl installation instructions.
jq
This tutorial uses jq to format JSON output. Install it:
For other installation methods, see the jq download page.
Create the working directory and SASL credentials
Create a working directory and a JAAS file that defines the SASL/PLAIN user for the broker:
mkdir -p ~/kafka-log-test && cd ~/kafka-log-test
cat <<'EOF' > kafka_server_jaas.conf
KafkaServer {
org.apache.kafka.common.security.plain.PlainLoginModule required
username="kafka_user"
password="kafka-password"
user_kafka_user="kafka-password";
};
EOFStart a Kafka broker
Run a single-node KRaft broker with two listeners:
- SASL on port
9092, advertised ashost.docker.internal. The data plane connects to this listener. - Plaintext on port
9094, advertised aslocalhost. Local tooling connects to this listener, becausehost.docker.internaldoesn’t resolve from the host itself.
cat <<'EOF' > docker-compose.yaml
name: kafka_aigw
services:
kafka:
image: apache/kafka:4.3.1
container_name: kafka
ports:
- "9092:9092"
- "9094:9094"
environment:
KAFKA_NODE_ID: 0
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_CONTROLLER_QUORUM_VOTERS: 0@localhost:9093
KAFKA_LISTENERS: CONTROLLER://localhost:9093,SASL_PLAINTEXT://0.0.0.0:9092,PLAINTEXT://0.0.0.0:9094
KAFKA_ADVERTISED_LISTENERS: SASL_PLAINTEXT://host.docker.internal:9092,PLAINTEXT://localhost:9094
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT,PLAINTEXT:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_SASL_ENABLED_MECHANISMS: PLAIN
KAFKA_OPTS: -Djava.security.auth.login.config=/etc/kafka/kafka_server_jaas.conf
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_CLUSTER_ID: 'abcdefghijklmnopqrstuv'
KAFKA_LOG_DIRS: /tmp/kraft-combined-logs
volumes:
- ./kafka_server_jaas.conf:/etc/kafka/kafka_server_jaas.conf
EOF
docker compose up -dVerify SASL authentication
Run the SASL check from a separate container so it uses the same network path as the data plane:
cat <<'EOF' > client.properties
security.protocol=SASL_PLAINTEXT
sasl.mechanism=PLAIN
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="kafka_user" password="kafka-password";
EOF
docker run --rm \
-v "$PWD/client.properties:/tmp/client.properties" \
apache/kafka:4.3.1 \
/opt/kafka/bin/kafka-broker-api-versions.sh \
--bootstrap-server host.docker.internal:9092 \
--command-config /tmp/client.propertiesThe output lists the broker API versions, including SaslHandshake(17) and SaslAuthenticate(36), without a SaslAuthenticationException.
Create the Kafka log topic
Create a kafkactl context that connects through the plaintext listener, then create the kong-log topic:
cat <<'EOF' > kafkactl.yaml
contexts:
direct:
brokers:
- localhost:9094
EOF
kafkactl -C kafkactl.yaml --context direct create topic kong-logStart an HTTP upstream
The AI Agent needs an HTTP upstream to proxy requests to. Start an nginx container to serve that role:
docker run -d --name log-upstream -p 8080:80 nginx:alpineConfirm that nginx responds on port 8080:
curl -i http://localhost:8080Create a Kafka Log Policy and an AI Agent
Create a global Kafka Log Policy, which logs every request, and an AI Agent that supplies a Route to send requests through.
The Agent’s config.url points at the nginx container, so a GET request returns a 200.
This example sets security.ssl to false so the test setup doesn’t need certificates.
We don’t recommend this in production because the traffic isn’t encrypted.
cat <<'EOF' > kafka-log.yaml
_defaults:
kongctl:
namespace: kafka-log-test
ai_gateway_policies:
- ref: kafka-log
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: kafka-log
display_name: Kafka Log
type: kafka-log
enabled: true
global: true
config:
bootstrap_servers:
- host: host.docker.internal
port: 9092
topic: kong-log
authentication:
strategy: sasl
mechanism: PLAIN
user: kafka_user
password: kafka-password
security:
ssl: false
ai_gateway_agents:
- ref: kafka-log-agent
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: kafka-log-agent
display_name: "Kafka Log Agent"
type: http
enabled: true
config:
url: http://host.docker.internal:8080
route:
paths:
- /log-test
protocols:
- http
- https
methods:
- GET
- POST
strip_path: true
EOF
kongctl sync -f kafka-log.yaml --pat "$KONNECT_TOKEN"Validate
Send requests first, then read the topic.
-
Wait a few seconds for the data plane to pick up the new configuration. Requests sent before the data plane picks up the configuration return
404. Then send aGETrequest to the Agent’s Route to generate a log entry:curl -s -o /dev/null "$KONNECT_PROXY_URL/log-test" -
Send a
POSTrequest with a JSON body to generate a second log entry:curl -s -o /dev/null -X POST "$KONNECT_PROXY_URL/log-test" \ -H 'Content-Type: application/json' \ -d '{"hello":"world"}' -
Read the log entries from the topic. Because
config.producer_asyncdefaults totrue, entries can take a second or two to appear:kafkactl -C kafkactl.yaml --context direct consume kong-log --from-beginning --exit | jq .The
GETis logged withresponse.status: 200. ThePOSTis logged with405, because nginx rejects aPOSTto a static file. The Policy logs both requests regardless of the response status.The following is a trimmed log entry:
{ "source": "kong", "client_ip": "192.168.97.1", "started_at": 1787579347089, "workspace_name": "default", "upstream_uri": "/", "upstream_status": "200", "request": { "method": "GET", "uri": "/log-test", "url": "http://localhost:8000/log-test", "size": 86, "querystring": {}, "headers": { "host": "localhost:8000", "user-agent": "curl/8.17.0", "accept": "*/*" } }, "response": { "status": 200, "size": 853, "headers": { "content-type": "text/html", "server": "kong/2.0.2-ai-gateway" } }, "latencies": { "kong": 1, "proxy": 4, "request": 5 }, "route": { "name": "kafka-log-agent-route", "paths": ["/log-test"], "methods": ["GET", "POST"], "strip_path": true }, "service": { "name": "kafka-log-agent", "host": "host.docker.internal", "port": 8080, "protocol": "http" }, "tries": [] }
Cleanup
Stop the Kafka broker and the HTTP upstream
Stop the Kafka broker:
docker compose downRemove the nginx container:
docker rm -f log-upstreamClean up AI Gateway resources
To clean up all AI Gateway resources created in this guide, run:
curl -Ls https://get.konghq.com/ai | bash -s -- -d