Skip to main content

loki-logger

The loki-logger plugin sends gateway request and response logs to Grafana Loki in batches through the Loki HTTP API. The plugin serializes each log entry as JSON and supports customized log fields and labels. This integration centralizes gateway logs for querying and visualization in Grafana.

Examples​

The examples below configure the loki-logger plugin for common logging scenarios.

To follow the examples, start Loki and Grafana. In Docker, the gateway, Loki, and Grafana share a dedicated network so that the containers can reach one another by name.

The following Loki and Grafana deployments are intended for evaluation. Follow Grafana's Loki deployment guidance and Grafana security guidance for production environments.

Set GATEWAY_CONTAINER to the name of the running APISIX or API7 Gateway container. Create a dedicated Docker network and connect the gateway to it:

export GATEWAY_CONTAINER=replace-with-gateway-container-name

docker network create gateway-loki-net
docker network connect gateway-loki-net "$GATEWAY_CONTAINER"

Download the configuration that matches the pinned Loki release:

curl -fsSL \
"https://raw.githubusercontent.com/grafana/loki/v3.7.6/cmd/loki/loki-local-config.yaml" \
-o loki-config.yaml

Start Loki on the shared network:

docker run -d \
--name loki \
--network gateway-loki-net \
-v "$PWD/loki-config.yaml:/etc/loki/local-config.yaml:ro" \
-p 127.0.0.1:3100:3100 \
grafana/loki:3.7.6 \
-config.file=/etc/loki/local-config.yaml

Wait for Loki to become ready:

until curl -fsS "http://127.0.0.1:3100/ready" > /dev/null; do
sleep 2
done

Start Grafana on the same network:

docker run -d \
--name grafana \
--network gateway-loki-net \
-p 127.0.0.1:3000:3000 \
grafana/grafana:13.2.1

Open Grafana at http://localhost:3000 and sign in with the initial username and password admin. Go to Connections → Add new connection, select Loki, and add a new data source. Set the URL to http://loki:3100, select Save & test, and verify that the connection succeeds.

Log Requests and Responses in Default Log Format​

This example logs requests and responses that pass through a route using the default log format.

Create a route with the loki-logger plugin and configure the address of Loki:

curl "http://127.0.0.1:9180/apisix/admin/routes/loki-logger-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/anything",
"plugins": {
"loki-logger": {
"endpoint_addrs": ["http://loki:3100"]
}
},
"upstream": {
"nodes": {
"httpbin.org:80": 1
},
"type": "roundrobin"
}
}'

❶ endpoint_addrs: Loki base URLs reachable from the gateway.

Send a few requests to the route to generate log entries:

curl "http://127.0.0.1:9080/anything"

You should receive HTTP/1.1 200 OK responses for all requests.

Open the Grafana Explore view and run the LogQL query {job="apisix"}. You should see log entries for the requests, including an entry similar to the following:

{
"route_id": "loki-logger-route",
"response": {
"status": 200,
"headers": {
"date": "Wed, 16 Sep 2026 12:16:13 GMT",
"server": "APISIX/3.18.0",
"access-control-allow-credentials": "true",
"content-length": "399",
"access-control-allow-origin": "*",
"content-type": "application/json",
"connection": "close"
},
"size": 627
},
"start_time": 1789560971406,
"client_ip": "192.168.155.1",
"service_id": "",
"apisix_latency": 1340.0001735687,
"upstream": "34.198.63.32:80",
"upstream_latency": 753,
"server": {
"hostname": "dd2886d0b7bf",
"version": "3.18.0"
},
"request": {
"headers": {
"user-agent": "curl/8.7.1",
"accept": "*/*",
"host": "127.0.0.1:9080"
},
"size": 85,
"method": "GET",
"url": "http://127.0.0.1:9080/anything",
"querystring": {},
"uri": "/anything"
},
"latency": 2093.0001735687
}

This verifies that Loki receives logs from the gateway. You can also create Grafana dashboards to visualize and analyze the logs.

Customize Log Format with Plugin Metadata​

This example uses plugin metadata to customize the log format for every loki-logger instance that does not define its own format.

