awk ile Syslog Verilerinden Servis Bazlı Hata Frekansı Matrisi Oluşturma

Üretim ortamında gece 2’de çalan telefon, genellikle “bir şeyler bozuldu ama ne olduğunu bilmiyoruz” cümlesiyle başlar. O anlarda syslog dosyalarına bakıp “hangi servis ne kadar hata üretiyor” sorusunu hızlıca cevaplayabilmek, saatlerce log okumakla saatlerce awk scripti çalıştırmak arasındaki fark demektir. Bu yazıda, /var/log/syslog veya /var/log/messages gibi dosyalardan servis bazlı hata frekansı matrisi çıkarmayı, bunu yaparken awk’ın gerçek gücünü kullanmayı anlatacağım.

Syslog Formatını Anlamak

Önce neyle çalıştığımızı netleştirelim. Tipik bir syslog satırı şöyle görünür:

Jan 15 03:22:41 hostname sshd[1234]: error: PAM authentication failed for user root
Jan 15 03:22:42 hostname kernel: EXT4-fs error (device sda1): ...
Jan 15 03:22:45 hostname mysqld[5678]: [ERROR] Table 'db.users' doesn't exist

Yapı şu şekilde: Ay Gün Saat Hostname Servis[PID]: Mesaj. awk’ın alan tabanlı işleme mantığı bu format için biçilmiş kaftan. Varsayılan alan ayırıcı boşluk olduğu için $1 ay, $2 gün, $3 saat, $4 hostname, $5 servis adı ve PID, geri kalanı mesaj oluyor.

Ama hemen bir uyarı: Gerçek dünya syslog dosyaları hiçbir zaman bu kadar temiz olmaz. Multi-line mesajlar, farklı daemon formatları, rsyslog ile journald karışıklıkları… Bunların hepsini göz önünde bulundurarak yazmak gerekiyor.

Temel Servis Çıkarma

İlk adım, servis adını düzgün çekmek. $5 alanı genellikle sshd[1234]: formatında gelir, yani hem servis adını hem PID’i içerir. Bunu temizlemek için:

awk '{
    # $5 alanından servis adini cek, PID ve iki noktayi temizle
    service = $5
    gsub(/[.*]/, "", service)
    gsub(/:$/, "", service)
    print service
}' /var/log/syslog | sort | uniq -c | sort -rn | head -20

Bu basit script bile bana şunu söyler: hangi servis en çok log üretiyor. Ama biz sadece hataları istiyoruz. Hata kelimelerini filtrelemek için koşul ekleyelim:

awk 'tolower($0) ~ /error|err|fail|critical|crit|emerg|alert/ {
    service = $5
    gsub(/[.*]/, "", service)
    gsub(/:$/, "", service)
    print service
}' /var/log/syslog | sort | uniq -c | sort -rn

tolower() kullanmak önemli, çünkü bazı servisler “ERROR” yazar, bazıları “error”, bazıları “Error”. Hepsini yakalamak istiyoruz.

Asıl Matris: Servis x Saat Frekansı

Şimdi gerçek işe gelelim. Sadece “hangi servis kaç hata üretti” değil, “hangi servis hangi saatte ne kadar hata üretti” sorusunu cevaplamak istiyoruz. Bu bir frekans matrisidir ve awk’ın associative array’leri bunun için mükemmeldir.

awk 'tolower($0) ~ /error|err|fail|critical/ {
    service = $5
    gsub(/[.*]/, "", service)
    gsub(/:$/, "", service)
    
    # Saati $3 alanından cek (HH:MM:SS formatinda)
    split($3, time_parts, ":")
    hour = time_parts[1]
    
    # Matris: matrix[servis][saat] = count
    matrix[service][hour]++
    
    # Toplam hata sayisini da tut
    total[service]++
    
    # Hangi saatler var?
    hours[hour] = 1
}
END {
    # Baslik satirini yazdir
    printf "%-20s", "SERVIS"
    for (h = 0; h < 24; h++) {
        printf " %02d", h
    }
    printf " | TOPLAMn"
    
    # Her servis icin satir yazdir
    for (svc in total) {
        printf "%-20s", svc
        for (h = 0; h < 24; h++) {
            printf " %2d", (matrix[svc][sprintf("%02d", h)] + 0)
        }
        printf " | %dn", total[svc]
    }
}' /var/log/syslog | sort -t'|' -k2 -rn

Bu script çalıştığında terminalinizde her servis için 24 saatlik hata dağılımını göreceksiniz. Hangi saatte spike olduğu, hangi servisin gece saatlerinde aktif olduğu, hangi servisin sürekli hata ürettiği hemen anlaşılır hale gelir.

Hata Seviyesi Bazlı Sınıflandırma

Sadece “hata var/yok” demek yetmez. Syslog’da severity seviyeleri var: emerg, alert, crit, err, warning, notice, info, debug. Bunları ayrı ayrı saymak çok daha anlamlı bir tablo verir:

awk '{
    service = $5
    gsub(/[.*]/, "", service)
    gsub(/:$/, "", service)
    
    line = tolower($0)
    
    # Severity tespiti - oncelik sirasina gore kontrol et
    if (line ~ /emerg|panic/) {
        severity = "EMERG"
    } else if (line ~ /bcrit(ical)?b/) {
        severity = "CRIT"
    } else if (line ~ /balertb/) {
        severity = "ALERT"
    } else if (line ~ /berr(or)?b/) {
        severity = "ERROR"
    } else if (line ~ /bwarn(ing)?b/) {
        severity = "WARN"
    } else {
        next  # Ilgilenmedigimiz satirlari atla
    }
    
    count[service][severity]++
    services[service] = 1
}
END {
    printf "%-25s %8s %8s %8s %8s %8sn", 
        "SERVIS", "EMERG", "ALERT", "CRIT", "ERROR", "WARN"
    printf "%sn", "----------------------------------------------------------------------"
    
    for (svc in services) {
        printf "%-25s %8d %8d %8d %8d %8dn",
            svc,
            count[svc]["EMERG"] + 0,
            count[svc]["ALERT"] + 0,
            count[svc]["CRIT"] + 0,
            count[svc]["ERROR"] + 0,
            count[svc]["WARN"] + 0
    }
}' /var/log/syslog

Bu çıktıyı bir ekrana döktüğünüzde hangi servislerin kritik sorunlar yaşadığı ilk bakışta görünür. EMERG veya CRIT kolonu dolu olan bir servis, anlık müdahale gerektiriyor demektir.

Tarih Aralığı Filtreleme

Gerçek senaryolarda genellikle “dün gece 23:00 ile bugün sabah 06:00 arası neler oldu” gibi sorular gelir. Bunu awk içinde ele almak biraz daha dikkat ister çünkü syslog timestamp’i işlemek zahmetlidir:

awk -v start_hour="23" -v end_hour="06" '{
    # $3 alani HH:MM:SS formatinda
    split($3, t, ":")
    h = t[1] + 0
    
    # Gece gecisi araligini kontrol et
    if (start_hour > end_hour) {
        # Ornegin 23:00 - 06:00 arasi
        if (h < start_hour && h >= end_hour) next
    } else {
        if (h < start_hour || h >= end_hour) next
    }
    
    service = $5
    gsub(/[.*]/, "", service)
    gsub(/:$/, "", service)
    
    if (tolower($0) ~ /error|fail|crit/) {
        freq[service]++
    }
}
END {
    for (svc in freq) {
        print freq[svc], svc
    }
}' /var/log/syslog | sort -rn

Aynı mantığı birden fazla log dosyasına da uygulayabilirsiniz. awk otomatik olarak birden fazla dosyayı sırayla işler:

awk 'tolower($0) ~ /error|crit|fail/ {
    service = $5
    gsub(/[.*]/, "", service)
    gsub(/:$/, "", service)
    freq[service][FILENAME]++
}
END {
    for (svc in freq) {
        for (f in freq[svc]) {
            print svc, f, freq[svc][f]
        }
    }
}' /var/log/syslog /var/log/syslog.1 /var/log/syslog.2.gz

Dikkat: .gz dosyalarını direkt awk ile açamazsınız. Bunun için:

zcat /var/log/syslog.*.gz | awk '{...}'

Gerçek Dünya Senaryosu: MySQL ve Apache Hata Korelasyonu

Bir e-ticaret sisteminde şöyle bir şikayet geldi: “Öğle saatlerinde site yavaşlıyor.” Syslog’a baktım, hem Apache hem MySQL hataları görüyorum ama hangisi tetikleyici? Korelasyon matrisi bu soruyu çözdü:

awk 'tolower($0) ~ /error|fail/ {
    service = $5
    gsub(/[.*]/, "", service)
    gsub(/:$/, "", service)
    
    # Sadece ilgilendigimiz servislere bak
    if (service !~ /apache2|httpd|mysqld|mysql/) next
    
    split($3, t, ":")
    hour = t[1]
    minute = int(t[2] / 15) * 15  # 15 dakikalik dilimler
    
    slot = hour ":" sprintf("%02d", minute)
    matrix[service][slot]++
    slots[slot] = 1
}
END {
    # Slot listesini sirala
    n = asorti(slots, sorted_slots)
    
    printf "%-10s", "ZAMAN"
    for (svc in matrix) printf " %-10s", svc
    printf "n"
    
    for (i = 1; i <= n; i++) {
        slot = sorted_slots[i]
        printf "%-10s", slot
        for (svc in matrix) {
            printf " %-10d", matrix[svc][slot] + 0
        }
        printf "n"
    }
}' /var/log/syslog

Bu script 15 dakikalık dilimlerde Apache ve MySQL hatalarını yan yana koydu. Sonuç açıktı: MySQL hataları Apache hatalarından tam 2-3 dakika önce başlıyordu. Yani sorun MySQL’deydi, Apache sadece buna tepki veriyordu. Connection pool’u artırınca sorun çözüldü.

Script Haline Getirme ve Parametreleştirme

Bu komutları her seferinde yeniden yazmak yerine kullanışlı bir script haline getirelim:

#!/bin/bash
# servis_hata_matrisi.sh
# Kullanim: ./servis_hata_matrisi.sh [log_dosyasi] [min_hata_sayisi]

LOG_FILE="${1:-/var/log/syslog}"
MIN_COUNT="${2:-5}"

if [ ! -f "$LOG_FILE" ] && [ "$LOG_FILE" != "-" ]; then
    echo "Hata: $LOG_FILE bulunamadi" >&2
    exit 1
fi

echo "=== Servis Bazli Hata Frekans Matrisi ==="
echo "Log: $LOG_FILE | Minimum hata esigi: $MIN_COUNT"
echo ""

awk -v min_count="$MIN_COUNT" '
BEGIN {
    # Severity agirliklari - puanlama icin
    weight["EMERG"] = 100
    weight["ALERT"] = 50
    weight["CRIT"]  = 20
    weight["ERROR"] = 5
    weight["WARN"]  = 1
}
{
    service = $5
    gsub(/[.*]/, "", service)
    gsub(/:$/, "", service)
    if (service == "") next
    
    line = tolower($0)
    
    if      (line ~ /emerg|panic/)        sev = "EMERG"
    else if (line ~ /balertb/)          sev = "ALERT"
    else if (line ~ /bcrit(ical)?b/)    sev = "CRIT"
    else if (line ~ /berr(or)?b/)       sev = "ERROR"
    else if (line ~ /bwarn(ing)?b/)     sev = "WARN"
    else next
    
    count[service][sev]++
    total[service]++
    score[service] += weight[sev]
    services[service] = 1
}
END {
    printf "%-25s %6s %6s %6s %6s %6s %8s %8sn",
        "SERVIS", "EMERG", "ALERT", "CRIT", "ERROR", "WARN", "TOPLAM", "SKOR"
    
    # Skora gore sirali cikti icin once diziye al
    n = 0
    for (svc in services) {
        if (total[svc] >= min_count) {
            svc_list[++n] = svc
        }
    }
    
    # Bubble sort - kucuk veri seti icin yeterli
    for (i = 1; i <= n; i++) {
        for (j = i+1; j <= n; j++) {
            if (score[svc_list[i]] < score[svc_list[j]]) {
                tmp = svc_list[i]
                svc_list[i] = svc_list[j]
                svc_list[j] = tmp
            }
        }
    }
    
    for (i = 1; i <= n; i++) {
        svc = svc_list[i]
        printf "%-25s %6d %6d %6d %6d %6d %8d %8dn",
            svc,
            count[svc]["EMERG"] + 0,
            count[svc]["ALERT"] + 0,
            count[svc]["CRIT"]  + 0,
            count[svc]["ERROR"] + 0,
            count[svc]["WARN"]  + 0,
            total[svc],
            score[svc]
    }
}' "$LOG_FILE"

Buradaki “skor” konsepti özellikle işe yarar. Her severity seviyesi farklı ağırlıkta puan taşır; böylece 1 EMERG alan servis, 100 WARNING üreten servisten önce gelir. Çünkü operasyonel öncelik açısından bu doğru yaklaşımdır.

