Log Okumak 2: Pipe, Redirection, grep, awk, sed, sort ve uniq ile Log'dan Cevap Çıkarmak
Log'dan cevap çıkarmanın yolu küçük araçları pipe ile zincirlemektir: awk alanı çıkarır, grep filtreler, sort sıralar, uniq -c sayar. stdin/stdout/stderr ve redirection'ın temelinden sonra Bookstore'un gerçek Nginx access log'unda en çok istek atan IP'leri, dakika başına 5xx'i, en yavaş istekleri ve bir request id'nin uygulama log'undaki izini bulup uniq, regex ve locale tuzaklarını gösteriyoruz.
Sürüm notu: Bu yazıdaki komutlar Ubuntu Server 24.04.5 LTS (bash 5.2, grep 3.11, gawk 5.2, sed 4.9, coreutils 9.4, jq 1.7, Nginx 1.24) çalıştıran bir test VM'inde; Bookstore.Api .NET SDK 10.0.402 ile derlenip ASP.NET Core 10.0.15 runtime'ıyla çalıştırılmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) sunucusunda en çok istek atan IP'yi bulmak istiyorsun ve şunu yazıyorsun:
awk '{print $1}' /var/log/nginx/bookstore.access.log | uniq -c | sort -rn | head -n 1
Komut hata vermeden bir IP ve bir sayı basıyor. Bu sayı o IP'nin toplam istek sayısı mı? Küçük bir örnekle cevabı Sık Düşülen Tuzaklar bölümünde göreceğiz.
Önceki yazıda log'ların nerede olduğunu ve journalctl ile nasıl daraltıldığını gördük. Bu yazıda log'dan cevap çıkarıyoruz: redirection ve pipe'ın temeli, sonra grep, awk, cut, sed, sort, uniq ve wc'yi zincirleyerek gerçek bir Nginx access log'unda dört soru. Bu komutları bir script'e döküp exit status ve pipefail gibi konuları ele almak Bash Scripting serisinin işi; burada komut satırında, etkileşimli kullanıyoruz.
stdin, stdout, stderr ve Redirection
Her process üç akışla başlar: 0 numaralı stdin (girdi), 1 numaralı stdout (normal çıktı) ve 2 numaralı stderr (hata mesajları). İkisi de varsayılan olarak terminale gittiği için farkını genelde ancak çıktıyı bir dosyaya yönlendirince görürsün:
$ ls -d /opt/bookstore /opt/catalog
ls: cannot access '/opt/catalog': No such file or directory
/opt/bookstore
$ ls -d /opt/bookstore /opt/catalog > out.txt
ls: cannot access '/opt/catalog': No such file or directory
$ cat out.txt
/opt/bookstore
$ ls -d /opt/bookstore /opt/catalog > out.txt 2>&1; cat out.txt
ls: cannot access '/opt/catalog': No such file or directory
/opt/bookstore
$ ls -d /opt/bookstore /opt/catalog 2>&1 > out.txt
ls: cannot access '/opt/catalog': No such file or directory
$ ls -d /opt/catalog 2>/dev/null; echo "exit=$?"
exit=2
> yalnızca stdout'u dosyaya yazar; hata yine terminalde. 2>&1 "stderr'i şu anda stdout neredeyse oraya bağla" demek ve soldan sağa okunur. Bu yüzden sıra önemli: > out.txt 2>&1'de önce stdout dosyaya gider, sonra stderr onun peşinden; 2>&1 > out.txt'te ise stderr, stdout henüz terminaldeyken terminale bağlanır ve hata dosyaya hiç girmez. 2>/dev/null hatayı tamamen atar; çıkış kodu ($?) yine de 2, yani hata gizlenmiş ama olmamış değil. > dosyanın üzerine yazar, >> sonuna ekler:
$ echo "deploy 2026-11-14-6" > notes.txt; echo "deploy 2026-11-14-7" >> notes.txt; cat notes.txt
deploy 2026-11-14-6
deploy 2026-11-14-7
Production'da bir komutun çıktısını > ile yanlış dosyaya yönlendirmek o dosyayı anında sıfırlar; config dosyası ya da log için emin değilsen >> kullan ya da önce kopyasını al.
Pipe ve Küçük Araçlar
| (pipe) soldaki komutun stdout'unu sağdakinin stdin'ine bağlar. Unix araçlarının arkasındaki fikir de bu: her araç tek bir işi iyi yapar, düz metin okur, düz metin yazar ve sen onları zincirleyerek soruyu cevaplarsın. Bu yazıdaki zincirlerin hemen hepsi aynı iskeleti kullanıyor: bir alanı çıkar (awk, cut), filtrele (grep), sırala (sort), say (uniq -c, wc -l), ilk birkaçını al (head). Ara sonucu hem ekranda görmek hem dosyaya kaydetmek istersen tee:
$ awk '{print $1}' /var/log/nginx/bookstore.access.log | sort | uniq -c | sort -rn | tee top-ips.txt | head -n 3
1482 198.51.100.23
533 203.0.113.8
492 192.0.2.44
$ wc -l top-ips.txt
13 top-ips.txt
tee girdiğini hem dosyaya yazar hem de bir sonraki komuta aktarır: ekranda ilk üç satır var, dosyada 13 IP'nin hepsi. Bir olay sırasında bulduklarını ekiple paylaşmak için pratik.
Access Log'un Alanları
Önceki yazıda Nginx Bookstore için varsayılan combined formatta yazıyordu. "En yavaş istek hangisi" sorusu için istek süresi, "bu isteğin uygulama log'u nerede" sorusu için de bir istek kimliği lazım. Site configuration'ına (repoda deploy/linux/nginx/bookstore.conf) kendi formatımızı ekledik:
# /etc/nginx/sites-available/bookstore
log_format bookstore '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time $request_id';
server {
listen 80 default_server;
server_name _;
# Önümüzdeki load balancer gerçek istemci IP'sini gönderiyor
set_real_ip_from 10.20.30.0/24;
real_ip_header X-Forwarded-For;
access_log /var/log/nginx/bookstore.access.log bookstore;
error_log /var/log/nginx/bookstore.error.log;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-ID $request_id;
}
}
Format combined'ın aynısı, sonuna üç alan eklendi: $request_time (Nginx'in isteği almasından cevabın son byte'ını göndermesine kadar geçen süre, saniye), $upstream_response_time (Bookstore.Api'nin cevap süresi) ve $request_id (Nginx'in her istek için ürettiği rastgele kimlik; aynı değeri X-Request-ID header'ıyla uygulamaya da gönderiyoruz). set_real_ip_from satırları, önündeki load balancer'ın X-Forwarded-For'da gönderdiği gerçek istemci IP'sini $remote_addr'a yazar; bunun neden ve ne zaman güvenli olduğunu Web Server'lar ve Hosting serisinin Forwarded Headers yazısı anlatıyor. Lab'da load balancer'ı istemci makineden belge örneklerine ayrılmış IP'lerle (198.51.100.x, 203.0.113.x, 192.0.2.x) gönderdiğim yaklaşık 5.800 istekle taklit ettim; aşağıdaki bütün çıktılar bu trafikten.
$ head -n 2 bookstore.access.log
198.51.100.23 - - [14/Nov/2026:16:00:23 +0000] "GET /api/books/9780141439518 HTTP/1.1" 404 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0" 0.064 0.064 891c7f5d4c5bb4c3c589541f2a307dc4
192.0.2.44 - - [14/Nov/2026:16:00:23 +0000] "GET /api/books/9780141439518 HTTP/1.1" 404 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0 Safari/537.36" 0.052 0.052 493b0ea0859a0fe3d0a0e895dadb4b5c
$ awk '{print $1, $9, $(NF-2)}' bookstore.access.log | head -n 3
198.51.100.23 404 0.064
192.0.2.44 404 0.052
203.0.113.57 200 0.140
awk satırı boşluklardan böler ve parçalara $1, $2… diye erişir. Bu formatta $1 IP, $4 [14/Nov/2026:16:00:23, $7 path, $9 status, $10 byte. User agent'ın içinde boşluk olduğu için ondan sonraki alanların numarası satırdan satıra değişir; bu yüzden sondan sayıyoruz: NF alan sayısı, $NF son alan (request id), $(NF-2) sondan üçüncü ($request_time). Tırnak içindeki bir alanı bütün olarak almak için cut daha kolay; " karakterine göre bölünce altıncı parça user agent:
$ cut -d'"' -f6 bookstore.access.log | sort | uniq -c | sort -rn
1686 bookstore-web/22.2
1651 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0 Safari/537.36
1624 Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0
239 python-requests/2.32.3
Senaryo 1: Kim Ne Kadar İstek Atıyor?
$ awk '{print $1}' bookstore.access.log | sort | uniq -c | sort -rn | head
1482 198.51.100.23
533 203.0.113.8
492 192.0.2.44
486 203.0.113.5
266 198.51.100.7
252 203.0.113.57
251 198.51.100.66
248 192.0.2.201
247 203.0.113.150
239 192.0.2.99
$ awk '{print $9}' bookstore.access.log | sort | uniq -c | sort -rn
4289 200
774 404
71 502
66 503
Zincir şöyle çalışıyor: awk IP'leri çıkarır, sort aynı IP'leri alt alta getirir, uniq -c art arda gelen aynı satırları sayar, sort -rn sayıya göre (-n) büyükten küçüğe (-r) sıralar, head ilk 10'u verir. Bir IP toplam trafiğin dörtte birinden fazlasını gönderiyor. 404'ler de dikkat çekici; o IP'nin hangi adreslere 404 aldığına bakmak için awk'ın koşul yazabildiği yanını kullandım:
$ awk '$1 == "198.51.100.23" && $9 == 404 {print $7}' bookstore.access.log | sort | uniq -c
90 /api/books/9780000000000
84 /api/books/9780141439518
$ grep -E "wp-login|\.env|phpmyadmin|\.git" bookstore.access.log | awk '{print $1, $9, $7}' | sort | uniq -c
48 192.0.2.99 404 /.env
63 192.0.2.99 404 /.git/config
61 192.0.2.99 404 /phpmyadmin/
63 192.0.2.99 404 /wp-login.php
2 192.0.2.99 502 /.env
1 192.0.2.99 502 /phpmyadmin/
1 192.0.2.99 502 /wp-login.php
awk'ta süslü parantezin önüne yazılan koşul ($9 == 404 gibi), parantez içindeki eylemin yalnızca o koşulu sağlayan satırlarda çalışmasını sağlar. İlk sonuç bir istemcinin katalogda olmayan iki ISBN'i (kitabın uluslararası kimlik numarası) durmadan sorduğunu gösteriyor; muhtemelen bir cache'leme hatası ya da eski bir link. İkinci sonuç tanıdık bir tablo: .env ve .git/config gibi gizli dosyaları arayan bir bot. grep -E extended regex açar ve | "ya da" demektir; nokta regex'te "herhangi bir karakter" olduğu için \. ile kaçırdık. Böyle bir IP'yi engellemek on birinci yazının konusu.
Senaryo 2: Dakika Başına 5xx ve Bir Olayın Penceresi
Status dağılımında 71 tane 502 var. Ne zaman oldular?
$ awk '$9 ~ /^5/ {print substr($4, 14, 5), $9}' bookstore.access.log | sort | uniq -c
24 16:00 503
7 16:01 503
35 16:04 503
71 16:05 502
$9 ~ /^5/ status'u 5 ile başlayanları seçer. substr($4, 14, 5) [14/Nov/2026:16:05:07 alanının 14. karakterinden başlayıp beş karakter alır, yani saat ve dakikayı. Tablo iki ayrı sorunu ayırıyor: 503'ler gün boyunca az sayıda ve dağınık (önceki yazıda eklediğimiz ödeme sağlayıcısı zaman aşımları), 502'ler ise tek bir dakikada toplanmış. O dakikaya yakından bakmak için sed'in aralık seçimi:
$ sed -n "/:16:05:00 /,/:16:05:30 /p" /var/log/nginx/bookstore.access.log | awk "{print \$9}" | sort | uniq -c
102 200
22 404
71 502
$ grep -o "connect() failed ([^)]*)" bookstore.error.log | sort | uniq -c
71 connect() failed (111: Connection refused)
sed -n varsayılan çıktıyı kapatır, p yalnızca seçilen satırları basar; /A/,/B/ A'yı içeren ilk satırdan B'yi içeren ilk satıra kadar her şeyi seçer. sed -n '100,120p' gibi satır numarasıyla da çalışır. Bütün 502'ler 16:05:00 ile 16:05:30 arasında ve error log'daki 71 satırın hepsi aynı: upstream'e bağlantı reddedilmiş, yani arkada dinleyen yoktu. grep -o satırın tamamını değil yalnızca eşleşen kısmı basar; farklı hata türlerini saymak için kullanışlı. Aynı pencereye journal'dan bakınca sebep çıkıyor:
$ journalctl -u bookstore-api --since 16:04 --until 16:06 -o short-iso --no-pager | grep -E "Stopped|started"
2026-11-14T16:05:07+00:00 bookstore-dev bookstore-api[3992]: Bookstore.Api[0] Stopped cleanly
2026-11-14T16:05:07+00:00 bookstore-dev systemd[1]: Stopped bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).
2026-11-14T16:05:54+00:00 bookstore-dev bookstore-api[4181]: Microsoft.Hosting.Lifetime[0] Application started. Hosting environment: Production; Content root path: /opt/bookstore/releases/2026-11-14-6
2026-11-14T16:05:54+00:00 bookstore-dev bookstore-api[4181]: Bookstore.Api[0] Bookstore.Api started, PID 4181
Service 16:05:07'de durdurulmuş ve 47 saniye sonra yeniden başlatılmış; 502'ler tam bu aralıkta. Önceki yazıda bir 502'yi elle bulmuştuk, bu sefer sayılarla ve zaman aralığıyla bulduk.
Senaryo 3: En Yavaş İstekler
$ awk '{print $(NF-2), $9, $7}' bookstore.access.log | sort -rn | head -n 20
3.052 200 /api/reports/sales?seconds=3
3.034 200 /api/reports/sales?seconds=3
3.016 200 /api/reports/sales?seconds=3
...
$ grep -v "/reports/" /var/log/nginx/bookstore.access.log | awk '{print $(NF-2), $9, $7}' | sort -rn | head -n 5
2.026 200 /api/books
2.010 200 /api/books/9780141439587
1.999 200 /api/books/9780141439587
1.980 200 /api/books
0.381 503 /api/orders/2596
İlk 20'nin hepsi bilerek yavaş olan rapor endpoint'i, bu beklenen bir durum. grep -v eşleşen satırları çıkarır; raporları atınca asıl ilginç olan görünüyor: normalde birkaç milisaniye süren /api/books iki saniye sürmüş. Zamanlarına bakınca bunların uygulama yeniden başladıktan hemen sonraki ilk istekler olduğu görülüyor: .NET'te ilk istekler JIT (Just-In-Time; kodun çalışma anında makine koduna derlenmesi) derlemesi ve başlangıç işleri yüzünden yavaştır. Tek tek satırlar yerine endpoint başına ortalama için awk'ın array'leri:
$ awk '{u=$7; sub(/\?.*/, "", u); gsub(/[0-9]{4,}/, "{id}", u); n[u]++; t[u]+=$(NF-2)} END {for (k in n) printf "%-24s %6d %8.3f\n", k, n[k], t[k]/n[k]}' bookstore.access.log | sort -k2,2rn
/api/books 1773 0.007
/api/orders/{id} 1709 0.076
/api/books/{id} 1326 0.008
/api/reports/sales 103 1.921
/wp-login.php 64 0.004
/.git/config 63 0.005
/phpmyadmin/ 62 0.006
/.env 50 0.004
/health 50 0.006
Her satırda path'i u'ya alıyoruz, sub ile query string'i siliyoruz, gsub ile dört ve daha uzun sayıları {id}'ye çeviriyoruz ki her ISBN ve sipariş numarası ayrı satır olmasın. n[u] istek sayısını, t[u] toplam süreyi biriktiriyor; END bloğu dosya bitince bir kez çalışıp ortalamayı basıyor. sort -k2,2rn ikinci sütuna göre sayısal ve ters sıralar. Siparişler kitaplardan on kat yavaş, çünkü uygulama ödeme sağlayıcısını beklerken 20–120 ms gecikmeyi simüle ediyor. Bu tablo bir APM (uygulama performans izleme) aracının yaptığının çok kaba bir hâli; tek sunucuda, hiçbir şey kurmadan, bir dakikada.
Senaryo 4: Bir İsteğin İzi, Sıkıştırılmış Log'lar ve jq
Bir müşteri sipariş sayfasında hata aldığını söylüyor. Nginx log'unda 503'ü bulup sondaki request id ile uygulama log'una geçiyoruz:
$ grep "\" 503 " /var/log/nginx/bookstore.access.log | tail -n 1
203.0.113.5 - - [14/Nov/2026:16:04:59 +0000] "GET /api/orders/4520 HTTP/1.1" 503 151 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0" 0.092 0.092 72acdb39420755c1047bf42ca7a3898d
$ journalctl -u bookstore-api -b --grep 72acdb39420755c1047bf42ca7a3898d -o short-iso --no-pager
2026-11-14T16:04:59+00:00 bookstore-dev bookstore-api[3992]: Bookstore.Api[0] Payment provider timeout for order 4520, request 72acdb39420755c1047bf42ca7a3898d
İki log aynı kimlikle birbirine bağlandı: Nginx'te kim, ne zaman, ne kadar sürede; uygulamada neden. grep "\" 503 " ifadesindeki tırnak ve boşluklar önemli: yalnızca 503 arasaydık bir sipariş numarasındaki ya da süredeki 503'ü de bulurduk. Bu kimliği birden çok servis ve queue arasında taşımayı .NET'te Logging serisinin correlation ID yazısı ve Debugging ve Problem Çözme serisinin log'lardan sebep bulma yazısı anlatıyor.
logrotate'in döndürüp sıkıştırdığı eski dosyalar için açmadan çalışan sürümler var: zgrep, zcat, zless.
$ zgrep -c " 503 " bookstore.access.log.2.gz
5
$ zcat bookstore.access.log.2.gz | awk '{print $9}' | sort | uniq -c | sort -rn
254 200
41 404
5 503
Uygulama log'u journal'da olduğu için onun düz metin dosyası yok, ama journal JSON verebilir ve JSON için doğru araç jq:
$ journalctl -u bookstore-api -p err --since "-30min" -o json --no-pager | jq -r '[.__REALTIME_TIMESTAMP, .PRIORITY, .MESSAGE] | @tsv' | tail -n 3
1794672294728805 3 Bookstore.Api[0] Payment provider timeout for order 3171, request a1447812bfcbcc5d1eb57dac8576cb48
1794672295231002 3 Bookstore.Api[0] Payment provider timeout for order 2308, request ae43270bdafca2663f17b2212b0a231b
1794672299199455 3 Bookstore.Api[0] Payment provider timeout for order 4520, request 72acdb39420755c1047bf42ca7a3898d
$ journalctl -u bookstore-api -p err --since "-30min" -o json --no-pager | jq -r '.MESSAGE' | grep -oE "order [0-9]+" | wc -l
73
-o json her kaydı tek satırlık bir JSON object olarak yazar; jq istediğin alanları seçer, @tsv tab ile ayrılmış satıra çevirir ve sonuç tekrar grep/awk zincirine girer. Uygulaman JSON formatında log yazıyorsa da aynı yöntem geçerli. jq'nun kendisini Terminal ve Command Line serisi ayrıntılı anlatıyor.
Üretimde Dikkat
Büyük dosyada önce daralt, sonra işle. Birkaç GB'lık bir access log'da sort hem bellek hem disk kullanır (geçici dosyalarını /tmp'ye yazar). Önce grep ya da sed -n ile ilgili saat aralığına in, sonra sırala. Canlı bir sunucuda ağır bir zinciri nice ile düşük öncelikte çalıştırmak (dördüncü yazı) uygulamanın CPU'suna dokunmamanı sağlar.
Log'u sunucuda okumak hızlı bir ilk bakış içindir. Aynı soruyu her hafta soruyorsan (dakika başına 5xx, en yavaş endpoint) cevap bir dashboard ve alert'te olmalı; ilerideki Observability serisi bunu metric'lerle anlatıyor. Bu zincirler, merkezi sistemin olmadığı ya da yetmediği anlarda elinde kalan araçlar.
Log'larda kişisel veri var: IP adresleri, user agent'lar, bazen query string'deki e-posta. Analiz için dosyayı kendi makinene kopyalıyorsan önce maskele; sed -E bunun için yeterli:
$ sed -E "s/^([0-9]+\.[0-9]+)\.[0-9]+\.[0-9]+/\1.x.x/" /var/log/nginx/bookstore.access.log | head -n 2 | cut -c1-80
198.51.x.x - - [14/Nov/2026:16:00:23 +0000] "GET /api/books/9780141439518 HTTP/1
192.0.x.x - - [14/Nov/2026:16:00:23 +0000] "GET /api/books/9780141439518 HTTP/1.
s/eski/yeni/ değiştirme, -E extended regex, \1 parantez içinde yakalanan ilk grup. KVKK'ya (Kişisel Verilerin Korunması Kanunu) uygun logging .NET'te Logging serisinin hassas veri yazısında.
Sık Düşülen Tuzaklar
uniq'i sırasız girdiyle kullanmak. Tahminin cevabı: uniq yalnızca art arda gelen aynı satırları birleştirir.
$ printf "203.0.113.5\n198.51.100.23\n203.0.113.5\n" | uniq -c
1 203.0.113.5
1 198.51.100.23
1 203.0.113.5
$ printf "203.0.113.5\n198.51.100.23\n203.0.113.5\n" | sort | uniq -c
1 198.51.100.23
2 203.0.113.5
Gerçek bir log'da aynı IP binlerce parçaya bölünür ve sort -rn | head -n 1 sana yalnızca en uzun kesintisiz seriyi verir; komut hata vermediği için yanlış sayıyı fark etmezsin. Kural: uniq'ten önce her zaman sort.
Regex kaçışlarını unutmak. IP aramak masum görünür:
$ grep -c "203.0.113.5" bookstore.access.log
738
$ grep -c "^203\.0\.113\.5 " bookstore.access.log
486
$ grep -c "/api/reports/sales?seconds=1" bookstore.access.log
38
$ grep -cE "/api/reports/sales?seconds=1" bookstore.access.log
0
İlk komut 203.0.113.57'yi de saydı, çünkü nokta herhangi bir karakterdir ve sonda bir sınır yok. Doğrusu noktaları kaçırmak ve satır başına (^) ve arkadaki boşluğa sabitlemek; sabit bir metin arıyorsan grep -F regex'i tamamen kapatır. İkinci çift daha sinsi: basit regex'te ? düz bir karakter, -E ile "önceki karakter isteğe bağlı" anlamına geliyor ve aynı ifade 38 yerine 0 buluyor.
Sıralamanın locale'e bağlı olduğunu unutmak:
$ printf "bookstore\nBookstore\nAPI\napi\n" | LC_ALL=en_US.UTF-8 sort
api
API
bookstore
Bookstore
$ printf "bookstore\nBookstore\nAPI\napi\n" | LC_ALL=C sort
API
Bookstore
api
bookstore
Aynı girdi, iki sonuç. en_US.UTF-8 sözlük sırası kullanır, C byte sırası. Bu sunucu C.UTF-8 ile geliyor, ama kendi makinende ya da başka bir sunucuda locale farklıysa iki dosyayı sort ile sıralayıp comm ya da join ile karşılaştırmak sessizce yanlış sonuç verir. Script'lerde ve karşılaştırmalarda LC_ALL=C yaz; hem tutarlı hem daha hızlı.
cat dosya | grep yazmak. Çalışır ama gereksiz bir process ve pipe ekler; grep ifade dosya aynı işi yapar ve dosyanın adını da bilir (-c ile birden çok dosyada dosya başına sayı verir). Büyük dosyalarda grep'i zincirin başına koymak, arkadaki her komuta daha az satır gönderir.
Alan numarasının sabit olduğunu varsaymak. User agent gibi boşluk içeren alanlardan sonra $12, $13 satıra göre değişir. Sondan say ($NF, $(NF-2)) ya da tırnaklı alanlar için cut -d'"' kullan. Log formatını değiştirdiğinde de bu zincirleri kontrol et.
Küçük Bir Deneme
★☆☆ Kendi sunucundaki Nginx access log'unda status dağılımını çıkar ve 404 alan ilk 10 path'i awk '$9 == 404 {print $7}' | sort | uniq -c | sort -rn | head ile bul.
★★☆ Saat başına istek sayısını ve aynı saatteki 5xx oranını tek bir awk komutuyla yazdır (ipucu: iki array, END'de bölme). Sonucu tee ile dosyaya da kaydet.
★★★ Bookstore'un log_format'ına $upstream_response_time zaten var. $request_time ile farkının 0.1 saniyeden büyük olduğu istekleri bul: bu fark yavaş istemciyi mi, yavaş uygulamayı mı gösterir? Bir isteği curl --limit-rate 1k ile yavaşlatıp kendi cevabını doğrula.
Log'lardan IP, port ve upstream hataları çıktı; sıradaki soru "bu port gerçekten açık mı ve kim dinliyor". Bir sonraki yazıda network komutlarına geçiyoruz: ip, ss, curl, dig ve nc ile katman katman "neden ulaşamıyorum" teşhisi.