awk ile Apache Hata Loglarından Durum Kodu Bazlı Anomali Tespiti ve Zaman Serisi Raporu Oluşturma

Üretim ortamında bir Apache sunucusunun log dosyasına bakıp “acaba bu gece bir şeyler ters gitti mi?” sorusunu elle gözlemleyerek yanıtlamaya çalışmak, iğneyle kuyu kazmak gibi bir şey. Özellikle günlük milyonlarca istek gören bir sunucuda 500’lerin ya da 403’lerin ani bir sıçrama yaptığı anı fark etmek, ham log dosyasına bakarak neredeyse imkânsız. İşte tam bu noktada awk, sandığınızdan çok daha güçlü bir araç olarak öne çıkıyor.

Bu yazıda sadece “şu kadar 404 var” türünde basit sayımlar değil, zaman bazlı anomali tespiti, dakika/saat bazlı durum kodu dağılımı ve gerçek anlamda operasyonel değer taşıyan raporlar üretmeyi ele alacağım. Kopyala-yapıştır komutlar değil, nasıl düşünmeniz gerektiğini anlatmaya çalışacağım.

Apache Log Formatını Tanımak

Önce temel: Apache’nin combined log formatı şöyle görünür:

192.168.1.10 - frank [10/Oct/2024:13:55:36 -0400] "GET /api/users HTTP/1.1" 200 2326 "http://example.com" "Mozilla/5.0"

awk açısından alanlar şöyle ayrışır:

  • $1: IP adresi
  • $4: Tarih ve saat ([10/Oct/2024:13:55:36)
  • $5: Zaman dilimi (-0400])
  • $6: HTTP metodu ("GET)
  • $7: İstek yolu
  • $9: HTTP durum kodu
  • $10: Yanıt boyutu (byte)

Bu ayrımı kafanıza iyice kazıyın çünkü her komutun mantığı buraya dayanıyor. $4‘teki köşeli parantezi sonradan temizlemek gerektiğini de şimdiden belirteyim.

Temel Durum Kodu Dağılımı

İlk adım olarak log dosyasındaki tüm durum kodlarını sayalım:

awk '{print $9}' /var/log/apache2/access.log | sort | uniq -c | sort -rn

Bu komut size şöyle bir çıktı verir:

 142847 200
  18432 304
   4821 404
   1203 500
    342 403
     87 502

Fakat bu çıktı tek başına anlamsız. 1203 tane 500 hatası mı? Bunlar son bir saatte mi birikti, yoksa altı aylık birikimi mi gösteriyor? İşte zaman serisine ihtiyaç tam buradan doğuyor.

Saatlik Durum Kodu Dağılımı

Saatlik bazda dağılımı görmek için $4 alanından saat bilgisini çekip gruplamamız gerekiyor:

awk '{
    split($4, dt, ":");
    hour = substr(dt[1], 2) ":" dt[2];
    print hour, $9
}' /var/log/apache2/access.log | sort | uniq -c | awk '{print $2, $3, $1}' | sort

