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 | awkzincirinde 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
sedihtiyacı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.
