Nginx logging cheatsheet.
log_format
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log main;
JSON logs
log_format json escape=json '{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"method":"$request_method",'
'"path":"$request_uri",'
'"status":$status,'
'"bytes":$body_bytes_sent,'
'"referer":"$http_referer",'
'"ua":"$http_user_agent",'
'"xff":"$http_x_forwarded_for",'
'"rt":$request_time,'
'"urt":"$upstream_response_time",'
'"request_id":"$request_id"'
'}';
access_log /var/log/nginx/access.log json;
escape=json properly escapes special characters.
Conditional logging
map $request_uri $loggable {
default 1;
~*/health 0;
~*/metrics 0;
}
access_log /var/log/nginx/access.log main if=$loggable;
Skip noisy health/metric endpoints.
Per-status logging
map $status $errlog {
~^[45] 1;
default 0;
}
access_log /var/log/nginx/errors.log main if=$errlog;
Separate log for 4xx/5xx.
error_log levels
error_log /var/log/nginx/error.log warn;
# levels: debug info notice warn error crit alert emerg
Debug requires nginx built with --with-debug.
Log per server
server {
server_name api.example.com;
access_log /var/log/nginx/api.access.log json;
error_log /var/log/nginx/api.error.log;
}
stub_status
location = /nginx_status {
stub_status;
access_log off;
allow 127.0.0.1;
deny all;
}
Output:
Active connections: 291
server accepts handled requests
16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106
Prometheus exporter
docker run -d \
-p 9113:9113 \
nginx/nginx-prometheus-exporter:latest \
-nginx.scrape-uri=http://nginx:80/nginx_status
Or use nginx-vts module for richer metrics (per-zone, per-upstream).
Real IP behind LB
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Now $remote_addr is real client IP.
Slow request detection
awk '$NF > 1.0 {print}' /var/log/nginx/access.log
# Field positions vary by log_format
For JSON:
jq 'select(.rt > 1) | "\(.time) \(.path) \(.rt)"' access.log
Logrotate
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
endscript
}
USR1 makes nginx reopen log files.
Ship logs (Loki / ELK / S3)
Promtail / Vector / Filebeat tails files; or write to syslog:
access_log syslog:server=127.0.0.1:514,tag=nginx main;
OpenTelemetry
Module nginx-otel:
load_module modules/ngx_otel_module.so;
http {
otel_exporter { endpoint otel-collector:4317; }
otel_service_name nginx;
otel_trace on;
server {
location / {
otel_trace_context inject;
proxy_pass http://app;
}
}
}
Sampling logs
split_clients $request_id $sampled {
1% 1;
* 0;
}
access_log /var/log/nginx/sample.log main if=$sampled;
For very high-volume sites, sample for debugging.
Common mistakes
- Plain log format → hard to parse.
- Logging healthchecks → log spam.
- Buffer-disabled
access_log buffer=offin high-traffic → I/O spike. - Missing
set_real_ip_from→ wrong IPs logged. - Not rotating → disk full.
Read this next
If you want my JSON log format + dashboards, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .