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.
