İçeriğe geç
academia.sh

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 bilgi iken on, ayrintili iken 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat