awk ile Dinamik Rapor Formatı: Değişkenlere Dayalı Çıktı Şablonlama

Yıllarca log analizi yapan biri olarak şunu söyleyebilirim: awk öğrenmenin en zor kısmı syntax değil, aslında ne kadar güçlü olduğunu kavramak. Çoğu sysadmin awk '{print $1}' seviyesinde kalıp geçiyor. Ama awk‘ın asıl büyüsü, değişkenler ve şablonlama mekanizmasıyla dinamik raporlar üretebilmesinde yatıyor. Bu yazıda, salt “alanı yazdır” mantığının çok ötesine geçeceğiz.

Temel Mantık: awk’ta Değişken Nedir?

awk‘ta değişkenler iki kategoriye ayrılır: yerleşik değişkenler (built-in) ve kullanıcı tanımlı değişkenler. Yerleşik olanlar NR, NF, FS, OFS, RS, ORS gibi şeyler. Kullanıcı tanımlılar ise istediğiniz her şey olabilir.

Kritik fark şu: awk‘ta değişkenleri önceden tanımlamanıza gerek yok. İlk kullanımda otomatik olarak ya 0 (sayısal) ya da "" (string) olarak başlatılıyorlar. Bu bazen kurtarıcı, bazen de sinir bozucu bir özellik.

# Basit bir kullanıcı tanımlı değişken örneği
awk '{
    toplam += $3
    satir_say++
}
END {
    print "Toplam:", toplam
    print "Satır sayısı:", satir_say
    print "Ortalama:", toplam/satir_say
}' dosya.txt

Bu kadar basit. Şimdi bunu gerçek bir rapora dönüştürelim.

-v Parametresi ile Dışarıdan Değişken Geçmek

Dinamik raporlamanın ilk adımı, awk script’ine dışarıdan parametre geçebilmektir. -v flagı tam bunun için var. Shell değişkenlerini awk‘a taşımanın en temiz yolu bu.

# Basit -v kullanımı
ESIK=1000
awk -v esik="$ESIK" '$3 > esik {print $1, "yüksek değer:", $3}' veriler.txt

# Birden fazla değişken geçmek
DEPARTMAN="IT"
AY="Aralik"
awk -v dept="$DEPARTMAN" -v ay="$AY" '
BEGIN {
    print "=== " dept " Departmanı " ay " Raporu ==="
    print "-----------------------------------"
}
$2 == dept {
    print $1, $3
}
END {
    print "-----------------------------------"
    print "Rapor tamamlandı."
}' calisanlar.txt

Burada dikkat edilmesi gereken bir nokta var: -v ile geçilen değişkenler BEGIN bloğunda bile kullanılabilir. Shell değişkenlerini doğrudan awk içinde $DEGISKEN şeklinde kullanmaya çalışırsanız, awk bunu kendi syntax’ında algılamaz. Bu klasik bir hata.

printf ile Şablonlu Çıktı

print yeterli değil. Gerçek rapor formatlaması için printf şart. C’deki printf ile birebir aynı mantık.

# Sütun hizalamalı rapor
awk '
BEGIN {
    printf "%-20s %10s %15s %10sn", "Kullanici", "CPU(%)", "Bellek(MB)", "Islem"
    printf "%-20s %10s %15s %10sn", "--------------------", "----------", "---------------", "----------"
}
NR > 1 {
    printf "%-20s %10.2f %15.1f %10dn", $1, $2, $3, $4
}
END {
    printf "nToplam %d kayıt işlendi.n", NR-1
}' sistem_metrikleri.txt

printf format belirteçleri:

  • %s: String
  • %d: Integer
  • %f: Float (ondalıklı sayı)
  • %e: Bilimsel notasyon
  • %-20s: Sola hizalı, 20 karakter genişliğinde
  • %10.2f: 10 karakter genişliğinde, 2 ondalık basamak

Bu kadar bilgiyle bile ciddi raporlar yazabilirsiniz. Ama daha ilginç yerlere gideceğiz.

İlişkisel Diziler ile Gruplama ve Özet Raporlar

awk‘ın en güçlü silahlarından biri ilişkisel diziler (associative arrays). Bunlar aslında hash map, yani anahtar-değer çiftleri. Dinamik rapor formatlamasında bunlar olmadan hiçbir yere gidemezsiniz.

Senaryo: Nginx access log’unuzu analiz ediyorsunuz. Her IP’nin kaç istek attığını, toplam byte tüketimini ve en sık ziyaret ettiği URL’yi öğrenmek istiyorsunuz.

# Nginx log analizi - ilişkisel dizi ile gruplama
# Log formatı: IP - - [tarih] "METHOD /path HTTP/1.1" status bytes
awk '
BEGIN {
    print "Nginx Erişim Raporu"
    print "==================="
}
{
    ip = $1
    status = $9
    bytes = $10 + 0    # +0 ile sayıya zorla, "-" gelirse 0 olsun
    url = $7

    istek_say[ip]++
    toplam_bytes[ip] += bytes

    # Her IP için en son URL'yi sakla (basit yaklaşım)
    son_url[ip] = url

    # Status kodlarını say
    if (status >= 400) {
        hata_say[ip]++
    }
}
END {
    printf "n%-18s %8s %12s %8s %sn", 
        "IP Adresi", "İstek", "Bytes(KB)", "Hata", "Son URL"
    printf "%-18s %8s %12s %8s %sn", 
        "------------------", "--------", "------------", "--------", "-------"

    for (ip in istek_say) {
        printf "%-18s %8d %12.1f %8d %sn", 
            ip, 
            istek_say[ip], 
            toplam_bytes[ip]/1024, 
            hata_say[ip]+0, 
            son_url[ip]
    }

    print "nNot: Hata = HTTP 4xx/5xx yanıtları"
}' /var/log/nginx/access.log

Bu script’i ilk kez çalıştırdığımda, elle grep/sort/uniq zinciri yazmaya çalıştığım çözümün 10 katı bilgi verdiğini gördüm. Hepsini tek bir geçişte, dosyayı bir kez okuyarak.

Çoklu Dosya İşleme ve FILENAME Değişkeni

Gerçek ortamlarda birden fazla log dosyası var. FILENAME yerleşik değişkeni, hangi dosyayı işlediğinizi takip etmenizi sağlar.

# Birden fazla log dosyasını tek raporda birleştir
awk '
FNR == 1 {
    # Her yeni dosyanın başında başlık yazdır
    print "n--- Dosya:", FILENAME, "---"
    dosya_say++
    dosya_istek[FILENAME] = 0
}
/ERROR/ {
    hata_toplam++
    dosya_hata[FILENAME]++
    printf "  [%s] Satır %d: %sn", FILENAME, FNR, $0
}
{
    dosya_istek[FILENAME]++
}
END {
    print "n=============================="
    print "ÖZET RAPOR"
    print "=============================="
    printf "Toplam işlenen dosya: %dn", dosya_say
    printf "Toplam satır: %dn", NR
    printf "Toplam hata: %dn", hata_toplam
    print ""
    print "Dosya bazlı hata dağılımı:"
    for (f in dosya_hata) {
        printf "  %-40s : %d hata / %d satırn", 
            f, dosya_hata[f], dosya_istek[f]
    }
}' /var/log/app/*.log

NR ile FNR arasındaki farka dikkat edin. NR tüm dosyalar genelinde toplam satır numarası, FNR ise o anki dosyadaki satır numarası. Bu farkı bilmeden çoklu dosya işleme yaparken ciddi hatalar yapabilirsiniz.

Koşullu Şablonlar: Değere Göre Farklı Format

Raporun gerçekten dinamik olması demek, verinin değerine göre farklı çıktı formatı üretmek demek. Örneğin kritik değerler için farklı bir uyarı göstergesi, normal değerler için farklı bir format.

# Disk kullanım raporu - eşiğe göre farklı format
df -P | awk '
BEGIN {
    # Dışarıdan -v ile geçilebilir, yoksa default değer
    if (uyari_esik == "") uyari_esik = 80
    if (kritik_esik == "") kritik_esik = 90

    print ""
    print "╔══════════════════════════════════════════════╗"
    print "║           DISK KULLANIM RAPORU               ║"
    print "╚══════════════════════════════════════════════╝"
    printf "n%-25s %8s %8s %8s %sn", 
        "Dosya Sistemi", "Boyut", "Kullanılan", "Boş", "Durum"
    print "-----------------------------------------------------------"
}
NR > 1 {
    # Yüzde işaretini temizle
    gsub(/%/, "", $5)
    kullanim = $5 + 0

    if (kullanim >= kritik_esik) {
        durum = "[KRİTİK !!!]"
    } else if (kullanim >= uyari_esik) {
        durum = "[UYARI]     "
    } else {
        durum = "[OK]        "
    }

    printf "%-25s %8s %8s %8s %s %d%%n", 
        $6, $2, $3, $4, durum, kullanim
}
END {
    print "-----------------------------------------------------------"
    printf "Rapor zamanı: "
    system("date")
}' 

Bunu cron’a koymak ve mail ile göndermek istiyorsanız:

#!/bin/bash
RAPOR=$(df -P | awk -v uyari_esik=75 -v kritik_esik=85 '
# ... yukarıdaki awk scripti ...
')

echo "$RAPOR" | mail -s "Disk Kullanım Raporu - $(hostname)" [email protected]

Fonksiyon Tanımlama ile Yeniden Kullanılabilir Şablonlar

awk‘ın az bilinen ama son derece işe yarayan bir özelliği: fonksiyon tanımlayabilirsiniz. Büyük script’lerde aynı formatlama mantığını defalarca yazmak zorunda kalmamak için bunu mutlaka kullanın.

# Fonksiyonlarla modüler rapor şablonu
awk '
function baslik_yaz(baslik,    uzunluk, cizgi) {
    uzunluk = length(baslik) + 4
    cizgi = ""
    for (i = 0; i < uzunluk; i++) cizgi = cizgi "="
    print cizgi
    print "| " baslik " |"
    print cizgi
}

function boyut_formatla(bytes) {
    if (bytes >= 1073741824)
        return sprintf("%.2f GB", bytes/1073741824)
    else if (bytes >= 1048576)
        return sprintf("%.2f MB", bytes/1048576)
    else if (bytes >= 1024)
        return sprintf("%.2f KB", bytes/1024)
    else
        return bytes " B"
}

function durum_belirle(deger, uyari, kritik) {
    if (deger >= kritik) return "KRITIK"
    if (deger >= uyari)  return "UYARI"
    return "NORMAL"
}

BEGIN {
    baslik_yaz("Sistem Kaynak Kullanım Raporu")
    print ""
}
{
    boyut = $2 * 1024    # KB cinsinden geliyor, byte'a çevir
    durum = durum_belirle($3+0, 70, 90)

    printf "%-30s %-12s %sn", $1, boyut_formatla(boyut), durum
}
END {
    print ""
    print "Toplam " NR " kaynak kontrol edildi."
}' kaynak_listesi.txt

Fonksiyon parametrelerindeki uzunluk, cizgi kısmına bakın. awk‘ta yerel değişken tanımlamanın tek yolu bu: fazladan parametre olarak listelemek. Garip ama bu dilin özelliği.

Gerçek Dünya Senaryosu: Uygulama Performans Raporu

Şimdi her şeyi bir araya getirelim. Bir uygulama sunucusunda response time log’u işleyip yöneticiye günlük özet raporu göndermek istiyorsunuz.

Log formatı varsayalım: 2024-01-15 14:23:45 POST /api/users 201 245ms

#!/bin/bash
# Günlük performans raporu oluşturucu

TARIH=$(date +%Y-%m-%d)
LOG_DOSYA="/var/log/app/access-${TARIH}.log"
CIKTI="/tmp/performans_raporu_${TARIH}.txt"

awk -v rapor_tarihi="$TARIH" -v host="$(hostname)" '
function ms_cevir(ms_str,    sayi) {
    gsub(/ms/, "", ms_str)
    return sayi = ms_str + 0
}

function yuzde(bolum, bolen) {
    if (bolen == 0) return 0
    return (bolum / bolen) * 100
}

BEGIN {
    uyari_ms    = 500
    kritik_ms   = 2000
    print "UYGULAMA PERFORMANS RAPORU"
    print "=========================="
    print "Sunucu  : " host
    print "Tarih   : " rapor_tarihi
    print "=========================="
    print ""
}
{
    endpoint = $4
    status   = $5
    sure     = ms_cevir($6)

    toplam_istek++
    endpoint_istek[endpoint]++
    endpoint_sure[endpoint] += sure

    # En yavaş isteği bul
    if (sure > en_yavas_sure) {
        en_yavas_sure = sure
        en_yavas_endpoint = endpoint
    }

    # Hata sayımı
    if (status >= 500) {
        server_hata++
        hata_endpoint[endpoint]++
    } else if (status >= 400) {
        client_hata++
    }

    # Yavaş istek sayımı
    if (sure >= kritik_ms) {
        kritik_yavas++
    } else if (sure >= uyari_ms) {
        uyari_yavas++
    }
}
END {
    # Genel özet
    print "GENEL ÖZET"
    print "----------"
    printf "  Toplam İstek     : %dn", toplam_istek
    printf "  Başarı Oranı     : %.1f%%n", yuzde(toplam_istek - server_hata, toplam_istek)
    printf "  Sunucu Hataları  : %d (%.1f%%)n", server_hata, yuzde(server_hata, toplam_istek)
    printf "  İstemci Hataları : %d (%.1f%%)n", client_hata, yuzde(client_hata, toplam_istek)
    printf "  En Yavaş İstek   : %dms (%s)n", en_yavas_sure, en_yavas_endpoint
    printf "  Kritik Yavaş     : %d istek (>%dms)n", kritik_yavas, kritik_ms
    printf "  Uyarı Yavaş      : %d istek (>%dms)n", uyari_yavas, uyari_ms

    print ""
    print "ENDPOINT BAZLI ORTALAMA YANIT SÜRESİ"
    print "-------------------------------------"
    for (ep in endpoint_istek) {
        ort = endpoint_sure[ep] / endpoint_istek[ep]
        isaretci = (ort >= kritik_ms) ? " [KRİTİK]" : 
                   (ort >= uyari_ms)  ? " [UYARI]"  : ""
        printf "  %-35s : %7.1fms  (%d istek)%sn", 
            ep, ort, endpoint_istek[ep], isaretci
    }

    print ""
    print "HATA VEREN ENDPOINT'"'"'LER"
    print "------------------------"
    for (ep in hata_endpoint) {
        printf "  %-35s : %d hatan", ep, hata_endpoint[ep]
    }

    print ""
    print "--- Rapor sonu ---"
}
' "$LOG_DOSYA" > "$CIKTI"

cat "$CIKTI"
# mail -s "Performans Raporu - $TARIH" [email protected] < "$CIKTI"

OFS ve ORS ile Çıktı Formatını Köklü Değiştirmek

Son olarak, OFS (Output Field Separator) ve ORS (Output Record Separator) değişkenlerinden bahsetmek gerekiyor. CSV’den TSV’ye, TSV’den başka bir formata dönüşüm yaparken bunlar hayat kurtarıcı.

# CSV'i farklı bir formata dönüştür
awk '
BEGIN {
    FS  = ","       # Girdi ayırıcısı virgül
    OFS = "|"       # Çıktı ayırıcısı pipe
    ORS = "n---n" # Her kayıt sonuna çizgi ekle
}
NR > 1 {
    # Sadece belirli alanları al ve yeniden formatla
    print $1, $3, $5
}
' veri.csv

# Ya da JSON benzeri çıktı (gerçek JSON için jq kullanın ama fikir vermesi için)
awk '
BEGIN { FS = ","; print "[" }
NR > 1 {
    printf "  {"id": "%s", "ad": "%s", "deger": %s}", 
        $1, $2, $3
    # Son satırda virgül koyma
    if (NR < toplam_satir) print ","
    else print ""
}
END { print "]" }
' veri.csv

Sonuç

awk ile dinamik rapor formatlamasının özü şu: veriyi bir kez okuyun, işleme sırasında durumu izleyin, sonunda tutarlı ve okunabilir çıktı üretin. Bu üç adımı BEGIN, ana blok ve END üçlüsüyle mükemmel şekilde hayata geçirebilirsiniz.

Öğrendiklerimizi özetlersek:

  • -v parametresi ile shell değişkenlerini awk‘a temiz şekilde taşıyın
  • printf ile sütun hizalamalı, profesyonel görünümlü çıktılar oluşturun
  • İlişkisel diziler ile gruplama ve özetleme yapın
  • FILENAME/FNR/NR ile çoklu dosya senaryolarını yönetin
  • Fonksiyonlar ile tekrar eden formatlama mantığını merkeze alın
  • OFS/ORS ile çıktı formatını kökten değiştirin

Bunların hepsini script olarak kaydedin, parametreli hale getirin ve cron’a bağlayın. Haftalarca elle rapor hazırladığınız işler, dakikalar içinde halloluyor. Ben ilk bu düzeyde bir awk scripti yazdığımda ardı arkası kesilmeyen “bunu nasıl yaptın” soruları geldi. Cevap her zaman aynı: zaten aracın içinde var, sadece kullanmak lazım.

Bir yanıt yazın

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