Yazılımcı olarak hepimiz yazdığımız kodun üretim ortamında (production) nasıl davrandığını anlamak isteriz. Bu isteğin en doğal sonucu da kodun her köşesine, her karar mekanizmasına birer log satırı serpiştirmektir. Ancak doğru bir log seviye stratejisi kurulmadığında, geliştiricinin lokal bilgisayarında hayat kurtaran o satırlar, canlı sunucularda diskleri dolduran, CPU’yu eriten ve operasyon ekiplerini canından bezdiren birer canavara dönüşüyor.
Saha tecrübem boyunca loglama yüzünden çöken çok sistem gördüm. Bir keresinde, bir avuç mikroservisin debug logları yüzünden kısa sürede diskin tamamen dolduğuna ve bunun sonucunda veritabanının kilitlendiğine şahit oldum. Bu yazıda, loglamanın sadece teknik bir detay değil, aynı zamanda yazılım ekibi ile operasyon ekibi arasındaki o ince empati köprüsü olduğunu kendi deneyimlerim üzerinden anlatacağım.
Log Seviye Stratejisi: Rahatlığın Getirdiği Gizli Maliyet
Geliştiriciler olarak çoğunlukla kodun yazım aşamasına odaklanırız. “Buraya bir log atayım da ne olur ne olmaz, hata çıkarsa bakarız” düşüncesi, aslında gelecekteki kendimize veya sistem yöneticisine bırakılmış saatli bir bombadır. Eğer log seviye stratejisi projenin başında net kurallarla belirlenmediyse, her geliştirici kendi yoğurt yiyişine göre log seviyesi seçmeye başlar.
# Geliştirici A'nın tercihi (Fazla iyimser)
logger.info("Kullanıcı sisteme girdi, işlemler başlıyor...")
# Geliştirici B'nin tercihi (Paranoyak)
logger.debug(f"SQL sorgusu tetiklendi: SELECT * FROM users WHERE id = {user_id}")
Yukarıdaki basit örnek bile üretim ortamında büyük farklar yaratır. İkinci satırdaki gibi binlerce kullanıcının bağlandığı bir sistemde her SQL sorgusunu dinamik string formatlama ile loglamak, disk I/O limitlerini zorlamanın en kestirme yoludur. Bu hata genellikle sinsi ilerler: Nginx reverse proxy arkasındaki uygulama sunucusunun yanıt süresi belirgin biçimde fırlar, sebebi de tamamen disk yazma kuyruğunun şişmesidir.
DEBUG Seviyesinin Üretim Ortamındaki İhaneti
DEBUG seviyesi, adından da anlaşılacağı üzere sadece geliştirme (development) ve test aşamalarında kullanılmak üzere tasarlanmıştır. Üretim ortamında bu seviyeyi açık bırakmak, sisteme yapılabilecek en büyük kötülüklerden biridir. Bir üretim ERP’sinin stok modülünü yazarken, depodaki el terminallerinden gelen her barkod okuma işlemini DEBUG seviyesinde loglamıştık.
Sistem canlıya geçtikten kısa süre sonra, systemd-journald servisinin CPU tüketimi tavan yaptı. Journald, gelen aşırı log yükü karşısında kendi koruma mekanizmasını devreye soktu ve logları drop etmeye (dökmeye) başladı. Sonuç olarak, gerçek bir hata oluştuğunda elimizde o hataya dair hiçbir iz kalmamıştı çünkü sistem gürültüden boğulmuştu.
# journalctl çıktısında karşılaştığımız o meşhur uyarı satırı:
Jun 06 14:20:11 srv-erp-01 systemd-journald[452]: Suppressed 8420 messages from /user.slice/user-1000.slice
Eğer log seviyeniz canlıda DEBUG olarak kalırsa, sadece diski doldurmazsınız; aynı zamanda hassas verilerin (kişisel veriler, şifreler, token bilgileri) log dosyalarına sızma riskini de katlarsınız. Güvenlik tarafında kernel hardening ve switch hardening gibi konularla ne kadar uğraşırsanız uğraşın, uygulama seviyesindeki bu dikkatsizlik tüm güvenlik duvarlarınızı anlamsız kılabilir.
INFO Seviyesinin Sınırları: “Gürültü” ile “Sinyal” Arasındaki İnce Çizgi
Peki, INFO seviyesinde ne loglamalıyız? Bu soru, yazılım mimarisinin en kritik tartışma konularından biridir. INFO, sistemin normal işleyişi sırasındaki önemli durum değişikliklerini belirtmelidir. Her döngünün (loop) içindeki adımı değil, sadece o döngünün başarıyla tamamlandığını veya başladığını göstermelidir.
Bir üretim ERP’sinde tasarladığım yapay zeka destekli üretim planlama modülünde, log seviyelerini şu şekilde kategorize etmiştim:
| Log Seviyesi | Ne Zaman Kullanılır? | Hedef Kitle | Örnek Senaryo |
|---|---|---|---|
| DEBUG | Detaylı kod akışı, değişken durumları | Geliştirici | Connection pool active connections: 14 |
| INFO | İş mantığı başlangıç ve bitişi | Sistem Yöneticisi | AI Production Plan #4812 started. |
| WARNING | Beklenmeyen ama sistemin tolere ettiği durumlar | L1 Destek / Ops | Database connection retry #1 failed. Retrying... |
| ERROR | İşlemin yarıda kalması, müdahale gereksinimi | SRE / L2 Destek | Failed to write plan to DB. Transaction aborted. |
Bu tabloya sadık kalmak, operasyon ekibinin gece yarısı gereksiz yere uykusundan uyanmasını engeller. Eğer INFO seviyesini bir DEBUG gibi kullanırsanız, log izleme araçlarınızda (OpenSearch, Grafana Loki gibi) saniyede binlerce satır akar ve gerçek bir anomalinin tespiti imkansız hale gelir.
ERROR ve WARNING: Ne Zaman Alarma Basmalıyız?
En çok karıştırılan iki kavram: Bir durum ne zaman WARNING, ne zaman ERROR’dur? Benim bu konudaki kuralım çok basittir: Eğer bir log satırı için bir insanın hemen yatağından kalkıp müdahale etmesi gerekmiyorsa, o log ERROR değildir.
Geliştirdiğim bir mobil uygulamanın backend tarafında Sentry üzerinde çok yüksek sayıda unhandled exception (hata) biriktiğini gördüm. Detaylıca incelediğimde, bunların büyük çoğunluğunun aslında istemci tarafındaki ağ kopmalarından kaynaklanan, sunucunun hiçbir şey yapamayacağı geçici bağlantı hataları olduğunu fark ettim. Bunlar ERROR olarak işaretlendiği için nöbetçi mühendis sık sık uyarı alıyordu ve bir süre sonra bu uyarıları tamamen görmezden gelmeye (alert fatigue) başlamıştı.
# KÖTÜ PRATİK (Her hatayı felaket gibi göstermek)
try:
response = requests.get("https://api.external-service.com/data", timeout=5)
except requests.exceptions.Timeout as e:
logger.error(f"External API timeout! Error: {e}") # Bu bir ERROR değil, geçici bir durumdur.
# İYİ PRATİK (Tolere edilebilir durumları WARNING ile geçmek)
try:
response = requests.get("https://api.external-service.com/data", timeout=5)
except requests.exceptions.Timeout as e:
logger.warning("External API timeout. Retrying with fallback data.")
response = get_cached_fallback_data()
Eğer dış servis kesintisi sistemin tamamen durmasına yol açmıyorsa ve uygulamanız bunu tolere edebiliyorsa (örneğin önbellekten veri sunarak), bu durum bir WARNING’dir. Ancak tüm sistemin durmasına yol açan, veritabanı bağlantısının tamamen kopması veya diskte yer kalmaması gibi durumlar gerçek birer ERROR’dur ve anında alarm tetiklemelidir.
Log Yönetiminde İki Büyük Günah: Boş Catch Blokları ve String Formatlama
Yazılım geliştirme sürecinde sıkça yapılan iki büyük teknik hata, tüm log seviye stratejinizi sıfırlayabilir. Birincisi, hataları yutup hiçbir yere yazmamaktır (swallowing exceptions). İkincisi ise logları yapılandırılmamış (unstructured) düz metin olarak yazıp, sistemin analiz edilmesini zorlaştırmaktır.
Boş bir catch bloğu, bir sorunun sessizce büyümesine zemin hazırlar; örneğin yoğun bir PostgreSQL replikasyon yapısında WAL bloat (arka arkaya biriken işlem günlükleri) gibi bir problemin kaynağını ararken, hatayı yutan bir fonksiyon yüzünden iz sürmek çok daha zor hale gelir.
# SESSİZ ÖLÜM (Boş catch bloğu)
try:
process_payment(order_id)
except Exception:
pass # Asla bunu yapmayın! Sistem hata aldığını bilmez.
# YAPILANDIRILMIŞ LOGLAMA (Structured Logging)
import json
import logging
logger = logging.getLogger("payment_processor")
def process_payment_safe(order_id):
try:
# Ödeme işlemleri...
pass
except Exception as e:
# JSON formatında log üreterek analiz araçlarının işini kolaylaştırıyoruz
log_payload = {
"event": "payment_failed",
"order_id": order_id,
"error_message": str(e),
"status": "failed"
}
logger.error(json.dumps(log_payload))
Yapılandırılmış loglama (JSON formatı), modern log analiz sistemlerinin logları kolayca indekslemesini sağlar. Bu sayede, “Hangi siparişlerde ödeme hatası alınmış?” sorusunun cevabını saniyeler içinde bulabiliriz. Düz metin loglarda ise regex yazarak debelenmek zorunda kalırız ki bu da operasyonel süreyi uzatır.
İnsan Odaklı Bir Operasyon Kültürü Kurmak
Yazının başında da belirttiğim gibi, loglama aslında tamamen bir kültür ve empati meselesidir. Kodu yazan geliştirici ile o kodu canlıda ayakta tutmaya çalışan sistem yöneticisi veya site güvenliği mühendisi (SRE) arasındaki ilişkiyi belirler. Geliştirici, yazdığı her log satırının bir maliyeti olduğunu bilmeli; operasyoncu ise log seviyelerini dinamik olarak değiştirebileceği altyapıyı (örneğin log level hot-reload) sağlamalıdır.
Bu noktada dinamik log seviyesi büyük bir esneklik sağlar. Örneğin canlı bir geçiş (migration) sırasında log seviyesini geçici olarak DEBUG’a çekip, geçiş tamamlandıktan sonra sunucuyu yeniden başlatmadan tekrar INFO seviyesine indirmek hayat kurtarır. Nginx reverse proxy ayarlarında veya systemd servis unit’lerinde yapacağınız ufak bir sinyal (SIGHUP) yapılandırması, uygulamanızı kapatıp açmadan log seviyesini değiştirmenize olanak tanır.
Sonuç olarak; loglama, geliştiricinin lokalinde rahat etmesi için rastgele kullanacağı bir debug aracı değildir. Loglar, üretim ortamındaki sisteminizin kara kutusudur. O kutuyu gürültüyle doldurup işe yaramaz hale getirmek de, net ve okunabilir sinyallerle donatıp hayat kurtarmasını sağlamak da bizim elimizde.
Net pozisyonum: Üretim ortamında DEBUG log seviyesini tamamen yasaklayın, loglarınızı mutlaka JSON formatında yapılandırılmış hale getirin ve her hata logunun arkasına “Bu hata gece yarısı birini uyandırmalı mı?” sorusunu ekleyin. Bir sonraki yazımda, PostgreSQL üzerindeki indeksleme stratejilerinde yaptığımız hataları ve sorgu planlayıcısının (planner) nasıl ters köşe olabildiğini anlatacağım.