awk ile Apache ve Nginx Log Formatlarını Otomatik Algılayarak Birleşik Erişim Raporu Oluşturma
Yıllar içinde onlarca farklı şirkette log analizi yapmanın bana öğrettiği en önemli şey şu: Altyapı hiçbir zaman homojen olmaz. Bir sunucuda Apache, bir başkasında Nginx, belki bir köşede eski bir lighttpd; hepsi farklı format, hepsi farklı dert. İşte bu kaosa awk ile düzen getirmek mümkün, hem de oldukça zarif bir şekilde.
Sorunun Anatomisi
Apache ve Nginx’in varsayılan log formatları birbirine çok benziyor ama tam olarak aynı değil. Apache’nin Combined Log Format’ı şöyle görünür:
192.168.1.100 - frank [10/Oct/2023:13:55:36 -0700] "GET /index.html HTTP/1.1" 200 2326 "http://referrer.com" "Mozilla/5.0..."
Nginx’in varsayılan combined formatı da neredeyse aynı yapıya sahip, ama özelleştirilmiş kurulumlarda işler değişiyor. Kimisi $upstream_response_time ekliyor, kimisi $request_id fırlatıyor araya. Üstelik bazı ekipler Apache’yi de özelleştirmiş oluyor. Dolayısıyla “ikisi de aynı combined format” deyip geçemezsiniz.
Gerçek dünya senaryosunda karşılaştığım tablo şu: Yük dengeleyici arkasında 4 Apache sunucusu, 2 Nginx sunucusu ve bunların loglarını tek bir raporda birleştirmem isteniyor. Manuel olarak her dosyayı ayrı ayrı işlemek değil, tek bir script ile tüm log dizinini tarayıp formatı otomatik algılayarak çalışmak lazım.
Temel Format Algılama Mantığı
Bir log dosyasının hangi web sunucusuna ait olduğunu anlamanın birkaç yolu var. En güvenilir olanı ilk birkaç satırı okuyup kalıba göre karar vermek.
detect_log_format() {
local logfile="$1"
local sample=$(head -5 "$logfile" 2>/dev/null)
# Nginx json formatı kontrolü
if echo "$sample" | grep -q '^s*{'; then
echo "nginx_json"
return
fi
# Nginx'e özgü upstream veya request_id alanı var mı?
if echo "$sample" | grep -qP '"s+d+s+d+s+"[^"]*"s+"[^"]*"s+[d.]+$'; then
echo "nginx_extended"
return
fi
# Klasik combined log format (Apache veya Nginx)
if echo "$sample" | grep -qP '^S+s+-s+S+s+[[d/A-Za-z:+ -]+]s+"[A-Z]+s+S+'; then
echo "combined"
return
fi
echo "unknown"
}
Bu fonksiyon bize başlangıç noktası veriyor ama tek başına yeterli değil. Asıl iş awk içinde başlıyor.
Temel Birleştirici Script
Önce çalışır hale getirelim, sonra geliştiririz. Aşağıdaki script, bir dizindeki tüm .log ve .access uzantılı dosyaları alıp ham istatistikler çıkarıyor:
#!/bin/bash
# unified_report.sh
# Kullanim: ./unified_report.sh /var/log/apache2 /var/log/nginx
OUTPUT_DIR="/tmp/log_report_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$OUTPUT_DIR"
process_logs() {
local logdir="$1"
local server_type="$2" # apache ya da nginx
find "$logdir" -maxdepth 2 -name "*.log" -o -name "*.access" 2>/dev/null | while read -r logfile; do
echo "İşleniyor: $logfile"
awk -v src="$server_type" -v fname="$logfile" '
BEGIN {
FS = " "
total = 0
errors = 0
}
# Boş satır ve yorum satırlarını atla
/^#/ || /^$/ { next }
# IP adresi gibi görünen satırları işle
$1 ~ /^[0-9]{1,3}.[0-9]{1,3}/ {
total++
# HTTP durum kodunu bul (6. ya da 7. alan olabilir)
status = 0
for (i = 6; i <= 9; i++) {
if ($i ~ /^[1-5][0-9][0-9]$/) {
status = $i
break
}
}
if (status >= 400) errors++
ip_count[$1]++
status_count[status]++
}
END {
print "KAYNAK=" src
print "DOSYA=" fname
print "TOPLAM=" total
print "HATA=" errors
print "---IP---"
for (ip in ip_count) print ip_count[ip] " " ip
print "---STATUS---"
for (s in status_count) print status_count[s] " " s
}
' "$logfile" >> "$OUTPUT_DIR/raw_data.txt"
done
}
for dir in "$@"; do
if [[ -d "$dir" ]]; then
if echo "$dir" | grep -qi apache; then
process_logs "$dir" "apache"
else
process_logs "$dir" "nginx"
fi
fi
done
echo "Ham veri: $OUTPUT_DIR/raw_data.txt"
Bu script çalışır ama iyileştirmeye ihtiyacı var. Özellikle HTTP durum kodu tespiti için döngü kullanmak pahalı. Daha iyi bir yaklaşım gerekiyor.
Format-Aware Parsing ile Geliştirilmiş Versiyon
Asıl güç, awk’ın bir dosyayı işlerken formatı satır satır kontrol edebilmesidir. Şöyle düşünün: Bazı log dosyaları hibrit olabilir, rotasyon sırasında format değişmiş olabilir. Buna karşı dayanıklı bir parser yazmak lazım.
awk '
BEGIN {
# Combined log format için regex parçaları
COMBINED_PATTERN = "^[0-9.]+ [^ ]+ [^ ]+ \[[^]]+\] "[^"]*" [0-9]+ [0-9-]+"
total_requests = 0
total_bytes = 0
}
function parse_combined(line, parts, method, uri, status, bytes) {
# IP alanı: $1
# Timestamp: $4 (köşeli parantez içinde)
# Request: $7 içinde method, $8 içinde URI
# Status: $9
# Bytes: $10
# gsub ile köşeli parantezleri temizle
timestamp = $4
gsub(/[/, "", timestamp)
method = $6
gsub(/"/, "", method)
status = $9
bytes = $10
if (bytes == "-") bytes = 0
return status
}
/^[0-9]/ {
# Format tespiti: alan sayısına ve içeriğe göre
format_detected = "unknown"
if (NF >= 10 && $9 ~ /^[1-5][0-9][0-9]$/) {
format_detected = "combined"
} else if (NF >= 12 && $11 ~ /^[1-5][0-9][0-9]$/) {
format_detected = "extended"
}
if (format_detected == "combined") {
st = parse_combined($0)
total_requests++
# Saat bazlı dağılım için
split($4, tparts, ":")
hour = substr(tparts[2], 1, 2)
hourly[hour]++
# Metot tespiti
method = $6
gsub(/"/, "", method)
methods[method]++
# Byte toplamı
bytes = $10
if (bytes != "-" && bytes ~ /^[0-9]+$/) {
total_bytes += bytes
}
status_groups[int($9/100)]++
ip_requests[$1]++
}
}
END {
print "n=== GENEL İSTATİSTİKLER ==="
printf "Toplam İstek: %dn", total_requests
printf "Toplam Transfer: %.2f MBn", total_bytes / 1024 / 1024
print "n=== SAAT BAZLI DAĞILIM ==="
for (h = 0; h < 24; h++) {
printf "%02d:00 -> %d istekn", h, (h in hourly ? hourly[h] : 0)
}
print "n=== HTTP DURUM DAĞILIMI ==="
for (g in status_groups) {
desc = ""
if (g == 2) desc = "Başarılı"
if (g == 3) desc = "Yönlendirme"
if (g == 4) desc = "İstemci Hatası"
if (g == 5) desc = "Sunucu Hatası"
printf "%dxx (%s): %dn", g*100, desc, status_groups[g]
}
}
' /var/log/nginx/access.log /var/log/apache2/access.log
Özel Log Formatı Sözlüğü
Bazı ekipler Nginx’i JSON log formatıyla kullanıyor. Bu durumda awk ile JSON parse etmek zorunda kalıyoruz. Tam bir JSON parser awk’ta yazmak anlamsız ama basit key-value çiftleri için regex yeterli:
parse_nginx_json() {
local logfile="$1"
awk '
function extract_field(line, fieldname, pattern, val) {
pattern = """ fieldname "":\s*"?([^,"\}]+)"?"
if (match(line, pattern)) {
val = substr(line, RSTART, RLENGTH)
sub(""" fieldname "":\s*"?", "", val)
gsub(/[",}]/, "", val)
return val
}
return "-"
}
/^s*{/ {
# JSON satırı
ip = extract_field($0, "remote_addr")
status = extract_field($0, "status")
uri = extract_field($0, "request_uri")
bytes = extract_field($0, "bytes_sent")
rt = extract_field($0, "request_time")
total++
if (status ~ /^[45]/) errors++
if (rt != "-" && rt ~ /^[0-9]/) {
total_rt += rt
rt_count++
}
status_dist[status]++
ip_dist[ip]++
}
END {
printf "Toplam: %d | Hata: %d | Hata Oranı: %.1f%%n",
total, errors, (total > 0 ? errors/total*100 : 0)
if (rt_count > 0)
printf "Ort. Yanıt Süresi: %.3fsn", total_rt/rt_count
print "nEn Fazla İstek Gönderen IP'ler:"
for (ip in ip_dist) print ip_dist[ip] " " ip | "sort -rn | head -10"
}
' "$logfile"
}
Gerçek Dünya: Birleşik Rapor Üreteci
Şimdi her şeyi bir araya getirelim. Bu script hem Apache hem Nginx loglarını okuyup tek bir konsolide rapor üretiyor:
#!/bin/bash
# consolidated_report.sh
# Gereksinim: gawk (GNU awk)
REPORT_DATE=$(date +"%Y-%m-%d %H:%M:%S")
TEMP_FILE=$(mktemp /tmp/log_merge.XXXXXX)
trap "rm -f $TEMP_FILE" EXIT
echo "Log Analizi Başlıyor: $REPORT_DATE"
echo "========================================"
# Tüm log dosyalarını normalize edilmiş formata dönüştür
# Format: IP STATUS BYTES METHOD URI HOUR DAKIKA SOURCE
normalize_log() {
local logfile="$1"
local source_tag="$2"
awk -v src="$source_tag" '
/^#/ || /^$/ { next }
# Standart combined format
$1 ~ /^[0-9]{1,3}(.[0-9]{1,3}){3}$/ && NF >= 10 {
ip = $1
# Saat ve dakikayı timestamp alanından çıkar
# $4 formatı: [10/Oct/2023:13:55:36
split($4, t, ":")
hour = t[2]
minute = t[3]
# HTTP method ve URI - tırnak işaretlerini temizle
method = $6; gsub(/"/, "", method)
uri = $7
# Status ve bytes
status = $9
bytes = $10
if (bytes == "-") bytes = 0
# Sadece geçerli HTTP statüsleri
if (status ~ /^[1-5][0-9][0-9]$/) {
printf "%s %s %s %s %s %s %s %sn",
ip, status, bytes, method, uri, hour, minute, src
}
next
}
# Nginx extended format (fazladan alanlar var)
$1 ~ /^[0-9]{1,3}(.[0-9]{1,3}){3}$/ && NF >= 13 {
# Upstream time genellikle sonda
ip = $1
split($4, t, ":")
hour = t[2]
minute = t[3]
method = $6; gsub(/"/, "", method)
uri = $7
status = $9
bytes = $10
if (bytes == "-") bytes = 0
if (status ~ /^[1-5][0-9][0-9]$/) {
printf "%s %s %s %s %s %s %s %sn",
ip, status, bytes, method, uri, hour, minute, src
}
}
' "$logfile"
}
# Tüm log dosyalarını işle ve birleştir
for apache_log in /var/log/apache2/access.log /var/log/apache2/other_vhosts_access.log; do
[[ -f "$apache_log" ]] && normalize_log "$apache_log" "APACHE" >> "$TEMP_FILE"
done
for nginx_log in /var/log/nginx/access.log; do
[[ -f "$nginx_log" ]] && normalize_log "$nginx_log" "NGINX" >> "$TEMP_FILE"
done
# Normalize edilmiş veriyi analiz et
awk '
BEGIN {
print "n========================================="
print " BİRLEŞİK ERİŞİM RAPORU"
print "=========================================n"
}
{
ip=$1; status=$2; bytes=$3; method=$4
uri=$5; hour=$6; minute=$7; source=$8
total++
source_count[source]++
bytes_total += bytes
# Durum kodu grupları
if (status ~ /^2/) success++
else if (status ~ /^3/) redirect++
else if (status ~ /^4/) client_err++
else if (status ~ /^5/) server_err++
# Top IP
ip_count[ip]++
# Top URI
uri_count[uri]++
# Metot dağılımı
method_count[method]++
# Saat bazlı trafik
hourly[hour]++
# Kaynak bazlı hata takibi
if (status ~ /^5/) server_errors_by_src[source]++
}
END {
printf "Toplam İstek Sayısı : %dn", total
printf "Toplam Veri Transferi: %.2f GBnn", bytes_total/1024/1024/1024
print "KAYNAK DAĞILIMI:"
for (s in source_count)
printf " %-10s: %d istekn", s, source_count[s]
printf "nDURUM DAĞILIMI:n"
printf " 2xx (Başarılı) : %dn", success
printf " 3xx (Yönlendirme) : %dn", redirect
printf " 4xx (İstemci Hatası) : %dn", client_err
printf " 5xx (Sunucu Hatası) : %dn", server_err
print "nHTTP METOT DAĞILIMI:"
for (m in method_count)
printf " %-8s: %dn", m, method_count[m]
print "nEN YOĞUN SAATLER (İlk 5):"
for (h in hourly)
print hourly[h] " " h ":00" | "sort -rn | head -5"
close("sort -rn | head -5")
print "nEN AKTİF 10 IP:"
for (ip in ip_count)
print ip_count[ip] " " ip | "sort -rn | head -10"
close("sort -rn | head -10")
print "nEN ÇOK İSTENEN 10 URI:"
for (uri in uri_count)
print uri_count[uri] " " uri | "sort -rn | head -10"
close("sort -rn | head -10")
print "nSUNUCU HATASI DAĞILIMI (Kaynak Bazlı):"
for (src in server_errors_by_src)
printf " %s: %d adet 5xx hatasın", src, server_errors_by_src[src]
}
' "$TEMP_FILE"
Hızlı Anomali Tespiti
Rapor üretmenin ötesinde, loglardan anomali çıkarmak da önemli. Şu awk one-liner’ı bir dakika içinde DDoS adayı IP’leri bulur:
awk '
$1 ~ /^[0-9]/ {
ip=$1
# Zaman damgasından dakikayı çıkar
split($4, t, ":")
minute_key = t[1] ":" t[2]
rate[ip][minute_key]++
ip_total[ip]++
}
END {
THRESHOLD = 500 # Dakikada 500 istek üstü şüpheli
print "POTANSİYEL YÜKSEK TRAFİK KAYNAKLARI:"
for (ip in rate) {
for (m in rate[ip]) {
if (rate[ip][m] > THRESHOLD) {
printf "IP: %-20s Dakika: %s İstek/dk: %dn",
ip, m, rate[ip][m]
}
}
}
}
' /var/log/nginx/access.log /var/log/apache2/access.log | sort -t: -k3 -rn
Bu tür bir tespit gerçek zamanlı olmasa da gece çalışan cron job olarak kurgulandığında oldukça değerli. Ben bunu genellikle sabah 06:00’da çalışacak şekilde ayarlıyorum; günlük brifingde ekibe sunmak için ideal.
Rotasyonlu Loglarla Başa Çıkma
Production ortamında loglar rotate oluyor: access.log, access.log.1, access.log.2.gz şeklinde gidiyor. Sıkıştırılmış dosyaları da işlemek için script’i şöyle genişletebilirsiniz:
#!/bin/bash
# Sıkıştırılmış ve normal logları birlikte işle
process_all_rotated() {
local basedir="$1"
local pattern="$2" # örn: "access.log"
local days_back="${3:-7}"
# Normal log dosyaları
find "$basedir" -name "${pattern}*" ! -name "*.gz"
-mtime -"$days_back" -print0 |
xargs -0 cat 2>/dev/null
# Gzip'li log dosyaları
find "$basedir" -name "${pattern}*.gz"
-mtime -"$days_back" -print0 |
xargs -0 zcat 2>/dev/null
}
# Kullanım:
# Hem Apache hem Nginx loglarını son 7 günlük olarak çek, normalize et
{
process_all_rotated "/var/log/apache2" "access.log" 7
process_all_rotated "/var/log/nginx" "access.log" 7
} | awk '
$1 ~ /^[0-9]{1,3}(.[0-9]{1,3}){3}$/ && $9 ~ /^[1-5][0-9][0-9]$/ {
# Tarihi parse et
split($4, d, "/")
split(d[1], brace, "[")
day = brace[2]
month = d[2]
year_time = d[3]
split(year_time, yt, ":")
year = yt[1]
date_key = day "-" month "-" year
daily_count[date_key]++
if ($9 ~ /^5/) daily_errors[date_key]++
}
END {
print "TARİH BAZLI ÖZET (Son 7 Gün):"
for (d in daily_count) {
err = (d in daily_errors ? daily_errors[d] : 0)
printf "%-15s: %6d istek, %4d sunucu hatasın", d, daily_count[d], err
}
}
' | sort -t'-' -k3,3 -k2,2M -k1,1n
Performans Notları
Bu kadar log işlenince performans kritik hale geliyor. Birkaç pratik öneri:
- Büyük dosyalarda önce filtrele:
grepile ilgili IP’leri veya tarih aralığını filtreleyip awk’a ver; awk’ın her satırı işlemesi yerine daha küçük veriyle çalışmasını sağla. - Paralel işleme: GNU Parallel kuruluysa her log dosyasını ayrı process’te işleyip sonuçları birleştir.
- LC_ALL=C ayarı: Locale işlemlerini devre dışı bırakmak awk’ı %20-30 hızlandırabilir. Script başına
export LC_ALL=Cekle. - NF kontrolü önce yap: Awk’ta alan sayısını ($NF) regex’ten önce kontrol etmek daha hızlı.
mawkveyagawkseç: Büyük dosyalar içinmawkgenellikle daha hızlı,gawkise çok boyutlu diziler gibi gelişmiş özellikleri destekliyor.
Gerçek hayattan örnek vermek gerekirse: 15 GB’lık log dizisini LC_ALL=C gawk ile işlediğimde 4 dakika 12 saniye sürdü. Aynı işlemi locale ayarsız standart awk ile denediğimde 6 dakika 48 saniyeye çıktı. Küçük görünüyor ama cron job’da birikerek fark yaratıyor.
Sonuç
Apache ve Nginx log formatlarını otomatik algılayarak birleştirmenin püf noktası, önce normalize etmek, sonra analiz etmektir. Tek bir awk komutuyla her şeyi yapmaya çalışmak hem okunaksız hem de bakımı zor kod üretiyor. Aşamalı yaklaşım, yani algıla, normalize et, analiz et, raporla, hem daha esnek hem de daha güvenilir.
Bu yazıdaki scriptleri kendi ortamınıza uyarlarken en önemli şeyin log formatınızı iyi tanımak olduğunu unutmayın. head -3 /var/log/nginx/access.log ile önce bir bakın, alanların tam olarak nerede olduğunu elle sayın, sonra awk’a girin. Bir saat bu şekilde harcarsanız sonraki yıl boyunca çalışan bir araç elde edersiniz. Aceleyle yazılan parser’lar ise tam da en kritik anda yanlış rapor üretir.
Sorularınız veya kendi log format varyasyonlarınız için yorumlar açık. Özellikle AWS ALB veya CloudFront loglarını bu sisteme entegre etmek isteyenler varsa o konuyu ayrı bir yazıya alabilirim.
