Bir mesaj bir PHP servisi tarafından üretiliyor, bir kuyruğa düşüyor, bir Go worker’ı onu tüketiyor, o da bir devam mesajı yayınlıyor ve onu bir Python servisi işliyor. Dört hop, üç dil, aralarda bir ya da daha fazla broker. O akış yavaşladığında ya da on binde bir mesaj öldüğünde soru hep aynı: bu mesaj gerçekte nereye gitti ve her adımda ne oldu?
Çoğu polyglot kuyruk sistemi için dürüst cevap: kimse bilmiyor. Log’larda bir korelasyon kimliğin var — üç dilde de loglamayı hatırladıysan — ve farklı host’lardaki üç log akışında grep çekecek sabrın var. Sahip olmadığın şey bir resim. “PHP’de 2ms’de üretildi → Redis’te 40ms bekledi → Go’da 210ms’de işlendi, iki kez retry oldu, DLQ’landı”yı tek bir bağlı şey olarak göremiyorsun.
İşte o resim bir dağıtık trace’tir ve onu çizmenin standart yolu OpenTelemetry’dir. Bu yazı, onu tel formatı donmuş ve çekirdekleri sıfır bağımlılık taşıyan bir mesaj standardına eklemekle ilgili — bu iki kısıt, bir arada, en bariz yaklaşımı yasadışı kılıp daha ilginç olanı zorunlu hâle getiriyor.
Tek kuralı baştan söyleyeyim
Her şeyden önce şunu netleştireyim: envelope’a bir alan ekleme.
Bir mesaj veriyolu boyunca dağıtık izleme istediğinde ilk içgüdü, bir W3C traceparent taşımaktır — bir trace id’yi, bir span id’yi ve flag’leri kodlayan o standart 55 karakterlik string. HTTP tam olarak bunu bir header’da yapar. Bu içgüdü HTTP için doğru, burada yanlış; çünkü envelope donmuş bir kontrattır. Her dildeki her SDK, byte-bazında aynı şekli yayınlar: job, trace_id, data, meta, attempts. Bir traceparent alanı eklemek — opsiyonel olsa bile — o şekli değiştirir, bu da bir versiyon artışı, bu da altı dil implementasyonu ve her broker bağlaması boyunca koordine edilen bir tel değişikliği demektir. Opsiyonel gözlemlenebilirlik olması gereken bir özellik için bu saçma bir bedel.
Yani kural, kısıtın kendisi: izlemeyi tele dokunmadan çöz. Aşağıdaki her şey bunu ciddiye almaktan çıkıyor.
Kimsenin kullanmadığı içgörü
Envelope’un sana bedavaya verdiği şey şu. trace_id alanı bir korelasyon kimliği — üretim anında basılan ve her hop boyunca değişmeden taşınan bir UUID. Zaten tel üzerinde, zaten taşınıyor, zaten tüm akışı birbirine bağlayan o tek değer.
Şimdi OpenTelemetry’nin bir trace’i birbirine bağlamak için kullandığı şeye bak: bir TraceID. Spesifikasyonda bir TraceID tam olarak 16 byte’tır.
Bir UUID tam olarak 16 byte’tır.
Bütün hüner bu. UUID bir trace_id, bir OTel TraceID’ye birebir eşlenir — tireleri at, 32 hex karakteri 16 byte’lık kimlik olarak oku, bitti. (UUID olmayan bir trace_id — diyelim babelqueue dışı bir üreticiden geldi — SHA-256 ile deterministik olarak 16 byte’a hash’lenir.) Dolayısıyla aynı trace_id’yi paylaşan her hop, aynı OTel TraceID’yi türetir — bir anlaşma protokolü ve yeni bir alan olmadan. Zaten taşıdığın korelasyon kimliği dağıtık trace’in ta kendisidir.
Tasarım da kendini yazıyor:
- Tüketici tarafında handler’ı sar. Çalışmadan önce
process <urn>adında bir span başlat, ama o span’i mesajıntrace_id’sinden türetilen trace’in içine zorla. Onu messaging convention’larıyla etiketle —messaging.system,messaging.destination.name,messaging.message.id, vetrace_id’nin kendisine eşitlenenmessaging.message.conversation_id— handler’ı çalıştır, ve herhangi bir exception’ı span’in hata durumu olarak kaydet. Runtime’ın retry / dead-letter davranışı el değmeden kalır; span sadece onu gözlemler. - Üretici tarafında aynası. Bir
publish <urn>span’i aç, onun trace id’sini al, tekrar bir UUID’ye biçimlendir ve envelope’u kurarken mesajıntrace_id’sine bas. Aşağı akıştaki tüketici, TraceID’sini aynıtrace_id’den türeterek aynı trace’e iner.
Jaeger, Tempo, Honeycomb ya da Datadog için bir TracerProvider bağla; akış üç dil boyunca tek bir şelale olarak belirir, her hop’un zamanlamasıyla ve retry/hata işaretleriyle. Bağlama, hiçbir şey değişmez — tamamen opt-in.
Mekanizma ve bir hayalet
Net olmaya değer bir ayrıntı var, çünkü tasarımın hem zekice hem de sınırlı olduğu yer orası.
OpenTelemetry’de bir span’i belirli bir trace’in içinde başlatmak için ona bir uzak ebeveyn (remote parent) verirsin — istediğin trace id’yi taşıyan bir span context. Ama bir span context yalnızca hem bir trace id hem de bir span id’si varsa geçerlidir. Trace id’miz var (trace_id’den); span id’miz yok, çünkü bilinçle bir tane taşımadık. Bu yüzden trace_id’yi hash’leyerek deterministik, sıfır-olmayan bir span id sentezliyoruz. Ebeveyn geçerli, tüketici span’i doğru trace’e iniyor — ama ebeveyni hiç var olmamış bir span’i gösteriyor. Bir hayalet.
Dürüst sınır bu ve açıkça söylemeye değer. Hop’lar-arası span’ler hepsi tek bir trace’i paylaşır — bir mesajın hayatının her adımını bir arada, zamanlanmış, hatalar işaretlenmiş görebilirsin. Elde edemediğin şey, hop’lar arasında birebir ebeveyn-çocuk bağı: tüketicinin span’i üreticinin span’inin çocuğu olarak bağlanmaz, çünkü üreticinin gerçek span id’si tel boyunca hiç taşınmadı. Tek bir process içinde hiyerarşi doğrudur; kuyruğun ötesinde tek trace altında düzdür.
Gerçek hop’lar-arası ebeveyn-çocuğu geri almak, bir span id taşımak demek, o da bir traceparent demek — ve envelope donmuş olduğundan, o traceparent bant dışında, broker’ların zaten taşıdığı bq-trace-id’nin yanında bir transport header olarak gitmek zorunda. Bu gerçek bir özellik, ama farklı ölçekte bir iş: her SDK’daki her transport bağlamasına dokunur. Yani bilinçli bir ikinci faz. Yukarıdaki birinci-faz tasarımı korelasyonu, hop başına zamanlamayı ve hata/retry görünürlüğünü — yani %90’ı — sıfır tel değişikliğiyle ve dil başına birkaç yüz satırla teslim eder. %90’ı şimdi, son %10’u daha büyük bir işin ardına bırakarak göndermek doğru sıradır — tıpkı pahalı ölçekleme adımlarından önce ucuzları yaptığın gibi.
Tek semantik, altı paketleme deyimi
Çekirdeğin bağımlılıksız kalması kısıtının bir sonucu var: OpenTelemetry kodu çekirdekte yaşayamaz, çünkü OTel API’sini import etmesi gerekir. Bu yüzden her dilde çekirdeğin yanında yaşar, yalnızca opt-in ettiğinde erişilir — ve “çekirdeğin yanında, opsiyonel” her ekosistemde farklı yazılır. Altısında da semantik aynı; paketleme her dilin karakterini gösterdiği yer:
- Go — ayrı bir modül (
babelqueue-go/otel, kendigo.mod’u), tıpkı transport alt-modülleri gibi. Çekirdek modül OTel bağımlılığını hiç görmez. - Python — bir
[otel]extra’sı.pip install babelqueue[otel],opentelemetry-api’yi getirir; modül onu import eder, yani yalnızca sen istediğinde import edilebilir. Buradaki bir TraceID byte değil, 128-bit birint’tir — aynı değer, farklı biçim. - Node — bir subpath export,
@babelqueue/core/otel,@opentelemetry/apiile opsiyonel bir peer dependency olarak. Kritik nokta: izleme kodu paket kökünden yeniden export edilmez: edilseydi,@babelqueue/core’u import etmek OTel import’unu hevesle yükler ve peer’i kurmayan herkes için kırılırdı. Subpath, ana girişi gerçekten bağımlılıksız tutar. - Java —
opentelemetry-apiüzerindeoptionalbir Maven bağımlılığı. Optional bağımlılıklar transitive değildir, yani izleme sınıflarına hiç dokunmayan bir tüketici OTel’i classpath’ine hiç çekmez. - .NET — ilginç olanı. .NET’te idiomatik izleme primitifi
System.Diagnostics.ActivitySource’tur, base class library içinde yaşar — ve OpenTelemetry .NET’in üzerine kurulduğu tam olarak odur. Yani .NET modülünün hiçbir bağımlılığı yok:Activitynesneleri yayınlar, tüketicinin OTel boru hattı onlarıAddSource("BabelQueue")çağırarak toplar. Çekirdek bağımlılığı izole ederek değil, bağımlılığa sahip olmayarak sıfır-dep kalır. - PHP — bir Composer
suggestartı bir dev gereksinimi, mevcut opsiyonel yardımcıları aynalayarak. İzleme namespace’i orada;open-telemetry/apiyalnızca kullanırsan gerekir.
Altı deyim, hepsinde tutulan tek kural: gözlemlenebilirliği kullanmak tüketicinin yaptığı bir seçimdir, asla çekirdeğin kestiği bir vergi değil.
Gerçekte ne elde ediyorsun
Mekanizmayı bir kenara bırak, kazanç tarif etmesi küçük, sahip olması büyük. Mevcut izleme backend’inde, eskiden üç log samanlığında bir iğne olan bir mesaj tek bir şelaleye dönüşür: onu hangi servis üretti, broker’da ne kadar bekledi, her tüketici ne kadar sürdü, retry oldu mu, DLQ’landı mı — zaten ortalıkta dolaştırdıkları bir UUID dışında hiçbir konuda anlaşmasını gerektirmediğin diller ve broker’lar boyunca.
Ve bunun tel üzerindeki maliyeti sıfır. İzleme var olmadan önce giden envelope ile onunla birlikte giden envelope byte-byte aynı. Trace baştan beri trace_id’nin içinde saklıydı; bütün iş, bir UUID ile bir TraceID’nin aynı on altı byte olduğunu fark etmekteydi.
İlgili: BabelQueue observability spec’i bu tasarımı bir standart olarak yazıya döker; onu uygulayan SDK’lar — PHP, Go, Python, Node, Java ve .NET boyunca — BabelQueue ekosisteminde yaşar.
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.