Bu komutun mantığını açayım: split($4, dt, ":") ile [10/Oct/2024 ve 13 ve 55 ve 36 şeklinde parçalara ayırıyoruz. substr(dt[1], 2) ile baştaki köşeli parantezi atıyoruz. Sonuç olarak her satır için tarih:saat statüskodu formatında bir çıktı üretiyoruz.

Ama daha temiz bir yaklaşım şu:

awk '{
    match($4, /[([^:]+):([0-9]{2})/, arr);
    key = arr[1] " " arr[2] ":00";
    status[$9][key]++;
}
END {
    for (code in status)
        for (ts in status[code])
            print ts, code, status[code][ts]
}' /var/log/apache2/access.log | sort

Bu versiyon çok boyutlu dizi kullanıyor. status[$9][key] ifadesi her durum kodu için ayrı bir saat bazlı sayaç tutuyor. Çıktıyı sıraladığınızda hangi saatte hangi kodun ne kadar tetiklendiğini net görürsünüz.

Anomali Eşiği Belirleme ve Tespit

Salt sayım yeterli değil. Önemli olan normalden sapma. Bunun için önce saatlik ortalamayı hesaplayıp sonra bunu aşan saatleri tespit etmek gerekiyor.

awk '
/HTTP// {
    split($4, dt, ":");
    hour = substr(dt[1], 2) ":" dt[2] ":00";
    if ($9 >= 500) {
        errors[hour]++;
        total[hour]++;
    } else {
        total[hour]++;
    }
}
END {
    sum = 0; count = 0;
    for (h in errors) {
        sum += errors[h];
        count++;
    }
    if (count > 0) avg = sum / count;
    else avg = 0;

    print "Saatlik 5xx Ortalama:", avg;
    print "---";
    for (h in errors) {
        ratio = (total[h] > 0) ? errors[h] / total[h] * 100 : 0;
        if (errors[h] > avg * 2) {
            print "ANOMALI ->", h, "| 5xx:", errors[h], "| Oran: %" ratio;
        }
    }
}' /var/log/apache2/access.log

Burada ortalamanın 2 katını aşan saatleri anomali olarak işaretliyoruz. Bu eşiği ortamınıza göre ayarlamanız gerekir; yoğun e-ticaret sitelerinde bu çarpan 3-4’e çıkarılabilir, kritik API’lerde 1.5’e düşürülebilir.

Dakika Bazlı Yoğunluk Analizi (Spike Tespiti)

Bir DDoS saldırısı ya da bozuk bir deployment anını yakalamak istiyorsanız dakika bazlı analiz çok daha kıymetli:

awk '{
    split($4, dt, ":");
    minute = substr(dt[1], 2) ":" dt[2] ":" dt[3];
    if ($9 ~ /^[45]/) {
        errmin[minute]++;
    }
    allmin[minute]++;
}
END {
    for (m in allmin) {
        if (errmin[m] != "") {
            pct = errmin[m] / allmin[m] * 100;
            printf "%s | Toplam: %d | Hata: %d | Hata Oranı: %.1f%%n",
                m, allmin[m], errmin[m], pct;
        }
    }
}' /var/log/apache2/access.log | sort | awk '$NF+0 > 10'

Son awk '$NF+0 > 10' kısmı hata oranı yüzde 10’u aşan dakikaları filtreler. $NF son alandaki yüzde değerini alır ve sayısal karşılaştırma yapar. Bu sayede “gece 02:47’de 2 dakika boyunca hata oranı yüzde 45’e çıktı” gibi bulgular elde edersiniz; bu tam olarak bir deployment sonrası restart sorununun ya da bir backend bağlantı kopuşunun imzasıdır.

IP Bazlı Durum Kodu Korelasvonu

Tek bir IP’nin sürekli 403 ya da 404 üretmesi tarayan bir bot ya da kötü niyetli bir tarama çalışmasına işaret eder:

awk '
$9 == "404" || $9 == "403" {
    ip_code[$1][$9]++;
    ip_total[$1]++;
}
END {
    for (ip in ip_total) {
        if (ip_total[ip] >= 50) {
            printf "IP: %-18s | 404: %d | 403: %d | Toplam: %dn",
                ip,
                ip_code[ip]["404"]+0,
                ip_code[ip]["403"]+0,
                ip_total[ip];
        }
    }
}' /var/log/apache2/access.log | sort -t'|' -k4 -rn | head -20

Eşiği 50 olarak belirledim ama bu sizin trafiğinize göre değişir. Günlük 500 istek yapan ama bunların 480’i 404 olan bir IP’yi bu komutla kolayca tespit edersiniz.

Kapsamlı Zaman Serisi Raporu Oluşturma

Artık tüm bu parçaları bir araya getirip operasyonel bir rapor üretelim. Bu scripti bir cron job’a bağlayabilir, her sabah mail ile alabilirsiniz:

