रात के 3 बजे हैं। PagerDuty चीख रहा है। कुछ कस्टमर्स के लिए चेकआउट फेल हो रहा है पर कुछ के लिए नहीं। आप एक बॉक्स में SSH करते हैं, लॉग्स को tail करते हैं, और आपका स्वागत होता है INFO: processing request लाइनों की एक दीवार से — जिसमें न कोई ऑर्डर आईडी है, न यूज़र आईडी, न इस बात का कोई निशान कि कौन सी डाउनस्ट्रीम सर्विस अटक गई। अब आप डीबग नहीं कर रहे — आप पुरातत्व (archaeology) कर रहे हैं।
मैं उस रात के उस पार जितनी बार रहा हूँ, उतना मानने में मुझे थोड़ी हिचक होती है। और सच कहूँ तो, 5 मिनट के फिक्स और 3 घंटे के आउटेज के बीच का फर्क लगभग कभी भी खुद बग नहीं होता। फर्क इस बात का होता है कि आपके लॉग्स ने आपको कुछ उपयोगी बताया या नहीं। तो चलिए बात करते हैं कि अच्छी लॉगिंग असल में कैसी दिखती है — किताबी “सब कुछ लॉग करो” वाली सलाह नहीं, बल्कि वो चीज़ें जो तब आपको बचाती हैं जब प्रोडक्शन में आग लगी हो।
जब आपको लॉगिंग की सबसे ज़्यादा ज़रूरत होती है, तब ज़्यादातर लॉगिंग बेकार क्यों होती है
यह रही असहज सच्चाई: ज़्यादातर टीमें गलत मात्रा में लॉग नहीं करतीं, वे गलत चीज़ों को गलत तरीके से लॉग करती हैं। एक स्पष्ट रणनीति के बिना, लॉग्स शोरगुल वाले, बिना स्ट्रक्चर के, महँगे और किसी इंसिडेंट के दौरान लगभग बेकार बन जाते हैं [1]। आप user clicked button के गीगाबाइट्स स्टोर करने के लिए एक ख़ज़ाना खर्च कर देते हैं और फिर वो एक लाइन नहीं ढूँढ पाते जो बताती है कि पेमेंट क्यों फेल हुआ।
एक लॉग लाइन का अस्तित्व ठीक एक वजह से होता है: ताकि भविष्य का आप, इंसिडेंट के बीचों-बीच, बिना उसे दोबारा प्रोड्यूस किए यह पुनर्निर्माण कर सके कि क्या हुआ था। यही कसौटी है। अगर कोई लॉग आपको “सिस्टम क्या कर रहा था, किसके लिए, और क्या गलत हुआ” का जवाब देने में मदद नहीं करता, तो वह शोर है। लॉग्स को इतना संदर्भ देना चाहिए कि किसी इवेंट को बिना इश्यू दोबारा प्रोड्यूस किए समझा जा सके — टाइमस्टैम्प, स्पष्ट एरर मैसेज, और सिस्टम्स के आर-पार ट्रेस करने के लिए यूनीक आइडेंटिफ़ायर [1]।
उस एक वाक्य को अपने दिमाग में रखिए और नीचे के ज़्यादातर फैसले अपने आप साफ़ हो जाएँगे।
लॉग लेवल: इन्हें सजावट की तरह इस्तेमाल करना बंद करें
लगभग हर कोई लॉग लेवल को थोड़ा-बहुत गलत करता है। वे logger.info और logger.debug को बस अंदाज़े से इधर-उधर छिड़क देते हैं, और फिर प्रोडक्शन में या तो सब कुछ खामोश रहता है या फिर एक फायरहोस। लॉग लेवल एक सीवियरिटी कॉन्ट्रैक्ट हैं, न कि कोई स्टाइल चॉइस। कॉन्ट्रैक्ट सही करें और आप तुरंत शोर में से सिग्नल छाँट सकते हैं।
मानक पदानुक्रम, सबसे कम से सबसे ज़्यादा प्राथमिकता तक [4]:
| लेवल | कब इस्तेमाल करें | क्या इसे किसी को पेज करना चाहिए? |
|---|---|---|
| TRACE | अल्ट्रा-फाइन-ग्रेन्ड फ्लो, प्रति-लूप इटरेशन। प्रोड में शायद ही कभी ऑन। | नहीं |
| DEBUG | वेरिएबल स्टेट्स, API पेलोड्स, एग्ज़िक्यूशन ब्रांचेस — डेवलपर डायग्नोस्टिक्स [2] | नहीं |
| INFO | सामान्य रनटाइम इवेंट्स: स्टार्टअप, शटडाउन, यूज़र ऐक्शन्स, स्टेट चेंजेस | नहीं |
| WARN | कुछ गड़बड़ है पर सिस्टम संभल गया या ग्रेसफुली डीग्रेड हुआ | शायद (ट्रेंड) |
| ERROR | कोई ऑपरेशन फेल हुआ, पर ऐप चलता रहता है [3] | अक्सर |
| FATAL | अपूरणीय — ऐप बाहर निकलने वाला है (बूट पर मिसिंग कॉन्फ़िग, OOM) [3] | हाँ |
कुछ राय जिन पर मैं अड़ा रहूँगा:
- INFO आपका प्रोडक्शन डिफ़ॉल्ट है। थ्रेशोल्ड को INFO पर सेट करें और आप INFO, WARN, ERROR, FATAL कैप्चर करते हैं जबकि DEBUG का शोर छोड़ देते हैं [5]। लगभग हर सर्विस के लिए यही सही बेसलाइन है।
- DEBUG अस्थायी डीबगिंग के लिए है, फिर इसे बंद कर दें। विस्तृत DEBUG लॉग्स लिखने से असली I/O ओवरहेड जुड़ता है जो हाई-थ्रूपुट सिस्टम्स को धीमा करता है और लेटेंसी बढ़ाता है [1]। इसे “बस यूँ ही, ज़रूरत पड़ी तो” के लिए ऑन मत रहने दें।
- WARN सबसे ज़्यादा दुरुपयोग किया जाने वाला लेवल है। एक रिट्राई जो सफल हुई वह WARN है। एक फेल हुई रिक्वेस्ट जिसे यूज़र ने सचमुच देखा वह ERROR है। अगर आप “हमने इसे संभाल लिया” और “कस्टमर को 500 मिला” के बीच फर्क नहीं बता सकते, तो आपके अलर्ट्स झूठ बोल रहे हैं।
- रनटाइम पर किसी एक सर्विस के लिए DEBUG ऑन कर पाने की क्षमता — बिना रीडिप्लॉय के — सोने के मोल बराबर है। प्रोड में DEBUG का यही वैध इस्तेमाल है: दस मिनट के लिए ऑन करें, अजीब केस कैप्चर करें, फिर बंद कर दें।
इसे सही करें और आपका ऑन-कॉल डैशबोर्ड बन जाता है “मुझे पिछले 15 मिनट के सारे ERROR और FATAL दिखाओ” — एक ऐसी क्वेरी जिसका सचमुच कोई मतलब है।
असल में क्या लॉग करें (और क्या कभी न करें)
हर लॉग लाइन में जो फ़ील्ड्स होने चाहिए
अगर आप इस लेख से एक चीज़ लेते हैं, तो यह लें: स्ट्रिंग्स लॉग करना बंद करें, स्ट्रक्चर्ड की-वैल्यू रिकॉर्ड्स लॉग करना शुरू करें। स्ट्रक्चर्ड लॉगिंग फ्री टेक्स्ट के बजाय मशीन-रीडेबल की-वैल्यू पेयर (आमतौर पर JSON) निकालती है, ताकि हर फ़ील्ड को फ़िल्टरिंग और सर्विसेस के आर-पार कोरिलेशन के लिए स्वतंत्र रूप से क्वेरी किया जा सके [12]। क्वेरी टाइम पर फर्क ज़मीन-आसमान का है — grep बनाम एक असली WHERE order_id = '...'।
हर प्रोडक्शन लॉग लाइन में, कम से कम, ये होने चाहिए [12]:
- timestamp — ISO 8601, UTC में। हमेशा UTC। किसी इंसिडेंट के दौरान टाइमज़ोन बग्स अपने आप में एक खास नर्क हैं।
- level — ऊपर वाली सीवियरिटी
- service — किस सर्विस ने इसे निकाला
- event — एक छोटा, सुसंगत नाम जैसे
payment.capturedयाorder.rejected - trace_id और request_id — कोरिलेशन का गोंद (इस पर आगे और बात)
- domain fields —
user_id,order_id,tenant_id— जो भी आपको किसके और किसके हिसाब से स्लाइस करने दे
यहाँ वही फेलियर दो तरीकों से लॉग किया गया है। बिना स्ट्रक्चर के:
ERROR Payment failed for user after timeout
स्ट्रक्चर्ड:
{
"timestamp": "2026-06-20T15:42:11.204Z",
"level": "ERROR",
"service": "payments-api",
"event": "payment.capture_failed",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"request_id": "req_8821aa",
"user_id": "usr_4471",
"order_id": "ord_99812",
"gateway": "stripe",
"error_code": "gateway_timeout",
"duration_ms": 30021
}
पहला आपको बताता है कि कुछ टूट गया। दूसरा आपको बताता है कि payments-api ने एक खास ऑर्डर के लिए Stripe से बात करते हुए 30 सेकंड बाद टाइमआउट किया, और आपको तुरंत हर वह दूसरी रिक्वेस्ट ढूँढने देता है जो उसी गेटवे टाइमआउट से टकराई। इनमें से एक ही 3am के इंसिडेंट में टिकता है।
फ़ील्ड नामों पर एक और बात: एक कन्वेंशन चुनें और उसे कभी न तोड़ें। अगर यह एक सर्विस में user_id है, तो इसे दूसरी में userId या user मत बनाइए [4]। मैंने उन सर्विसेस के लॉग्स को जोड़ने में असली समय बर्बाद किया है जो केसिंग पर असहमत थीं। snake_case पर टिके रहें, इसे एक शेयर्ड लॉगिंग लाइब्रेरी में लिखें, और इंसानों के याद रखने पर निर्भर रहना बंद करें।
एरर्स को स्ट्रक्चर्ड डेटा के रूप में लॉग करें, स्टैक-ट्रेस सूप के रूप में नहीं
एक कच्चे स्टैक ट्रेस को एक विशाल स्ट्रिंग फ़ील्ड के रूप में मत उड़ेलिए। स्ट्रक्चर्ड एरर जानकारी लॉग करें — एरर टाइप, मैसेज, कोड, और स्टैक को उसके अपने फ़ील्ड के रूप में [12]। एक स्ट्रिंगिफ़ाइड स्टैक ट्रेस खोजा नहीं जा सकता और आपकी स्टोरेज को फुला देता है। स्ट्रक्चर्ड एरर फ़ील्ड्स आपको रेगेक्स की कसरत किए बिना “पिछले एक घंटे में कितने gateway_timeout एरर्स” पूछने देते हैं।
क्या कभी लॉग न करें
यहीं लापरवाही एक ब्रीच या कंप्लायंस जुर्माने में बदल जाती है। सीक्रेट्स या PII कभी लॉग न करें। यूज़रनेम, पासवर्ड, सिक्योरिटी क्वेश्चन, API कीज़, एक्सेस टोकन, प्राइवेट कीज़, और सेशन आईडी कभी किसी लॉग में नहीं आने चाहिए [13][16]। यही व्यक्तिगत रूप से पहचान योग्य जानकारी के लिए भी — नाम, पते, सरकारी आईडी, पूरी पेमेंट डिटेल्स [17]।
और यह रहा वह हिस्सा जो लोग गलत करते हैं: रिडैक्ट करना याद रखने के लिए डेवलपर्स पर निर्भर मत रहिए। कोई न कोई हमेशा भूल जाता है। सही तरीका है ऑटोमेटेड फ़िल्टर्स जो लिखे जाने से पहले सीक्रेट्स को हटा दें, साथ ही एक allowlist — स्पष्ट रूप से उन गैर-संवेदनशील फ़ील्ड्स को परिभाषित करें जिन्हें शामिल करने की अनुमति है, बजाय हर संवेदनशील फ़ील्ड को ब्लॉकलिस्ट करने की कोशिश के [16][17]। Allowlists फेल होने पर सुरक्षित रहती हैं; blocklists फेल होने पर खुली रहती हैं।
OWASP Logging Cheat Sheet यहाँ का प्रामाणिक संदर्भ है, और अगर आप कुछ भी रेगुलेटेड संभाल रहे हैं तो यह पढ़ने लायक है [15]।
सर्विसेस के आर-पार कोरिलेशन: वह हिस्सा जो असल में मायने रखता है
यहीं डिस्ट्रिब्यूटेड सिस्टम में लॉगिंग वाकई मुश्किल हो जाती है। एक मोनोलिथ में, एक रिक्वेस्ट = एक थ्रेड = एक सतत लॉग स्ट्रीम। माइक्रोसर्विसेस में, एक अकेला यूज़र ऐक्शन छह सर्विसेस, दो क्यू, और एक बैकग्राउंड जॉब को छू सकता है — और हर एक अपनी ही स्ट्रीम में लॉग करता है। उन्हें जोड़ने वाले एक धागे के बिना, आपके पास एक के बजाय छह अलग-अलग खून के मामले होते हैं।
वह धागा है एक कोरिलेशन आईडी (या ट्रेस आईडी): एक यूनीक आइडेंटिफ़ायर जो किसी रिक्वेस्ट से जुड़ा होता है और हर सर्विस सीमा के आर-पार उसके साथ यात्रा करता है [9]। जब कोई रिक्वेस्ट आपके सिस्टम में प्रवेश करती है, तो उसे एक आईडी मिलती है। हर सर्विस उस आईडी को आने वाली रिक्वेस्ट से निकालती है और किसी भी डाउनस्ट्रीम कॉल के हेडर्स में उसे आगे पास करती है [10]। फिर हर लॉग लाइन उसे शामिल करती है। अब आप एक ही क्वेरी से उस एक रिक्वेस्ट के लिए हर सर्विस के आर-पार के हर लॉग को खींच सकते हैं।
अपना खुद का हेडर मत बनाइए — W3C Trace Context इस्तेमाल करें
आप अपना खुद का X-Correlation-ID हेडर बना सकते हैं, और बहुत सी टीमें बनाती हैं। पर अब एक स्टैंडर्ड है, और इसे अपनाना सार्थक है। W3C Trace Context स्पेक एक traceparent हेडर परिभाषित करता है जिसे सब कुछ समझता है [13]। इसका फ़ॉर्मैट बेहद सरल है:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ │ │ │
version trace-id (32 hex) parent span-id flags
ट्रेस आईडी पूरी रिक्वेस्ट यात्रा की पहचान करती है; स्पैन आईडी उसके भीतर एक ऑपरेशन की पहचान करती है [14]। इसे इस्तेमाल करें और कोई भी कंप्लायंट टूल — Jaeger, Zipkin, Sentry, Datadog — बिना कस्टम गोंद के आपके ट्रेस को सिल सकता है [10]।
लॉग्स बनाम ट्रेसेस — ये चचेरे भाई हैं, जुड़वाँ नहीं
लोग इन्हें घालमेल कर देते हैं, तो मुझे फर्क के बारे में सीधा-सीधा कहने दीजिए:
- एक trace सर्विसेस के आर-पार किसी रिक्वेस्ट की स्ट्रक्चर्ड टाइमलाइन है, जो नेस्टेड spans से बनी होती है, हर स्पैन एक ऑपरेशन होता है जिसका एक स्टार्ट, एंड और ड्यूरेशन होता है।
- एक log संदर्भ के साथ एक पॉइंट-इन-टाइम इवेंट है।
जादू तब होता है जब आप अपनी लॉग लाइनों में trace_id डालते हैं। तब आप अपने ट्रेस व्यू में एक धीमे स्पैन से सीधे उस स्पैन के दौरान निकले ठीक उन्हीं लॉग्स पर कूद सकते हैं। यही आधुनिक डिस्ट्रिब्यूटेड ट्रेसिंग का पूरा खेल है [8]।
OpenTelemetry वह टूल है जिसे मैं यहाँ चुनूँगा। इसके SDK डिफ़ॉल्ट प्रोपगेशन फ़ॉर्मैट के रूप में W3C Trace Context इस्तेमाल करते हैं, तो अगर आप OTel से इंस्ट्रूमेंट करते हैं तो आपको क्रॉस-सर्विस कोरिलेशन लगभग मुफ़्त में मिल जाता है [13]। हर HTTP क्लाइंट से हाथ से हेडर पिरोने की ज़रूरत नहीं।
एक व्यावहारिक टिप: थ्रेड-लोकल कॉन्टेक्स्ट
ज़्यादातर फ्रेमवर्क्स में आप कोरिलेशन आईडी को हर फंक्शन सिग्नेचर के ज़रिए हाथ से पास नहीं करना चाहेंगे — वह दुखदायी है। एक कॉन्टेक्स्ट मैकेनिज़्म इस्तेमाल करें: Java/SLF4J दुनिया में MDC (Mapped Diagnostic Context), Go में context.Context, Node में async local storage। जब रिक्वेस्ट आती है तब आप ट्रेस आईडी एक बार सेट करते हैं, और लॉगिंग फ्रेमवर्क उस रिक्वेस्ट के बाकी हिस्से के लिए हर लाइन पर इसे अपने आप ठप्पा लगा देता है [10]। इसे मिडलवेयर में सेट करें, फिर भूल जाइए।
कैनोनिकल लॉग लाइन: मेरा पसंदीदा कम आँका गया पैटर्न
इसने मेरे लॉगिंग के बारे में सोचने का तरीका बदल दिया, और यह Stripe के इंजीनियरिंग ब्लॉग से आता है। विचार: अपने सामान्य बिखरे हुए लॉग्स के साथ-साथ, हर रिक्वेस्ट के अंत में एक मोटी, स्ट्रक्चर्ड लॉग लाइन निकालें जिसमें सारी ज़रूरी टेलीमेट्री एक ही जगह हो [20]।
तो किसी रिक्वेस्ट के अंत में आप एक अकेली लाइन निकालते हैं जिसमें होता है: रूट, यूज़र आईडी, रिस्पॉन्स स्टेटस, कुल ड्यूरेशन, कौन सी डाउनस्ट्रीम सर्विसेस को कॉल किया गया और हर एक ने कितना समय लिया, ऑथ मेथड, ट्रेस आईडी — उस रिक्वेस्ट के बारे में जो भी मायने रखता है, सब एक साथ।
यह इतना अच्छा क्यों है? क्योंकि किसी इंसिडेंट के दौरान, आप पाँच अलग-अलग लॉग स्टेटमेंट्स को जोड़ने के बजाय एक लाइन पर एक क्वेरी लिखते हैं [20]। कम्युनिटी ने इसी विचार को wide events नाम से दोबारा खोजा है — हर रिक्वेस्ट के लिए एक व्यापक इवेंट जिसमें हर ऐट्रिब्यूट जुड़ा हो [20]। वही अवधारणा, और ऑब्ज़र्वेबिलिटी इसी दिशा में जा रही है।
Stripe से चतुर इम्प्लीमेंटेशन डिटेल: वे एमिशन को एक ensure ब्लॉक (मूलतः एक finally) में लपेटते हैं ताकि कैनोनिकल लाइन तब भी लिखी जाए जब रिक्वेस्ट बीच में ही एक्सेप्शन फेंक दे [20]। यह रही इसकी आकृति:
def handle_request(req):
ctx = {"request_id": req.id, "route": req.route, "user_id": req.user_id}
try:
result = process(req)
ctx["status"] = result.status
return result
finally:
ctx["duration_ms"] = elapsed_ms()
logger.info("canonical_line", **ctx) # always runs
सामान्य लॉगिंग और प्रति रिक्वेस्ट एक कैनोनिकल लाइन, दोनों करें। सामान्य लॉग्स आपको पल-पल का ब्योरा देते हैं; कैनोनिकल लाइन आपको स्कोरकार्ड देती है।
लॉग्स को किफ़ायती रखना: बिना अंधे हुए सैम्पलिंग
लॉग्स जल्दी महँगे हो जाते हैं, और भोली प्रतिक्रिया — “बस कम लॉग करो” — आमतौर पर मतलब है कि आप वही चीज़ बंद कर देते हैं जिसकी आपको ज़रूरत थी। समझदारी भरा कदम है सैम्पलिंग: स्ट्रीम का केवल एक हिस्सा रखें, पर इस बारे में सोच-समझकर रहें कि कौन सा हिस्सा [18]।
हाई-वॉल्यूम सिस्टम्स के लिए जो रणनीति मैं सुझाऊँगा [18][19]:
- एरर्स और वॉर्निंग्स को कभी सैम्पल न करें। वे दुर्लभ और बहुमूल्य हैं। उनका 100% रखें।
- उबाऊ चीज़ों को आक्रामक रूप से सैम्पल करें — हेल्थ चेक्स, सफल ऑथ, रूटीन 200s। 100 में से 1 रखना भी आपको साफ़ ट्रैफ़िक पैटर्न दिखाता है [18]।
- सैम्पल रेट को हमेशा लॉग में ही रिकॉर्ड करें, ताकि आपका विश्लेषण इसे वापस गुणा कर सके और वॉल्यूम्स के बारे में आपसे झूठ न बोले [18]।
- अपनी स्टोरेज को टियर करें — हाल के लॉग्स के लिए हॉट/सर्चेबल, पुराने के लिए सस्ता कोल्ड आर्काइव। स्ट्रक्चर्ड लॉग्स इसे आसान बनाते हैं क्योंकि हर लाइन में रूट करने के लिए क्वेरी करने योग्य मेटाडेटा होता है [12]।
Datadog, Better Stack और दूसरों के पास इसे ट्यून करने पर अच्छे गहन-विश्लेषण हैं, पर सिद्धांत हर जगह टिकता है: शोर को सैम्पल करें, सिग्नल को कभी सैम्पल न करें, और अपना रेट हमेशा जानें।
साथ ही — उबाऊ पर लोगों को काटता है — अपने लॉग्स को रोटेट और आर्काइव करें। असीमित लॉग फ़ाइलें डिस्क भर देती हैं और ठीक उसी सर्विस को गिरा देती हैं जिसे आप ऑब्ज़र्व करने की कोशिश कर रहे थे [1]। रोटेशन सेट करें, रिटेंशन सेट करें, आगे बढ़ें।
एक झटपट गट-चेक लिस्ट
अपनी लॉगिंग को “हो गई” कहने से पहले, इससे गुज़रिए:
- क्या लॉग्स सुसंगत फ़ील्ड नामों के साथ स्ट्रक्चर्ड JSON हैं? [12]
- क्या हर लाइन में एक
trace_idहै जो सर्विसेस के आर-पार प्रोपगेट होता है? [13] - क्या INFO आपका प्रोड डिफ़ॉल्ट है, जिसमें DEBUG रनटाइम पर फ्लिप किया जा सके? [5]
- क्या ERROR और FATAL का मतलब कुछ ऐसा है जिसकी एक ऑन-कॉल इंसान को परवाह करनी चाहिए? [3]
- क्या सीक्रेट्स और PII एक ऑटोमेटेड allowlist द्वारा हटाए जाते हैं, न कि डेवलपर की याददाश्त द्वारा? [16]
- क्या आप प्रति रिक्वेस्ट एक कैनोनिकल/वाइड लाइन निकालते हैं? [20]
- क्या एरर्स और वॉर्निंग्स सैम्पलिंग से मुक्त हैं? [18]
- क्या टाइमस्टैम्प UTC और ISO 8601 हैं? [12]
अगर आप इन पर टिक लगा सकते हैं, तो आपका अगला 3am पेज काफ़ी छोटा होने वाला है।
लक्ष्य कभी “सब कुछ लॉग करो” नहीं था। यह सुनिश्चित करना था कि जब कुछ टूटे — और यह टूटेगा — तो जवाब पहले से ही एक ऐसी क्वेरी में बैठा हो जिसे आप पंद्रह सेकंड में, आधी नींद में, पेजर के अब भी भनभनाते हुए लिख सकें।
स्रोत
- Logging Best Practices: 12 Tips for Developers and SREs — Parseable
- Logging Levels Explained — SigNoz
- Log Debug vs. Info vs. Warn vs. Error and Fatal — Edge Delta
- Log Levels: Different Types and How to Use Them — Last9
- Log Levels Explained and How to Use Them — Better Stack
- Logging Best Practices: 12 Dos and Don’ts — Better Stack
- Logging Best Practices: The 13 You Should Know — DataSet
- Pattern: Distributed tracing — microservices.io
- Understanding and Implementing Correlation ID in Microservices — Anil Goyal
- Distributed Tracing Logs: How They Work & Best Practices — groundcover
- Structured Logging in Production: Best Practices for Scalable Systems — OpenObserve
- Structured Logging: Best Practices & JSON Examples — Uptrace
- OpenTelemetry Context Propagation: W3C TraceContext — Uptrace
- Traceparent: How OpenTelemetry Connects Your Microservices — Last9
- Logging Cheat Sheet — OWASP
- Best Logging Practices for Safeguarding Sensitive Data — Better Stack
- How to Keep Sensitive Data Out of Your Logs: 9 Best Practices — Skyflow
- Log Sampling: Techniques, Challenges & Best Practices — groundcover
- How to Reduce Logging Costs with Log Sampling — Better Stack
- Fast and flexible observability with canonical log lines — Stripe
- How to optimize high-volume log data without compromising visibility — Datadog