awk ile Anahtar-Değer Çiftlerinden Yapılandırılmış Rapor Üretme

Sistem yöneticiliğinin güzel yanlarından biri, günlük hayatta karşılaştığın kaotik veri yığınlarından düzgün, okunabilir raporlar çıkarmayı öğrenmektir. Bu işin tam ortasında da awk oturuyor. Yıllarca Python yazmadan, Perl öğrenmeden, sadece bu tek araçla tonlarca rapor ürettim. Özellikle anahtar-değer çifti formatındaki yapılandırma dosyalarını ve logları işlemek söz konusu olduğunda awk gerçek anlamda parlıyor.

Anahtar-Değer Formatı Neden Bu Kadar Yaygın?

Pratik hayatta ne kadar çok dosya bu formatta olduğunu fark ettiğinizde şaşırıyorsunuz. /etc/os-release, /proc/meminfo, uygulama log dosyaları, yapılandırma dosyaları, monitoring araçlarının çıktıları… Hepsi şu şablona uyuyor:

KEY=value
KEY: value
KEY = value

Ayırıcı bazen =, bazen :, bazen boşluklu =. Büyük-küçük harf tutarsızlıkları ayrı mesele. Gerçek ortamda bu dosyalar genellikle yüzlerce satırdan oluşuyor ve sen sadece birkaç tanesine ihtiyaç duyuyorsun. İşte tam burada awk devreye giriyor.

Temel Anahtar-Değer Ayrıştırma

En basit senaryo ile başlayalım. Diyelim ki /etc/os-release dosyasından sadece işletim sistemi adını ve sürümünü çekmek istiyoruz:

awk -F'=' '/^(NAME|VERSION)=/ {gsub(/"/, "", $2); print $1 ": " $2}' /etc/os-release

Bu komut şunu yapıyor: = karakterini alan ayırıcı olarak kullanıyor, NAME veya VERSION ile başlayan satırları buluyor, tırnak işaretlerini temizliyor ve okunabilir formatta yazdırıyor. Çıktı şöyle görünecek:

NAME: Ubuntu
VERSION: 22.04.3 LTS (Jammy Jellyfish)

Ama gerçek hayatta bu kadar basit olmaz. Gelin daha karmaşık senaryolara geçelim.

Proc Dosya Sisteminden Bellek Raporu

/proc/meminfo klasik bir anahtar-değer dosyasıdır ve sistem yöneticilerinin sık başvurduğu yerlerden biridir. Ham haliyle 50 küsur satır vardır, ama çoğu zaman ihtiyacınız olan sadece 5-6 değerdir:

awk '
BEGIN {
    print "===== BELLEK RAPORU ====="
    print ""
}
/^MemTotal:/ { total = $2 }
/^MemFree:/ { free = $2 }
/^MemAvailable:/ { available = $2 }
/^Buffers:/ { buffers = $2 }
/^Cached:/ { cached = $2 }
/^SwapTotal:/ { swap_total = $2 }
/^SwapFree:/ { swap_free = $2 }
END {
    used = total - free - buffers - cached
    printf "Toplam RAM    : %8.2f GBn", total/1024/1024
    printf "Kullanilan    : %8.2f GBn", used/1024/1024
    printf "Kullanilabilir: %8.2f GBn", available/1024/1024
    printf "Swap Toplam   : %8.2f GBn", swap_total/1024/1024
    printf "Swap Kullanim : %8.2f GBn", (swap_total - swap_free)/1024/1024
    print ""
    printf "RAM Kullanim Orani: %.1f%%n", (used/total)*100
}
' /proc/meminfo

Bu script’te dikkat edilmesi gereken birkaç nokta var. BEGIN bloğu dosyayı okumadan önce, END bloğu ise tüm satırlar işlendikten sonra çalışır. Değerleri değişkenlerde biriktirip END bloğunda hesaplama yapmak, awk‘ın en güçlü kullanım kalıplarından biridir.

Çoklu Dosyadan Veri Toplama

Gerçek dünya senaryolarının büyük çoğunluğunda tek bir dosya değil, birden fazla dosyadan veri çekmeniz gerekir. Örneğin bir monitoring sistemi için tüm sunuculardan gelen disk kullanım raporlarını birleştirdiğinizi düşünün. Her dosya şu formatta:

server=web01
disk=/dev/sda1
total_gb=500
used_gb=234
mount=/

Bu formattaki onlarca dosyadan konsolide rapor üretmek için:

awk '
BEGIN {
    FS="="
    print "SUNUCU DİSK KULLANIM RAPORU"
    print "Tarih: " strftime("%Y-%m-%d %H:%M")
    print "----------------------------------------"
}
FNR==1 { 
    # Her yeni dosyada onceki kaydi temizle
    if (server != "") {
        pct = (used/total)*100
        printf "%-10s %-15s %6.1f GB / %6.1f GB (%5.1f%%)n", 
               server, mount, used, total, pct
    }
    server=""; disk=""; total=0; used=0; mount=""
}
/^server/ { server=$2 }
/^disk/ { disk=$2 }
/^total_gb/ { total=$2 }
/^used_gb/ { used=$2 }
/^mount/ { mount=$2 }
END {
    if (server != "") {
        pct = (used/total)*100
        printf "%-10s %-15s %6.1f GB / %6.1f GB (%5.1f%%)n",
               server, mount, used, total, pct
    }
    print "----------------------------------------"
}
' /var/reports/disk_*.txt

FNR==1 ifadesi her yeni dosyanın ilk satırında tetiklenir. Bu sayede dosyalar arasında geçiş yaparken bir önceki kaydı yazdırıp değişkenleri sıfırlayabiliyoruz.

Farklı Ayırıcılarla Başa Çıkma

Bazen aynı dosya içinde bile tutarsız ayırıcılar kullanılmış olabilir. Bu durum özellikle eski legacy uygulamaların konfigürasyon dosyalarında sık görülür. Şöyle bir dosyanız varsa:

database_host = 192.168.1.10
database_port=5432
max_connections: 100
timeout : 30

Hem = hem : ile hem de etraflarındaki boşluklarla baş etmeniz gerekiyor:

awk '
{
    # Yorum satirlarini ve bos satirlari atla
    if ($0 ~ /^[[:space:]]*#/ || $0 ~ /^[[:space:]]*$/) next
    
    # Anahtar-deger ayir (= veya : ile)
    if (match($0, /^([^=:]+)[=:](.*)/, arr)) {
        key = arr[1]
        val = arr[2]
        
        # Basta ve sondaki boslukları temizle
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
        
        # Buyuk harfe cevir ve kaydet
        config[toupper(key)] = val
    }
}
END {
    print "Veritabani Yapılandırması:"
    print "  Host       :", config["DATABASE_HOST"]
    print "  Port       :", config["DATABASE_PORT"]
    print "  Max Conn.  :", config["MAX_CONNECTIONS"]
    print "  Timeout    :", config["TIMEOUT"]
}
' uygulama.conf

match() fonksiyonunun üçlü parametre kullanımı GNU awk’a özgüdür ama modern sistemlerde varsayılan olarak gawk yüklü geldiği için sorun yaşamazsınız. Regex grupları ile ayırıcıdan bağımsız bir ayrıştırma yapıyoruz burada.

Dinamik Alan Adlarıyla Rapor Üretmek

Yukarıdaki örneklerde hangi anahtarları çekeceğimizi önceden biliyorduk. Ama bazen dosyanın yapısını bilmeden, tüm anahtarları dinamik olarak işlemeniz gerekir. Bu durum özellikle farklı uygulamaların ürettiği monitoring metriklerini topladığınızda ortaya çıkar:

awk -F'=' '
BEGIN {
    print "=== YAPILANDIRMA DENETIM RAPORU ==="
    OFS=" | "
}
!/^#/ && NF==2 {
    # Anahtari ve degeri kaydet
    key = $1
    val = $2
    gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
    gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
    
    if (key != "") {
        keys[NR] = key
        values[key] = val
        count++
    }
}
END {
    printf "%-30s %-40s %sn", "ANAHTAR", "DEGER", "DURUM"
    print "----------------------------------------------------------------------"
    
    for (i=1; i<=NR; i++) {
        if (keys[i] != "") {
            k = keys[i]
            v = values[k]
            status = (v == "" || v == "null" || v == "none") ? "EKSIK" : "OK"
            printf "%-30s %-40s %sn", k, v, status
        }
    }
    print "----------------------------------------------------------------------"
    printf "Toplam %d yapılandırma anahtarı bulundu.n", count
}
' uygulama.conf

Bu yaklaşımın güzel yanı, hangi anahtar-değer çiftleri olduğunu bilmeden çalışmasıdır. Ayrıca boş veya tanımsız değerleri otomatik olarak “EKSIK” olarak işaretler; bu tip bir denetim raporu günlük operasyonlarda oldukça işe yarar.

Log Dosyalarından Yapılandırılmış Özet

Uygulama logları da çoğu zaman anahtar-değer formatında yazılır, özellikle modern yapılandırılmış loglama (structured logging) kullanıldığında. Örneğin bir web uygulaması şöyle loglar üretiyorsa:

timestamp=2024-01-15T10:23:45 level=ERROR service=auth user=admin action=login status=failed duration_ms=234
timestamp=2024-01-15T10:23:46 level=INFO service=api user=john action=request status=success duration_ms=45
timestamp=2024-01-15T10:23:47 level=WARN service=db user=system action=query status=slow duration_ms=1205

Bu loglardan servis bazlı özet rapor çıkarmak için:

awk '
{
    # Her alanı anahtar-deger olarak ayır
    delete kv
    for (i=1; i<=NF; i++) {
        n = split($i, pair, "=")
        if (n==2) kv[pair[1]] = pair[2]
    }
    
    svc = kv["service"]
    lvl = kv["level"]
    dur = kv["duration_ms"] + 0
    
    if (svc != "") {
        service_count[svc]++
        service_total_ms[svc] += dur
        if (lvl == "ERROR") service_errors[svc]++
        if (dur > service_max[svc]) service_max[svc] = dur
    }
    
    total_requests++
}
END {
    print "nSERVIS PERFORMANS RAPORU"
    print "========================="
    print ""
    
    for (svc in service_count) {
        cnt = service_count[svc]
        avg = service_total_ms[svc] / cnt
        errs = service_errors[svc] + 0
        err_rate = (errs / cnt) * 100
        
        print "Servis: " svc
        print "  Toplam İstek  : " cnt
        printf "  Ort. Sure     : %.1f msn", avg
        print "  Maks. Sure    : " service_max[svc] " ms"
        printf "  Hata Orani    : %.1f%%n", err_rate
        print ""
    }
    print "Toplam islem: " total_requests
}
' uygulama.log

Burada ilginç bir teknik kullandık: her satırın alanlarını ($i) split() ile pair dizisine bölüp oradan kv ilişkisel dizisine dolduruyoruz. delete kv komutu her satırda diziyi sıfırlıyor ki bir önceki satırın değerleri sızmaya neden olmasın.

Birden Fazla Kaynağı Karşılaştırma

Bir başka pratik senaryo: iki ortamın (örneğin production ve staging) yapılandırmasını karşılaştırmak istiyorsunuz. Bu işlemi iki awk çalıştırıp fark almak yerine tek bir komutla yapabiliriz:

awk -F'=' '
FNR==NR {
    # Ilk dosyayi oku (production)
    if (!/^#/ && NF==2) {
        gsub(/[[:space:]]/, "", $1)
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
        prod[$1] = $2
    }
    next
}
{
    # Ikinci dosyayi oku (staging) ve karsilastir
    if (!/^#/ && NF==2) {
        gsub(/[[:space:]]/, "", $1)
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
        staging[$1] = $2
        all_keys[$1] = 1
    }
}
END {
    # Production anahtarlarini da ekle
    for (k in prod) all_keys[k] = 1
    
    print "YAPILANDIRMA KARSILASTIRMA RAPORU"
    print "================================="
    printf "%-30s %-25s %-25s %sn", "ANAHTAR", "PRODUCTION", "STAGING", "DURUM"
    print "-------------------------------------------------------------------------------------"
    
    for (k in all_keys) {
        p_val = (k in prod) ? prod[k] : "TANIMLI DEGIL"
        s_val = (k in staging) ? staging[k] : "TANIMLI DEGIL"
        
        if (p_val == s_val) {
            status = "ESLESME"
        } else if (!(k in prod)) {
            status = "SADECE STAGING"
        } else if (!(k in staging)) {
            status = "SADECE PROD"
        } else {
            status = "FARKLI !"
        }
        
        printf "%-30s %-25s %-25s %sn", k, p_val, s_val, status
    }
}
' production.conf staging.conf

FNR==NR kalıbı awk dünyasının en klasik numaralarından biridir. NR toplam satır numarasını, FNR ise mevcut dosyadaki satır numarasını tutar. İlk dosyayı okurken ikisi eşit olur, ikinci dosyaya geçince NR artmaya devam eder ama FNR sıfırlanır. Bu sayede hangi dosyayı okuduğumuzu anlayabiliyoruz.

Raporu Dosyaya Yönlendirme ve Renklendirme

Raporların sadece terminale yazdırılması gerekmez. Hem dosyaya yazsın hem de renkli terminal çıktısı versin diye şöyle bir yaklaşım kullanabilirsiniz:

awk '
BEGIN {
    RED="33[0;31m"
    GREEN="33[0;32m"
    YELLOW="33[1;33m"
    NC="33[0m"
    
    report_file = "/var/log/config_report_" strftime("%Y%m%d") ".txt"
    FS="="
}
!/^#/ && NF==2 {
    gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1)
    gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
    
    key=$1; val=$2
    
    # Bos deger kontrolu
    if (val == "") {
        terminal_line = RED "  [EKSIK]" NC "  " key " = (bos)"
        file_line = "  [EKSIK]  " key " = (bos)"
    } else if (val ~ /^(true|yes|enabled)$/) {
        terminal_line = GREEN "  [AKTIF]" NC "  " key " = " val
        file_line = "  [AKTIF]  " key " = " val
    } else {
        terminal_line = "  [OK]    " key " = " val
        file_line = "  [OK]    " key " = " val
    }
    
    print terminal_line
    print file_line > report_file
}
' uygulama.conf

Renk kodları doğrudan dosyaya yazılırsa dosya okunaksız hale gelir, bu yüzden terminal çıktısı ve dosya çıktısı için ayrı satırlar oluşturuyoruz.

Pratikte Dikkat Edilmesi Gerekenler

Yıllarca bu tür script’ler yazarken öğrendiklerimden bazıları:

  • Boşluk hassasiyeti: KEY=value ile KEY = value farklı işlenir, gsub(/[[:space:]]/) ile temizlemeden önce asla varsaymayın.
  • Yorum satırları: Konfigürasyon dosyalarında # ile başlayan satırlar yorum satırıdır. !/^[[:space:]]*#/ ile bunları filtreleyin, aksi takdirde yanlış eşleşmeler olur.
  • Boş satırlar: NF==0 veya $0 ~ /^[[:space:]]*$/ kontrolü yapmadan ilerlemeyin.
  • Büyük-küçük harf: Anahtarları karşılaştırmadan önce toupper() veya tolower() ile normalize edin.
  • Çoklu satırlı değerler: Bazı konfigürasyon dosyalarında değerler birden fazla satıra yayılabilir (backslash continuation). Bu durum için awk tek başına yeterli olmayabilir, sed ile ön işlem yapmanız gerekebilir.
  • Encoding sorunları: Özellikle eski sistemlerde Latin-1 encoding ile yazılmış konfigürasyon dosyaları UTF-8 bekleyen awk‘ı karıştırabilir. LANG=C awk ... ile bu sorunu aşabilirsiniz.
  • gawk vs mawk vs nawk: match() fonksiyonunun dizi parametresi, strftime(), delete array gibi özellikler gawk’a özgüdür. Script’i farklı sistemlerde kullanacaksanız #!/usr/bin/gawk -f ile açıkça belirtin.

Sonuç

awk ile anahtar-değer işleme, sistem yöneticiliğinin az konuşulan ama çok kullanılan becerilerinden biridir. Burada ele aldığımız teknikler; /proc dosya sisteminden bellek raporları üretmek, uygulama konfigürasyonlarını denetlemek, log dosyalarından performans özetleri çıkarmak ve iki ortamı karşılaştırmak gibi günlük görevlerde doğrudan kullanılabilir.

Bu araçların gücü, kurulum gerektirmemelerinden gelir. Python yok, Ruby yok, özel bir kütüphane yok. Sadece birkaç satır awk ve elimizde yapılandırılmış, okunabilir bir rapor var. Özellikle container ortamlarında veya minimal kurulu sistemlerde bu minimalist yaklaşımın değeri kat kat artar.

Bir sonraki adım olarak, buradaki örnekleri kendi ortamınıza uyarlamanızı ve BEGIN/END blokları ile ilişkisel dizi kombinasyonunu iyice özümsemenizi öneririm. Bu iki yapıyı kavradığınızda awk ile neredeyse her türlü metin tabanlı raporu üretebilirsiniz.

Bir yanıt yazın

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