#!/usr/bin/env awk -f
# Dosya adı: apache_report.awk
# Kullanım: awk -f apache_report.awk /var/log/apache2/access.log

BEGIN {
    print "========================================";
    print "   APACHE DURUM KODU ANALİZ RAPORU";
    print "========================================n";
}

/HTTP// {
    # Saat bilgisini ayıkla
    split($4, dt, ":");
    hour = substr(dt[1], 2) ":" dt[2];

    # Durum kodu kategorileri
    code = $9 + 0;
    if (code >= 200 && code < 300) cat = "2xx";
    else if (code >= 300 && code < 400) cat = "3xx";
    else if (code >= 400 && code < 500) cat = "4xx";
    else if (code >= 500) cat = "5xx";
    else cat = "other";

    # Saatlik sayaçlar
    hourly[hour][cat]++;
    hourly_total[hour]++;

    # Genel sayaçlar
    global_cat[cat]++;
    global_total++;

    # 5xx için IP takibi
    if (code >= 500) {
        server_errors[$1]++;
    }
}

END {
    print "[ GENEL ÖZET ]";
    print "Toplam İstek:", global_total;
    for (c in global_cat)
        printf "  %-6s : %d (%.1f%%)n", c, global_cat[c], global_cat[c]/global_total*100;

    print "n[ SAATLİK DAĞILIM ]";
    n = asorti(hourly, sorted_hours);
    for (i = 1; i <= n; i++) {
        h = sorted_hours[i];
        printf "%s | 2xx:%-6d 3xx:%-6d 4xx:%-6d 5xx:%-6d Toplam:%-8dn",
            h,
            hourly[h]["2xx"]+0,
            hourly[h]["3xx"]+0,
            hourly[h]["4xx"]+0,
            hourly[h]["5xx"]+0,
            hourly_total[h];
    }

    print "n[ EN ÇOK 5xx ÜRETEN IP'LER ]";
    for (ip in server_errors)
        print server_errors[ip], ip | "sort -rn | head -10";
}

Bunu çalıştırmak için:

awk -f apache_report.awk /var/log/apache2/access.log

Ya da birden fazla log dosyasını birleştirerek analiz etmek için:

awk -f apache_report.awk /var/log/apache2/access.log.{1..7}

Log Rotasyonu Sonrası Analiz

Gerçek dünyada loglar rotate edilir ve access.log.1, access.log.2.gz gibi dosyalarda dağılır. Arşiv dosyalarını da dahil ederek haftalık analiz yapmak için:

zcat /var/log/apache2/access.log.*.gz | awk '
{
    split($4, dt, ":");
    date = substr(dt[1], 2);
    hour = date ":" dt[2];
    status_hour[$9][hour]++;
}
END {
    for (code in status_hour) {
        if (code ~ /^5/) {
            print "n=== Durum Kodu:", code, "===";
            n = asorti(status_hour[code], sorted);
            for (i = 1; i <= n; i++) {
                h = sorted[i];
                printf "  %s -> %d istekn", h, status_hour[code][h];
            }
        }
    }
}' | sort

zcat ile gzip’li dosyaları açık okuyorsunuz ve awk‘a pipe ile besliyorsunuz. Büyük log arşivlerinde bu yaklaşım gunzip ile açıp sonra işlemekten çok daha hızlı çünkü ara dosya yazmıyor.

Gerçek Dünya Senaryosu: Deployment Sonrası Kontrol

Geçen ay bir müşterimizde şöyle bir durum yaşandı: Saat 14:23’te yapılan bir deployment sonrasında kullanıcılardan şikayetler gelmeye başladı. Logları elle taramak yerine şu komutu çalıştırdım:

