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=off in 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 .