Create a route with the loki-logger plugin:

curl "http://127.0.0.1:9180/apisix/admin/routes/loki-logger-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/anything",
"plugins": {
"loki-logger": {
"endpoint_addrs": ["http://loki:3100"]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org": 1
}
}
}'

Configure plugin metadata for loki-logger to set the shared log format:

curl "http://127.0.0.1:9180/apisix/admin/plugin_metadata/loki-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",
"@timestamp": "$time_iso8601"
}
}'

Send a request to the route to generate a new log entry:

curl -i "http://127.0.0.1:9080/anything"

You should receive an HTTP/1.1 200 OK response.

Open the Grafana Explore view and run the LogQL query {job="apisix"}. You should see an entry similar to the following:

{
"@timestamp":"2026-09-16T12:16:38+00:00",
"client_ip":"192.168.155.1",
"route_id":"loki-logger-route",
"host":"127.0.0.1"
}

If the plugin on a route specifies a specific log format, it will take precedence over the log format specified in the plugin metadata. For instance, update the plugin on the previous route as such:

curl "http://127.0.0.1:9180/apisix/admin/routes/loki-logger-route" -X PATCH \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"plugins": {
"loki-logger": {
"log_format": {
"route_id": "$route_id",
"client_ip": "$remote_addr",
"@timestamp": "$time_iso8601"
}
}
}
}'

Send a request to the route to generate a new log entry:

curl -i "http://127.0.0.1:9080/anything"

You should receive an HTTP/1.1 200 OK response.

Return to the Grafana Explore view and rerun the LogQL query {job="apisix"}. The new entry uses the format configured on the route:

{
"client_ip":"192.168.155.1",
"route_id":"loki-logger-route",
"@timestamp":"2026-09-16T12:16:39+00:00"
}

Log Request Bodies Conditionally​

The following example conditionally logs request bodies in the default log format. The route configurations below remove the per-route custom format. Remove the shared plugin metadata before continuing:

Delete the loki-logger plugin metadata:

curl "http://127.0.0.1:9180/apisix/admin/plugin_metadata/loki-logger" -X DELETE \
-H "X-API-KEY: ${ADMIN_API_KEY}"

Create a route with the loki-logger plugin:

curl "http://127.0.0.1:9180/apisix/admin/routes/loki-logger-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/anything",
"plugins": {
"loki-logger": {
"endpoint_addrs": ["http://loki:3100"],
"include_req_body": true,
"include_req_body_expr": [["arg_log_body", "==", "yes"]]
}
},
"upstream": {
"nodes": {
"httpbin.org:80": 1
},
"type": "roundrobin"
}
}'

❶ include_req_body: set to true to include request body.

❷ include_req_body_expr: only include request body if the URL query string log_body is yes.

Send a request to the route with a URL query string satisfying the condition:

curl -i "http://127.0.0.1:9080/anything?log_body=yes" -X POST -d '{"env": "dev"}'

Open the Grafana Explore view and run the LogQL query {job="apisix"}. The matching log entry includes the request body:

{
"route_id": "loki-logger-route",
"request": {
"body": "{\"env\": \"dev\"}",
"size": 182,
"method": "POST",
"url": "http://127.0.0.1:9080/anything?log_body=yes",
"querystring": {
"log_body": "yes"
},
"uri": "/anything?log_body=yes"
}
}

Send a request to the route without any URL query string:

curl -i "http://127.0.0.1:9080/anything" -X POST -d '{"env": "dev"}'

Rerun the LogQL query {job="apisix"}. The new log entry does not include the request body:

{
"route_id": "loki-logger-route",
"request": {
"size": 169,
"method": "POST",
"url": "http://127.0.0.1:9080/anything",
"querystring": {},
"uri": "/anything"
}
}
info

Custom log formats do not add collected request or response bodies automatically. Include the corresponding variables in the format:

{
"include_req_body": true,
"include_resp_body": true,
"log_format": {
"request_body": "$request_body",
"response_body": "$resp_body"
}
}

Body size limits still apply. Use log_format_extra to add custom fields without replacing the default log entry.