awk -v start="14:20" -v end="14:40" '
{
    split($4, dt, ":");
    hour_min = dt[2] ":" dt[3];

    # Sadece ilgili zaman aralığını al
    if (hour_min >= start && hour_min <= end) {
        code = $9;
        ts[hour_min][code]++;
    }
}
END {
    n = asorti(ts, sorted);
    for (i = 1; i <= n; i++) {
        t = sorted[i];
        printf "%s | 200:%-5d 404:%-5d 500:%-5d 502:%-5dn",
            t,
            ts[t]["200"]+0,
            ts[t]["404"]+0,
            ts[t]["500"]+0,
            ts[t]["502"]+0;
    }
}' /var/log/apache2/access.log

Çıktıda 14:24’ten itibaren 502 sayısının dakika başına 0’dan 340’a fırladığını gördük. Bu, upstream backend’in henüz ayağa kalkmadığını net biçimde gösteriyordu. Klasik bir “uygulama yeniden başlarken load balancer hâlâ trafik yönlendirir” problemi. 4 dakikada tespit, 2 dakikada çözüm.

Raporları Otomatikleştirme

Bu analizleri bir cron job ile düzenli çalıştırıp çıktıyı bir dosyaya yazabilirsiniz:

# /etc/cron.d/apache-anomaly-check
# Her saat başı çalışır
0 * * * * root /usr/local/bin/apache_anomaly.sh >> /var/log/anomaly_reports/$(date +%Y%m%d).log 2>&1

Script içinde basit bir eşik aşıldığında mail atmak için:

#!/bin/bash
THRESHOLD=100
LOG=/var/log/apache2/access.log
REPORT=$(awk -v thresh="$THRESHOLD" '
$9 >= 500 {
    split($4, dt, ":");
    hour = substr(dt[1], 2) ":" dt[2];
    errors[hour]++;
}
END {
    for (h in errors)
        if (errors[h] > thresh+0)
            print "UYARI:", h, "saatinde", errors[h], "adet 5xx hatası";
}' "$LOG")

if [ -n "$REPORT" ]; then
    echo "$REPORT" | mail -s "[ANOMALI] Apache 5xx Esik Asildi" [email protected]
fi

Performans Notu

Büyük log dosyalarında (günlük 10GB+ trafik olan sistemler) awk hâlâ performanslı ama şu noktaları göz önünde bulundurun:

  • Mümkünse grep ile önce filtreleme yapın, sonra awk‘a verin. Örneğin sadece hataları analiz edecekseniz önce grep -E '" [45][0-9]{2} ' ile ilgili satırları alın.
  • Tek geçişte mümkün olduğunca çok işi halledin. Her awk çalıştırması dosyayı baştan okur.
  • Log rotasyonu yapılmış büyük arşivlerde paralel işleme için parallel aracıyla birden fazla dosyayı eş zamanlı analiz edebilirsiniz.
ls /var/log/apache2/access.log.*.gz | 
    parallel "zcat {} | awk '{print $9}'" | 
    sort | uniq -c | sort -rn

Sonuç

awk öğrenmesi zahmetli görünen ama bir kez içselleştirdiğinizde elinizden düşüremeyeceğiniz bir araç. Burada anlattıklarım, bir log analiz aracı satın almanıza ya da ELK stack kurmanıza gerek kalmadan, standart bir Linux sistemde dakikalar içinde yapabileceğiniz şeyler.

Tabii ki Kibana veya Grafana gibi araçlar çok daha görsel ve interaktif deneyim sunar. Ama gece 02:00’de bir prodüksiyon sunucusuna SSH ile bağlandığınızda, o görsel araçların ayağa kaldırılmasını beklemeye vaktiniz olmaz. O anın aracı awk‘tır.

Bu komutları kendi ortamınızda test edin, eşikleri kendi trafik profilinize göre ayarlayın ve kritik servisleriniz için cron tabanlı izleme kurun. Bir anomaliyi kullanıcıdan önce siz fark ettiğinizde, bu yazı işe yaramış demektir.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir