awk ile Çoklu Koşul ve Desen Kombinasyonuyla Akıllı Metin Filtreleme

Yıllar içinde onlarca farklı log analiz aracı denedim, Python scriptleri yazdım, Perl’e bulaştım, hatta bir dönem Go ile küçük bir parser bile geliştirmeye çalıştım. Ama her seferinde, gerçek anlamda hızlı ve esnek bir metin filtreleme gerektiğinde, elimim yine awk‘a gitti. Özellikle çoklu koşul ve desen kombinasyonları söz konusu olduğunda awk rakipsiz. Bu yazıda sizi teori değil, gerçek hayatta işe yarayan pratikler bekliyor.

awk Neden Sadece “Bir Metin Aracı” Değil?

awk‘ı bir sütun kesici olarak biliyorsanız, onun gücünün belki yüzde onunu kullanıyorsunuzdur. awk aslında küçük ama tam anlamıyla bir programlama dilidir. Kendi değişkenleri, dizileri, fonksiyonları, koşullu ifadeleri ve döngüleri vardır. Pipe’larla beslediğinizde, gigabaytlık log dosyalarını bile hafızayı patlatmadan işleyebilir.

Metin filtreleme söz konusu olduğunda grep tek satır aramalar için yeterlidir. Ama şunu yapmak istediğinizde:

  • “Hem A desenini hem B desenini içeren, ama C’yi içermeyen satırları bul”
  • “Belirli bir alan belirli bir değerin üzerindeyse ve başka bir alan şu pattern’e uyuyorsa listele”
  • “Bir blok başladığında o bloğu bir değişkende biriktir, bitiş koşulu geldiğinde karar ver”

…işte burada awk devreye girer.

Temel Yapıyı Hatırlayalım

Başlamadan önce temel sözdizimini gözden geçirelim. awk programları pattern { action } biçiminde çalışır.

awk 'pattern { action }' dosya.txt

Birden fazla kural aynı awk çağrısında kullanılabilir:

awk '
  pattern1 { action1 }
  pattern2 { action2 }
  pattern3 { action3 }
' dosya.txt

Her satır sırayla tüm kurallara karşı değerlendirilir. Bu özellik, çoklu koşul mantığının temelini oluşturur.

AND ve OR Kombinasyonları: Temel Mantıksal Filtreler

Bir Nginx access log’unu düşünün. Hem POST metoduyla gelen hem de 500 dönen istekleri bulmak istiyorsunuz.

awk '$6 == ""POST" && $9 == "500" { print $0 }' /var/log/nginx/access.log

Burada $6 HTTP metodunu, $9 ise durum kodunu temsil ediyor. && operatörü her iki koşulun da sağlanmasını zorunlu kılıyor.

Şimdi tersini yapalım. 400 ya da 500 ile başlayan tüm hata kodlarını yakalamak istiyoruz:

awk '$9 ~ /^[45]/ { print $1, $6, $7, $9 }' /var/log/nginx/access.log

~ operatörü regex eşleşmesi için kullanılır. !~ ise eşleşmeme durumunu test eder.

Şimdi biraz daha karmaşık bir senaryo: 4xx ya da 5xx dönen, ama /health endpoint’ine gelen istekleri hariç tutmak istiyorsunuz (monitoring probe’larını ezmek için sık kullanılan bir numara):

awk '($9 ~ /^[45]/) && ($7 !~ //health/) { print $1, $6, $7, $9 }' /var/log/nginx/access.log

Desen Aralıkları: Başlangıç ve Bitiş Koşulları

awk‘ın en az bilinen ama en güçlü özelliklerinden biri desen aralıklarıdır. pattern1, pattern2 sözdizimi, birinci desenin eşleştiği satırdan ikinci desenin eşleştiği satıra kadar olan bloğu seçer.

Bir uygulama log’unda bir exception bloğunu yakalamak istediğinizi düşünün:

awk '/ERROR: Database connection failed/,/Stack trace end/' /var/log/app/application.log

Bu komut, belirtilen başlangıç mesajını bulduğunda okumaya başlar ve bitiş mesajına kadar her şeyi yazdırır. Production’da bir incident sırasında bu komutu defalarca kullandım.

Daha gerçekçi bir senaryo: Apache log’larında belirli bir zaman aralığındaki istekleri almak:

awk '/[10/Nov/2024:14:00/,/[10/Nov/2024:15:00/' /var/log/apache2/access.log

Bu yaklaşımın grep üzerindeki avantajı, aralık içindeki satırları olduğu gibi almasıdır. İkinci deseni gördüğünde durur, ama o satırı da dahil eder.

Çok Satırlı Veri Bloklarını İşlemek

Gerçek dünyada veriler her zaman satır satır gelmez. Bazen bir kaydın birden fazla satıra yayıldığı durumlarla karşılaşırsınız. Örneğin şöyle bir log formatınız olsun:

BEGIN_REQUEST
timestamp: 2024-11-10 14:23:01
method: POST
path: /api/users
duration: 2341ms
status: 500
END_REQUEST

Bu bloklardan sadece duration değeri 1000ms’den fazla olanları almak istiyorsunuz:

awk '
  /BEGIN_REQUEST/ { block = ""; in_block = 1 }
  in_block { block = block $0 "n" }
  /END_REQUEST/ {
    in_block = 0
    if (block ~ /duration: [0-9]{4,}ms/) {
      printf "%s", block
    }
  }
' /var/log/app/requests.log

Bu script, her BEGIN_REQUEST gördüğünde bir buffer başlatır, satırları biriktirir, END_REQUEST‘e gelince regex kontrolü yaparak karar verir. Sadece awk ile, tek komutla.

Alana Dayalı Çoklu Koşul Filtreleme: CSV ve TSV Dosyaları

Sistem yöneticileri sıklıkla CSV veya TSV formatındaki raporlarla uğraşır. awk bu formatlar için de son derece yeteneklidir.

Diyelim ki bir kullanıcı aktivite raporunuz var, tab ile ayrılmış:

username  department  login_count  failed_attempts  last_login
john.doe  IT          142          0                2024-11-10
jane.smith Finance    23           5                2024-11-08
admin     IT          891          12               2024-11-10
bob.jones HR          7            3                2024-11-01

Son 7 gün içinde login yapmamış ve başarısız giriş denemesi 3’ten fazla olan kullanıcıları bulmak istiyorsunuz:

awk -F't' '
  NR > 1 && $4 > 3 && $5 < "2024-11-04" {
    print $1, "| Dept:", $2, "| Failed:", $4, "| Last login:", $5
  }
' user_activity.tsv

-F't' ile alan ayırıcıyı tab olarak belirtiyoruz. NR > 1 başlık satırını atlamamızı sağlıyor.

Daha gelişmiş bir örnek: Birden fazla dosyadan veri çekip birleştirme. Örneğin hem Nginx hem de uygulama loglarını aynı anda işlemek:

awk '
  FNR == 1 { source = FILENAME }
  /ERROR/ || /CRITICAL/ {
    print source, ":", NR, ":", $0
  }
' /var/log/nginx/error.log /var/log/app/application.log

FNR her dosya için sıfırlanan satır numarasıdır. FILENAME ise o an işlenen dosyanın adını verir. NR ise tüm dosyalar genelinde toplam satır sayısını tutar.

Değişken ve Dizi Kullanımıyla Akıllı Biriktirme

Bir sistemdeki IP adreslerinin kaç kez hata ürettiğini saymak ve belirli bir eşiği aşanları raporlamak oldukça yaygın bir güvenlik analizidir. awk‘ın dizileri bu iş için biçilmiş kaftandır:

awk '
  $9 ~ /^[45]/ {
    ip_errors[$1]++
  }
  END {
    for (ip in ip_errors) {
      if (ip_errors[ip] > 100) {
        print ip, "-->", ip_errors[ip], "hata"
      }
    }
  }
' /var/log/nginx/access.log

END bloğu dosya okunduktan sonra çalışır. Bu örnekte 100’den fazla hata üreten IP’leri listeliyoruz. Bunu fail2ban kuralı oluşturmak için bir pipeline’ın parçası olarak kullanabilirsiniz.

Şimdi daha gelişmiş bir senaryo: Her IP için hem toplam istek sayısını hem de hata sayısını izleyip hata oranı yüksek olanları bulmak:

awk '
  {
    total[$1]++
    if ($9 ~ /^[45]/) errors[$1]++
  }
  END {
    for (ip in total) {
      if (total[ip] > 50) {
        error_rate = (errors[ip] + 0) / total[ip] * 100
        if (error_rate > 20) {
          printf "IP: %-15s | Toplam: %5d | Hata: %5d | Oran: %.1f%%n",
            ip, total[ip], errors[ip]+0, error_rate
        }
      }
    }
  }
' /var/log/nginx/access.log

Bu komut, en az 50 istek yapmış ve bu isteklerin yüzde 20’sinden fazlası hatayla sonuçlanmış IP’leri raporlar. errors[ip] + 0 ifadesi, dizi elemanı hiç oluşturulmamışsa sıfır olarak değerlendirilmesini sağlar.

Koşullu Desen Zinciri: Birden Fazla Geçiş Gerektiren Analizler

Bazen tek bir awk geçişi yetmez, ama pipeline’la zincirleme yapmak da her zaman istenmez. awk içinde durumlu mantık kurarak bu sorunu aşabilirsiniz.

Bir syslog’unda: “Kernel panic” mesajından sonraki 5 dakika içinde OOM killer devreye girmişse uyar” gibi bir koşul:

awk '
  /kernel panic/ {
    panic_time = $0
    # Zaman damgasını basit bir sayıya çevirme (gerçek projede strptime kullanın)
    in_window = 1
    window_count = 0
    print "PANIC DETECTED:", $0
  }
  in_window && /Out of memory/ {
    print "  --> OOM EVENT WITHIN WINDOW:", $0
    window_count++
  }
  in_window {
    window_count++
    if (window_count > 300) in_window = 0
  }
' /var/log/syslog

Bu tür korelasyon sorguları için genellikle Elasticsearch açılır, ama basit bir kontrol için bu yeterli.

Pratik Senaryo: Disk Kullanım Raporundan Akıllı Uyarı Üretme

df -h çıktısından yüzde 85’in üzerinde dolu olan ve /boot ya da /tmp olmayan partition’ları bulalım:

df -h | awk '
  NR > 1 {
    # Yuzde isaretini kaldir ve sayiya cevir
    gsub(/%/, "", $5)
    usage = $5 + 0
    if (usage > 85 && $6 !~ /^/boot/ && $6 !~ /^/tmp/) {
      printf "UYARI: %s %d%% dolu (Mount: %s)n", $1, usage, $6
    }
  }
'

gsub() fonksiyonu global bir string değiştirme yapar. % işaretini kaldırmadan sayısal karşılaştırma yapamazdık.

Bunu cron’a ekleyip mail gönderecek şekilde yapılandırabilirsiniz:

#!/bin/bash
UYARI=$(df -h | awk '
  NR > 1 {
    gsub(/%/, "", $5)
    if ($5 + 0 > 85 && $6 !~ /^/boot/ && $6 !~ /^/tmp/)
      printf "UYARI: %s %d%% dolun", $1, $5+0
  }
')

if [ -n "$UYARI" ]; then
  echo "$UYARI" | mail -s "[UYARI] Disk Doluluk Alarmı - $(hostname)" [email protected]
fi

Çoklu Dosya ve FILENAME Kontrolü ile Karmaşık Filtreler

Bir DevOps mühendisinin sık karşılaştığı senaryo: Birden fazla sunucudan toplanan log dosyalarını tek bir dizinde analiz etmek. Dosya adından sunucu adını çıkarıp log analizine dahil etmek:

awk '
  FNR == 1 {
    # Dosya adindan sunucu adini al: /logs/web01-nginx.log -> web01
    split(FILENAME, parts, "/")
    split(parts[length(parts)], name_parts, "-")
    server = name_parts[1]
  }
  /ERROR/ && $0 !~ /DEBUG/ {
    server_errors[server]++
    if (server_errors[server] <= 5) {
      print "[" server "]", $0
    }
    if (server_errors[server] == 5) {
      print "[" server "] ... ve daha fazlasi (ozet icin END'e bakin)"
    }
  }
  END {
    print "n=== SUNUCU BAZLI HATA OZETI ==="
    for (s in server_errors) {
      print s, ":", server_errors[s], "hata"
    }
  }
' /var/log/collected/web*-nginx.log

Bu script her sunucu için ilk 5 hatayı gösterir, sonrasında özetler. Production’da “neden bu kadar çok log var” sorusunu sormadan önce bu tür bir ön filtre çok işe yarar.

BEGIN Bloğu ile Dinamik Eşik Değerleri

BEGIN bloğu, herhangi bir satır okunmadan önce çalışır. Bunu değişken tanımlamak için kullanabilirsiniz, bu da scriptleri parametrik hale getirir:

awk -v threshold=500 -v method="POST" '
  BEGIN {
    print "Analiz basliyor: Method=" method, "Esik=" threshold "ms"
  }
  $6 == """ method """ {
    # Response time son alan oldugunu varsayalim
    response_time = $NF + 0
    if (response_time > threshold) {
      slow_count++
      print "YAVAS:", $7, response_time "ms"
    }
    total++
  }
  END {
    if (total > 0)
      printf "Toplam %d istek icinde %d yavas istek (%.1f%%)n",
        total, slow_count, slow_count/total*100
  }
' /var/log/nginx/access.log

-v threshold=500 ile dışarıdan değer geçiriyoruz. Bu sayede aynı scripti farklı eşik değerleriyle çalıştırabilirsiniz. $NF son alanı temsil eder, kaç alan olduğunu bilmesek bile.

Sonuç

awk ile bu kadar zaman geçirdikten sonra şunu net olarak söyleyebilirim: Bu araç çöp değil, sadece bilinmiyor. Türkiye’deki sysadmin topluluğunda çoğunlukla grep | cut | sort | uniq zinciri kullanılıyor, ki bu da işe yarıyor. Ama gerçekten karmaşık filtreler söz konusu olduğunda o zincir hem okunaksız hem kırılgan hale geliyor.

Çoklu koşullar için && ve || kombinasyonları, desen aralıkları için pattern1,pattern2 sözdizimi, durum takibi için değişkenler ve diziler, dosya bazlı analiz için FILENAME ve FNR, dışarıdan parametre almak için -v seçeneği. Bunların hepsi tek bir araçta, ek kurulum gerektirmeden, her Linux sistemde hazır.

Bir sonraki log analiz oturumunda önce Python scriptini açmak yerine bir awk one-liner denemesini öneririm. Çoğu zaman işi tek satırda bitirir, geri kalan zamanlarda da ne istediğinizi netleştirmenize yardımcı olur.

Bir yanıt yazın

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