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.
- Docker
- Kubernetes
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.
Create a Kubernetes manifest for Loki, Grafana, and the Grafana data source:
apiVersion: v1
kind: ConfigMap
metadata:
namespace: aic
name: loki-config
data:
loki-config.yaml: |
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
path_prefix: /tmp/loki
storage:
filesystem:
chunks_directory: /tmp/loki/chunks
rules_directory: /tmp/loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
limits_config:
metric_aggregation_enabled: true
enable_multi_variant_queries: true
pattern_ingester:
enabled: true
metric_aggregation:
loki_address: localhost:3100
ruler:
alertmanager_url: http://localhost:9093
frontend:
encoding: protobuf
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: loki
spec:
replicas: 1
selector:
matchLabels:
app: loki
template:
metadata:
labels:
app: loki
spec:
containers:
- name: loki
image: grafana/loki:3.7.6
args:
- -config.file=/mnt/config/loki-config.yaml
ports:
- containerPort: 3100
readinessProbe:
httpGet:
path: /ready
port: 3100
volumeMounts:
- name: config
mountPath: /mnt/config
volumes:
- name: config
configMap:
name: loki-config
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: loki
spec:
selector:
app: loki
ports:
- port: 3100
targetPort: 3100
---
apiVersion: v1
kind: ConfigMap
metadata:
namespace: aic
name: grafana-datasources
data:
loki.yaml: |
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki.aic.svc:3100
isDefault: true
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: grafana
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
containers:
- name: grafana
image: grafana/grafana:13.2.1
ports:
- containerPort: 3000
readinessProbe:
httpGet:
path: /api/health
port: 3000
volumeMounts:
- name: datasources
mountPath: /etc/grafana/provisioning/datasources
volumes:
- name: datasources
configMap:
name: grafana-datasources
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: grafana
spec:
selector:
app: grafana
ports:
- port: 3000
targetPort: 3000
Apply the manifest to your cluster:
kubectl apply -f loki-deployment.yaml
Wait for both deployments to become ready:
kubectl rollout status deployment/loki -n aic
kubectl rollout status deployment/grafana -n aic
In a separate terminal, forward the Grafana service to your local machine:
kubectl port-forward -n aic service/grafana 3000:3000
Open Grafana at http://localhost:3000 and sign in with the initial username and password admin. The provisioned Loki data source uses the in-cluster address http://loki.aic.svc:3100.
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:
- Admin API
- ADC
- Ingress Controller
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"
}
}'
services:
- name: httpbin
routes:
- uris:
- /anything
name: loki-logger-route
plugins:
loki-logger:
endpoint_addrs:
- "http://loki:3100"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
ADC applies the label selector to the local service and reconciles only remote services with the same label. Preview the scoped changes and confirm that they contain no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=loki-logger
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=loki-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: loki-logger-plugin-config
spec:
plugins:
- name: loki-logger
config:
endpoint_addrs:
- "http://loki.aic.svc:3100"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: loki-logger-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: loki-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: loki-logger-route
spec:
ingressClassName: apisix
http:
- name: loki-logger-route
match:
paths:
- /anything
methods:
- GET
upstreams:
- name: httpbin-external-domain
plugins:
- name: loki-logger
config:
endpoint_addrs:
- "http://loki.aic.svc:3100"
Apply the configuration:
kubectl apply -f loki-logger-ic.yaml
❶ 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:
- Admin API
- ADC
- Ingress Controller
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
}
}
}'
services:
- name: httpbin
routes:
- uris:
- /anything
name: loki-logger-route
plugins:
loki-logger:
endpoint_addrs:
- "http://loki:3100"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
ADC applies the label selector to the local service and reconciles only remote services with the same label. Preview the changes for 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=loki-logger
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=loki-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: loki-logger-plugin-config
spec:
plugins:
- name: loki-logger
config:
endpoint_addrs:
- "http://loki.aic.svc:3100"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: loki-logger-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: loki-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: loki-logger-route
spec:
ingressClassName: apisix
http:
- name: loki-logger-route
match:
paths:
- /anything
methods:
- GET
upstreams:
- name: httpbin-external-domain
plugins:
- name: loki-logger
config:
endpoint_addrs:
- "http://loki.aic.svc:3100"
Apply the configuration:
kubectl apply -f loki-logger-ic.yaml
Configure plugin metadata for loki-logger to set the shared log format:
- Admin API
- ADC
- Ingress Controller
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"
}
}'
Export the complete plugin metadata collection:
adc dump -o adc.yaml --with-id --include-resource-type plugin_metadata
Add or update this entry under the exported plugin_metadata mapping while preserving every other entry:
plugin_metadata:
loki-logger:
log_format:
host: "$host"
client_ip: "$remote_addr"
route_id: "$route_id"
"@timestamp": "$time_iso8601"
Preview the complete plugin metadata collection 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:
adc sync -f adc.yaml --include-resource-type plugin_metadata
apiVersion: apisix.apache.org/v1alpha1
kind: GatewayProxy
metadata:
namespace: aic
name: apisix-config
spec:
provider:
type: ControlPlane
controlPlane:
# ...
# your control plane connection configuration
pluginMetadata:
loki-logger:
log_format:
host: "$host"
client_ip: "$remote_addr"
route_id: "$route_id"
"@timestamp": "$time_iso8601"
Apply the configuration:
kubectl apply -f gatewayproxy.yaml
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:
- Admin API
- ADC
- Ingress Controller
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"
}
}
}
}'
Update adc.yaml to add a per-route log_format to the loki-logger plugin:
services:
- name: httpbin
routes:
- uris:
- /anything
name: loki-logger-route
plugins:
loki-logger:
endpoint_addrs:
- "http://loki:3100"
log_format:
route_id: "$route_id"
client_ip: "$remote_addr"
"@timestamp": "$time_iso8601"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Preview the scoped service changes and confirm that they contain no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=loki-logger
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=loki-logger
- Gateway API
- APISIX CRD
Update loki-logger-ic.yaml to add a per-route log_format to the PluginConfig:
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: loki-logger-plugin-config
spec:
plugins:
- name: loki-logger
config:
endpoint_addrs:
- "http://loki.aic.svc:3100"
log_format:
route_id: "$route_id"
client_ip: "$remote_addr"
"@timestamp": "$time_iso8601"
Update loki-logger-ic.yaml to add a per-route log_format to the ApisixRoute:
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: loki-logger-route
spec:
ingressClassName: apisix
http:
- name: loki-logger-route
match:
paths:
- /anything
methods:
- GET
upstreams:
- name: httpbin-external-domain
plugins:
- name: loki-logger
config:
endpoint_addrs:
- "http://loki.aic.svc:3100"
log_format:
route_id: "$route_id"
client_ip: "$remote_addr"
"@timestamp": "$time_iso8601"
Apply the updated configuration:
kubectl apply -f loki-logger-ic.yaml
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:
- Admin API
- ADC
- Ingress Controller
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}"
Export the complete plugin metadata collection:
adc dump -o adc-metadata.yaml --with-id \
--include-resource-type plugin_metadata
Remove the loki-logger entry from adc-metadata.yaml while preserving every other plugin metadata entry. Preview the complete collection and confirm that no unrelated changes are included:
adc diff -f adc-metadata.yaml \
--include-resource-type plugin_metadata
Synchronize the reviewed collection:
adc sync -f adc-metadata.yaml \
--include-resource-type plugin_metadata
Remove the loki-logger entry from spec.pluginMetadata in the complete GatewayProxy manifest. Preserve every other metadata entry and the existing provider configuration, then apply the updated manifest through the deployment's normal Kubernetes or GitOps workflow.
Create a route with the loki-logger plugin:
- Admin API
- ADC
- Ingress Controller
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"
}
}'
services:
- name: httpbin
routes:
- uris:
- /anything
name: loki-logger-route
plugins:
loki-logger:
endpoint_addrs:
- "http://loki:3100"
include_req_body: true
include_req_body_expr:
- - "arg_log_body"
- "=="
- "yes"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
ADC applies the label selector to the local service and reconciles only remote services with the same label. Preview the changes for 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=loki-logger
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=loki-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: loki-logger-plugin-config
spec:
plugins:
- name: loki-logger
config:
endpoint_addrs:
- "http://loki.aic.svc:3100"
include_req_body: true
include_req_body_expr:
- - "arg_log_body"
- "=="
- "yes"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: loki-logger-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: loki-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: loki-logger-route
spec:
ingressClassName: apisix
http:
- name: loki-logger-route
match:
paths:
- /anything
methods:
- GET
- POST
upstreams:
- name: httpbin-external-domain
plugins:
- name: loki-logger
config:
endpoint_addrs:
- "http://loki.aic.svc:3100"
include_req_body: true
include_req_body_expr:
- - "arg_log_body"
- "=="
- "yes"
Apply the configuration:
kubectl apply -f loki-logger-ic.yaml
❶ 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"
}
}
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.