awk ile Tek Geçişte Metin Dönüştürme ve Filtreleme

Şunu açıkça söyleyeyim: çoğu sysadmin log dosyasını işlemek için grep | awk | sed | sort | uniq gibi uzun boru hatları kurar. Bu tamamen makul bir yaklaşım, ama bir noktada şunu fark edersiniz – aynı dosyayı beş kez okuyorsunuz, beş ayrı process başlatıyorsunuz ve her şeyi birbirine bağlamak için zihinsel enerji harcıyorsunuz. awk bunu tek geçişte yapabilir, hem de çoğu zaman daha temiz bir şekilde.

Bu yazıda awk‘ın gerçek gücünü göstereceğim. Temel sözdizimini anlatmayacağım – bunun için yeterince kaynak var. Doğrudan “bunu neden beş komutla yapayım ki” sorusunu cevaplayan senaryolara gireceğiz.

awk’ı Bir Pipeline Motoru Gibi Düşünmek

awk‘ı genellikle “şu kolonu yazdır” aracı olarak kullanıyoruz. Ama awk aslında tam bir programlama ortamı: değişkenler, diziler, aritmetik, string işleme, koşullar, döngüler – hepsi mevcut.

Klasik refleks şu şekilde çalışır:

# Uzun boru hattı yaklaşımı - aynı dosya 4 kez okunuyor
cat access.log | grep "POST" | grep -v "200" | awk '{print $1, $7}' | sort | uniq -c

Şimdi bunu tek awk komutuyla yapalım:

awk '/POST/ && $9 != "200" {
    count[$1" "$7]++
} 
END {
    for (key in count)
        print count[key], key
}' access.log | sort -rn

Fark şu: dosyayı tek sefer okuduk, filtreleme ve gruplama işlemlerini aynı anda yaptık. sort‘u tamamen awk içine almak da mümkün ama orada fazla karmaşıklaştırmaya gerek yok – sort zaten tek bir geçiş yapıyor ve bu kabul edilebilir.

BEGIN ve END Bloklarını Doğru Kullanmak

awk‘ın üç ana bloğu var: BEGIN, ana işlem bloğu ve END. Çoğu insan END‘i sadece “toplam yazdır” için kullanır, ama bu bloklar çok daha güçlü.

Gerçek bir senaryo: Bir uygulama sunucusunda her servisin kaç hata ürettiğini, ortalama yanıt süresini ve en yavaş endpoint’i bulmak istiyorsunuz.

awk 'BEGIN {
    FS = "|"
    print "Analiz başlıyor..."
    min_time = 999999
    max_time = 0
}
{
    service = $2
    status = $3
    response_time = $4 + 0
    endpoint = $5
    
    # Sadece hataları say
    if (status >= 400) {
        errors[service]++
    }
    
    # Yanıt süresi istatistikleri
    total_time[service] += response_time
    request_count[service]++
    
    # Global min/max takibi
    if (response_time > max_time) {
        max_time = response_time
        slowest_endpoint = endpoint
    }
}
END {
    print "n=== Servis Raporu ==="
    for (svc in request_count) {
        avg = total_time[svc] / request_count[svc]
        printf "%-20s Hata: %4d  Ort Süre: %6.2f msn", 
               svc, errors[svc]+0, avg
    }
    print "nEn yavaş endpoint:", slowest_endpoint, "(" max_time " ms)"
}' /var/log/app/requests.log

Bu tek komut, normalde en az üç ayrı awk veya python scriptiyle yapacağınız şeyi hallediyor.

Alan Ayırıcılarını Dinamik Olarak Değiştirmek

Gerçek hayatta log formatları tutarlı olmaz. Bazen alanlar boşlukla, bazen virgülle, bazen pipe ile ayrılır. Üstelik aynı dosyada karışık formatlar bile olabilir.

awk‘ta -F ile sabit bir ayırıcı tanımlarsınız, ama FS değişkenini satır bazında değiştirebilirsiniz:

awk '{
    # Satırda virgül varsa CSV gibi işle, yoksa boşlukla ayır
    if (index($0, ",") > 0) {
        FS = ","
        $0 = $0  # Satırı yeniden parse et
        format = "CSV"
    } else {
        FS = " "
        $0 = $0
        format = "SPACE"
    }
    
    if (NF >= 3) {
        printf "[%s] Kolon1: %s | Kolon3: %sn", format, $1, $3
    }
}' mixed_format.log

Not: $0 = $0 ataması awk’a “bu satırı yeni FS ile yeniden böl” talimatı verir. Pek bilinmeyen ama çok işe yarayan bir trick.

Çok Dosyalı Tek Geçiş Analizi

Birden fazla dosyayı ayrı ayrı işlemek yerine, awk dosyalar arasında geçiş yapabilir ve hangi dosyayı işlediğini bilir.

Senaryo: Eski ve yeni log dosyalarını karşılaştırarak IP bazında hata artışını tespit etmek:

awk 'FNR == 1 { filenum++ }
{
    ip = $1
    status = $9
    
    if (status ~ /^5/) {
        if (filenum == 1) {
            old_errors[ip]++
        } else {
            new_errors[ip]++
        }
    }
}
END {
    print "IP | Eski Hata | Yeni Hata | Değişim"
    print "--------------------------------------------"
    for (ip in new_errors) {
        old = old_errors[ip] + 0
        new = new_errors[ip]
        diff = new - old
        if (diff > 10) {
            printf "%-15s | %9d | %9d | +%dn", ip, old, new, diff
        }
    }
}' /var/log/nginx/access.log.1 /var/log/nginx/access.log

FILENAME değişkeni de kullanılabilir ama FNR == 1 kontrolüyle dosya numarası saymak daha güvenilir.

String İşleme: gsub, sub ve match

awk‘ın string fonksiyonları sed‘e ihtiyacı ciddi ölçüde azaltır.

Gerçek dünya senaryosu: Apache log dosyasından URL parametrelerini temizleyip sadece path’leri almak ve bunları normalize etmek:

awk '{
    url = $7
    
    # Query string'i temizle
    sub(/?.*/, "", url)
    
    # Trailing slash normalize et
    sub(//$/, "", url)
    
    # UUID benzeri path segmentlerini [ID] ile değiştir
    gsub(//[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/, "/[UUID]", url)
    
    # Sayısal ID'leri normalize et
    gsub(//[0-9]+/, "/[ID]", url)
    
    # Boş veya sadece slash olan URL'leri atla
    if (url == "" || url == "/") next
    
    endpoint_hits[url]++
    endpoint_method[url] = $6
}
END {
    for (url in endpoint_hits) {
        printf "%6d %s %sn", endpoint_hits[url], endpoint_method[url], url
    }
}' access.log | sort -rn | head -20

Bu script normalde awk + sed + sort pipeline’ı gerektirir. Tek geçişte hallettik.

getline ile Harici Veri Okumak

Bu özelliği çok az kişi kullanır ama çok güçlü. Bir dosyayı işlerken başka bir dosyadan veya komut çıktısından veri çekebilirsiniz.

Senaryo: IP loglarını işlerken DNS çözümlemesi yapmak (ya da önceden hazırlanmış bir IP-hostname eşleme dosyasını kullanmak):

awk 'BEGIN {
    # IP-hostname eşleme dosyasını önceden yükle
    while ((getline line < "/etc/hosts_custom") > 0) {
        split(line, parts, " ")
        hostname_map[parts[1]] = parts[2]
    }
    close("/etc/hosts_custom")
}
{
    ip = $1
    
    # Önce yerel eşleme dosyasına bak
    if (ip in hostname_map) {
        host = hostname_map[ip]
    } else {
        host = ip
    }
    
    requests[host] += 1
    bytes[host] += $10 + 0
}
END {
    for (h in requests) {
        printf "%-40s %8d istek  %12d bytesn", h, requests[h], bytes[h]
    }
}' access.log | sort -k2 -rn

getline‘ın bir de komuttan okuma versiyonu var:

awk '{
    cmd = "date -d @" $1 " +"%Y-%m-%d %H:%M""
    cmd | getline human_date
    close(cmd)
    
    $1 = human_date
    print
}' unix_timestamp.log

Dikkat: Her satır için close() çağırmazsanız, awk aynı komutun çıktısını cache’ler. Bu bazen istediğiniz davranış değildir.

Array Silme ve Bellek Yönetimi

Büyük log dosyalarında awk dizilerinin şişmesi sorun olabilir. Belirli aralıklarla temizlik yapabilirsiniz:

awk 'BEGIN {
    window_size = 1000  # Her 1000 satırda bir temizle
}
{
    buffer[NR] = $0
    
    # Son window_size satırı analiz et
    if (NR % window_size == 0) {
        # Bu penceredeki verileri işle
        for (i = NR - window_size + 1; i <= NR; i++) {
            # işlem yap
            if (buffer[i] ~ /ERROR/) {
                error_in_window++
            }
        }
        
        # Pencere doluysa ve hata oranı yüksekse uyar
        if (error_in_window > window_size * 0.1) {
            print "UYARI: Satır " NR-window_size " - " NR " arasında %10'dan fazla hata!"
        }
        
        # Eski kayıtları sil
        for (i = NR - window_size + 1; i <= NR - window_size; i++) {
            delete buffer[i]
        }
        error_in_window = 0
    }
}' /var/log/app.log

Gerçek Dünya: Nginx Log’larından Anlık Dashboard

Bu script, ops ekiplerimizde fiilen kullandığımız bir versiyonun sadeleştirilmiş halidir. Bir nginx log dosyasını tek geçişte okuyup birden fazla metrik üretir:

awk 'BEGIN {
    FS = " "
    OFS = "t"
    
    # Durum kodu grupları
    split("2xx 3xx 4xx 5xx", status_labels)
}
{
    # Alan atamaları
    ip = $1
    timestamp = $4
    method = $6
    url = $7
    status = $9
    bytes = $10 + 0
    referrer = $11
    agent = $12
    
    # Durum kodu sayımı
    if (status ~ /^2/) status_count["2xx"]++
    else if (status ~ /^3/) status_count["3xx"]++
    else if (status ~ /^4/) status_count["4xx"]++
    else if (status ~ /^5/) status_count["5xx"]++
    
    # IP bazında trafik
    ip_requests[ip]++
    ip_bytes[ip] += bytes
    
    # Saat bazında dağılım (timestamp formatı: [10/Oct/2024:13:55:36])
    split(timestamp, t, ":")
    hour = substr(t[2], 1, 2)
    hourly[hour]++
    
    # Yavaş endpoint takibi (response time varsa - $NF olabilir)
    total_bytes += bytes
    total_requests++
    
    # Bot tespiti (basit)
    if (agent ~ /bot|crawler|spider/i) {
        bot_requests++
        bot_ips[ip]++
    }
}
END {
    print "========================================="
    print "           NGINX LOG ANALİZİ"
    print "========================================="
    printf "Toplam İstek : %dn", total_requests
    printf "Toplam Trafik: %.2f MBn", total_bytes / 1024 / 1024
    printf "Bot İsteği   : %d (%.1f%%)n", bot_requests, (bot_requests/total_requests)*100
    
    print "n--- Durum Kodları ---"
    for (code in status_count) {
        printf "  %s: %d (%.1f%%)n", code, status_count[code], 
               (status_count[code]/total_requests)*100
    }
    
    print "n--- Saatlik Dağılım ---"
    for (h = 0; h < 24; h++) {
        hour_fmt = sprintf("%02d", h)
        bar_count = int(hourly[hour_fmt] / 100)
        bar = ""
        for (b = 0; b < bar_count; b++) bar = bar "#"
        printf "  %s:00 | %-30s %dn", hour_fmt, bar, hourly[hour_fmt]+0
    }
    
    print "n--- Top 5 IP ---"
    n = 0
    for (ip in ip_requests) {
        if (n++ < 5)
        printf "  %-15s %6d istek  %8.2f MBn", ip, ip_requests[ip], 
               ip_bytes[ip]/1024/1024
    }
}' /var/log/nginx/access.log

Bu script grep, cut, sort, uniq, wc, awk‘ın yerini tek seferde tutar.

Performans Notları

awk pipeline’dan ne kadar hızlı? Bunu somut olarak değerlendirmek için bakılması gereken nokta şu:

  • 5 process vs 1 process: Her pipe yeni bir fork/exec maliyeti demek. Büyük dosyalarda bu fark hissedilir.
  • I/O tekrarı: cat | grep | grep | awk zincirinde veri bellekte birden fazla kez kopyalanır.
  • Buffer overhead: Pipe’lar arasındaki veri transferi de maliyetsiz değil.

Tipik olarak 500MB+ log dosyalarında tek awk scripti, 4-5 komutluk pipeline’dan 2-3 kat hızlı çalışır. Daha küçük dosyalarda fark ihmal edilebilir, bu yüzden okunabilirliği de göz önünde bulundurun.

Bir de şunu söyleyeyim: bazen boru hattı daha okunabilirdir ve bakımı kolaydır. Her şeyi zorla awk‘a tıkmak zorunda değilsiniz. Ama “aynı dosyayı birden fazla kez okuyorum” veya “bu script yavaş” gibi durumlarla karşılaştığınızda, awk‘ın tek geçiş kapasitesi aklınızın bir köşesinde olsun.

Sonuç

awk öğrenmesi zahmetli bir araç gibi görünür, ama investmanın karşılığını çok çabuk alırsınız. Anlattığımız teknikler şunlar:

  • BEGIN/END blokları ile durum bilgisi taşıma ve raporlama
  • Birden fazla dosya üzerinde tek geçişli karşılaştırma
  • gsub/sub/match ile sed ihtiyacını ortadan kaldırma
  • getline ile harici veri kaynağı entegrasyonu
  • Dizi yönetimi ile büyük dosyalarda bellek kontrolü
  • Çok metrikli analiz ile birden fazla aracın yerini tek script’e taşıma

Bunların hepsini ezberlemek zorunda değilsiniz. Önemli olan refleksi geliştirmek: bir sonraki uzun pipeline kurduğunuzda “bunu tek awk scriptiyle yapabilir miyim?” diye sormak. Çoğu zaman cevap evet.

Son bir pratik öneri: awk scriptlerinizi tek satır olarak yazmak zorunda değilsiniz. Uzun ve karmaşık scriptleri bir .awk dosyasına yazıp awk -f script.awk logfile şeklinde çalıştırabilirsiniz. Böylece version control’e de alırsınız, ekip arkadaşlarınız da anlayabilir.

Bir yanıt yazın

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