Nginx log_format Direktifi ile Özel Log Yapılandırması

Nginx’in varsayılan log formatı işe yarar ama gerçek dünya senaryolarında çoğu zaman yetersiz kalır. Bir güvenlik olayı inceliyorsunuz, hangi IP’den kaç istek geldiğini anlamaya çalışıyorsunuz ya da CDN arkasındaki gerçek kullanıcı IP’sini loglardan çıkarmaya uğraşıyorsunuz. İşte tam bu noktada log_format direktifi hayat kurtarıcı oluyor. Bu yazıda Nginx log yapılandırmasını baştan sona ele alacağız, sadece teorik değil gerçekten işe yarayan örneklerle.

Nginx Log Sistemine Genel Bakış

Nginx iki temel log mekanizması sunar: access_log ve error_log. Access log, gelen her HTTP isteğini kaydeder. Error log ise Nginx’in kendi hatalarını, upstream sorunlarını ve konfigürasyon problemlerini yazar.

log_format direktifi yalnızca access log için geçerlidir ve http bloğu içinde tanımlanır. Bir kez tanımladığınız formatı birden fazla server veya location bloğunda kullanabilirsiniz. Bu modüler yapı, farklı uygulamalar için farklı log detay seviyeleri belirlemenizi sağlar.

Varsayılan olarak Nginx combined adında bir format kullanır:

log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

Bu format Apache’nin combined log formatından miras kalmıştır ve çoğu log analiz aracıyla uyumludur. Ancak modern altyapılarda eksik kalan pek çok bilgi var: upstream yanıt süresi, gerçek kullanıcı IP’si, istek süresi, SSL bilgileri ve daha fazlası.

log_format Direktifinin Sözdizimi

Temel sözdizimi oldukça basit:

log_format format_adi 'format_string';

Format adını kendiniz belirliyorsunuz. Daha sonra access_log direktifiyle bu formatı referans alıyorsunuz:

access_log /var/log/nginx/access.log format_adi;

Format string içinde $degisken_adi şeklinde Nginx değişkenleri kullanıyorsunuz. String birden fazla satıra bölünebilir, Nginx bunları otomatik birleştirir.

Temel Değişkenler ve Anlamları

Nginx onlarca built-in değişken sunar. Sık kullanılanlar şunlar:

  • $remote_addr: İsteği yapan istemcinin IP adresi
  • $remote_user: HTTP Basic Auth kullanıcı adı
  • $time_local: Yerel saat diliminde istek zamanı
  • $time_iso8601: ISO 8601 formatında zaman damgası
  • $request: Tam HTTP istek satırı (method, URI, protocol)
  • $request_method: GET, POST gibi HTTP metodu
  • $request_uri: Query string dahil URI
  • $uri: Query string hariç URI
  • $args: Query string parametreleri
  • $status: HTTP yanıt kodu
  • $body_bytes_sent: Yanıt gövdesinin boyutu (header hariç)
  • $bytes_sent: Toplam gönderilen byte miktarı
  • $request_length: İstek uzunluğu (header ve gövde dahil)
  • $request_time: İsteğin toplam işlenme süresi (saniye cinsinden)
  • $http_referer: Referer header değeri
  • $http_user_agent: User-Agent header değeri
  • $http_x_forwarded_for: X-Forwarded-For header değeri
  • $upstream_addr: Upstream sunucunun adresi ve portu
  • $upstream_response_time: Upstream sunucunun yanıt süresi
  • $upstream_status: Upstream sunucunun döndürdüğü HTTP kodu
  • $scheme: http veya https
  • $server_name: İsteğin geldiği server_name değeri
  • $server_port: Nginx’in dinlediği port
  • $ssl_protocol: TLS/SSL protokol versiyonu
  • $ssl_cipher: Kullanılan SSL cipher
  • $connection: Bağlantı seri numarası
  • $pipe: İstek pipelined ise “p”, değilse “.”
  • $gzip_ratio: Sıkıştırma oranı

Performans Odaklı Özel Format

Production ortamında en çok ihtiyaç duyduğum şey performans verisi. Hangi endpoint yavaş, upstream ne kadar sürüyor, toplam istek süresi ne? Bunları loglamak için şu formatı kullanıyorum:

http {
    log_format performance '$remote_addr - $remote_user [$time_iso8601] '
                           '"$request_method $uri $server_protocol" '
                           '$status $body_bytes_sent '
                           'rt=$request_time '
                           'uct=$upstream_connect_time '
                           'uht=$upstream_header_time '
                           'urt=$upstream_response_time '
                           'upstream=$upstream_addr '
                           'us=$upstream_status';

    server {
        listen 80;
        server_name example.com;
        access_log /var/log/nginx/performance.log performance;
    }
}

Bu formatta uct upstream bağlantı kurma süresi, uht upstream’in header gönderme süresi, urt ise toplam upstream yanıt süresidir. rt ile urt arasındaki fark size Nginx’in kendi işlem süresini verir. Eğer rt yüksek ama urt düşükse sorun Nginx tarafındadır. Tam tersi durumda uygulama sunucunuza bakmanız gerekir.

JSON Log Formatı

Modern log yönetim sistemleri (ELK Stack, Loki, Datadog) JSON formatını çok daha iyi işler. Elasticsearch’e log atıyorsanız veya Filebeat kullanıyorsanız JSON format hayatınızı kolaylaştırır:

log_format json_combined escape=json
    '{'
        '"time":"$time_iso8601",'
        '"remote_addr":"$remote_addr",'
        '"x_forwarded_for":"$http_x_forwarded_for",'
        '"request_id":"$request_id",'
        '"remote_user":"$remote_user",'
        '"bytes_sent":$bytes_sent,'
        '"request_time":$request_time,'
        '"status":$status,'
        '"vhost":"$host",'
        '"request_proto":"$server_protocol",'
        '"path":"$uri",'
        '"request_query":"$args",'
        '"request_length":$request_length,'
        '"method":"$request_method",'
        '"http_referrer":"$http_referer",'
        '"http_user_agent":"$http_user_agent",'
        '"upstream_addr":"$upstream_addr",'
        '"upstream_status":"$upstream_status",'
        '"upstream_response_time":"$upstream_response_time",'
        '"ssl_protocol":"$ssl_protocol",'
        '"ssl_cipher":"$ssl_cipher"'
    '}';

escape=json parametresine dikkat edin. Nginx 1.11.8 ile gelen bu özellik, değişken değerlerindeki özel karakterleri (tırnak işaretleri, ters eğik çizgi vb.) otomatik olarak JSON-safe hale getirir. Olmadan User-Agent içindeki tırnak işaretleri JSON’ınızı bozar.

$request_id değişkeni için de ngx_http_core_module‘ün ürettiği benzersiz istek ID’sini kullanıyorsunuz. Bu ID’yi hem loglara hem de response header’a eklerseniz, kullanıcı şikayetlerini loglarla eşleştirmek çok kolaylaşır:

server {
    listen 443 ssl;
    server_name api.example.com;

    add_header X-Request-ID $request_id;
    access_log /var/log/nginx/api_json.log json_combined;
}

CDN ve Load Balancer Arkasında Gerçek IP Loglama

Cloudflare, AWS ALB veya başka bir load balancer arkasındaysanız $remote_addr her zaman proxy IP’sini gösterir. Gerçek kullanıcı IP’si X-Forwarded-For veya X-Real-IP header’ında gelir. Bu durumu loglamak için:

http {
    # Güvenilir proxy IP aralıkları - Cloudflare örneği
    set_real_ip_from 103.21.244.0/22;
    set_real_ip_from 103.22.200.0/22;
    set_real_ip_from 103.31.4.0/22;
    set_real_ip_from 104.16.0.0/13;
    set_real_ip_from 172.64.0.0/13;
    set_real_ip_from 198.41.128.0/17;
    set_real_ip_from 2400:cb00::/32;
    real_ip_header CF-Connecting-IP;
    real_ip_recursive on;

    log_format cdn_aware '$realip_remote_addr - [$time_iso8601] '
                         '"$request_method $request_uri $server_protocol" '
                         '$status $body_bytes_sent '
                         '"$http_referer" "$http_user_agent" '
                         'cdn_ip=$remote_addr '
                         'x_forwarded_for="$http_x_forwarded_for" '
                         'cf_ray="$http_cf_ray" '
                         'cf_country="$http_cf_ipcountry"';

    server {
        access_log /var/log/nginx/cdn.log cdn_aware;
    }
}

$realip_remote_addr değişkeni real_ip modülü aktif olduğunda gerçek IP’yi tutar. $remote_addr ise hala proxy IP’sini gösterir, ikisini de loglarsanız hangisinin nereden geldiğini anlayabilirsiniz.

Güvenlik ve Debug için Kapsamlı Format

Bir güvenlik olayı yaşandığında elinizdeki log verisi ya size her şeyi söyler ya da sizi karanlıkta bırakır. Ben kritik uygulamalar için şu kapsamlı formatı kullanıyorum:

log_format security_verbose '$remote_addr '
                             '[$time_iso8601] '
                             '"$request_method $request_uri $server_protocol" '
                             '$status '
                             '$bytes_sent '
                             '$request_length '
                             'rt=$request_time '
                             '"$http_user_agent" '
                             '"$http_referer" '
                             'host=$host '
                             'scheme=$scheme '
                             'ssl_proto=$ssl_protocol '
                             'ssl_cipher=$ssl_cipher '
                             'conn=$connection '
                             'conn_reqs=$connection_requests '
                             'pipe=$pipe '
                             '"$http_x_forwarded_for"';

$connection_requests özellikle ilgi çekici: Aynı TCP bağlantısı üzerinden kaç istek geldiğini gösterir. Bir IP sürekli yeni bağlantı açıp tek istek atıyorsa bu bir bot işareti olabilir. $pipe değeri pipelined istekleri tespit etmenizi sağlar.

Koşullu Loglama

Her isteği loglamak disk I/O açısından maliyetlidir. Özellikle health check endpoint’leri, statik dosyalar veya belirli durum kodları için loglama yapmak istemeyebilirsiniz:

http {
    # Health check isteklerini loglamama
    map $request_uri $loggable {
        ~^/health$  0;
        ~^/ping$    0;
        default     1;
    }

    # 2xx ve 3xx başarılı yanıtları loglamama, sadece hataları logla
    map $status $log_errors_only {
        ~^[23]  0;
        default 1;
    }

    server {
        # Normal trafik - başarılı istekleri logla, health check'i atlat
        access_log /var/log/nginx/access.log combined if=$loggable;

        # Hata logu - sadece 4xx ve 5xx
        access_log /var/log/nginx/errors.log security_verbose if=$log_errors_only;

        # Location bazlı loglama kapatma
        location /static/ {
            access_log off;
            root /var/www;
        }
    }
}

if=$degisken parametresi değişken değeri 0 veya boş string olduğunda o isteği loglamaz. Bu yöntemle gereksiz log kirliliğinden kurtulursunuz.

Birden Fazla Log Dosyasına Yazma

Aynı istek için birden fazla log dosyasına yazabilirsiniz. Bu özellikle farklı ekiplerin farklı log ihtiyaçlarını karşılamak için kullanışlıdır:

server {
    listen 443 ssl;
    server_name app.example.com;

    # Ops ekibi için performans logu
    access_log /var/log/nginx/app_performance.log performance buffer=32k flush=5s;

    # SIEM için JSON format
    access_log /var/log/nginx/app_security.json json_combined buffer=64k flush=10s;

    # Geliştirici debug logu - sadece 5xx hataları
    access_log /var/log/nginx/app_errors.log security_verbose if=$log_errors_only;
}

buffer ve flush parametreleri dikkat çekici. buffer=32k logu önce bellekte biriktirip 32KB dolunca diske yazar. flush=5s ise buffer dolmasa bile 5 saniyede bir diske yazmasını sağlar. Bu ikili yüksek trafikli sitelerde disk I/O’yu ciddi ölçüde azaltır.

Log Rotasyonu Yapılandırması

İyi bir log formatı, iyi bir log rotasyon stratejisiyle tamamlanmalıdır. Nginx logları için /etc/logrotate.d/nginx dosyasını şöyle yapılandırıyorum:

