awk ile Koşullu Metin Filtreleme ve Çok Satırlı Kayıt İşleme

Yıllarca log dosyalarıyla boğuşan biri olarak şunu söyleyeyim: awk öğrendiğim gün terminal kullanımım ikiye katlandı. Belki biraz abartıyorum ama gerçekten öyle hissettim. Grep bir şeyi bulur, sed bir şeyi değiştirir, awk ise ikisini de yapar ve üstüne veri işleme katmanı ekler. Bu yazıda özellikle koşullu filtreleme ve çok satırlı kayıt işleme konularını ele alacağım çünkü bu iki özellik awk’ı rakipsiz kılan şeyler.

Neden Koşullu Filtreleme?

Basit bir grep ile “ERROR” içeren satırları bulabilirsiniz. Peki ya şu sorgu ne olacak: “Disk kullanımı %90’ın üzerinde olan ve son 10 dakika içinde gelen log kayıtlarını getir”? Grep ile bu iş içinden çıkılmaz bir hale gelir. awk’ta ise birkaç satır.

Koşullu filtreleme, awk’ın alan bazlı veri modeli üzerine kurulur. Her satır alanlara bölünür ($1, $2, …) ve bu alanlar üzerinde aritmetik, karşılaştırma ve regex işlemleri yapabilirsiniz. Bunu gerçek bir log analizi senaryosuyla açıklayalım.

Diyelim ki şu formatta bir nginx access log’unuz var:

192.168.1.45 - admin [15/Jan/2025:10:23:11 +0300] "GET /api/users HTTP/1.1" 200 4823
10.0.0.12 - - [15/Jan/2025:10:23:15 +0300] "POST /upload HTTP/1.1" 413 0
172.16.0.8 - root [15/Jan/2025:10:23:19 +0300] "GET /admin/panel HTTP/1.1" 403 512

Sadece HTTP 4xx ve 5xx hatalarını getirmek istiyorsunuz:

awk '$9 >= 400 && $9 < 600 {print $1, $7, $9}' /var/log/nginx/access.log

Çıktı: IP adresi, istek yolu ve status kodu. Sade, okunabilir, hızlı. Milyonluk log dosyasında bile saniyeler içinde tamamlanır.

Temel Koşul Yapıları

awk’ta koşullar iki şekilde yazılır. Birincisi pattern/action modeliyle, ikincisi if-else bloklarıyla. İkisi arasındaki farkı anlamak önemli.

Pattern/action modeli:

awk '/CRITICAL/ {print NR": "$0}' /var/log/syslog

Bu satır syslog’daki tüm CRITICAL içeren satırları satır numarasıyla birlikte basar. /CRITICAL/ kısmı pattern, {print NR": "$0} ise action. Pattern eşleşmezse action çalışmaz.

If-else yapısı:

awk '{
    if ($9 >= 500) {
        print "SERVER_ERROR:", $0
    } else if ($9 >= 400) {
        print "CLIENT_ERROR:", $0
    } else if ($9 >= 200 && $9 < 300) {
        print "SUCCESS:", $0
    }
}' /var/log/nginx/access.log

Bu iki yaklaşımın farkı şu: pattern/action daha okunabilir ve awk’a özgüdür, if-else ise aynı satır üzerinde birden fazla koşul değerlendirmeniz gerektiğinde zorunlu hale gelir.

Alan Karşılaştırmaları ve Veri Tipleri

awk’ın ince bir noktası: alanlar hem string hem sayı olarak kullanılabilir, bağlam belirler. Ancak bu bazen beklenmedik sonuçlar doğurur.

# Bu çalışır - sayısal karşılaştırma
awk '$5 > 1000000 {print $1, $5/1024/1024 "MB"}' disk_usage.txt

# Bu yanlış sonuç verebilir - string karşılaştırma
awk '$5 > "1000000" {print $1}' disk_usage.txt

Sayısal karşılaştırma yapmak istiyorsanız değerin gerçekten sayısal olduğundan emin olun veya +0 ile zorlayın:

awk '($5+0) > 1000000 {print $1}' disk_usage.txt

Gerçek Dünya: Sistem Kaynaklarını İzleme

Bir üretim sunucusunda CPU kullanımını ps aux çıktısından analiz etmek istiyorsunuz. Sadece CPU kullanımı %5’in üzerinde olan processleri, toplam CPU yüküyle birlikte raporlamak istediniz:

ps aux | awk '
NR > 1 && $3 > 5.0 {
    total_cpu += $3
    count++
    printf "%-20s PID:%-8s CPU:%.1f%% MEM:%.1f%%n", $11, $2, $3, $4
}
END {
    printf "n--- Ozet ---n"
    printf "Toplam %d proses, toplam CPU: %.1f%%n", count, total_cpu
}'

Bu script NR > 1 ile başlık satırını atlıyor, $3 > 5.0 ile CPU filtresi uyguluyor, değişkenleri toplayarak END bloğunda özet çıktı üretiyor. Bunu bir cron job’a koyup her 5 dakikada çalıştırırsanız basit bir performans loglaması elde edersiniz.

Birden Fazla Alan Üzerinde Koşul

Gerçek senaryolarda tek alan filtrelemek yetmez. Bir güvenlik analizi örneği: Apache log’larında aynı IP’den 1 dakika içinde 100’den fazla istek geliyorsa ve bu istekler 404 dönüyorsa muhtemelen bir tarayıcı (scanner) var.

Önce basit bir versiyon:

awk '
$9 == 404 {
    ip[$1]++
}
END {
    for (addr in ip) {
        if (ip[addr] > 100) {
            print addr, ip[addr], "404 istegi - muhtemel scanner"
        }
    }
}' /var/log/apache2/access.log

Burada associative array (ilişkisel dizi) kullanıyoruz. ip[$1]++ her IP için sayacı artırıyor. END bloğunda 100’ü geçenleri raporluyoruz.

Çok Satırlı Kayıt İşleme

İşte awk’ın gerçekten öne çıktığı kısım. Çoğu sistem yöneticisi awk’ı sadece satır bazlı işlemler için kullanır ama çok satırlı kayıtlar söz konusu olduğunda bambaşka bir dünya açılır.

RS Değişkeni ile Kayıt Ayırıcı Değiştirme

Varsayılan olarak awk’ta her satır bir kayıttır (RS="n"). Bunu değiştirerek farklı formatlardaki verileri işleyebilirsiniz.

Bir uygulama hata logu düşünün, hatalar boş satırla ayrılmış bloklar halinde geliyor:

Timestamp: 2025-01-15 10:23:11
Level: ERROR
Service: payment-service
Message: Connection timeout to database
Stack: com.example.PaymentService.charge(line:145)

Timestamp: 2025-01-15 10:23:19
Level: WARN
Service: auth-service
Message: Invalid token received
Stack: com.example.AuthFilter.doFilter(line:67)

Bu formatta sadece ERROR seviyesindeki kayıtları almak:

awk '
BEGIN { RS=""; FS="n" }
/Level: ERROR/ {
    print "=== HATA KAYDI ==="
    print $0
    print ""
}' uygulama.log

RS="" ayarlandığında awk boş satırı kayıt ayırıcı olarak kullanır. FS="n" ile her satır bir alan olur. Artık $1 ilk satır (Timestamp), $2 ikinci satır (Level) demek.

Çok Satırlı Kayıtlarda Alan Çıkarma

Şimdi biraz daha ileri gidelim. Aynı log formatından sadece hatalı servis adlarını ve mesajlarını çıkarmak istiyorsunuz:

awk '
BEGIN { RS=""; FS="n" }
/Level: ERROR/ {
    service = ""
    message = ""
    for (i = 1; i <= NF; i++) {
        if ($i ~ /^Service:/) {
            split($i, parts, ": ")
            service = parts[2]
        }
        if ($i ~ /^Message:/) {
            split($i, parts, ": ")
            message = parts[2]
        }
    }
    print service": "message
}' uygulama.log

Bu script her ERROR bloğunu okuyup içinde geziniyor, split() fonksiyonuyla alan değerlerini ayıklıyor. Çıktı:

payment-service: Connection timeout to database

State Machine Yaklaşımı

Bazı log formatları ne boş satırla ne de başka bir ayırıcıyla ayrılır. Örneğin systemd journal logları veya bazı özel uygulama logları belirli bir keyword’le başlayıp bir sonraki keyword’e kadar devam eder.

BEGIN_REQUEST id=4521
  method: POST
  path: /api/payment
  user: [email protected]
  duration_ms: 3420
END_REQUEST

BEGIN_REQUEST id=4522
  method: GET
  path: /api/users
  user: [email protected]
  duration_ms: 45
END_REQUEST

3 saniyeden uzun süren requestleri bulmak:

awk '
/BEGIN_REQUEST/ {
    in_record = 1
    duration = 0
    record = $0"n"
    next
}
in_record {
    record = record $0 "n"
    if (/duration_ms:/) {
        split($0, a, ": ")
        duration = a[2]
    }
}
/END_REQUEST/ && in_record {
    record = record $0
    if (duration > 3000) {
        print "YAVAS REQUEST (" duration "ms):"
        print record
    }
    in_record = 0
    record = ""
}' requests.log

Bu bir state machine. in_record bayrağı şu an bir kayıt bloğu içinde olup olmadığımızı tutuyor. next komutu mevcut satırı işlemeyi bitirip bir sonraki satıra geçiyor. END_REQUEST görününce bloğu değerlendirip karar veriyoruz.

Getline ile Dinamik Satır Okuma

Bazen mevcut satırın hemen altındaki satırı okumak gerekir. Bunu getline ile yapabilirsiniz.

Bir config dosyasında bir direktifin değerini bir sonraki satırda arıyorsunuz:

awk '
/^server_name/ {
    getline next_line
    if (next_line ~ /production/) {
        print "Production sunucu bulundu:", $0
        print "Sonraki satir:", next_line
    }
}' nginx.conf

getline next_line bir sonraki satırı okuyup next_line değişkenine atar. Dikkat: bu kullanımda NR artar ama $0 değişmez, next_line ayrı bir değişkendir.

Pratik: Çok Dosyalı İşlem ve FILENAME

Gerçek ortamlarda tek dosyayla çalışmak nadirdir. Birden fazla log dosyasını işlerken hangi dosyadan geldiğini bilmek kritik:

awk '
FNR == 1 {
    print "n=== Dosya: " FILENAME " ==="
}
$0 ~ /CRITICAL|FATAL/ {
    print FNR": "$0
}
END {
    print "nToplam satir islendi:", NR
}' /var/log/app/*.log

FNR her dosyada sıfırdan başlayan satır numarası, NR ise tüm dosyalar boyunca toplam satır numarası. FILENAME mevcut dosyanın adını verir. FNR == 1 koşulu her yeni dosya başladığında çalışır.

Farklı Alan Ayırıcılarla Çalışma

Gerçek log dosyaları her zaman boşlukla ayrılmış değildir. CSV, TSV veya özel ayırıcılı formatlar sıklıkla karşılaşılır:

# CSV dosyasında belirli sütunları filtrele
awk -F',' '
NR > 1 && $4 == "FAILED" && $6+0 > 500 {
    printf "ID: %s, User: %s, Hata: %s, Gecikme: %smsn", $1, $2, $4, $6
}' islemler.csv

# Pipe ile ayrılmış format
awk -F'|' '$3 ~ /^192.168./ && $5 > 1000 {print $1, $3, $5}' network_log.txt

-F ile alan ayırıcısı belirlenir. Regex de kullanılabilir:

# Bir veya daha fazla boşluk veya tab ile ayrılmış
awk -F'[[:space:]]+' '{print $2}' karmasik_format.txt

END Bloğunda Kapsamlı Raporlama

Tüm bu teknikleri birleştiren gerçekçi bir örnek: bir web sunucusunun günlük trafik raporu.

awk '
BEGIN {
    FS = " "
    print "Gunluk Trafik Analizi"
    print "===================="
}
NR > 0 {
    # Status kodlarini say
    status[$9]++

    # IP bazli istek sayisi
    requests[$1]++

    # Toplam transfer boyutu
    if ($10 ~ /^[0-9]+$/) {
        total_bytes += $10
    }

    # Hata orani icin
    if ($9 >= 400) errors++
    total++
}
END {
    print "n-- Status Kod Dagilimi --"
    for (code in status) {
        printf "HTTP %s: %d istekn", code, status[code]
    }

    print "n-- En Cok Istek Yapan 5 IP --"
    for (ip in requests) {
        print requests[ip], ip
    } | "sort -rn | head -5"

    print "n-- Genel Ozet --"
    printf "Toplam istek: %dn", total
    printf "Hata orani: %.2f%%n", (errors/total)*100
    printf "Toplam transfer: %.2f MBn", total_bytes/1024/1024
}' /var/log/nginx/access.log

Burada dikkat çekici bir detay var: } | "sort -rn | head -5" ifadesi. awk içinden shell komutlarına pipe edebilirsiniz. Bu güçlü ama dikkatli kullanılması gereken bir özellik; her çağrıda yeni bir shell process açar.

BEGIN Bloğunu Akıllıca Kullanmak

BEGIN bloğu dosya okunmadan önce çalışır. Bunu sadece başlık yazdırmak için değil, ortam değişkenlerini ayarlamak, lookup tabloları oluşturmak veya başka dosyalardan veri okumak için kullanabilirsiniz:

awk '
BEGIN {
    # Whitelist dosyasini oku
    while ((getline line < "/etc/monitoring/whitelist.txt") > 0) {
        whitelist[line] = 1
    }
    close("/etc/monitoring/whitelist.txt")
}
{
    ip = $1
    if (!(ip in whitelist) && $9 >= 400) {
        print "SUPHELI:", ip, $7, $9
    }
}' /var/log/nginx/access.log

Bu script önce bir whitelist dosyasını belleğe alıyor, sonra log’u işlerken her IP’yi bu listeye karşı kontrol ediyor. Whitelist’te olmayan IP’lerden gelen hatalar raporlanıyor.

Performans İpuçları

Büyük dosyalarla çalışırken birkaç önemli nokta:

  • next komutu: Koşul sağlandığında kalan kuralları atla. Gereksiz işlemi önler.
  • exit: İhtiyaç duyduğunuzu bulduktan sonra işlemi sonlandırır, geriye kalan GB’larca veriyi okumaz.
  • Regex derleme: Sık kullanılan regex’leri değişkene atın, her satırda yeniden derlenmesini önleyin.
  • Alan sınırlaması: Sadece ihtiyacınız olan alanları işleyin.
# İlk 10000 satırdan sonra çık
awk 'NR > 10000 {exit} /ERROR/ {print}' buyuk_dosya.log

# next ile erken çıkış
awk '/^#/ {next} $3 < 0 {next} {print $1, $3*2}' veri.txt

Sonuç

awk’ın koşullu filtreleme ve çok satırlı kayıt işleme özellikleri, günlük sysadmin işlerinde ciddi zaman tasarrufu sağlar. Temel mantığı kavradıktan sonra grep+sed kombinasyonundan çok daha güçlü ve okunabilir çözümler üretebilirsiniz.

Pratikte en çok işe yarayan şeyin önce basit bir şeyle başlamak olduğunu söyleyebilirim: bir satır awk yazın, çıktısına bakın, genişletin. Tek seferde mükemmel script yazmaya çalışmak zaman kaybettirir. Küçük adımlarla ilerleyip her adımın çıktısını doğrulayın.

Son olarak: awk scriptlerinizi awk -f script.awk dosya.txt şeklinde ayrı dosyalara alın. Tek satırlık kullanım için komut satırı yeterlidir ama karmaşık mantık içeren, tekrar kullanılacak scriptleri mutlaka dosyaya taşıyın. Hem versiyon kontrolü altına alabilirsiniz hem de meslektaşlarınız “bu ne işe yarıyor” diye sormadan anlayabilir.

Bir yanıt yazın

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