Ders 10 / 16
Günlükleme Kurulumu
Kütük satırları JSON nesnesi olarak üretilir; istek bağlamı eşzamansız yerel depoyla taşınır, derin katmanlar istek kimliğini parametre almadan kaydeder, düzey eşiği satır sayısını belirler ve tek bir isteğin bütün satırları sorguyla toplanır.
İçindekiler
Önceki dersin kütük satırları JSON biçimindeydi ve her satır bir istek kimliği taşıyordu, ama o kimlik yalnız iki yerde elle yazıldı. Gerçek bir uygulamada aynı istek zincirin halkalarından, işleyiciden, alan katmanından ve veri katmanından geçer; her katman kendi kaydını üretir. Kimliğin her satırda bulunması için her fonksiyonun onu parametre olarak alması gerekir mi?
Bu ders üç şeyi kurar: kütük satırının biçimi, istek bağlamının katmanlar arasında taşınması ve satırların sonradan sorgulanması. Ölçüm, eşzamanlı üç isteğin kayıtlarının birbirine karışmadığını gösterecek.
Satır Neden Nesnedir
Serbest metin bir kütük satırı insan tarafından okunur, makine tarafından okunmaz. “Ödünç isteği reddedildi, ISBN 978-0262033848” satırından ISBN’i çıkarmak için bir kalıp yazmak gerekir ve kalıp, ileti her değiştiğinde bozulur.
Yapılandırılmış kayıt (structured log record) bu sorunu adlandırılmış alanlarla çözer. Her satır bağımsız bir JSON nesnesidir; satırlar arasında ayraç yoktur, dosya satır satır işlenir. Bir kaydın taşıması gereken alanlar dört öbekte toplanır:
- Ne zaman: ISO 8601 biçiminde bir zaman damgası. Sabit uzunlukta, sıralanabilir ve saat dilimi taşır.
- Ne kadar önemli: düzey. Kaydın eşiğe göre yazılıp yazılmayacağını belirler.
- Ne oldu: olay adı. Serbest cümle değil, sabit bir tanımlayıcı — çünkü sorgulanacaktır.
- Hangi bağlamda: istek kimliği ve olaya özgü alanlar.
Bağlamı Taşımak
İstek kimliğini her fonksiyona parametre olarak vermek işe yarar ama bedeli yüksektir: kaydın kendisiyle ilgisi olmayan fonksiyonların imzaları değişir ve zincirin ortasındaki bir fonksiyon parametreyi iletmeyi unuttuğunda bağ kopar.
Çalışma zamanı bunun için eşzamansız yerel depo (async local storage) sağlar. Bir değer
belirli bir çağrı ağacına bağlanır; o ağacın içinden yapılan her okuma — kaç await sonrası
olursa olsun — aynı değeri görür. Eşzamanlı isteklerin ağaçları ayrı olduğu için değerler
karışmaz.
// kaynak/kurulum/kutuk.mjs — JSON satirlari uretir; istek baglamini es zamansiz depodan okur import { AsyncLocalStorage } from "node:async_hooks"; const DUZEYLER = { hata: 0, uyari: 1, bilgi: 2, ayrintili: 3 }; export const istekDeposu = new AsyncLocalStorage(); export const kutukKur = (esikAdi) => { const esik = DUZEYLER[esikAdi] ?? DUZEYLER.bilgi; const yaz = (duzey, olay, alanlar = {}) => { if (DUZEYLER[duzey] > esik) return; // duzey esigi: altindakiler yazilmaz const baglam = istekDeposu.getStore() ?? {}; // istek kimligi elden gecirilmez console.log(JSON.stringify({ an: new Date().toISOString(), duzey, olay, ...baglam, ...alanlar, })); }; return Object.fromEntries( Object.keys(DUZEYLER).map((d) => [d, (olay, alanlar) => yaz(d, olay, alanlar)])); };
Düzeyler sayısal karşılıklarıyla tanımlanmıştır; karşılaştırma bu sayılar üzerinden yapılır.
Eşik bilgi olduğunda ayrintili kayıtlar hiç yazılmaz — dosyaya yazılıp sonra süzülmezler,
üretilmezler bile. Bu ayrım maliyettedir: eşiğin altındaki bir kayıt için dizileştirme
yapılmaz.
Sunucu her isteği kendi bağlamında çalıştırır. Veri katmanındaki rafKontrol fonksiyonu
istek kimliğini almaz, bilmez ve iletmez; buna rağmen ürettiği satırlar kimliği taşır.
// kaynak/http/sunucu.mjs — her istek kendi baglaminda calisir; derin katmanlar kimligi tasimaz import { createServer } from "node:http"; import { setTimeout as bekle } from "node:timers/promises"; import { kutukKur, istekDeposu } from "../kurulum/kutuk.mjs"; const kutuk = kutukKur(process.env.KUTUPHANE_KUTUK_DUZEYI ?? "bilgi"); const RAFTAKI = new Set(["978-0262033848"]); let sayac = 0; // Veri katmanindaki derin fonksiyon: istek kimligini parametre olarak ALMAZ. const rafKontrol = async (isbn) => { kutuk.ayrintili("raf_sorgusu", { isbn }); await bekle(150); // depo erisimini taklit eden gecikme const bulundu = RAFTAKI.has(isbn); kutuk.bilgi("raf_sonucu", { isbn, bulundu }); return bulundu; }; createServer((istek, yanit) => { yanit.sendDate = false; const baglam = { istekKimligi: `i-${++sayac}` }; istekDeposu.run(baglam, async () => { // baglam bu istegin butun zincirinde const baslangic = process.hrtime.bigint(); const adres = new URL(istek.url, "http://yerel"); kutuk.bilgi("istek_alindi", { yontem: istek.method, yol: adres.pathname }); let durum = 404, govde = { hata: "yol_yok" }; if (adres.pathname === "/odunc") { const bulundu = await rafKontrol(adres.searchParams.get("isbn") ?? ""); durum = bulundu ? 201 : 409; govde = bulundu ? { odunc: "verildi" } : { hata: "raf_bos" }; if (!bulundu) kutuk.uyari("odunc_reddedildi", { neden: "raf_bos" }); } else if (adres.pathname === "/kitaplar") { durum = 200; govde = { kitapSayisi: RAFTAKI.size }; } const metin = JSON.stringify({ ...govde, istekKimligi: baglam.istekKimligi }); yanit.writeHead(durum, { "content-type": "application/json; charset=utf-8", "content-length": Buffer.byteLength(metin), "x-istek-kimligi": baglam.istekKimligi }); yanit.end(metin); const sure = Number(process.hrtime.bigint() - baslangic) / 1e6; kutuk.bilgi("istek_bitti", { durum, sureMs: Number(sure.toFixed(1)) }); }); }).listen(8438, "127.0.0.1", () => kutuk.bilgi("sunucu_acildi", { baglantiNoktasi: 8438 }));
İstek kimliği yanıtta da bulunur: hem x-istek-kimligi başlığında hem gövdede. Bir üye
destek istediğinde verdiği numara doğrudan kütüğe götürür.
Ölçüm
Ölçüm sunucuyu iki farklı eşikle çalıştırır. Her turda üç istek eşzamanlıya yakın gönderilir:
rafta bulunan bir kitap için ödünç, hızlı bir katalog isteği ve rafta bulunmayan bir kitap için
ödünç. Süzme işi jq ile yapılır; bu, JSON akışı üzerinde çalışan bir süzgeçtir.
#!/usr/bin/env bash # Sunucuyu iki duzey esigiyle acar; es zamanli uc istegin kutuk satirlarini toplar ve sorgular. tur() { # $1 = kutuk duzeyi, $2 = cikti dosyasi KUTUPHANE_KUTUK_DUZEYI="$1" node kaynak/http/sunucu.mjs > "$2" 2>&1 & s=$! sleep 0.7 curl -sS -o /dev/null 'http://127.0.0.1:8438/odunc?isbn=978-0262033848' & p1=$! sleep 0.05 curl -sS -o /dev/null 'http://127.0.0.1:8438/kitaplar' & p2=$! sleep 0.05 curl -sS -o /dev/null 'http://127.0.0.1:8438/odunc?isbn=978-0000000000' & p3=$! wait "$p1" "$p2" "$p3" sleep 0.2 kill "$s"; wait "$s" 2>/dev/null } tur bilgi kutuk-bilgi.jsonl tur ayrintili kutuk-ayrintili.jsonl echo "--- kutuk (esik=bilgi), zaman sirasinda ---" cat kutuk-bilgi.jsonl echo "--- tek bir istegin butun satirlari: i-1 ---" jq -c 'select(.istekKimligi == "i-1")' kutuk-bilgi.jsonl echo "--- duzeye gore satir sayisi ---" for dosya in kutuk-bilgi.jsonl kutuk-ayrintili.jsonl; do printf '%-24s %s\n' "$dosya" \ "$(jq -r .duzey "$dosya" | sort | uniq -c | tr '\n' ' ' | tr -s ' ')" done rm -f kutuk-bilgi.jsonl kutuk-ayrintili.jsonl
--- kutuk (esik=bilgi), zaman sirasinda ---
{"an":"2026-07-28T20:26:21.198Z","duzey":"bilgi","olay":"sunucu_acildi","baglantiNoktasi":8438}
{"an":"2026-07-28T20:26:21.897Z","duzey":"bilgi","olay":"istek_alindi","istekKimligi":"i-1","yontem":"GET","yol":"/odunc"}
{"an":"2026-07-28T20:26:21.954Z","duzey":"bilgi","olay":"istek_alindi","istekKimligi":"i-2","yontem":"GET","yol":"/kitaplar"}
{"an":"2026-07-28T20:26:21.955Z","duzey":"bilgi","olay":"istek_bitti","istekKimligi":"i-2","durum":200,"sureMs":1}
{"an":"2026-07-28T20:26:22.016Z","duzey":"bilgi","olay":"istek_alindi","istekKimligi":"i-3","yontem":"GET","yol":"/odunc"}
{"an":"2026-07-28T20:26:22.049Z","duzey":"bilgi","olay":"raf_sonucu","istekKimligi":"i-1","isbn":"978-0262033848","bulundu":true}
{"an":"2026-07-28T20:26:22.050Z","duzey":"bilgi","olay":"istek_bitti","istekKimligi":"i-1","durum":201,"sureMs":153.5}
{"an":"2026-07-28T20:26:22.166Z","duzey":"bilgi","olay":"raf_sonucu","istekKimligi":"i-3","isbn":"978-0000000000","bulundu":false}
{"an":"2026-07-28T20:26:22.167Z","duzey":"uyari","olay":"odunc_reddedildi","istekKimligi":"i-3","neden":"raf_bos"}
{"an":"2026-07-28T20:26:22.168Z","duzey":"bilgi","olay":"istek_bitti","istekKimligi":"i-3","durum":409,"sureMs":152.3}
--- tek bir istegin butun satirlari: i-1 ---
{"an":"2026-07-28T20:26:21.897Z","duzey":"bilgi","olay":"istek_alindi","istekKimligi":"i-1","yontem":"GET","yol":"/odunc"}
{"an":"2026-07-28T20:26:22.049Z","duzey":"bilgi","olay":"raf_sonucu","istekKimligi":"i-1","isbn":"978-0262033848","bulundu":true}
{"an":"2026-07-28T20:26:22.050Z","duzey":"bilgi","olay":"istek_bitti","istekKimligi":"i-1","durum":201,"sureMs":153.5}
--- duzeye gore satir sayisi ---
kutuk-bilgi.jsonl 9 bilgi 1 uyari
kutuk-ayrintili.jsonl 2 ayrintili 9 bilgi 1 uyari
Zaman damgaları ve süre alanları her çalıştırmada değişir; satırların birbirine göre sırası da zamanlamaya bağlı olarak kayabilir.
Ölçümün Söyledikleri
Satırlar iç içe geçmiştir. İlk isteğin istek_alindi satırı ile raf_sonucu satırı
arasında ikinci ve üçüncü isteğe ait dört satır vardır. Zaman sırasında okunan bir kütükte
tek bir isteğin akışı görünmez; bu, tek süreçte eşzamanlı çalışan bir uygulamanın kaçınılmaz
sonucudur.
Kimlikler karışmamıştır. rafKontrol fonksiyonu üç istekten ikisi tarafından çağrılmış,
her çağrı yüz elli milisaniye beklemiş ve bu süre boyunca başka isteklerin kodu çalışmıştır.
Buna rağmen raf_sonucu satırlarının biri i-1, diğeri i-3 kimliğini taşır. Fonksiyon bu
kimliği hiçbir parametreden almamıştır.
Sorgu akışı geri kurar. jq süzgeci tek bir kimliğe ait satırları seçtiğinde ortaya
isteğin kendi öyküsü çıkar: alındı, raf sorgulandı, bitti. Yapılandırılmış kaydın asıl kazancı
budur — kütük zaman sırasında yazılır, ihtiyaca göre yeniden düzenlenir.
Süre ölçümü kaydın parçasıdır. istek_bitti satırındaki sureMs alanı, ikinci isteğin
bir milisaniyede, ödünç isteklerinin yüz elli milisaniyenin üzerinde tamamlandığını gösterir.
Aynı alan bütün satırlarda aynı adı taşıdığı için yavaş isteklerin sorgulanması tek bir
süzgeç ifadesidir.
Eşik satır sayısını belirler. Eşik bilgi iken dosyada on satır vardır; ayrintili
olduğunda raf_sorgusu kayıtları eklenir ve sayı on ikiye çıkar. Eşiğin yapılandırmadan
gelmesi, aynı sürümü yeniden kurmadan ayrıntı düzeyini değiştirmeyi sağlar.
Kütük Nereye Yazılır
Sunucu kütüğü standart çıktıya yazar; dosya açmaz, döndürmez, sıkıştırmaz. Bu bir eksiklik değil, bilinçli bir sınırdır: uygulamanın işi kaydı üretmektir, saklamak değil.
Gerekçesi önceki bölümde ölçülmüştü. Uygulama birden çok süreç olarak çalışır ve süreçler ölümlüdür; her sürecin kendi dosyasına yazması, aynı isteğin izini birden çok dosyaya dağıtır ve ölen sürecin dosyası sahipsiz kalır. Standart çıktıya yazıldığında toplama işini süreci çalıştıran katman üstlenir: bütün süreçlerin çıktısı tek bir akışta birleşir.
Bu ayrımın adı ve gerekçesi bu bölümün son dersinde, taşınabilirlik ilkeleri arasında yeniden ele alınacak.
Özet
- Yapılandırılmış kayıt her satırı bağımsız bir JSON nesnesi olarak yazar; zaman damgası ISO 8601 biçimindedir, olay adı sabit bir tanımlayıcıdır ve bağlam adlandırılmış alanlarda taşınır.
- Düzey eşiği karşılaştırması sayısaldır ve eşiğin altındaki kayıt hiç üretilmez; ölçümde eşik
bilgiiken on,ayrintiliiken on iki satır yazılmıştır. - Eşzamansız yerel depo istek bağlamını çağrı ağacına bağlar; veri katmanındaki fonksiyon istek kimliğini parametre almadığı hâlde ürettiği satırlar doğru kimliği taşımıştır.
- Eşzamanlı isteklerin satırları zaman sırasında iç içe geçer; tek bir isteğin akışı ancak kimliğe göre süzülerek geri kurulur.
- Kütük standart çıktıya yazılır; toplama ve saklama işi süreci çalıştıran katmana bırakılır, çünkü süreçler çok sayıda ve ölümlüdür.
Sonraki Adım
Bu dersin sunucusunda hata durumu tek bir dala sıkışmıştı: rafta bulunmayan kitap 409
üretiyordu. Gerçek bir uygulamada hatalar iki ayrı kaynaktan gelir — kullanıcının yanlış
isteği ve uygulamanın kendi kusuru — ve ikisi aynı biçimde ele alınamaz. Sonraki ders ortak
hata yanıtı üretimini kurar: hangi hata hangi durum koduna eşlenir, yanıt gövdesi hangi
alanları taşır ve gövdenin iç ayrıntı sızdırmadığı hangi sınamayla doğrulanır?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.