/var/log/nginx/*.log {
    daily
    missingok
    rotate 30
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    prerotate
        if [ -d /etc/logrotate.d/httpd-prerotate ]; then
            run-parts /etc/logrotate.d/httpd-prerotate
        fi
    endscript
    postrotate
        invoke-rc.d nginx rotate >/dev/null 2>&1
    endscript
}

Nginx’e USR1 sinyali göndermek yerine nginx rotate komutunu kullanın. Bu graceful şekilde log dosyasını yeniden açar, süregelen bağlantılar veri kaybetmez.

Log Analizinde Pratik Komutlar

Güzel bir log formatı kurdunuz, şimdi bu veriyi kullanın. Sık kullandığım analiz komutları:

# En yavaş 10 istek (request_time'a göre)
awk '{print $9, $7}' /var/log/nginx/performance.log | 
    grep "^rt=" | 
    sort -t= -k2 -rn | 
    head -10

# JSON logdan slow query analizi (jq ile)
cat /var/log/nginx/app_security.json | 
    jq -r 'select(.request_time | tonumber > 2) | 
           "(.request_time)s (.method) (.path) (.status)"' | 
    sort -rn | head -20

# 5xx hata oranı son 1 saatte
awk -v d="$(date -d '1 hour ago' '+%Y-%m-%dT%H')" 
    '$0 > d && /" 5[0-9][0-9] /' 
    /var/log/nginx/access.log | wc -l

# IP bazlı istek sayısı - DDoS tespiti
awk '{print $1}' /var/log/nginx/access.log | 
    sort | uniq -c | sort -rn | head -20

# Upstream hata analizi - JSON logdan
cat /var/log/nginx/app_security.json | 
    jq -r 'select(.upstream_status != "" and 
           (.upstream_status | tonumber) >= 500) | 
           "(.upstream_addr) (.upstream_status) (.path)"' | 
    sort | uniq -c | sort -rn

Bu komutların işe yaraması için log formatınızın tutarlı ve parse edilebilir olması gerekir. JSON format burada öne çıkıyor, jq ile çok güçlü sorgular yazabiliyorsunuz.

Nginx Log Konfigürasyonunu Test Etme

Değişiklik yapmadan önce ve sonra konfigürasyonu test edin:

# Nginx konfigürasyon testi
nginx -t

# Konfigürasyonu yeniden yükle (sıfır downtime)
nginx -s reload

# Logların doğru yazıldığını anlık takip et
tail -f /var/log/nginx/access.log

# JSON logları izle ve güzelleştir
tail -f /var/log/nginx/app_security.json | jq .

# Belirli bir format değişkeninin değerini test etmek için
# Geçici bir location ekle
location /nginx-log-test {
    return 200 "remote_addr=$remote_addrnrequest_id=$request_idn";
    add_header Content-Type text/plain;
}

Sık Yapılan Hatalar

Yıllardır Nginx konfigürasyonu yaparken gördüğüm yaygın hatalar şunlar:

  • Upstream değişkenlerini her zaman loglamak: $upstream_response_time gibi değişkenler yalnızca proxy_pass kullandığınızda değer alır. Yoksa - yazar, bu normaldir ama bazen sürpriz yaratabilir.
  • escape=json unutmak: JSON formatında özel karakterler için escape=json kullanmayı atlamak, log analiz araçlarının parse hatasıyla karşılaşmasına neden olur.
  • Buffer boyutunu yanlış ayarlamak: Çok büyük buffer değerleri, sistem crash durumunda log kaybına yol açar. Kritik sistemlerde buffer kullanmayı tamamen kapatabilirsiniz.
  • $http_x_forwarded_for’u direkt kullanmak: Proxy güvenilir değilse bu header spooflanabilir. real_ip_from direktifiyle güvenilir kaynakları tanımlayın.
  • Log dosyasını yanlış kullanıcıya açmak: Nginx worker process’i genellikle www-data veya nginx kullanıcısıyla çalışır. Log dosyasının yazma izni bu kullanıcıda olmalı.

Sonuç

Nginx log_format direktifi göründüğünden çok daha güçlü bir araç. Doğru yapılandırıldığında loglarınız salt kayıt tutma işlevinin ötesine geçer; performans sorunlarını tespit eden, güvenlik olaylarını aydınlatan ve sistem davranışını anlayan bir veri kaynağına dönüşür.

Benim önerim şu: Geliştirme ortamında performans ve güvenlik odaklı detaylı bir format kullanın. Production’da ise hem detaylı JSON formatı hem de daha sade bir hata formatı çalıştırın. Log rotasyonunu, buffer ayarlarını ve koşullu loglama kurallarını ilk günden doğru ayarlayın. Disk dolunca ya da bir güvenlik olayı sonrasında “keşke şunu loglasaydım” demek yerine, ihtiyacınız olan veriyi baştan sisteminize kazıyın.

JSON formatına geçişi özellikle şiddetle tavsiye ederim. ELK Stack veya benzeri bir log yönetim sistemi kurduğunuzda, düzgün JSON logları parse etme headache’ini tamamen ortadan kaldırır ve sizi asıl önemli olan analiz işine odaklanmanızı sağlar.

Yorum yapın