awk ile Tarih ve Saat Verilerini Ayrıştırma, Formatlama ve Zaman Aralığına Göre Filtreleme
Log dosyalarıyla uzun yıllar geçirmiş biri olarak şunu net söyleyebilirim: awk olmadan tarih/saat bazlı log analizi yapmak, kör bağcıkla ip kesmek gibi bir şey. Evet, grep ile regex’e girebilirsiniz, sed ile biraz şekle sokarsınız, ama gerçek gücü awk‘ın aritmetik ve desen eşleştirme kapasitesi birleştirdiğinde görürsünüz. Bu yazıda günlük işlerinizde karşılaacağınız tarih/saat senaryolarını ve bunları awk ile nasıl çözeceğinizi somut örneklerle aktarıyorum.
Tarih Verisiyle Çalışmanın Temel Mantığı
awk‘ta tarih/saat verisi aslında düz metinden ibarettir; özel bir veri tipi yoktur. Gücü, bu metni sayıya veya karşılaştırılabilir bir formata dönüştürebilme yeteneğinden alır. İki temel yaklaşım vardır:
- String karşılaştırması: Tarihler
YYYY-MM-DD HH:MM:SSformatındaysa, leksikografik sıralama zaten kronolojik sırayla örtüşür. Yani"2024-03-15" > "2024-02-20"gibi düz string karşılaştırması çalışır. - Epoch dönüşümü: Karmaşık tarih formatları veya zaman farkı hesaplamaları için tarihi Unix timestamp’e (epoch saniyesine) çevirmek gerekir.
awk‘ınmktime()fonksiyonu burada devreye girer.
gawk (GNU awk) kullanıyorsanız mktime(), strftime() ve systime() fonksiyonlarına erişiminiz var demektir. Çoğu Linux dağıtımında varsayılan awk zaten gawk‘tır ama emin olmak için:
awk --version | head -1
Çıktıda “GNU Awk” görüyorsanız hazırsınız.
Temel: mktime ve strftime Kullanımı
mktime() fonksiyonu "YYYY MM DD HH MM SS" formatında bir string alır ve epoch saniyesi döndürür. strftime() ise tam tersi yönde çalışır; epoch değerini istediğiniz formata çevirir.
Basit bir örnekle başlayalım. Elimizde şu formatta bir log olsun:
2024-03-15 14:23:05 INFO Kullanici giris yapti: ahmet
2024-03-15 14:45:11 ERROR Veritabani baglantisi kesildi
2024-03-15 15:02:33 INFO Servis yeniden baslatildi
2024-03-15 15:18:47 WARN Disk doluluk orani %85 gecti
Bu log’dan belirli bir saat aralığındaki kayıtları çekmek için:
awk '
{
# Tarihi ve saati ayristir
split($1, tarih, "-")
split($2, saat, ":")
# mktime icin string olustur: "YYYY MM DD HH MM SS"
epoch = mktime(tarih[1] " " tarih[2] " " tarih[3] " "
saat[1] " " saat[2] " " saat[3])
# 14:30 ile 15:10 arasindaki kayitlari goster
baslangic = mktime("2024 03 15 14 30 00")
bitis = mktime("2024 03 15 15 10 00")
if (epoch >= baslangic && epoch <= bitis)
print $0
}
' uygulama.log
Bu yaklaşımın güzel yanı: her satır için baslangic ve bitis yeniden hesaplanıyor, bu da gereksiz iş yükü demek. Bunları BEGIN bloğuna taşımak çok daha verimli:
awk '
BEGIN {
baslangic = mktime("2024 03 15 14 30 00")
bitis = mktime("2024 03 15 15 10 00")
}
{
split($1, t, "-")
split($2, s, ":")
epoch = mktime(t[1] " " t[2] " " t[3] " " s[1] " " s[2] " " s[3])
if (epoch >= baslangic && epoch <= bitis)
print $0
}
' uygulama.log
Farklı Log Formatlarını Ayrıştırma
Gerçek dünyada log formatları standart değildir. Birkaç yaygın formatı ele alalım.
Apache/Nginx Access Log Formatı
Apache’nin varsayılan Combined Log Format’ı şöyle görünür:
192.168.1.105 - ahmet [15/Mar/2024:14:23:05 +0300] "GET /api/users HTTP/1.1" 200 1482
Bu formattaki tarihi ayrıştırmak için ayraçlarla biraz uğraşmak gerekir:
awk '
BEGIN {
# Ay adlarindan numaraya donusum tablosu
aylar["Jan"]=1; aylar["Feb"]=2; aylar["Mar"]=3
aylar["Apr"]=4; aylar["May"]=5; aylar["Jun"]=6
aylar["Jul"]=7; aylar["Aug"]=8; aylar["Sep"]=9
aylar["Oct"]=10; aylar["Nov"]=11; aylar["Dec"]=12
baslangic = mktime("2024 03 15 14 00 00")
bitis = mktime("2024 03 15 16 00 00")
}
{
# $4 = [15/Mar/2024:14:23:05
# Koseli parantezi temizle
gsub(/[/, "", $4)
# Tarihi parcalara ayir
split($4, parca, /[/:]/)
# parca[1]=gun, parca[2]=ay_adi, parca[3]=yil
# parca[4]=saat, parca[5]=dakika, parca[6]=saniye
epoch = mktime(parca[3] " " aylar[parca[2]] " " parca[1] " "
parca[4] " " parca[5] " " parca[6])
if (epoch >= baslangic && epoch <= bitis)
print $0
}
' /var/log/nginx/access.log
Syslog Formatı
/var/log/syslog veya /var/log/messages dosyaları şu formattadır:
Mar 15 14:23:05 webserver01 sshd[2341]: Accepted publickey for deploy
Mar 15 14:45:11 webserver01 kernel: Out of memory: Kill process 4821
Burada yıl bilgisi yok, bu klasik bir syslog sorunudur:
awk '
BEGIN {
aylar["Jan"]=1; aylar["Feb"]=2; aylar["Mar"]=3
aylar["Apr"]=4; aylar["May"]=5; aylar["Jun"]=6
aylar["Jul"]=7; aylar["Aug"]=8; aylar["Sep"]=9
aylar["Oct"]=10; aylar["Nov"]=11; aylar["Dec"]=12
# Yili sistem tarihinden al, ya da sabit yaz
"date +%Y" | getline yil
baslangic = mktime(yil " 3 15 14 00 00")
bitis = mktime(yil " 3 15 15 30 00")
}
{
split($3, saat, ":")
epoch = mktime(yil " " aylar[$1] " " $2 " "
saat[1] " " saat[2] " " saat[3])
if (epoch >= baslangic && epoch <= bitis)
print $0
}
' /var/log/syslog
"date +%Y" | getline yil kısmı çok işe yarar bir awk tekniğidir; shell komutunun çıktısını doğrudan awk değişkenine alır.
Zaman Aralığı Hesaplama ve Raporlama
Şimdi daha pratik bir senaryo: iki olay arasındaki süreyi hesaplamak. Bir deployment log’umuz olsun ve her deployment’ın ne kadar sürdüğünü bulmak isteyelim:
2024-03-15 09:00:00 DEPLOY_START release-v2.1.5
2024-03-15 09:04:37 DEPLOY_END release-v2.1.5 SUCCESS
2024-03-15 11:30:00 DEPLOY_START release-v2.1.6
2024-03-15 11:38:22 DEPLOY_END release-v2.1.6 SUCCESS
2024-03-15 14:00:00 DEPLOY_START release-v2.1.7
2024-03-15 14:09:55 DEPLOY_END release-v2.1.7 FAILED
awk '
function tarih_epoch(tarih_str, saat_str, t, s) {
split(tarih_str, t, "-")
split(saat_str, s, ":")
return mktime(t[1] " " t[2] " " t[3] " " s[1] " " s[2] " " s[3])
}
/DEPLOY_START/ {
surum = $4
baslangic_epoch[surum] = tarih_epoch($1, $2)
}
/DEPLOY_END/ {
surum = $4
durum = $5
bitis = tarih_epoch($1, $2)
sure = bitis - baslangic_epoch[surum]
dakika = int(sure / 60)
saniye = sure % 60
printf "%-20s %s Sure: %d dakika %02d saniyen",
surum, durum, dakika, saniye
}
' deployment.log
Bu örnekte awk fonksiyonu tanımladığımıza dikkat edin. Tekrar eden tarih-epoch dönüşüm kodunu tarih_epoch() fonksiyonuna taşımak hem okunabilirliği artırır hem de bakımı kolaylaştırır.
Saatlik ve Günlük Trafik Analizi
Bir başka yaygın senaryo: access log’larını saate göre gruplandırıp istek sayısını bulmak. Bu, hangi saatlerde yük arttığını anlamak için kritiktir.
awk '
BEGIN {
aylar["Jan"]=1; aylar["Feb"]=2; aylar["Mar"]=3
aylar["Apr"]=4; aylar["May"]=5; aylar["Jun"]=6
aylar["Jul"]=7; aylar["Aug"]=8; aylar["Sep"]=9
aylar["Oct"]=10; aylar["Nov"]=11; aylar["Dec"]=12
}
{
gsub(/[/, "", $4)
split($4, p, /[/:]/)
# p[4] = saat bilgisi (HH)
saat = p[4]
# HTTP durum kodu $9 icinde
durum = $9
saatlik_toplam[saat]++
if (durum >= 500)
saatlik_hata[saat]++
}
END {
print "Saat Toplam 5xx Hata Hata Orani"
print "---- ------ -------- ----------"
for (s = 0; s <= 23; s++) {
saat_str = sprintf("%02d", s)
if (saatlik_toplam[saat_str] > 0) {
hata = saatlik_hata[saat_str] + 0
toplam = saatlik_toplam[saat_str]
oran = (hata / toplam) * 100
printf "%s %-6d %-8d %.1f%%n",
saat_str, toplam, hata, oran
}
}
}
' /var/log/nginx/access.log
END bloğunda for (s = 0; s <= 23; s++) döngüsüyle saatleri sıralı yazdırıyoruz. awk dizileri hash table olduğu için sırasız gelirdi, bu döngü bunu çözüyor.
Komut Satırından Dinamik Tarih Aralığı Geçirme
Sabit tarih yazmak yerine, script’i çalıştırırken tarih aralığını dışarıdan vermek çok daha kullanışlıdır. -v parametresiyle bunu yapabilirsiniz:
# Son 1 saatin loglarini getir
SIMDI=$(date +%s)
BIR_SAAT_ONCE=$((SIMDI - 3600))
awk -v bas="$BIR_SAAT_ONCE" -v bit="$SIMDI" '
{
split($1, t, "-")
split($2, s, ":")
epoch = mktime(t[1] " " t[2] " " t[3] " " s[1] " " s[2] " " s[3])
if (epoch >= bas+0 && epoch <= bit+0)
print $0
}
' uygulama.log
Bu yaklaşımı bir shell fonksiyonuna sarabilirsiniz:
son_n_saat_log() {
local dosya="$1"
local saat="${2:-1}" # Varsayilan: 1 saat
local simdi
simdi=$(date +%s)
local bitis=$simdi
local baslangic=$((simdi - saat * 3600))
awk -v bas="$baslangic" -v bit="$bitis" '
{
split($1, t, "-")
split($2, s, ":")
epoch = mktime(t[1] " " t[2] " " t[3] " " s[1] " " s[2] " " s[3])
if (epoch >= bas && epoch <= bit) print $0
}
' "$dosya"
}
# Kullanim:
# son_n_saat_log /var/log/app/production.log 3
Bu fonksiyonu .bashrc veya .bash_profile‘a ekleyip her oturumda kullanabilirsiniz. Prodüksiyonda bir şeyler patladığında “son 2 saatin logları” demek için ne kadar zaman harcadığınızı düşünün; bu fonksiyon o anın kurtarıcısı olabilir.
Tarih Formatı Dönüştürme
Farklı sistemlerden gelen log’ları aynı formata getirmek de sık karşılaşılan bir ihtiyaç. Örneğin Windows event log’larından export edilmiş MM/DD/YYYY HH:MM:SS formatını ISO 8601’e (YYYY-MM-DD HH:MM:SS) çevirmek:
awk '
{
# MM/DD/YYYY HH:MM:SS formatini ayristir
split($1, tarih, "/")
# tarih[1]=ay, tarih[2]=gun, tarih[3]=yil
# ISO 8601 formatina donustur
yeni_tarih = sprintf("%04d-%02d-%02d",
tarih[3], tarih[1], tarih[2])
# Geri kalani koru
$1 = yeni_tarih
print $0
}
' windows_events.log
Ya da Unix timestamp olarak gelen logları okunabilir formata çevirmek:
# Ornek: "1710507785 ERROR Veritabani timeout"
awk '
{
okunabilir = strftime("%Y-%m-%d %H:%M:%S", $1)
$1 = okunabilir
print $0
}
' epoch_log.txt
strftime() burada kilit fonksiyon. Format string’i C’deki strftime ile aynı syntax’ı kullanır:
- %Y: 4 haneli yıl
- %m: 2 haneli ay (01-12)
- %d: 2 haneli gün (01-31)
- %H: 24 saat formatında saat (00-23)
- %M: Dakika (00-59)
- %S: Saniye (00-59)
- %s: Epoch timestamp (sadece gawk’ta)
Çok Satırlı Log Kayıtları ve Zaman Damgası Eşleştirme
Java stack trace gibi çok satıra yayılan log kayıtlarında ilk satırdaki zaman damgasını sonraki satırlarla ilişkilendirmek gerekir. Bu klasik bir awk pattern matching problemidir:
awk '
BEGIN {
baslangic = mktime("2024 03 15 14 00 00")
bitis = mktime("2024 03 15 15 00 00")
yazdir = 0
tampon = ""
}
/^[0-9]{4}-[0-9]{2}-[0-9]{2}/ {
# Yeni bir log kaydi basladi
# Onceki tamponu yazdir (eger zaman araligindaysa)
if (yazdir && tampon != "")
printf "%s", tampon
# Yeni kaydin zamanini kontrol et
split($1, t, "-")
split($2, s, ":")
epoch = mktime(t[1] " " t[2] " " t[3] " " s[1] " " s[2] " " s[3])
if (epoch >= baslangic && epoch <= bitis) {
yazdir = 1
tampon = $0 "n"
} else {
yazdir = 0
tampon = ""
}
next
}
{
# Devam satiri (stack trace vs.)
if (yazdir)
tampon = tampon $0 "n"
}
END {
if (yazdir && tampon != "")
printf "%s", tampon
}
' uygulama.log
Bu pattern biraz daha karmaşık ama production’da Java veya Python uygulamalarının loglarına bakıyorsanız şart. Stack trace’i kesmeden tam kayıtları almanızı sağlar.
Performans Notları
Büyük log dosyalarında (awk ile GB’larca dosyayı işlediğim oldu) birkaç şeye dikkat etmek gerekir:
BEGINbloğunda sabit değerleri hesaplayın:mktimeile ürettiğiniz başlangıç/bitiş epoch’larını her satırda yeniden hesaplamayın, bir kereBEGIN‘de yapın.- String karşılaştırmasını tercih edin: Tarihleriniz ISO 8601 formatındaysa (
YYYY-MM-DD HH:MM:SS), epoch’a çevirmeden direkt string karşılaştırması yapın. Çok daha hızlıdır. nextile erken çıkın: Koşulunuz sağlanmadığındanextile sonraki satıra atlayın, gereksiz işlemleri atlarsınız.zcatile pipe edin:.gzlogları içinzcat access.log.gz | awk '...'şeklinde kullanın, geçici dosya açmayın.
String karşılaştırması örneği, basit ama son derece etkili:
# ISO 8601 formatindaki loglar icin epoch gerekmez
awk '
$0 >= "2024-03-15 14:00:00" && $0 <= "2024-03-15 16:00:00"
' uygulama.log
Bu tek satır, mktime içeren versiyondan çok daha hızlı çalışır ve büyük dosyalarda farkı hissedersiniz.
Sonuç
awk ile tarih/saat işleme ilk bakışta karmaşık görünse de temel mantığı kavradıktan sonra log analizinin en güçlü araçlarından biri haline gelir. Özetlemek gerekirse:
mktime()ile tarih string’ini epoch’a çevirin, aritmetik ve karşılaştırma için.strftime()ile epoch’u istediğiniz formata dönüştürün.BEGINbloğunu sabit hesaplamalar için kullanın, performansa etkinizi minimumda tutun.- Mümkün olduğunda ISO 8601 formatındaki verilerde string karşılaştırması yapın; basit ve hızlıdır.
- Shell değişkenlerini
-vileawk‘a geçirin; dinamik tarih aralıkları için şart.
Prodüksiyonda bir sorun anında doğru awk one-liner’ına sahip olmak ile saatlerce log kaydını manuel incelemek arasındaki fark, çoğu zaman bu tür teknikleri önceden pratiğe dökmüş olmaktan geçiyor. Bu örnekleri bir yere not edin, kendi log formatlarınıza uyarlayın ve gerektiğinde hazır olun.
