Skip to content

Bölüm 9 / 12

Hata ayıklama

Breakpoint, watch, çağrı yığını, koşullu breakpoint, uzaktan debug.

13 dakikalık okuma

Testler hatanın varlığını söyler; hata ayıklama (debugging), nedenini bulma zanaatıdır — ve ilan metninde adıyla geçen üç beceriden biridir. Bu dersin iki katmanı var. Önce yöntem: hata ayıklama rastgele kurcalama değil, bilimsel döngüdür — yeniden üret, gözlemle, hipotez kur, tek değişkenli deney yap, ikiye böl, kanıtla. Sonra alet çantası: breakpoint'in koşullusu, isabet sayaçlısı ve koda dokunmadan log basanı (logpoint); watch ve immediate penceresi; çağrı yığınını okumak; ilk fırlatıldığı anda istisnayı yakalamak; uzaktaki sürece ve konteynere bağlanmak. Kapanış, mesleğin olgunluk sınavı: üretimde hata ayıklayıcı yoktur — orada kanıtın log korelasyonu, sayaçlar ve bellek dökümüdür.

01Ne öğreneceksin

  • Sistematik yöntem: yeniden üret → hipotez → tek değişkenli deney → ikiye böl → kanıtla → regresyon testi
  • Breakpoint temelleri ve adımlama; F5 ↔ Ctrl+F5 farkı
  • Koşullu breakpoint, isabet sayacı, logpoint/tracepoint
  • Watch, immediate/Debug Console ve yan etkili ifade tehlikesi
  • Çağrı yığınını okumak; async yığınlar, Just My Code
  • İstisna ayarları: ilk fırlatmada durmak, yutulmuş istisnayı bulmak
  • Hot Reload / Edit and Continue'nun sınırları
  • Uzaktan ve konteynerde attach — ve üretimde attach'in bedeli
  • Hata ayıklayıcısız teşhis: log korelasyonu, dotnet-counters/trace/dump

02Yöntem: kurcalama değil, deney

Kötü hata ayıklamanın görüntüsü hep aynıdır: rastgele satırlar değiştirilir, "belki budur" diye bir şey denenir, hata bir ara kaybolur ve kimse nedenini bilmez — 14-14'ün diliyle, belirti kapatılmıştır, kök neden yaşıyordur. İyi hata ayıklama bir bilimsel döngüdür:

  1. Yeniden üret. Hatayı istikrarlı biçimde tetikleyen en küçük adım dizisini bul. Bu, işin yarısıdır: yeniden üretemediğin hatayı düzelttiğini asla kanıtlayamazsın (5. dersin kırmızı kuralı). Üretemiyorsan veriyi, ortamı, zamanlamayı sorgula — "bende çalışıyor" cümlesi, iki ortam arasındaki farkın adını bulamamış olmaktır.
  2. Gözlemle — ve hata mesajını gerçekten oku. Şaşırtıcı sayıda seans, mesajın ilk satırı okunmadan geçer. Satır numarası, istisna tipi, iç istisna (InnerException — asıl neden çoğu kez oradadır, Hat 3), yığının en üstteki kendi kodun olan çerçevesi: dördü de mesajın içindedir.
  3. Hipotez kur. "Sanırım tarih ayrıştırma yerel kültürle yapılıyor" gibi test edilebilir tek bir cümle. Hipotezsiz atılan her adım kurcalamadır.
  4. Tek değişkenli deney. Hipotezi sınayacak en küçük değişikliği yap ve sonucu gözle. İki şeyi birden değiştirirsen sonucun hangisinden geldiğini bilemezsin — 5. dersteki "tek kırmızı" disiplininin buradaki karşılığı.
  5. İkiye böl. Arama uzayını yarıla: kodun ortasına gözlem noktası koy — hata öncesinde mi sonrasında mı? Veriyi yarıla — hangi kayıtta? Zamanı yarıla — hangi değişiklikten beri? (Sürüm geçmişinde bu aramayı otomatik yapan git bisect var; 16. hatta göreceksin.) Logaritmik arama, bin satırlık şüpheyi on adımda köşeye sıkıştırır.
  6. Kanıtla ve mühürle. Neden bulununca düzeltmeden önce hatayı üreten regresyon testini yaz (5. dersin dört adımı — senet burada ödendi), kırmızıyı gör, düzelt, yeşili gör. "Bir ara düzeldi" bir kapanış değildir.

