awk ile Tarih ve Saat Verilerini Ayrıştırma, Formatlama ve Zaman Aralığına Göre Filtreleme

Log dosyalarıyla uzun yıllar geçirmiş biri olarak şunu net söyleyebilirim: awk olmadan tarih/saat bazlı log analizi yapmak, kör bağcıkla ip kesmek gibi bir şey. Evet, grep ile regex’e girebilirsiniz, sed ile biraz şekle sokarsınız, ama gerçek gücü awk‘ın aritmetik ve desen eşleştirme kapasitesi birleştirdiğinde görürsünüz. Bu yazıda günlük işlerinizde karşılaacağınız tarih/saat senaryolarını ve bunları awk ile nasıl çözeceğinizi somut örneklerle aktarıyorum.

Tarih Verisiyle Çalışmanın Temel Mantığı

awk‘ta tarih/saat verisi aslında düz metinden ibarettir; özel bir veri tipi yoktur. Gücü, bu metni sayıya veya karşılaştırılabilir bir formata dönüştürebilme yeteneğinden alır. İki temel yaklaşım vardır:

  • String karşılaştırması: Tarihler YYYY-MM-DD HH:MM:SS formatındaysa, leksikografik sıralama zaten kronolojik sırayla örtüşür. Yani "2024-03-15" > "2024-02-20" gibi düz string karşılaştırması çalışır.
  • Epoch dönüşümü: Karmaşık tarih formatları veya zaman farkı hesaplamaları için tarihi Unix timestamp’e (epoch saniyesine) çevirmek gerekir. awk‘ın mktime() fonksiyonu burada devreye girer.

gawk (GNU awk) kullanıyorsanız mktime(), strftime() ve systime() fonksiyonlarına erişiminiz var demektir. Çoğu Linux dağıtımında varsayılan awk zaten gawk‘tır ama emin olmak için:

awk --version | head -1

Çıktıda “GNU Awk” görüyorsanız hazırsınız.

Temel: mktime ve strftime Kullanımı

mktime() fonksiyonu "YYYY MM DD HH MM SS" formatında bir string alır ve epoch saniyesi döndürür. strftime() ise tam tersi yönde çalışır; epoch değerini istediğiniz formata çevirir.

Basit bir örnekle başlayalım. Elimizde şu formatta bir log olsun:

2024-03-15 14:23:05 INFO  Kullanici giris yapti: ahmet
2024-03-15 14:45:11 ERROR Veritabani baglantisi kesildi
2024-03-15 15:02:33 INFO  Servis yeniden baslatildi
2024-03-15 15:18:47 WARN  Disk doluluk orani %85 gecti

Bu log’dan belirli bir saat aralığındaki kayıtları çekmek için:

awk '
{
    # Tarihi ve saati ayristir
    split($1, tarih, "-")
    split($2, saat,  ":")
    
    # mktime icin string olustur: "YYYY MM DD HH MM SS"
    epoch = mktime(tarih[1] " " tarih[2] " " tarih[3] " " 
                   saat[1] " " saat[2] " " saat[3])
    
    # 14:30 ile 15:10 arasindaki kayitlari goster
    baslangic = mktime("2024 03 15 14 30 00")
    bitis     = mktime("2024 03 15 15 10 00")
    
    if (epoch >= baslangic && epoch <= bitis)
        print $0
}
' uygulama.log

Bu yaklaşımın güzel yanı: her satır için baslangic ve bitis yeniden hesaplanıyor, bu da gereksiz iş yükü demek. Bunları BEGIN bloğuna taşımak çok daha verimli:

awk '
BEGIN {
    baslangic = mktime("2024 03 15 14 30 00")
    bitis     = mktime("2024 03 15 15 10 00")
}
{
    split($1, t, "-")
    split($2, s, ":")
    epoch = mktime(t[1] " " t[2] " " t[3] " " s[1] " " s[2] " " s[3])
    
    if (epoch >= baslangic && epoch <= bitis)
        print $0
}
' uygulama.log

Farklı Log Formatlarını Ayrıştırma

Gerçek dünyada log formatları standart değildir. Birkaç yaygın formatı ele alalım.

Apache/Nginx Access Log Formatı

Apache’nin varsayılan Combined Log Format’ı şöyle görünür:

192.168.1.105 - ahmet [15/Mar/2024:14:23:05 +0300] "GET /api/users HTTP/1.1" 200 1482

Bu formattaki tarihi ayrıştırmak için ayraçlarla biraz uğraşmak gerekir:

awk '
BEGIN {
    # Ay adlarindan numaraya donusum tablosu
    aylar["Jan"]=1; aylar["Feb"]=2;  aylar["Mar"]=3
    aylar["Apr"]=4; aylar["May"]=5;  aylar["Jun"]=6
    aylar["Jul"]=7; aylar["Aug"]=8;  aylar["Sep"]=9
    aylar["Oct"]=10; aylar["Nov"]=11; aylar["Dec"]=12
    
    baslangic = mktime("2024 03 15 14 00 00")
    bitis     = mktime("2024 03 15 16 00 00")
}
{
    # $4 = [15/Mar/2024:14:23:05
    # Koseli parantezi temizle
    gsub(/[/, "", $4)
    
    # Tarihi parcalara ayir
    split($4, parca, /[/:]/)
    # parca[1]=gun, parca[2]=ay_adi, parca[3]=yil
    # parca[4]=saat, parca[5]=dakika, parca[6]=saniye
    
    epoch = mktime(parca[3] " " aylar[parca[2]] " " parca[1] " " 
                   parca[4] " " parca[5] " " parca[6])
    
    if (epoch >= baslangic && epoch <= bitis)
        print $0
}
' /var/log/nginx/access.log

Syslog Formatı

/var/log/syslog veya /var/log/messages dosyaları şu formattadır:

Mar 15 14:23:05 webserver01 sshd[2341]: Accepted publickey for deploy
Mar 15 14:45:11 webserver01 kernel: Out of memory: Kill process 4821

Burada yıl bilgisi yok, bu klasik bir syslog sorunudur:

awk '
BEGIN {
    aylar["Jan"]=1; aylar["Feb"]=2;  aylar["Mar"]=3
    aylar["Apr"]=4; aylar["May"]=5;  aylar["Jun"]=6
    aylar["Jul"]=7; aylar["Aug"]=8;  aylar["Sep"]=9
    aylar["Oct"]=10; aylar["Nov"]=11; aylar["Dec"]=12
    
    # Yili sistem tarihinden al, ya da sabit yaz
    "date +%Y" | getline yil
    
    baslangic = mktime(yil " 3 15 14 00 00")
    bitis     = mktime(yil " 3 15 15 30 00")
}
{
    split($3, saat, ":")
    epoch = mktime(yil " " aylar[$1] " " $2 " " 
                   saat[1] " " saat[2] " " saat[3])
    
    if (epoch >= baslangic && epoch <= bitis)
        print $0
}
' /var/log/syslog

"date +%Y" | getline yil kısmı çok işe yarar bir awk tekniğidir; shell komutunun çıktısını doğrudan awk değişkenine alır.

Zaman Aralığı Hesaplama ve Raporlama

Şimdi daha pratik bir senaryo: iki olay arasındaki süreyi hesaplamak. Bir deployment log’umuz olsun ve her deployment’ın ne kadar sürdüğünü bulmak isteyelim:

2024-03-15 09:00:00 DEPLOY_START release-v2.1.5
2024-03-15 09:04:37 DEPLOY_END   release-v2.1.5 SUCCESS
2024-03-15 11:30:00 DEPLOY_START release-v2.1.6
2024-03-15 11:38:22 DEPLOY_END   release-v2.1.6 SUCCESS
2024-03-15 14:00:00 DEPLOY_START release-v2.1.7
2024-03-15 14:09:55 DEPLOY_END   release-v2.1.7 FAILED
awk '
function tarih_epoch(tarih_str, saat_str,    t, s) {
    split(tarih_str, t, "-")
    split(saat_str,  s, ":")
    return mktime(t[1] " " t[2] " " t[3] " " s[1] " " s[2] " " s[3])
}

/DEPLOY_START/ {
    surum    = $4
    baslangic_epoch[surum] = tarih_epoch($1, $2)
}

/DEPLOY_END/ {
    surum  = $4
    durum  = $5
    bitis  = tarih_epoch($1, $2)
    sure   = bitis - baslangic_epoch[surum]
    
    dakika = int(sure / 60)
    saniye = sure % 60
    
    printf "%-20s %s  Sure: %d dakika %02d saniyen", 
           surum, durum, dakika, saniye
}
' deployment.log

Bu örnekte awk fonksiyonu tanımladığımıza dikkat edin. Tekrar eden tarih-epoch dönüşüm kodunu tarih_epoch() fonksiyonuna taşımak hem okunabilirliği artırır hem de bakımı kolaylaştırır.

Saatlik ve Günlük Trafik Analizi

Bir başka yaygın senaryo: access log’larını saate göre gruplandırıp istek sayısını bulmak. Bu, hangi saatlerde yük arttığını anlamak için kritiktir.

awk '
BEGIN {
    aylar["Jan"]=1; aylar["Feb"]=2;  aylar["Mar"]=3
    aylar["Apr"]=4; aylar["May"]=5;  aylar["Jun"]=6
    aylar["Jul"]=7; aylar["Aug"]=8;  aylar["Sep"]=9
    aylar["Oct"]=10; aylar["Nov"]=11; aylar["Dec"]=12
}
{
    gsub(/[/, "", $4)
    split($4, p, /[/:]/)
    # p[4] = saat bilgisi (HH)
    saat = p[4]
    
    # HTTP durum kodu $9 icinde
    durum = $9
    
    saatlik_toplam[saat]++
    
    if (durum >= 500)
        saatlik_hata[saat]++
}
END {
    print "Saat  Toplam  5xx Hata  Hata Orani"
    print "----  ------  --------  ----------"
    for (s = 0; s <= 23; s++) {
        saat_str = sprintf("%02d", s)
        if (saatlik_toplam[saat_str] > 0) {
            hata    = saatlik_hata[saat_str] + 0
            toplam  = saatlik_toplam[saat_str]
            oran    = (hata / toplam) * 100
            printf "%s    %-6d  %-8d  %.1f%%n", 
                   saat_str, toplam, hata, oran
        }
    }
}
' /var/log/nginx/access.log

END bloğunda for (s = 0; s <= 23; s++) döngüsüyle saatleri sıralı yazdırıyoruz. awk dizileri hash table olduğu için sırasız gelirdi, bu döngü bunu çözüyor.

Komut Satırından Dinamik Tarih Aralığı Geçirme

Sabit tarih yazmak yerine, script’i çalıştırırken tarih aralığını dışarıdan vermek çok daha kullanışlıdır. -v parametresiyle bunu yapabilirsiniz:

# Son 1 saatin loglarini getir
SIMDI=$(date +%s)
BIR_SAAT_ONCE=$((SIMDI - 3600))

awk -v bas="$BIR_SAAT_ONCE" -v bit="$SIMDI" '
{
    split($1, t, "-")
    split($2, s, ":")
    epoch = mktime(t[1] " " t[2] " " t[3] " " s[1] " " s[2] " " s[3])
    
    if (epoch >= bas+0 && epoch <= bit+0)
        print $0
}
' uygulama.log

Bu yaklaşımı bir shell fonksiyonuna sarabilirsiniz:

son_n_saat_log() {
    local dosya="$1"
    local saat="${2:-1}"   # Varsayilan: 1 saat
    
    local simdi
    simdi=$(date +%s)
    local bitis=$simdi
    local baslangic=$((simdi - saat * 3600))
    
    awk -v bas="$baslangic" -v bit="$bitis" '
    {
        split($1, t, "-")
        split($2, s, ":")
        epoch = mktime(t[1] " " t[2] " " t[3] " " s[1] " " s[2] " " s[3])
        if (epoch >= bas && epoch <= bit) print $0
    }
    ' "$dosya"
}

# Kullanim:
# son_n_saat_log /var/log/app/production.log 3

Bu fonksiyonu .bashrc veya .bash_profile‘a ekleyip her oturumda kullanabilirsiniz. Prodüksiyonda bir şeyler patladığında “son 2 saatin logları” demek için ne kadar zaman harcadığınızı düşünün; bu fonksiyon o anın kurtarıcısı olabilir.

Tarih Formatı Dönüştürme

Farklı sistemlerden gelen log’ları aynı formata getirmek de sık karşılaşılan bir ihtiyaç. Örneğin Windows event log’larından export edilmiş MM/DD/YYYY HH:MM:SS formatını ISO 8601’e (YYYY-MM-DD HH:MM:SS) çevirmek:

awk '
{
    # MM/DD/YYYY HH:MM:SS formatini ayristir
    split($1, tarih, "/")
    # tarih[1]=ay, tarih[2]=gun, tarih[3]=yil
    
    # ISO 8601 formatina donustur
    yeni_tarih = sprintf("%04d-%02d-%02d", 
                         tarih[3], tarih[1], tarih[2])
    
    # Geri kalani koru
    $1 = yeni_tarih
    print $0
}
' windows_events.log

Ya da Unix timestamp olarak gelen logları okunabilir formata çevirmek:

# Ornek: "1710507785 ERROR Veritabani timeout"
awk '
{
    okunabilir = strftime("%Y-%m-%d %H:%M:%S", $1)
    $1 = okunabilir
    print $0
}
' epoch_log.txt

strftime() burada kilit fonksiyon. Format string’i C’deki strftime ile aynı syntax’ı kullanır:

  • %Y: 4 haneli yıl
  • %m: 2 haneli ay (01-12)
  • %d: 2 haneli gün (01-31)
  • %H: 24 saat formatında saat (00-23)
  • %M: Dakika (00-59)
  • %S: Saniye (00-59)
  • %s: Epoch timestamp (sadece gawk’ta)

Çok Satırlı Log Kayıtları ve Zaman Damgası Eşleştirme

Java stack trace gibi çok satıra yayılan log kayıtlarında ilk satırdaki zaman damgasını sonraki satırlarla ilişkilendirmek gerekir. Bu klasik bir awk pattern matching problemidir:

awk '
BEGIN {
    baslangic = mktime("2024 03 15 14 00 00")
    bitis     = mktime("2024 03 15 15 00 00")
    yazdir    = 0
    tampon    = ""
}
/^[0-9]{4}-[0-9]{2}-[0-9]{2}/ {
    # Yeni bir log kaydi basladi
    # Onceki tamponu yazdir (eger zaman araligindaysa)
    if (yazdir && tampon != "")
        printf "%s", tampon
    
    # Yeni kaydin zamanini kontrol et
    split($1, t, "-")
    split($2, s, ":")
    epoch = mktime(t[1] " " t[2] " " t[3] " " s[1] " " s[2] " " s[3])
    
    if (epoch >= baslangic && epoch <= bitis) {
        yazdir = 1
        tampon = $0 "n"
    } else {
        yazdir = 0
        tampon = ""
    }
    next
}
{
    # Devam satiri (stack trace vs.)
    if (yazdir)
        tampon = tampon $0 "n"
}
END {
    if (yazdir && tampon != "")
        printf "%s", tampon
}
' uygulama.log

Bu pattern biraz daha karmaşık ama production’da Java veya Python uygulamalarının loglarına bakıyorsanız şart. Stack trace’i kesmeden tam kayıtları almanızı sağlar.

Performans Notları

Büyük log dosyalarında (awk ile GB’larca dosyayı işlediğim oldu) birkaç şeye dikkat etmek gerekir:

  • BEGIN bloğunda sabit değerleri hesaplayın: mktime ile ürettiğiniz başlangıç/bitiş epoch’larını her satırda yeniden hesaplamayın, bir kere BEGIN‘de yapın.
  • String karşılaştırmasını tercih edin: Tarihleriniz ISO 8601 formatındaysa (YYYY-MM-DD HH:MM:SS), epoch’a çevirmeden direkt string karşılaştırması yapın. Çok daha hızlıdır.
  • next ile erken çıkın: Koşulunuz sağlanmadığında next ile sonraki satıra atlayın, gereksiz işlemleri atlarsınız.
  • zcat ile pipe edin: .gz logları için zcat access.log.gz | awk '...' şeklinde kullanın, geçici dosya açmayın.

String karşılaştırması örneği, basit ama son derece etkili:

# ISO 8601 formatindaki loglar icin epoch gerekmez
awk '
$0 >= "2024-03-15 14:00:00" && $0 <= "2024-03-15 16:00:00"
' uygulama.log

Bu tek satır, mktime içeren versiyondan çok daha hızlı çalışır ve büyük dosyalarda farkı hissedersiniz.

Sonuç

awk ile tarih/saat işleme ilk bakışta karmaşık görünse de temel mantığı kavradıktan sonra log analizinin en güçlü araçlarından biri haline gelir. Özetlemek gerekirse:

  • mktime() ile tarih string’ini epoch’a çevirin, aritmetik ve karşılaştırma için.
  • strftime() ile epoch’u istediğiniz formata dönüştürün.
  • BEGIN bloğunu sabit hesaplamalar için kullanın, performansa etkinizi minimumda tutun.
  • Mümkün olduğunda ISO 8601 formatındaki verilerde string karşılaştırması yapın; basit ve hızlıdır.
  • Shell değişkenlerini -v ile awk‘a geçirin; dinamik tarih aralıkları için şart.

Prodüksiyonda bir sorun anında doğru awk one-liner’ına sahip olmak ile saatlerce log kaydını manuel incelemek arasındaki fark, çoğu zaman bu tür teknikleri önceden pratiğe dökmüş olmaktan geçiyor. Bu örnekleri bir yere not edin, kendi log formatlarınıza uyarlayın ve gerektiğinde hazır olun.

Bir yanıt yazın

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