clickhouse-logger
The clickhouse-logger plugin sends request and response logs to ClickHouse in batches. The plugin can write the default gateway log entry or a custom format configured on the plugin instance or in plugin metadata.
Examples
The examples below configure a current ClickHouse server and verify default and custom gateway log formats.
The examples use an HTTP connection and a fixed local password to keep the workflow reproducible. For production, use TLS, store the ClickHouse password in a secret manager, and keep ssl_verify enabled.
Start ClickHouse with a dedicated database and user. The Docker setup uses a dedicated network so that a running APISIX or API7 Gateway container can reach ClickHouse by container name.
- Docker
- Kubernetes
Set GATEWAY_CONTAINER to the name of the running APISIX or API7 Gateway container. Create a dedicated network and connect the gateway to it:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network create gateway-clickhouse-net
docker network connect gateway-clickhouse-net "$GATEWAY_CONTAINER"
Start ClickHouse on the same network and bind its HTTP API to the host loopback interface:
docker run -d \
--name clickhouse-server \
--network gateway-clickhouse-net \
-p 127.0.0.1:8123:8123 \
-e CLICKHOUSE_DB=apisix_logs \
-e CLICKHOUSE_USER=apisix_logger \
-e CLICKHOUSE_PASSWORD=apisix-logger-pass \
-e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \
--ulimit nofile=262144:262144 \
clickhouse/clickhouse-server:26.8.5.13
Wait until the HTTP API is ready:
until curl -fsS -u "apisix_logger:apisix-logger-pass" \
"http://127.0.0.1:8123/ping" | grep -q "Ok"; do
sleep 2
done
Create a Kubernetes manifest for the ClickHouse deployment and service:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: clickhouse-server
spec:
replicas: 1
selector:
matchLabels:
app: clickhouse-server
template:
metadata:
labels:
app: clickhouse-server
spec:
containers:
- name: clickhouse-server
image: clickhouse/clickhouse-server:26.8.5.13
env:
- name: CLICKHOUSE_DB
value: apisix_logs
- name: CLICKHOUSE_USER
value: apisix_logger
- name: CLICKHOUSE_PASSWORD
value: apisix-logger-pass
- name: CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT
value: "1"
ports:
- containerPort: 8123
readinessProbe:
tcpSocket:
port: 8123
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: clickhouse-server
spec:
selector:
app: clickhouse-server
ports:
- name: http
port: 8123
targetPort: 8123
type: ClusterIP
Apply the manifest and wait for ClickHouse to become ready:
kubectl apply -f clickhouse-deployment.yaml
kubectl rollout status -n aic deployment/clickhouse-server
Log Requests in the Default Format
The following example writes the plugin's default request and response log entry to ClickHouse.
Create a table named default_logs in your ClickHouse database with columns corresponding to your log format:
- Docker
- Kubernetes
curl "http://127.0.0.1:8123" -X POST -d '
CREATE TABLE apisix_logs.default_logs (
host String,
client_ip String,
route_id String,
service_id String,
start_time String,
latency String,
upstream_latency String,
apisix_latency String,
consumer String,
request String,
response String,
server String,
PRIMARY KEY(`start_time`)
)
ENGINE = MergeTree()
' -u "apisix_logger:apisix-logger-pass"
kubectl exec -n aic deploy/clickhouse-server -- clickhouse-client --user apisix_logger --password apisix-logger-pass --query "
CREATE TABLE apisix_logs.default_logs (
host String,
client_ip String,
route_id String,
service_id String,
start_time String,
latency String,
upstream_latency String,
apisix_latency String,
consumer String,
request String,
response String,
server String,
PRIMARY KEY(start_time)
)
ENGINE = MergeTree()
"
Create a route with clickhouse-logger as follows:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes/clickhouse-logger-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/anything/clickhouse",
"plugins": {
"clickhouse-logger": {
"user": "apisix_logger",
"password": "apisix-logger-pass",
"database": "apisix_logs",
"logtable": "default_logs",
"endpoint_addrs": ["http://clickhouse-server:8123"]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
services:
- name: clickhouse-example
routes:
- uris:
- /anything/clickhouse
name: clickhouse-logger-route
plugins:
clickhouse-logger:
user: apisix_logger
password: apisix-logger-pass
database: apisix_logs
logtable: default_logs
endpoint_addrs:
- "http://clickhouse-server:8123"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
ADC reconciles services as desired state. Preview the changes scoped to this example and confirm that they contain no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=clickhouse-logger
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=clickhouse-logger
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: clickhouse-logger-plugin-config
spec:
plugins:
- name: clickhouse-logger
config:
user: apisix_logger
password: apisix-logger-pass
database: apisix_logs
logtable: default_logs
endpoint_addrs:
- "http://clickhouse-server.aic.svc:8123"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: clickhouse-logger-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything/clickhouse
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: clickhouse-logger-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: clickhouse-logger-route
spec:
ingressClassName: apisix
http:
- name: clickhouse-logger-route
match:
paths:
- /anything/clickhouse
methods:
- GET
upstreams:
- name: httpbin-external-domain
plugins:
- name: clickhouse-logger
config:
user: apisix_logger
password: apisix-logger-pass
database: apisix_logs
logtable: default_logs
endpoint_addrs:
- "http://clickhouse-server.aic.svc:8123"
Apply the configuration:
kubectl apply -f clickhouse-logger-ic.yaml
Send a request to the route to generate a log entry:
curl -i "http://127.0.0.1:9080/anything/clickhouse"
You should see an HTTP/1.1 200 OK response.
Send a request to ClickHouse to see the log entries:
- Docker
- Kubernetes
echo 'SELECT host, client_ip, route_id, start_time FROM apisix_logs.default_logs ORDER BY start_time DESC LIMIT 1 FORMAT PrettyCompactMonoBlock' | \
curl "http://127.0.0.1:8123/?" \
-u "apisix_logger:apisix-logger-pass" \
--data-binary @-
kubectl exec -n aic deploy/clickhouse-server -- \
clickhouse-client \
--user apisix_logger \
--password apisix-logger-pass \
--query \
'SELECT host, client_ip, route_id, start_time FROM apisix_logs.default_logs ORDER BY start_time DESC LIMIT 1 FORMAT PrettyCompactMonoBlock'
You should see a log entry similar to the following:
┌─host─┬─client_ip─────┬─route_id────────────────┬─start_time────┐
1. │ │ 192.168.155.1 │ clickhouse-logger-route │ 1789560520546 │
└──────┴───────────────┴─────────────────────────┴───────────────┘
Customize Log Format With Plugin Metadata
The following example demonstrates how you can customize log format using plugin metadata.
Create a table named custom_logs in your ClickHouse database with columns corresponding to your customized log format:
- Docker
- Kubernetes
curl "http://127.0.0.1:8123" -X POST -d '
CREATE TABLE apisix_logs.custom_logs (
host String,
client_ip String,
route_id String,
service_id String,
`@timestamp` String,
PRIMARY KEY(`@timestamp`)
)
ENGINE = MergeTree()
' -u "apisix_logger:apisix-logger-pass"
kubectl exec -n aic deploy/clickhouse-server -- clickhouse-client --user apisix_logger --password apisix-logger-pass --query "
CREATE TABLE apisix_logs.custom_logs (
host String,
client_ip String,
route_id String,
service_id String,
\`@timestamp\` String,
PRIMARY KEY(\`@timestamp\`)
)
ENGINE = MergeTree()
"
Create a route with the clickhouse-logger plugin that is used to forward logs in the specified format to ClickHouse:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes/clickhouse-logger-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/anything/clickhouse",
"plugins": {
"clickhouse-logger": {
"user": "apisix_logger",
"password": "apisix-logger-pass",
"database": "apisix_logs",
"logtable": "custom_logs",
"endpoint_addrs": ["http://clickhouse-server:8123"]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
services:
- name: clickhouse-example
routes:
- uris:
- /anything/clickhouse
name: clickhouse-logger-route
plugins:
clickhouse-logger:
user: apisix_logger
password: apisix-logger-pass
database: apisix_logs
logtable: custom_logs
endpoint_addrs:
- "http://clickhouse-server:8123"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
ADC reconciles services as desired state. Preview the changes scoped to this example and confirm that they contain no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=clickhouse-logger
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=clickhouse-logger
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: clickhouse-logger-plugin-config
spec:
plugins:
- name: clickhouse-logger
config:
user: apisix_logger
password: apisix-logger-pass
database: apisix_logs
logtable: custom_logs
endpoint_addrs:
- "http://clickhouse-server.aic.svc:8123"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: clickhouse-logger-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything/clickhouse
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: clickhouse-logger-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: clickhouse-logger-route
spec:
ingressClassName: apisix
http:
- name: clickhouse-logger-route
match:
paths:
- /anything/clickhouse
methods:
- GET
upstreams:
- name: httpbin-external-domain
plugins:
- name: clickhouse-logger
config:
user: apisix_logger
password: apisix-logger-pass
database: apisix_logs
logtable: custom_logs
endpoint_addrs:
- "http://clickhouse-server.aic.svc:8123"
Apply the configuration:
kubectl apply -f clickhouse-logger-ic.yaml
Configure plugin metadata for clickhouse-logger:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/plugin_metadata/clickhouse-logger" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"log_format": {
"host": "$host",
"client_ip": "$remote_addr",
"route_id": "$route_id",
"service_id": "$service_id",
"@timestamp": "$time_iso8601"
}
}'
Plugin metadata is a global collection and cannot be isolated with a label selector. Export the complete collection before changing this entry:
adc dump -o adc.yaml --with-id \
--include-resource-type plugin_metadata
Add or update the clickhouse-logger entry while preserving every other entry in adc.yaml:
plugin_metadata:
# Keep all other plugin metadata entries from the exported file.
clickhouse-logger:
log_format:
host: "$host"
client_ip: "$remote_addr"
route_id: "$route_id"
service_id: "$service_id"
"@timestamp": "$time_iso8601"
Preview the complete metadata change and confirm that it contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type plugin_metadata
Synchronize the reviewed plugin metadata collection:
adc sync -f adc.yaml \
--include-resource-type plugin_metadata
Add the following entry under spec.pluginMetadata in the complete GatewayProxy manifest used by the deployment:
clickhouse-logger:
log_format:
host: "$host"
client_ip: "$remote_addr"
route_id: "$route_id"
service_id: "$service_id"
"@timestamp": "$time_iso8601"
Apply the updated complete manifest through the deployment's normal Kubernetes or GitOps workflow.
Send a request to the route to generate a log entry:
curl -i "http://127.0.0.1:9080/anything/clickhouse"
You should see an HTTP/1.1 200 OK response.
Send a request to ClickHouse to see the log entries:
- Docker
- Kubernetes
echo 'SELECT host, client_ip, route_id, `@timestamp` FROM apisix_logs.custom_logs ORDER BY `@timestamp` DESC LIMIT 1 FORMAT PrettyCompactMonoBlock' | \
curl "http://127.0.0.1:8123/?" \
-u "apisix_logger:apisix-logger-pass" \
--data-binary @-
kubectl exec -n aic deploy/clickhouse-server -- \
clickhouse-client \
--user apisix_logger \
--password apisix-logger-pass \
--query \
'SELECT host, client_ip, route_id, `@timestamp` FROM apisix_logs.custom_logs ORDER BY `@timestamp` DESC LIMIT 1 FORMAT PrettyCompactMonoBlock'
You should see a log entry similar to the following:
┌─host──────┬─client_ip─────┬─route_id────────────────┬─@timestamp────────────────┐
1. │ 127.0.0.1 │ 192.168.155.1 │ clickhouse-logger-route │ 2026-09-16T12:09:05+00:00 │
└───────────┴───────────────┴─────────────────────────┴───────────────────────────┘