İki yardımcı taktik: "En son ne değişti?" — hataların çoğu son değişikliğin komşuluğundadır; commit geçmişi ilk şüpheli listesidir. Ve ördeğe anlatmak: sorunu birine (ya da bir nesneye) baştan sesli anlatmak, varsayımlarını sırayla söyletir — çoğu seans, "…ve burada elbette X oluyor… dur, oluyor mu gerçekten?" cümlesinde biter. Console.WriteLine ile iz sürmeye gelince: küçümseme — bazen tek araçtır (üretim, zamanlama hataları) — ama elle serpilen geçici satırların disiplinli hâli zaten var: birazdan göreceğin logpoint ve 14-10'daki yapılandırılmış log.

03Breakpoint ve adımlama

Hata ayıklayıcı (debugger), süreci istediğin satırda dondurup içini gösteren araçtır. Temel klavye dili (VS ve VS Code'da aynı):

TuşİşNot
F5Hata ayıklayıcıyla başlat / devam et Ctrl+F5 ise bağlanmadan çalıştırır — fark önemlidir: debugger, JIT ve zamanlamayı değiştirir; "debugger'da olmuyor, normalde oluyor" hatası gerçektir (özellikle yarış durumları)
F9Satıra breakpoint koy/kaldırSürerken de eklenebilir
F10Adımla — üstünden (step over)Satırı çalıştır, çağrının içine girme
F11Adımla — içine (step into)Çağrılan metodun ilk satırına in
Shift+F11Dışına (step out)İçinde olduğun metodu bitir, çağırana dön

Verimli seansın deseni: şüpheli bölgenin başına breakpoint, oradan F10 ile yürü; ilgisiz derinliğe F11 ile dalma, daldıysan Shift+F11 ile çık. "Run to cursor" (imlece kadar koş) geçici breakpoint'in kısayoludur. Testle birleşimi ayrıca güçlüdür: kırılan birim testini debugger ile başlatmak (test gezgininde "Debug"), hatayı yeniden üretme adımını bedavaya getirir — test zaten en küçük yeniden üretimdir.

04Breakpoint'in ileri vitesleri

Bin elemanlı döngüde hata 734. kayıttaysa, F5'e 734 kez basmak bir işkencedir. Doğru araçlar:

  • Koşullu breakpoint: breakpoint'e sağ tık → koşul: basvuru.KadroKodu == "MUH-2025" && basvuru.Yas > 35. Yalnız koşul doğruyken durur. Bedelini bil: koşul her geçişte değerlendirilir, sıcak döngüde koşuyu gözle görülür yavaşlatır.
  • İsabet sayacı (hit count): "bu satırdan 734. geçişte dur" — kaçıncı yinelemede bozulduğunu bildiğin durumlar için.
  • Logpoint / tracepoint: durmak yerine mesaj basar: Kayıt {basvuru.Id}: ucret={ucret} (süslü parantez içi ifadedir). VS Code'da logpoint, VS'te tracepoint adıyla geçer. Bu, Console.WriteLine serpiştirmenin medeni hâlidir: koda dokunmaz, derleme istemez, commit'e sızma riski yoktur — zamanlamaya duyarlı hatalarda (durdurmak davranışı değiştirdiğinde) ilk tercih.
  • Yalnız şu thread'de dur (filter): paralel koşuda tek isteğin izini sürerken diğer thread'lerin aynı satıra takılmasını engeller.

05Durumu okumak: watch, immediate, çağrı yığını

Duraklamışken üç pencere iş görür. Watch: izlemek istediğin ifadeleri yazarsın (basvuru.Durum, liste.Count), her adımda güncellenir; tek seferlik bakış için fareyle üzerine gelmek (data tip) yeter. Immediate / Debug Console: duraklamış bağlamda ifade çalıştırırsın — hesap.Hesapla(yas: 35) diye çağırıp hipotezini oracıkta test edebilirsin. Güçlü ve tehlikeli: yan etkili bir ifade (liste.Clear(), bir property'nin tembel yüklemeyi tetiklemesi) sürecin durumunu değiştirir — gözlemlediğin şey artık gözlemlediğin şey değildir. Sorgularını okuma amaçlı tut.

Çağrı yığını (call stack) ise en az kullanılan en değerli penceredir. Okuma yönü: en üst çerçeve "şu an neredeyim", altındakiler "buraya kim, hangi sırayla getirdi". Kritik beceri çerçeve değiştirmektir: yığında bir alt çerçeveye çift tıkla — o metodun yerel değişkenlerini o çağrı anındaki hâliyle görürsün; hatanın "nerede patladığı" ile "nerede doğduğu" çoğu kez farklı çerçevelerdir, patlayan çerçevede bozuk gelen değerin kaynağını yığını yukarı yürüyerek bulursun. İki ayar bu pencereyi ehlileştirir: Just My Code framework çerçevelerini gizler (kapatman gereken gün de gelir: hata framework ile senin kodunun sınırındaysa); async çağrı yığını desteği, await zincirini fiziksel thread yığını yerine mantıksal akış olarak gösterir — Hat 3'te öğrendiğin "await sonrası devam eden kod" burada görünür olur.

06İstisna ayarları: ilk fırlatmada dur

Varsayılan davranış, yalnız yakalanmamış istisnada durmaktır. Ama asıl sinsi hatalar yakalanan istisnalarda saklanır: bir yerlerde catch (Exception) { } yutuyordur (Hat 3'te yasakladığımız desen) ve sen yalnız "değer neden boş" belirtisini görürsün. Çözüm, istisna ayarlarından ilgili tipin fırlatıldığı anda (first chance) durmayı açmaktır: istisna daha catch'e ulaşmadan, doğduğu satırda, tam bağlamıyla yakalarsın. Tüm istisnalar için açmak gürültülüdür (framework'ler istisnayı akış kontrolü gibi kullanabilir); şüphelendiğin tip için aç, işin bitince kapat. Debug Console'daki $exception sözde değişkeni, o an işlenmekte olan istisnayı gösterir.

Bu bölümün testle bağı: istisna testleri (3. ders Assert.Throws) doğru istisna tipinin doğru yerde fırladığını mühürler; hata ayıklarken bulduğun her yutulmuş istisna, bir refactor adayıdır — ya yutma kaldırılır ya bilinçli olduğu loglamayla belgelenir (14-10).

07Canlı düzenleme: Hot Reload'un sınırları

Edit and Continue / Hot Reload, duraklamışken (ya da .NET'te çalışırken) kodu değiştirip süreci yeniden başlatmadan devam etmektir — deneme döngüsünü ciddi kısaltır. Sınırlarını bilerek kullan: metot gövdesi değişiklikleri genelde çalışır; imza/tip/generic değişiklikleri, yeni alanlar ve bazı lambda değişiklikleri çalışmaz ya da "uygulanamadı" der. Asıl tuzak sessiz olanıdır: değişikliğin uygulandığını sandığın ama uygulanmadığı an — dakikalarca "neden hâlâ eski davranış" kovalarsın. Şüphede kural basittir: yeniden başlat, temiz durumla doğrula. Hot Reload bir konfor aracıdır, kanıt aracı değil; kanıt, temiz koşudaki testtir.

08Uzakta ve konteynerde

Hata her zaman senin makinende değildir. Attach to process, hata ayıklayıcıyı çalışan bir sürece sonradan bağlamaktır: yerelde başka bir sürece, SSH üzerinden uzak sunucudaki sürece, ya da konteynerin içindeki uygulamaya. .NET tarafında uzak uçta küçük bir hata ayıklama aracısı (vsdbg) çalışır; VS/VS Code konteyner araçları bunu Docker için otomatikleştirir (17. hatta konteyner tarafını kuracaksın). İki pratik şart: uzaktaki derlemenin sembol dosyaları (PDB) ve kaynak sürümü seninkiyle eşleşmeli — Release derlemesinde optimizasyon, değişkenleri "optimized away" gösterebilir; ve izin: üretim sürecine bağlanmak, 14. hattın diliyle, yetkilendirme gerektiren bir operasyondur.

Ve büyük uyarı: attach, breakpoint'te süreci durdurur — üretimde bu, canlı trafiği durdurmaktır. Vatandaşın başvuru isteği, senin F10'una basmanı beklemez; zaman aşımına düşer, kuyruklar dolar. Üretimde canlı hata ayıklama en son çaredir ve durdurmayan teknikler tercih edilir: logpoint benzeri anlık görüntü (snapshot) araçları — ya da doğrusu, bir sonraki bölüm.

09Debugger'sız teşhis: log, sayaç, döküm

Üretimdeki hatanın standart teşhis zinciri hata ayıklayıcıdan geçmez:

  • Log korelasyonu (ilk durak): 14-10'da kurduğun yapı burada hasat verir — yapılandırılmış log + korelasyon kimliğiyle, tek bir isteğin tüm katmanlardaki izini süzersin; UTC/NTP disiplini sayesinde sıralama güvenilirdir. Hata ayıklamanın 02. bölümdeki döngüsü aynen işler; gözlem aracı breakpoint değil, sorgulanabilir logdur. Loglayamadığın şeyi teşhis edemezsin — bu yüzden her "bu hatayı loglardan bulamadım" seansı, bir log iyileştirme kaydıyla kapanır.
  • dotnet-counters: canlı sürecin nabzı — CPU, bellek, GC, thread havuzu, istek hızı: dotnet-counters monitor -p <pid>. "Yavaşladı" şikâyetinin ilk sorusu olan "neresi şişiyor"a saniyeler içinde cevap verir.
  • dotnet-trace: süreci durdurmadan CPU örnekleme izi toplar (dotnet-trace collect -p <pid>); çıktı, hangi metotların zamanı yediğini gösterir — performans hatalarının aracı.
  • dotnet-dump: sürecin bellek fotoğrafı (collect), sonra ölü inceleme (analyze): clrstack tüm thread'lerin yığınını, dumpheap -stat bellekteki nesne dağılımını döker. Kilitlenme (deadlock) ve bellek sızıntısının standart teşhis yoludur; çökme anında otomatik döküm alınacak şekilde ortam da yapılandırılabilir. Derinliğine inmek ayrı bir uzmanlıktır — senin seviyende beklenen, hangi aracın hangi soruya cevap verdiğini bilmek ve dökümü alabilmektir.

Toparlayan cümle: geliştirmede breakpoint, üretimde kanıt. İyi mühendis ikisini karıştırmaz; üretim hatasını log-sayaç-dökümle köşeye sıkıştırır, yerelde testle yeniden üretir, debugger ile doğrular, regresyon testiyle mühürler.

10Mini alıştırma — bir hatayı yöntemle avla

Yaklaşık 20 dakika · IDE + 3-6. derslerdeki çözümün

  1. Hata ek. Ücret hesaplayıcına bilerek sinsi bir hata koy: döngüde 42. kayıtta yanlış dal seçilsin (ör. i == 42 özel durumu) ve bir yardımcı metotta istisna yutulsun: catch { return 0; }.
  2. Koşullu av. Bin kayıtlık listeyle koştur; düz breakpoint yerine koşullu breakpoint (i == 42) ve bir logpoint (i={i}, ucret={ucret}) ile hatalı kaydı tek duruşta yakala. F5'e kaç kez bastın?
  3. Yutulanı bul. "Sonuç neden 0?" sorusundan geriye git: istisna ayarlarından ilgili istisna tipini ilk fırlatmada durmaya aç; istisnanın doğduğu satırı ve mesajını gör. Çağrı yığınında bir çerçeve yukarı çıkıp bozuk argümanın kaynağını bul.
  4. Immediate deneyi. Duraklamışken Debug Console'da metodu farklı argümanla çağırarak hipotezini sına. Sonra yan etkili bir ifade dene (listeye eleman ekle) ve devam ettiğinde davranışın değiştiğini gözle — tehlikeyi bizzat gör.
  5. Mühürle. Bulduğun iki hata için önce regresyon testi yaz (kırmızı), sonra düzelt (yeşil). 02. bölümün altı adımını yaptığın sırayla, birer cümleyle rapor et — sözlü sınav cevabının hammaddesi bu rapordur.

11Kendini sına

Sistematik hata ayıklama döngüsünün adımları nelerdir? "Yeniden üretme" ve "tek değişkenli deney" neden pazarlıksızdır?

Döngü: (1) Yeniden üret — hatayı istikrarlı tetikleyen en küçük adım dizisini bul; (2) gözlemle — hata mesajını gerçekten oku: tip, satır, iç istisna, yığındaki ilk kendi-kodun çerçevesi; (3) hipotez — test edilebilir tek cümle; (4) tek değişkenli deney — hipotezi sınayan en küçük değişiklik; (5) ikiye böl — kodu, veriyi ya da zamanı yarılayarak arama uzayını logaritmik daralt (zaman ekseninin otomasyonu git bisect'tir); (6) kanıtla ve mühürle — önce hatayı üreten regresyon testini yaz, kırmızıyı gör, düzelt, yeşili gör. Yeniden üretme pazarlıksızdır çünkü üretemediğin hatayı düzelttiğini kanıtlayamazsın — "bir ara düzeldi" bir kapanış değil, ertelemedir; "bende çalışıyor" da iki ortam arasındaki farkın adını bulamamış olmaktır. Tek değişkenli deney pazarlıksızdır çünkü iki şeyi birden değiştirdiğinde sonucun hangisinden geldiğini bilemezsin — gözlemin bilgi değeri sıfıra iner ve rastgele kurcalamaya dönersin; kurcalamayla kaybolan hata, kök nedeni yaşayan kapatılmış bir belirtidir. Yardımcı taktikler: "en son ne değişti" sorusu (commit geçmişi ilk şüpheli listesi) ve sorunu baştan sesli anlatmak — varsayımları tek tek söyletir, çoğu seans "burada elbette X oluyor… oluyor mu gerçekten?" cümlesinde çözülür.

Koşullu breakpoint, isabet sayacı ve logpoint ne zaman kullanılır? Logpoint'in Console.WriteLine serpiştirmeye üstünlüğü nedir?

Koşullu breakpoint, duraklamayı bir ifadeye bağlar (basvuru.Yas > 35): bin elemanlı döngüde yalnız ilgilendiğin durumda durursun; bedeli, koşulun her geçişte değerlendirilmesi — sıcak döngüde koşuyu belirgin yavaşlatır. İsabet sayacı "bu satırdan N. geçişte dur" der — kaçıncı yinelemede bozulduğunu bildiğin durumların aracı. Logpoint (VS'te tracepoint) ise hiç durmaz; geçişte süslü parantezle ifade gömülü bir mesaj basar (i={i}, ucret={ucret}). Console.WriteLine serpiştirmeye dört üstünlüğü vardır: koda dokunmaz (derleme yok, değişiklik yok), commit'e sızma riski yoktur (geçici izleme satırlarının depoya girmesi klasik kirliliktir), çalışan oturumda anında eklenip kaldırılır ve durdurmadığı için zamanlamaya duyarlı hatalarda (yarış durumları — durdurmak davranışı değiştirir) kullanılabilir. F5 ↔ Ctrl+F5 farkı da buraya bağlanır: debugger bağlıyken zamanlama ve JIT davranışı değişir; "debugger'da olmuyor" gerçek bir vakadır ve o durumda gözlem aracı olarak durduran breakpoint yerine logpoint ya da yapılandırılmış log seçilir.

Çağrı yığını nasıl okunur ve çerçeve değiştirmek neden kritik beceridir? İstisna ayarlarında "ilk fırlatmada dur" hangi hata sınıfını çözer?

Çağrı yığını, duraklama anının soy ağacıdır: en üst çerçeve "şu an neredeyim", altındakiler "buraya hangi çağrı zinciri getirdi". Kritik beceri çerçeve değiştirmektir: alttaki bir çerçeveye geçince o metodun yerel değişkenlerini o çağrı anındaki değerleriyle görürsün. Önemi şudur: hatanın patladığı yer ile doğduğu yer çoğu kez farklıdır — bozuk değer üç kat yukarıda üretilmiş, en altta istisnaya dönüşmüştür; patlayan çerçevede "bu argüman neden null" diye sorup yığını yukarı yürüyerek kaynağı bulursun. Just My Code framework çerçevelerini gizleyerek okumayı kolaylaştırır (sınır vakalarında kapatılır); async yığın desteği await zincirini fiziksel thread yerine mantıksal akış olarak gösterir. "İlk fırlatmada dur" (first chance) ise yutulmuş istisna sınıfını çözer: bir yerde catch { } istisnayı yutuyorsa sen yalnız belirtiyi (boş değer, sessiz başarısızlık) görürsün; ilgili istisna tipi için ilk fırlatmada durmayı açınca istisna catch'e ulaşmadan, doğduğu satırda, tam bağlamıyla yakalanır. Tümü için açmak gürültülüdür (bazı kütüphaneler istisnayı akışta kullanır); şüpheli tip için aç, bitince kapat. Bulunan her yutulmuş istisna bir refactor adayıdır: ya yutma kalkar ya loglamayla bilinçli hâle gelir.

Üretimdeki bir hata neden hata ayıklayıcıyla incelenmez? Debugger'sız teşhis zincirini (log, counters, trace, dump) hangi soruya hangi araç düşecek şekilde kur.

İki neden: attach edilen debugger breakpoint'te süreci durdurur — üretimde bu canlı trafiği durdurmaktır: istekler zaman aşımına düşer, kuyruklar dolar; ve üretim sürecine bağlanmak yetki/denetim gerektiren bir operasyondur. Ayrıca Release optimizasyonu ve sembol eşleşmesi sorunları gözlemi güvenilmez kılar. Teşhis zinciri soruya göre kurulur: "Bu istekte ne oldu?" → log korelasyonu: yapılandırılmış log + korelasyon kimliğiyle isteğin tüm katmanlardaki izi süzülür (UTC/NTP sayesinde sıralama güvenilirdir); loglardan bulunamayan her hata, bir log iyileştirme kaydıyla kapanır. "Neresi şişiyor / yavaşladı mı?" → dotnet-counters: canlı CPU, bellek, GC, thread havuzu, istek hızı. "Zamanı hangi kod yiyor?" → dotnet-trace: durdurmadan CPU örnekleme izi. "Neden kilitlendi / bellek neden büyüyor?" → dotnet-dump: bellek fotoğrafı alınır, analyze içinde clrstack tüm thread yığınlarını, dumpheap -stat nesne dağılımını gösterir; çökme anında otomatik döküm de yapılandırılabilir. Beklenen seviye, döküm analizinde uzmanlık değil, hangi aracın hangi soruya cevap verdiğini bilmek ve kanıtı toplayabilmektir. Özet ilke: geliştirmede breakpoint, üretimde kanıt — üretim hatası logla köşeye sıkıştırılır, yerelde testle yeniden üretilir, debugger ile doğrulanır, regresyon testiyle mühürlenir.

12Sınav köşesi

Yazılıda nasıl sorulur

(1) Hata ayıklama (debugging) nedir? (2) Breakpoint nedir, türleri nelerdir? (3) Koşullu breakpoint hangi durumda kullanılır? (4) Step over, step into ve step out arasındaki fark nedir? (5) Watch penceresi ne işe yarar? (6) Çağrı yığını (call stack) nedir, nasıl okunur? (7) İstisna ayarlarında "fırlatıldığında dur" seçeneği ne sağlar? (8) Uzaktan hata ayıklama nedir, hangi durumlarda gerekir? (9) Üretim ortamındaki hatalar nasıl incelenir? (10) Sistematik hata ayıklama süreci nasıl işler?

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Yöntem — "Hata ayıklamayı deney gibi yürütürüm: önce hatayı en küçük adımlarla yeniden üretirim — üretemediğim hatayı düzelttiğimi kanıtlayamam; sonra hipotez kurar, tek değişkenli deneyle sınar, kod/veri/zaman eksenlerinde ikiye bölerek daraltırım." (2) Araç — "Döngüde koşullu breakpoint ve isabet sayacı kullanırım; durdurmak davranışı değiştiriyorsa logpoint ile koda dokunmadan iz basarım. Yutulmuş istisnayı, istisna ayarlarından ilk fırlatmada durarak doğduğu satırda yakalarım." (3) Okuma — "Çağrı yığınında çerçeve değiştirerek hatanın patladığı yerden doğduğu yere yürürüm; async yığınları mantıksal akış olarak okurum." (4) Üretim — "Üretimde debugger'a bağlanmam — attach canlı trafiği durdurur; kanıtım korelasyon kimlikli log, dotnet-counters ile nabız, gerekirse dotnet-trace ve dump analizidir." (5) Kıdem vuruşu — "Her seansı iki çıktıya bağlarım: bulduğum hatayı önce kırmızı bir regresyon testiyle mühürler sonra düzeltirim; loglardan bulamadığım her hata için de bir log iyileştirmesi açarım — aynı hatayı ikinci kez körlemesine aramamak için."