journald ile Çalışmak

Modern sistemlerde /var/log/syslog yerine journalctl kullanıyorsunuzdur. journald çıktısını awk’a pipe etmek çok kolay:

journalctl --since "24 hours ago" --no-pager -o short-iso | 
awk 'tolower($0) ~ /error|fail|crit/ {
    # journald formati: 2024-01-15T03:22:41+0300 hostname sshd[1234]: mesaj
    service = $3
    gsub(/[.*]/, "", service)
    gsub(/:$/, "", service)
    
    # ISO timestamp den saat cekme
    split($1, dt, "T")
    split(dt[2], time_parts, ":")
    hour = substr(time_parts[1], 1, 2)
    
    freq[service][hour]++
    total[service]++
}
END {
    for (svc in total) {
        if (total[svc] < 3) continue
        printf "%-20s toplam:%d dagilim:", svc, total[svc]
        for (h = 0; h < 24; h++) {
            cnt = freq[svc][sprintf("%02d", h)] + 0
            if (cnt > 0) printf " %02d:%d", h, cnt
        }
        print ""
    }
}' | sort -t: -k2 -rn

Priority filtresi eklemek için journalctl’in kendi parametrelerini kullanmak daha doğru:

# Sadece error ve ustunu getir (priority 0-3)
journalctl -p err --since "yesterday" --no-pager | 
awk '{
    service = $5
    gsub(/[.*]/, "", service)
    gsub(/:$/, "", service)
    count[service]++
}
END {
    for (s in count) print count[s], s
}' | sort -rn | head -15

Çıktıyı Daha Okunabilir Hale Getirme

Terminal genişliği sınırlı olduğunda 24 saatlik matris sığmayabilir. Bunun yerine yoğunluk karakterleri kullanmak hoş bir çözüm:

awk 'tolower($0) ~ /error|fail|crit/ {
    service = $5
    gsub(/[.*]/, "", service)
    gsub(/:$/, "", service)
    
    split($3, t, ":")
    hour = t[1] + 0
    
    matrix[service][hour]++
    if (matrix[service][hour] > max_val[service])
        max_val[service] = matrix[service][hour]
    
    services[service] = 1
}
END {
    # Yogunluk karakterleri
    density[0] = "."
    density[1] = ":"
    density[2] = "+"
    density[3] = "#"
    
    printf "%-20s 00 03 06 09 12 15 18 21n", "SERVIS"
    
    for (svc in services) {
        printf "%-20s ", svc
        for (h = 0; h < 24; h++) {
            cnt = matrix[svc][h] + 0
            mx = max_val[svc]
            
            if (cnt == 0) {
                printf " "
            } else if (mx > 0) {
                level = int((cnt / mx) * 3)
                printf "%s", density[level]
            }
        }
        printf "  max:%dn", max_val[svc]
    }
}' /var/log/syslog

Bu yaklaşım 24 karakterlik bir “histogram” çizer. Nokta az hata, diyez yoğun hata demek. Hangi saatte patlama olduğu bir bakışta görülür.

Sonuç

awk’ı syslog analizi için kullanmanın gerçek değeri, kurulum gerektirmemesinde yatar. Nagios, Grafana, ELK Stack güzel araçlar ama gece 2’de bir prodüksiyon sunucusunda bunlara erişiminiz olmayabilir ya da henüz kurulmamış olabilir. Ama awk her Linux sistemde hazır bekliyor.

Bu yazıda anlattığım yaklaşımların birkaç pratik özeti:

  • Associative array’ler awk’ın en güçlü özelliği, matris yapıları için birebir
  • Servis adı çıkarma için gsub(/[.*]/, "", service) deseni evrensel çalışır
  • Severity ağırlıklandırma operasyonel önceliklendirme için kritik
  • 15 dakikalık dilimler saatlik dilimlerden çok daha anlamlı korelasyon verir
  • journalctl pipe modern sistemlerde syslog okumaktan daha güvenilir

Bir de şunu söyleyeyim: Bu scriptleri bir kere yazıp /usr/local/bin/ altına koyun, çalıştırılabilir yapın. Bir sonraki alarm geldiğinde tek komutla 5 dakikalık analiz yapmak, 45 dakika log okumaktan hem daha hızlı hem de daha doğru sonuç verir. Deneyim bu işin temelini oluşturuyor, ama doğru araçlara sahip olmak o deneyimi eyleme dönüştürür.

Bir yanıt yazın

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