Log Okumak 1: journalctl, /var/log ve logrotate
journalctl -u ile bir service'in log'unu okursun; -p err seviyeye, --since zamana, -b açılışa göre daraltır, kullanıcı adm grubunda değilse journal boş görünür. Bookstore.Api'nin log'larını journalctl filtreleriyle okumayı, AddSystemd'nin .NET log seviyelerini journal'a nasıl taşıdığını, persistent ve volatile journal farkını gerçek bir reboot'la, /var/log'daki auth.log, kern.log ve Nginx log'larını, logrotate'te create ile copytruncate farkını gösteriyoruz.
Sürüm notu: Bu yazıdaki komutlar Ubuntu Server 24.04.5 LTS (systemd 255, rsyslog, logrotate 3.21, Nginx 1.24) çalıştıran bir test VM'inde; kod .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çı) API'si log'larını /var/log/bookstore/api.log dosyasına yazıyor. Disk dolmasın diye logrotate'e şu kuralı ekledin: her gün dosyayı api.log.1 olarak yeniden adlandır ve yerine boş bir api.log oluştur. Ertesi sabah bakıyorsun:
$ sudo ls -l /var/log/bookstore/
total 8
-rw-r----- 1 bookstore bookstore 0 Nov 14 15:48 api.log
-rw-r--r-- 1 root root 4815 Nov 14 15:48 api.log.1
Uygulama çalışıyor, istek de alıyor. Yeni log satırları nerede ve neden api.log 0 byte? Cevap logrotate bölümünde.
Beşinci yazıda Bookstore'u systemd'ye devrettik ve bütün çıktısının journal'a gittiğini gördük. Bu yazıda o log'ları okumayı öğreniyoruz: journalctl'ın filtreleri, bir journal kaydının içinde ne olduğu, journal'ın diskte nerede ve ne kadar yer kapladığı, /var/log'daki klasik dosyalar ve logrotate. Log'lardan grep, awk ve sort ile cevap çıkarmak bir sonraki yazının konusu.
journalctl ve Yetki: "No entries" Her Zaman Boş Demek Değil
Sunucuya girip ilk iş Bookstore'un log'una bakıyorsun:
$ id
uid=1000(ada) gid=1000(ada) groups=1000(ada),27(sudo)
$ journalctl -u bookstore-api -n 3 --no-pager
Hint: You are currently not seeing messages from other users and the system.
Users in groups 'adm', 'systemd-journal' can see all messages.
Pass -q to turn off this notice.
-- No entries --
$ tail -n 2 /var/log/syslog
tail: cannot open '/var/log/syslog' for reading: Permission denied
Service log yazıyor, ama normal bir kullanıcı journal'da yalnızca kendi process'lerinin kayıtlarını görür. -- No entries -- burada "log yok" değil "sana gösterilecek log yok" demek ve üstündeki ipucu bunu açıkça söylüyor. Her seferinde sudo yazmak yerine kullanıcıyı adm grubuna ekle; Ubuntu'da bu grup hem journal'ı hem de /var/log'daki syslog:adm sahipli dosyaları okuma yetkisi verir. Grup değişikliği yeni oturumda geçerli olur, bu yüzden SSH (uzak bağlantı protokolü) oturumundan çıkıp tekrar bağlan:
$ sudo usermod -aG adm ada
$ id
uid=1000(ada) gid=1000(ada) groups=1000(ada),4(adm),27(sudo)
$ journalctl -u bookstore-api -n 3 --no-pager
Nov 14 15:43:07 bookstore-dev bookstore-api[1430]: Microsoft.AspNetCore.Routing.EndpointMiddleware[0] Executing endpoint 'HTTP: GET /api/books'
Nov 14 15:43:07 bookstore-dev bookstore-api[1430]: Microsoft.AspNetCore.Routing.EndpointMiddleware[1] Executed endpoint 'HTTP: GET /api/books'
Nov 14 15:43:07 bookstore-dev bookstore-api[1430]: Microsoft.AspNetCore.Hosting.Diagnostics[2] Request finished HTTP/1.0 GET http://127.0.0.1/api/books - 200 - application/json;+charset=utf-8 65.6287ms
adm salt okuma grubudur, sudo yetkisi vermez; log'a bakması gereken ama sunucuyu yönetmeyen bir geliştirici için doğru seviye. Windows'ta WSL (Windows Subsystem for Linux; Windows içinde Linux çalıştıran katman) 2 kullanıyorsan aynı komutları orada da deneyebilirsin: wsl --install ile kurulan güncel Ubuntu'da systemd varsayılan olarak açık, değilse /etc/wsl.conf'a [boot] altında systemd=true yazıp WSL'i yeniden başlatman gerekiyor.
Filtreler: -u, -f, -p, --since, --grep ve -b
Bu yazı için Bookstore.Api'ye iki endpoint ekledik: bulunamayan kitapta Warning, ödeme sağlayıcısı zaman aşımına düştüğünde Error yazan birer satır. İkincisi Nginx'in gönderdiği X-Request-ID header'ını da log'a koyuyor; yedinci yazıda bir isteği Nginx log'undan uygulama log'una bu değerle takip edeceğiz.
app.MapGet("/api/books/{isbn}", (string isbn) =>
{
Book? book = books.FirstOrDefault(b => b.Isbn == isbn);
if (book is null)
{
app.Logger.LogWarning("Book {Isbn} not found", isbn);
return Results.NotFound();
}
return Results.Ok(book);
});
// Sipariş sorgusu: ödeme sağlayıcısı arada bir zaman aşımına düşüyor (simülasyon)
app.MapGet("/api/orders/{id:int}", async (int id, HttpContext context) =>
{
string requestId = context.Request.Headers["X-Request-ID"].FirstOrDefault() ?? context.TraceIdentifier;
await Task.Delay(Random.Shared.Next(20, 120));
if (Random.Shared.Next(100) < 4)
{
app.Logger.LogError("Payment provider timeout for order {OrderId}, request {RequestId}", id, requestId);
return Results.Problem("Payment provider timeout", statusCode: 503);
}
return Results.Ok(new { id, status = "Shipped", total = 24.49m });
});
Nginx'i de Bookstore'un önüne koyduk; site configuration'ı repoda deploy/linux/nginx/bookstore.conf'ta ve kendi access/error log dosyalarına yazıyor. 120 sipariş isteği gönderip yalnızca önemli satırlara baktım:
$ for i in $(seq 1 120); do curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1/api/orders/$((1000+i)); done | sort | uniq -c
119 200
1 503
$ journalctl -u bookstore-api -p warning --since "10 min ago" --no-pager
Nov 14 15:43:30 bookstore-dev bookstore-api[1430]: Bookstore.Api[0] Book 9780000000000 not found
Nov 14 15:44:07 bookstore-dev bookstore-api[1430]: Bookstore.Api[0] Payment provider timeout for order 1088, request 681cdf147ca6bf3d8b9906fcd64b0e3f
$ journalctl -u bookstore-api -p err -o cat --no-pager | tail -n 2
bookstore-api.service: Service has more than one ExecStart= setting, which is only allowed for Type=oneshot services. Refusing.
Bookstore.Api[0] Payment provider timeout for order 1088, request 681cdf147ca6bf3d8b9906fcd64b0e3f
-p (priority) verilen seviye ve daha önemlilerini getirir: -p warning Warning ve Error'ları, -p err yalnızca Error ve üstünü. Binlerce bilgi satırının arasından iki satırı bulmanın en hızlı yolu bu. -p err çıktısındaki ilk satır uygulamanın değil systemd'nin: beşinci yazıdaki hatalı drop-in denemesinden kalmış. -u filtresi unit'le ilgili systemd mesajlarını da getirdiği için "service neden başlamadı" sorusunun cevabı da aynı komutta çıkar. -o cat tarih ve host sütunlarını atıp yalnızca mesajı gösterir.
Canlı izlemek için -f (follow). -n 0 geçmişi göstermeden yalnızca yeni satırları bekler; başka bir terminalden bulunamayan bir kitap istedim:
$ journalctl -u bookstore-api -f -n 0 -o short-iso
2026-11-14T15:45:01+00:00 bookstore-dev bookstore-api[1430]: Microsoft.AspNetCore.Hosting.Diagnostics[1] Request starting HTTP/1.0 GET http://127.0.0.1/api/books/123 - - -
2026-11-14T15:45:01+00:00 bookstore-dev bookstore-api[1430]: Microsoft.AspNetCore.Routing.EndpointMiddleware[0] Executing endpoint 'HTTP: GET /api/books/{isbn}'
2026-11-14T15:45:01+00:00 bookstore-dev bookstore-api[1430]: Bookstore.Api[0] Book 123 not found
2026-11-14T15:45:01+00:00 bookstore-dev bookstore-api[1430]: Microsoft.AspNetCore.Http.Result.NotFoundObjectResult[1] Setting HTTP status code 404.
2026-11-14T15:45:01+00:00 bookstore-dev bookstore-api[1430]: Microsoft.AspNetCore.Routing.EndpointMiddleware[1] Executed endpoint 'HTTP: GET /api/books/{isbn}'
2026-11-14T15:45:01+00:00 bookstore-dev bookstore-api[1430]: Microsoft.AspNetCore.Hosting.Diagnostics[2] Request finished HTTP/1.0 GET http://127.0.0.1/api/books/123 - 404 0 - 58.8711ms
Deploy sırasında bir terminalde journalctl -u bookstore-api -f açık tutmak, uygulamanın kalkıp kalkmadığını ve ilk isteklerde hata verip vermediğini anında gösterir. -o short-iso tarihi yılıyla ve saat dilimiyle yazar; birden fazla sunucunun log'unu yan yana koyarken işe yarar.
Zaman ve açılış filtreleri:
$ journalctl --list-boots --no-pager
IDX BOOT ID FIRST ENTRY LAST ENTRY
-6 39489c2fcaa34d998572392a615a5b5a Sat 2026-11-14 14:04:18 UTC Sat 2026-11-14 14:33:37 UTC
-5 b7a826ba3af84beca5d69bb62516e1fc Sat 2026-11-14 14:35:04 UTC Sat 2026-11-14 14:45:09 UTC
-4 1585691d6a3b473485cfa818d19c7c3a Sat 2026-11-14 14:47:38 UTC Sat 2026-11-14 15:08:40 UTC
-3 61774f4e01d14ea3910e04590fa0c4ea Sat 2026-11-14 15:09:09 UTC Sat 2026-11-14 15:22:03 UTC
-2 72c22ea694844292a574072371e29e8d Sat 2026-11-14 15:24:27 UTC Sat 2026-11-14 15:26:52 UTC
-1 28fb5a9cf9fb4779a1b91e5fa10376ce Sat 2026-11-14 15:27:39 UTC Sat 2026-11-14 15:38:21 UTC
0 c6bbd2c1c7364e02a4b8f6d43fe19a8f Sat 2026-11-14 15:38:50 UTC Sat 2026-11-14 15:45:01 UTC
$ journalctl -u bookstore-api -b -1 -n 2 --no-pager
Nov 14 15:38:07 bookstore-dev systemd[1]: Stopped bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).
Nov 14 15:38:07 bookstore-dev systemd[1]: bookstore-api.service: Consumed 13.317s CPU time.
$ journalctl -u bookstore-api --grep "not found" --since today --no-pager
-- Boot 61774f4e01d14ea3910e04590fa0c4ea --
-- Boot 72c22ea694844292a574072371e29e8d --
-- Boot 28fb5a9cf9fb4779a1b91e5fa10376ce --
-- Boot c6bbd2c1c7364e02a4b8f6d43fe19a8f --
Nov 14 15:43:30 bookstore-dev bookstore-api[1430]: Bookstore.Api[0] Book 9780000000000 not found
Nov 14 15:45:01 bookstore-dev bookstore-api[1430]: Bookstore.Api[0] Book 123 not found
-b bu açılışı, -b -1 bir öncekini verir; "gece reboot'tan önce ne oldu" sorusu journalctl -b -1 -e ile (-e sona atlar) başlar. --since ve --until hem mutlak ("2026-11-14 15:40") hem göreli ("10 min ago", "-1h", today, yesterday) değer alır. --grep mesaj alanında regex arar; çıktıdaki -- Boot ... -- satırları sonuç değil, aranan aralıktaki her açılışın ayıracı. Başka işe yarayan filtreler: -t bookstore-db-backup (SYSLOG_IDENTIFIER'a göre, beşinci yazıdaki yedek işinin satırları), -k (yalnızca kernel), _PID=1430 gibi alan eşleşmeleri ve -e. Çıktı less'e gider; --no-pager yalnızca script'te ya da başka bir komuta pipe ederken gerekir.
Bir Journal Kaydının İçi ve .NET'in Log Seviyeleri
Ekrandaki tek satır aslında alanlardan oluşan bir kayıt. -o json-pretty bunu gösterir; uzun listeyi --output-fields ile kısalttım:
$ journalctl -u bookstore-api -p warning -n 1 -o json-pretty --output-fields=MESSAGE,PRIORITY,SYSLOG_IDENTIFIER,_PID,_UID,_SYSTEMD_UNIT,_TRANSPORT --no-pager
{
"__REALTIME_TIMESTAMP" : "1794671611747890",
"PRIORITY" : "4",
"_PID" : "2985",
"_TRANSPORT" : "stdout",
"MESSAGE" : "Bookstore.Api[0] Book 123 not found",
"SYSLOG_IDENTIFIER" : "bookstore-api",
"_UID" : "999",
"_BOOT_ID" : "2f27f34ea7c24b8c835d6db6288c245f",
"__CURSOR" : "s=687fadfccf6440fab19e618b2799cf33;i=4989;b=2f27f34ea7c24b8c835d6db6288c245f;m=19e9a442;t=6603e8be81e32;x=9ab4ccd4e054ae79",
"__SEQNUM" : "18825",
"_SYSTEMD_UNIT" : "bookstore-api.service",
"__MONOTONIC_TIMESTAMP" : "434742338",
"__SEQNUM_ID" : "687fadfccf6440fab19e618b2799cf33"
}
Alt çizgiyle başlayan alanları (_PID, _UID, _SYSTEMD_UNIT) journald kendisi ekler, process bunları değiştiremez; denetim için güvenilir olan bunlardır. _UID 999, üçüncü yazıdaki bookstore service kullanıcısı. _TRANSPORT=stdout satırın uygulamanın standart çıktısından geldiğini söylüyor. İlginç olan PRIORITY=4: uygulama düz metin yazdı, journald bunun bir Warning olduğunu nereden bildi?
Cevap beşinci yazıda eklediğimiz builder.Services.AddSystemd() satırında. Uygulama systemd altında çalıştığında bu satır console logger'ın formatter'ını Systemd formatter'a çevirir: her log tek satıra yazılır, renk kodu olmaz ve satırın başına syslog seviyesi <N> biçiminde eklenir. journald bu öneki okuyup PRIORITY alanına koyar ve mesajdan siler. Aynı çıktıyı journal yerine bir dosyaya yönlendirdiğimde önek olduğu gibi görünüyor:
$ sudo tail -n 1 /var/log/bookstore/api.log
<6>Microsoft.AspNetCore.Hosting.Diagnostics[2] Request finished HTTP/1.0 GET http://127.0.0.1/api/books - 200 - application/json;+charset=utf-8 1.3489ms
Eşleme şöyle: Critical 2 (crit), Error 3 (err), Warning 4 (warning), Information 6 (info), Debug ve Trace 7 (debug). AddSystemd olmasaydı her satır PRIORITY=6 olarak kaydedilirdi ve -p err uygulamanın hatalarını hiç bulamazdı. Bir .NET uygulamasını systemd altında çalıştırıyorsan bu tek satır log okumayı ciddi şekilde kolaylaştırıyor.
Journal Diskte: Persistent mi Volatile mi, Ne Kadar Yer?
journald kayıtları ya /var/log/journal'da (persistent, reboot'tan sonra da durur) ya /run/log/journal'da (volatile, RAM'de, reboot'ta silinir) tutar. Hangisi olacağını /etc/systemd/journald.conf'taki Storage= belirler:
$ grep -E "^#?(Storage|SystemMaxUse|SystemKeepFree|MaxRetentionSec)=" /etc/systemd/journald.conf
#Storage=auto
#SystemMaxUse=
#SystemKeepFree=
#MaxRetentionSec=
$ journalctl -u systemd-journald -b -o cat --no-pager | grep -i "journal (" | tail -n 2
Runtime Journal (/run/log/journal/7a5216d613204956b9683a4a5dcfa072) is 1.2M, max 9.6M, 8.3M free.
System Journal (/var/log/journal/7a5216d613204956b9683a4a5dcfa072) is 96.2M, max 1.8G, 1.7G free.
Yorum satırları systemd 255'in varsayılanlarını gösterir. auto, /var/log/journal klasörü varsa persistent, yoksa volatile demek. Ubuntu bu klasörü kurulumda oluşturduğu için burada journal persistent; ama klasörü olmayan bir image'da (bazı minimal image'lar, container'dan türetilmiş sunucular) aynı ayar sessizce volatile çalışır. Üst sınır da varsayılan olarak dosya sisteminin %10'u, en fazla 4 GB: 19 GB'lık diskte 1.8G.
Volatile'ın ne demek olduğunu bir denemeyle gördüm. Bir drop-in ile journal'ı volatile yaptım, deploy bittiğini söyleyen bir satır yazdım ve sunucuyu yeniden başlattım:
$ printf "[Journal]\nStorage=volatile\n" | sudo tee /etc/systemd/journald.conf.d/10-volatile.conf
[Journal]
Storage=volatile
$ sudo systemctl restart systemd-journald
$ logger -t bookstore-test "deploy 2026-11-14-5 finished"
$ journalctl -t bookstore-test --no-pager
Nov 14 15:45:45 bookstore-dev bookstore-test[2383]: deploy 2026-11-14-5 finished
$ sudo reboot
...
$ journalctl -t bookstore-test --no-pager
-- No entries --
$ journalctl --list-boots --no-pager | tail -n 3
-2 28fb5a9cf9fb4779a1b91e5fa10376ce Sat 2026-11-14 15:27:39 UTC Sat 2026-11-14 15:38:21 UTC
-1 c6bbd2c1c7364e02a4b8f6d43fe19a8f Sat 2026-11-14 15:38:50 UTC Sat 2026-11-14 15:45:44 UTC
0 2f27f34ea7c24b8c835d6db6288c245f Sat 2026-11-14 15:46:36 UTC Sat 2026-11-14 15:47:53 UTC
$ grep bookstore-test /var/log/syslog
2026-11-14T15:45:45.277562+00:00 bookstore-dev bookstore-test: deploy 2026-11-14-5 finished
Önceki açılış 15:45:44'te bitiyor; ondan sonra reboot'a kadar yazılan her şey, deploy satırı dahil, journal'dan gitti. Bir sunucu gece kendiliğinden yeniden başladıysa en çok ihtiyaç duyacağın log'lar tam da bunlardır. Satır yalnızca /var/log/syslog'da kaldı: Ubuntu'da /usr/lib/systemd/journald.conf.d/syslog.conf içindeki ForwardToSyslog=yes her kaydı rsyslog'a da iletiyor ve rsyslog diske yazıyor. Drop-in'i silip journald'ı yeniden başlattım ve sudo journalctl --flush ile RAM'deki kayıtları diske aktardım.
Boyut tarafında iki araç var: anlık temizlik için --vacuum-size ve --vacuum-time, kalıcı sınır için bir drop-in. Bookstore sunucusu için repoya deploy/linux/journald/50-bookstore.conf ekledik:
# /etc/systemd/journald.conf.d/50-bookstore.conf
[Journal]
Storage=persistent
SystemMaxUse=500M
SystemKeepFree=2G
$ sudo journalctl --vacuum-size=100M
Vacuuming done, freed 0B of archived journals from /run/log/journal.
Deleted archived journal /var/log/journal/7a5216d613204956b9683a4a5dcfa072/system@0006603d7389d3d6-e916cddc445e49fd.journal~ (8.0M).
Deleted archived journal /var/log/journal/7a5216d613204956b9683a4a5dcfa072/user-1000@0006603d75abe0e1-5ab7215cf91a98d6.journal~ (8.0M).
Deleted archived journal /var/log/journal/7a5216d613204956b9683a4a5dcfa072/system@0006603da0836caa-b4c1690e4e1190e9.journal~ (8.0M).
Vacuuming done, freed 24.0M of archived journals from /var/log/journal/7a5216d613204956b9683a4a5dcfa072.
Vacuuming done, freed 0B of archived journals from /var/log/journal.
$ sudo cp ~/50-bookstore.conf /etc/systemd/journald.conf.d/
$ sudo systemctl restart systemd-journald
$ journalctl -u systemd-journald -n 3 -o cat --no-pager | grep "System Journal"
System Journal (/var/log/journal/7a5216d613204956b9683a4a5dcfa072) is 95.5M, max 500.0M, 404.4M free.
Vacuum yalnızca arşivlenmiş dosyaları siler, yazılmakta olan aktif dosyalara dokunmaz; bu yüzden 100 MB istedim ama toplam 95.5 MB'a, yani yalnızca 24 MB azaldı. Storage=persistent'ı açıkça yazmak, klasör bir gün silinse bile journald'ın onu yeniden oluşturmasını sağlar. SystemKeepFree=2G ise disk dolmaya yaklaşırken journal'ın kendini küçültmesini sağlar.
/var/log'daki Klasik Dosyalar
Ubuntu'da rsyslog hâlâ çalışıyor ve journal'a gelen her şeyi düz metin dosyalara da yazıyor. Bunlar grep, tail -f ve logrotate ile çalışan eski script'lerin ve araçların okuduğu dosyalar:
/var/log/syslog: auth hariç neredeyse her şey; journal'ın düz metin kopyası gibi düşün./var/log/auth.log: SSH girişleri, başarısız denemeler,sudokullanımı./var/log/kern.log: kernel mesajları; donanım, disk, OOM (Out Of Memory; belleğin tükenmesi) killer, segfault./var/log/nginx/: Nginx'in kendi yazdığı access ve error log'ları; bunlar journal'a hiç gitmez.
Lab'daki istemciden iki var olmayan kullanıcıyla ve yanlış şifreyle bağlanmayı denedim:
$ grep -E "Invalid user|Failed password" /var/log/auth.log | tail -n 4
2026-11-14T15:44:39.376674+00:00 bookstore-dev sshd[1987]: Failed password for invalid user admin from 10.20.30.5 port 37674 ssh2
2026-11-14T15:44:40.100714+00:00 bookstore-dev sshd[1989]: Invalid user oracle from 10.20.30.5 port 37690
2026-11-14T15:44:41.921808+00:00 bookstore-dev sshd[1989]: Failed password for invalid user oracle from 10.20.30.5 port 37690 ssh2
2026-11-14T15:44:45.354925+00:00 bookstore-dev sshd[1991]: Failed password for ada from 10.20.30.5 port 47742 ssh2
İnternete açık her sunucuda bu dosya admin, oracle, root gibi adlarla deneme yapan bot'larla dolar. Aynı bilgi journalctl -u ssh ile de gelir. Şifreyle girişi kapatmak ve bu denemeleri engellemek on birinci yazının konusu.
Kernel log'u için klasik komut dmesg, ama Ubuntu'da normal kullanıcı okuyamaz:
$ dmesg -T | tail -n 3
dmesg: read kernel buffer failed: Operation not permitted
$ sysctl kernel.dmesg_restrict
kernel.dmesg_restrict = 1
$ sudo dmesg -T | tail -n 2
[Sat Nov 14 15:53:00 2026] python3[3115]: segfault at 0 ip 0000741d29d8badd sp 00007fffd8219198 error 4 in libc.so.6[741d29c28000+189000] likely on CPU 0 (core 0, socket 0)
[Sat Nov 14 15:53:00 2026] Code: 00 00 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 89 f8 48 89 fa c5 f9 ef c0 25 ff 0f 00 00 3d e0 0f 00 00 0f 87 33 01 00 00 <c5> fd 74 0f c5 fd d7 c1 85 c0 74 57 f3 0f bc c0 c5 f8 77 c3 66 66
$ journalctl -k -b --grep segfault --no-pager
Nov 14 15:52:59 bookstore-dev kernel: python3[3115]: segfault at 0 ip 0000741d29d8badd sp 00007fffd8219198 error 4 in libc.so.6[741d29c28000+189000] likely on CPU 0 (core 0, socket 0)
Bu satırı bilerek NULL adresi okuyan tek satırlık bir Python komutuyla ürettim. dmesg -T kernel'in açılıştan beri saydığı süreyi saate çevirir ve bu çeviri bir saniye kadar kayabilir (15:53:00 ve 15:52:59); journal'daki zaman daha güvenilir. adm grubundaysan journalctl -k sudo istemez. Bellek yetmediği için kernel'in bir process'i öldürdüğünü (OOM killer) de burada görürsün; onu onuncu yazıda gerçek bir vakayla inceleyeceğiz.
Nginx'in log'ları ayrı. Bookstore.Api'yi durdurup bir istek gönderdim:
$ tail -n 3 /var/log/nginx/bookstore.access.log
127.0.0.1 - - [14/Nov/2026:15:52:22 +0000] "GET /api/books HTTP/1.1" 200 179 "-" "curl/8.5.0"
127.0.0.1 - - [14/Nov/2026:15:52:39 +0000] "GET /api/books/9780547928227 HTTP/1.1" 200 70 "-" "curl/8.5.0"
127.0.0.1 - - [14/Nov/2026:15:52:40 +0000] "GET /api/books HTTP/1.1" 502 166 "-" "curl/8.5.0"
$ tail -n 1 /var/log/nginx/bookstore.error.log
2026/11/14 15:52:40 [error] 714#714: *663 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /api/books HTTP/1.1", upstream: "http://127.0.0.1:5000/api/books", host: "127.0.0.1"
Access log'da yalnızca 502 görünür; sebebi error log'dadır: upstream'e (127.0.0.1:5000) bağlantı reddedildi, yani arkada dinleyen process yok. 502 aldığında ilk bakacağın satır budur. Access log'un sütunlarını (IP, zaman, istek, status, byte, user agent) yedinci yazıda awk ile parçalayacağız.
logrotate: Dosyaları Döndürmek
Journal kendi boyutunu yönetir; düz metin dosyaları ise logrotate döndürür. Ubuntu 24.04'te logrotate'i cron değil, bir systemd timer çalıştırıyor:
$ systemctl list-timers logrotate.timer --no-pager
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sun 2026-11-15 00:00:00 UTC 8h Sat 2026-11-14 14:05:09 UTC - logrotate.timer logrotate.service
1 timers listed.
Pass --all to see loaded but inactive timers, too.
$ grep -v "^#" /etc/logrotate.conf | grep -v "^$"
weekly
su root adm
rotate 4
create
include /etc/logrotate.d
$ cat /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
prerotate
if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
run-parts /etc/logrotate.d/httpd-prerotate; \
fi \
endscript
postrotate
invoke-rc.d nginx rotate >/dev/null 2>&1
endscript
}
/etc/cron.daily/logrotate da duruyor ama systemd çalışıyorsa hiçbir şey yapmadan çıkıyor. Nginx'in kuralı satır satır: daily her gün, rotate 14 14 eski dosya tut, compress eskileri gzip'le, delaycompress en yeni eski dosyayı (.1) bir tur sıkıştırmadan bırak, missingok ve notifempty dosya yoksa ya da boşsa hata verme ve döndürme, create 0640 www-data adm yerine bu izinlerle yeni dosya oluştur. Asıl önemli kısım postrotate: dosya yeniden adlandırıldıktan sonra Nginx'e rotate komutu gönderiliyor, bu da master process'e SIGUSR1 gönderir ve Nginx log dosyalarını kapatıp yeniden açar. delaycompress'in sebebi de bu: Nginx dosyayı yeniden açana kadar .1'e yazmaya devam edebilir, o anda sıkıştırılmamalı.
Tahminin cevabı burada. Bookstore'u bir süreliğine journal yerine dosyaya yazdırdım (unit'e bir drop-in ile StandardOutput=append:/var/log/bookstore/api.log) ve Nginx'inkine benzeyen ama postrotate'i olmayan bir kural yazdım:
$ cat /etc/logrotate.d/bookstore
/var/log/bookstore/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 bookstore bookstore
}
$ sudo logrotate -v -f /etc/logrotate.d/bookstore
...
rotating log /var/log/bookstore/api.log, log->rotateCount is 14
...
creating new /var/log/bookstore/api.log mode = 0640 uid = 999 gid = 988
$ sudo ls -l /var/log/bookstore/
total 8
-rw-r----- 1 bookstore bookstore 0 Nov 14 15:48 api.log
-rw-r--r-- 1 root root 4815 Nov 14 15:48 api.log.1
$ sudo ls -l /proc/$(systemctl show -p MainPID --value bookstore-api)/fd/1
l-wx------ 1 bookstore bookstore 64 Nov 14 15:48 /proc/1577/fd/1 -> /var/log/bookstore/api.log.1
-f zamanı beklemeden döndürmeye zorlar, -v ne yaptığını anlatır (çıktıyı kısalttım). Yeniden adlandırmak dosyayı değiştirmez, yalnızca adını değiştirir. Process'in elindeki file descriptor (açık dosyayı gösteren numara) hâlâ aynı dosyayı gösteriyor ve o dosyanın adı artık api.log.1. Dördüncü yazıda gördüğümüz /proc/<pid>/fd bunu açıkça gösteriyor: standart çıktı (fd 1) api.log.1'e yazıyor. Yeni api.log boş kalıyor, ertesi gece api.log.1 sıkıştırılıp .2.gz olunca da uygulama silinmiş bir dosyaya yazmaya başlıyor ve disk alanı geri gelmiyor (bunu onuncu yazıda göreceğiz).
İki çözüm var. Uygulama log dosyasını yeniden açmayı biliyorsa (Nginx'in SIGUSR1'i, rsyslog'un HUP'u gibi) postrotate'te ona sinyal gönder. .NET'in böyle bir sinyali yok ve systemctl restart her gece bir kesinti demek. O zaman copytruncate: logrotate dosyanın içeriğini kopyalar, sonra orijinali sıfırlar; process aynı dosyaya yazmaya devam eder:
$ sudo logrotate -f /etc/logrotate.d/bookstore
$ sudo ls -l /var/log/bookstore/
total 8
-rw-r--r-- 1 root root 2271 Nov 14 15:50 api.log
-rw-r--r-- 1 root root 2543 Nov 14 15:50 api.log.1
$ sudo ls -l /proc/$(systemctl show -p MainPID --value bookstore-api)/fd/1
l-wx------ 1 bookstore bookstore 64 Nov 14 15:50 /proc/1921/fd/1 -> /var/log/bookstore/api.log
$ sudo grep -c "Request finished" /var/log/bookstore/api.log /var/log/bookstore/api.log.1
/var/log/bookstore/api.log:5
/var/log/bookstore/api.log.1:5
Rotate'ten önceki beş istek .1'de, sonraki beş istek yine api.log'da. copytruncate'in bedeli küçük bir pencere: kopyalama ile sıfırlama arasında yazılan satırlar kaybolabilir. Bunun düzgün çalışması için dosyanın append modunda açılmış olması da gerekir; systemd'nin append:'i ve log library'lerinin file sink'leri öyle açar. Repoya koyduğumuz deploy/linux/logrotate/bookstore bu yüzden create yerine copytruncate içeriyor. Bookstore.Api için ise drop-in'i kaldırıp journal'a geri döndük: journal'ın kendi boyut sınırı var ve rotate sorunu hiç yok. Serilog gibi bir library ile dosyaya yazıyorsan onun kendi rolling ayarlarını kullan ve aynı dosyaya logrotate'i ayrıca uygulama; ikisi birbirinin işini bozar.
Üretimde Dikkat
Log hacmini ölç. Bookstore.Api'nin başından beri appsettings.json'u yoktu ve ASP.NET Core her istek için Information seviyesinde dört satır yazıyordu:
$ since=$(date "+%H:%M:%S"); for i in $(seq 1 100); do curl -s -o /dev/null http://127.0.0.1/api/books; done; journalctl -u bookstore-api --since "$since" -o cat | wc -l
400
Saniyede 50 istek alan bir API için bu günde 17 milyondan fazla satır demek. Proje şablonlarının getirdiği ayarı ekledik:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Yeni release'le aynı 100 istek 0 satır üretti; uygulamanın kendi Information log'ları ve bütün Warning ile Error'lar duruyor. Hangi seviyenin hangi ortamda açık olacağını ve log'u canlı değiştirmeyi .NET'te Logging serisi ayrıntılı anlatıyor.
journal ve /var/log tek sunucu içindir. İki sunucun varsa bir hatayı ararken iki ayrı SSH oturumunda grep yapmak zorunda kalırsın, sunucu silinince de log'ları gider. Production'da log'ları merkezi bir yerde topla: .NET'te Logging serisi Serilog ve Seq ile structured log'ları, ilerideki Observability serisi OpenTelemetry ile log, metric ve trace'i birlikte anlatıyor. Sunucudaki journal, merkezi sisteme ulaşamayan ya da uygulama hiç ayağa kalkamadığında bakılan yer olarak kalır.
Log dizininin bulunduğu disk dolarsa yalnızca log yazılamaz hâle gelmez; aynı diski kullanan veritabanı ve uygulama da hata vermeye başlar. Journal için SystemMaxUse, dosyalar için logrotate kuralı, ikisini de kontrol etmek için journalctl --disk-usage ve du -sh /var/log/*. Disk dolduğunda ne yapılacağı onuncu yazıda.
Sık Düşülen Tuzaklar
Journal'ı volatile bırakmak. Yukarıda gördüğümüz gibi reboot'tan önceki dakikalar kaybolur. Yeni bir sunucuda journalctl --list-boots çalıştır; tek satır görüyorsan ve sunucu daha önce yeniden başladıysa journal persistent değildir.
Yetkisi olmayan kullanıcıyla -- No entries -- görüp log olmadığını sanmak. Uyarı satırını oku, adm grubuna gir ya da sudo kullan.
Uygulama dosyaya yazarken logrotate'te create kullanmak. Uygulama eski dosyaya yazmaya devam eder; ya postrotate ile dosyayı yeniden açtır ya copytruncate kullan. Yeni bir kuralı devreye almadan önce sudo logrotate -d /etc/logrotate.d/bookstore ile dry run yap: hiçbir dosyaya dokunmadan ne yapacağını yazar.
Log diskini doldurmak. Varsayılan sınırlar diskin %10'u kadar journal'a izin verir, ama logrotate kuralı olmayan bir dosya sınırsız büyür. Uygulamanın Debug seviyesini sorun ararken açıp kapatmayı unutmak da aynı sonuca götürür.
Sadece access log'a bakmak. 502 ve 504'lerin sebebi Nginx'in error log'unda, uygulamanın exception'ı journal'dadır; üçüne birlikte bakmadan karar verme.
Küçük Bir Deneme
★☆☆ Kendi VM'inde kullanıcını adm grubuna ekle, yeniden bağlan ve journalctl -u ssh --since today ile bugün kimin bağlandığını, -p warning -b ile bu açılıştaki uyarıları listele.
★★☆ Bookstore.Api'den builder.Services.AddSystemd() satırını geçici olarak kaldır, deploy et ve bulunamayan bir kitap iste. journalctl -u bookstore-api -p warning bu satırı buluyor mu? -o json-pretty ile PRIORITY alanına bak.
★★★ StandardOutput=append: drop-in'iyle uygulamayı dosyaya yazdır, önce create'li kuralla logrotate -f çalıştırıp /proc/<pid>/fd/1'in nereyi gösterdiğine bak, sonra copytruncate'e geçip tekrarla. Rotate sırasında saniyede 100 istek gönderip copytruncate'in kaybettiği satır olup olmadığını say.
Artık log'un nerede olduğunu ve onu nasıl daraltacağını biliyorsun. Bir sonraki yazıda bu satırlardan cevap çıkarıyoruz: pipe ve redirection'la küçük araçları zincirleyip Nginx access log'undan en çok istek atan IP'leri, dakika başına 5xx sayısını ve en yavaş istekleri buluyoruz.