इस लेख में, हम यह समीक्षा करते हुए निष्कर्ष निकालेंगे कि कैसे लॉगिंग चिंताओं और कोड को बुनियादी ढांचे और व्यवसाय कोड से अलग और अलग किया जा सकता है। पिछले लेख में, हमने समीक्षा की कि बुनियादी ढांचे के संचालन के लिए डायग्नोस्टिकसोर्स और डायग्नोस्टिकलिस्टनर का उपयोग कैसे किया जाए।
अब, हम समीक्षा करेंगे कि अतिरिक्त संदर्भ के साथ लॉग किए गए डेटा को कैसे समृद्ध किया जाए।
अनिवार्य रूप से, लॉग किए गए डेटा को समृद्ध करने की अवधारणा अतिरिक्त संदर्भ, या डेटा को पंजीकृत करने के इर्द-गिर्द घूमती है, जिसे लॉग संदेशों के साथ जाना चाहिए। यह संदर्भ कुछ भी हो सकता है - एक साधारण स्ट्रिंग से लेकर एक जटिल वस्तु तक, और यह बुनियादी ढांचे या व्यवसाय कोड के भीतर किसी क्षण उत्पन्न या ज्ञात होता है, न कि उस समय जब लॉग संदेश लिखे जाते हैं।
इसलिए, हम लॉग संदेशों में बस एक और संपत्ति नहीं जोड़ सकते हैं, क्योंकि यह कोड कोई लॉगिंग आउटपुट उत्पन्न नहीं करता है, या हम चाहते हैं कि संदर्भ कई लॉग संदेशों से जुड़ा हो जो निष्पादन श्रृंखला के नीचे उत्पन्न हो सकते हैं, या नहीं भी हो सकते हैं।
यह संदर्भ सशर्त भी हो सकता है - जैसे कि केवल त्रुटि लॉग संदेशों में विशिष्ट डेटा जोड़ना जबकि अन्य सभी संदेशों को इसकी आवश्यकता नहीं है।
हम सेरिलॉग और इसकी संवर्धन सुविधा का उपयोग करेंगे, क्योंकि सेरिलॉग ने इसे बहुत लचीला और शक्तिशाली बना दिया है। अन्य समाधानों में भी विभिन्न परिपक्वता स्तरों पर समान विशेषताएं हैं, और हम सेरिलॉग के संवर्धन की तुलना Microsoft.Extensions.Logging द्वारा प्रदान की जाने वाली सुविधाओं से करेंगे।
सेरिलॉग कई उपयोगी समृद्ध पदार्थों से भरा हुआ आता है जो कुछ परिदृश्यों के लिए बहुत उपयोगी हो सकते हैं। समृद्धों की पूरी सूची और उनके विवरण देखने के लिए आप इस पृष्ठ - https://github.com/serilog/serilog/wiki/Enrichment - पर जा सकते हैं।
उदाहरण के लिए, LogContext और GlobalLogContext समृद्धकर्ता हैं जो लॉग संदेशों के साथ लॉग इन करने के लिए अतिरिक्त डेटा को पुश करने की अनुमति देते हैं यदि वे मेल खाने वाले दायरे में लिखे जाते हैं।
LogContext एनरिचर Microsoft.Extensions.Logging में लॉगिंग स्कोप की अवधारणा के समान है। अनिवार्य रूप से, वे दोनों कुछ कस्टम डेटा को पुश करते हैं और एक आईडीस्पोज़ेबल ऑब्जेक्ट प्रदान करते हैं जिसे लॉग संदर्भ से डेटा को हटाने के लिए निपटाया जाना चाहिए।
यानी, जब तक IDisposable ऑब्जेक्ट दायरे में है, डेटा उस दायरे में लिखे गए सभी लॉग संदेशों से जुड़ा रहेगा। जब इसका निपटान हो जाएगा, तो डेटा संलग्न नहीं किया जाएगा।
सेरिलॉग और माइक्रोसॉफ्ट दस्तावेज़ ये उदाहरण प्रदान करते हैं:
// For Serilog log.Information("No contextual properties"); using (LogContext.PushProperty("A", 1)) { log.Information("Carries property A = 1"); using (LogContext.PushProperty("A", 2)) using (LogContext.PushProperty("B", 1)) { log.Information("Carries A = 2 and B = 1"); } log.Information("Carries property A = 1, again"); } // For Microsoft.Extensions.Logging scopes using (logger.BeginScope(new List<KeyValuePair<string, object>> { new KeyValuePair<string, object>("TransactionId", transactionId), })) { _logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}", id); todoItem = await _context.TodoItems.FindAsync(id); if (todoItem == null) { _logger.LogWarning(MyLogEvents.GetItemNotFound, "Get({Id}) NOT FOUND", id); return NotFound(); } }
कुछ परिदृश्यों के लिए उपयोगी होते हुए भी, वे काफी सीमित हैं। इस प्रकार के लॉग संवर्धन के लिए सबसे अच्छा उपयोग मिडलवेयर या डेकोरेटर प्रकार के कार्यान्वयन में है, जहां दायरा अच्छी तरह से परिभाषित है, और डेटा को दायरे के निर्माण के समय जाना जाता है, और हम निश्चित हैं कि डेटा का इसके बाहर कोई मूल्य नहीं है दायरा।
उदाहरण के लिए, हम इसका उपयोग एकल HTTP अनुरोध प्रसंस्करण दायरे के भीतर सभी लॉग संदेशों में सहसंबंध आईडी संलग्न करने के लिए कर सकते हैं।
GlobalLogContext समृद्धकर्ता LogContext के समान है, लेकिन यह वैश्विक है - यह किसी एप्लिकेशन के भीतर लिखे गए सभी लॉग संदेशों में डेटा भेजता है। हालाँकि, इस प्रकार के संवर्धन के वास्तविक उपयोग के मामले बहुत सीमित हैं।
वास्तव में, सेरिलॉग के लिए कस्टम लॉग एनरिचर्स को लागू करना बहुत आसान है - बस ILogEventEnricher
इंटरफ़ेस को कार्यान्वित करें, और लॉगर कॉन्फ़िगरेशन के साथ एनरिचर्स को पंजीकृत करें। इंटरफ़ेस में कार्यान्वयन के लिए केवल एक ही विधि है - Enrich
- जो लॉग इवेंट को स्वीकार करता है और इसे आपके इच्छित डेटा से समृद्ध करता है।
आइए कस्टम लॉग समृद्ध के लिए एक नमूना कार्यान्वयन की समीक्षा करें।
public sealed class CustomLogEnricher : ILogEventEnricher { private readonly IHttpContextAccessor contextAccessor; public CustomLogEnricher(IHttpContextAccessor contextAccessor) { this.contextAccessor = contextAccessor; } public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var source = contextAccessor.HttpContext?.RequestServices.GetService<ICustomLogEnricherSource>(); if (source is null) { return; } var loggedProperties = source.GetCustomProperties(logEvent.Level); foreach (var item in loggedProperties) { var property = item.ProduceProperty(propertyFactory); logEvent.AddOrUpdateProperty(property); } } }
जैसा कि हम देखते हैं, यह समृद्धकर्ता लॉग ईवेंट को समृद्ध करने के लिए कस्टम गुण प्राप्त करने के लिए ICustomLogEnricherSource
पर निर्भर करता है। आम तौर पर, लॉग समृद्धकर्ता लंबे समय तक चलने वाले उदाहरण होते हैं और यहां तक कि सिंगलटन पैटर्न का भी पालन करते हैं - वे एप्लिकेशन स्टार्टअप पर एक बार पंजीकृत होते हैं और पूरे एप्लिकेशन जीवनकाल के लिए रहते हैं।
इसलिए, हमें कस्टम गुणों के स्रोत से समृद्ध को अलग करने की आवश्यकता है, और स्रोत सीमित जीवनकाल के साथ एक स्कोप्ड उदाहरण हो सकता है।
public sealed class CustomLogEnricherSource : ICustomLogEnricherSource { private readonly Dictionary<string, CustomLogEventProperty> properties = new(); public ICustomLogEnricherSource With<T>(string property, T? value, LogEventLevel logLevel) where T : class { if (value is null) { return this; } properties[property] = new CustomLogEventProperty(property, value, logLevel); return this; } public void Remove(string property) { properties.Remove(property); } public IEnumerable<CustomLogEventProperty> GetCustomProperties(LogEventLevel logLevel) { foreach (var item in properties.Values) { if (item.Level <= logLevel) { yield return item; } } } } // And CustomLogEventProperty is a simple struct (you can make it a class, too): public struct CustomLogEventProperty { private LogEventProperty? propertyValue; public CustomLogEventProperty(string property, object value, LogEventLevel level) { Name = property; Value = value; Level = level; } public string Name { get; } public object Value { get; } public LogEventLevel Level { get; } public LogEventProperty ProduceProperty(ILogEventPropertyFactory propertyFactory) { propertyValue ??= propertyFactory.CreateProperty(Name, Value, destructureObjects: true); return propertyValue; } }
इस कार्यान्वयन में कुछ सरल विवरण हैं:
ICustomLogEnricherSource
उदाहरणों की एक सूची का उपभोग करके समृद्ध बनाया जा सकता है, क्योंकि विभिन्न एप्लिकेशन घटक अलग-अलग कस्टम गुण उत्पन्न कर सकते हैं।
ICustomLogEnricherSource
) को किसी भी घटक में इंजेक्ट किया जा सकता है जिसे कस्टम गुणों के साथ लॉग संदेशों को समृद्ध करने की आवश्यकता होती है। आदर्श रूप से, यह एक स्कोप्ड उदाहरण होना चाहिए, क्योंकि यह कस्टम गुणों को समाप्त करने का एक सुविधाजनक तरीका प्रदान नहीं करता है।
LogEvent
गुणों या यहां तक कि एप्लिकेशन स्थिति पर भी निर्भर हो सकता है।
CustomLogEventProperty
एक उत्पादित LogEventProperty
इंस्टेंस को एक ही कस्टम प्रॉपर्टी के लिए कई बार बनाने से बचने के लिए कैश करता है, क्योंकि यह कई लॉग संदेशों से जुड़ सकता है।
कोड की इस पंक्ति में एक और महत्वपूर्ण विवरण है, destructureObjects
पैरामीटर:
propertyFactory.CreateProperty(Name, Value, destructureObjects: true);
जब हम जटिल ऑब्जेक्ट - कक्षाएं, रिकॉर्ड, संरचनाएं, संग्रह इत्यादि संलग्न करते हैं - और यह पैरामीटर true
पर सेट नहीं होता है, तो सेरिलॉग ऑब्जेक्ट पर ToString
कॉल करेगा और परिणाम को लॉग संदेश में संलग्न करेगा। जबकि रिकॉर्ड्स में एक ToString
कार्यान्वयन है जो सभी सार्वजनिक संपत्तियों को आउटपुट करेगा, और हम अपनी कक्षाओं और संरचनाओं के लिए ToString
ओवरराइड कर सकते हैं, यह हमेशा मामला नहीं होता है।
साथ ही, ऐसा आउटपुट संरचित लॉगिंग के लिए अच्छा नहीं करता है, क्योंकि यह एक साधारण स्ट्रिंग होगी, और हम संलग्न ऑब्जेक्ट के गुणों के आधार पर लॉग संदेशों को खोजने और फ़िल्टर करने में सक्षम नहीं होंगे। इसलिए, हमने यहां इस पैरामीटर को true
पर सेट किया है। सरल प्रकार (मान प्रकार और स्ट्रिंग) इस पैरामीटर के किसी भी मान के साथ ठीक काम करेंगे।
इस कार्यान्वयन का एक अन्य लाभ यह है कि एक बार जब कोई कस्टम प्रॉपर्टी स्रोत में पंजीकृत हो जाती है, तो यह सभी लॉग संदेशों के लिए तब तक बनी रहती है जब तक कि इसे हटा नहीं दिया जाता है या स्रोत का निपटान नहीं कर दिया जाता है (याद रखें, यह एक स्कोप्ड उदाहरण होना चाहिए)। कुछ कार्यान्वयनों को कस्टम संपत्तियों के जीवनकाल पर बेहतर नियंत्रण की आवश्यकता हो सकती है, और इसे विभिन्न तरीकों से प्राप्त किया जा सकता है।
उदाहरण के लिए, विशिष्ट CustomLogEventProperty
कार्यान्वयन प्रदान करके या यह जांचने के लिए कॉलबैक प्रदान करके कि क्या कस्टम प्रॉपर्टी को हटाया जाना चाहिए।
इस समाप्ति तर्क का उपयोग GetCustomProperties
विधि में एक कस्टम प्रॉपर्टी की पूर्व-जांच करने और यदि इसकी समय सीमा समाप्त हो गई है तो इसे स्रोत से हटाने के लिए किया जा सकता है।
खैर, आदर्श रूप से हम लॉगिंग संबंधी चिंताओं को व्यवसाय और बुनियादी ढांचे कोड के साथ नहीं मिलाना चाहेंगे। इसे विभिन्न डेकोरेटर्स, मिडलवेयर और अन्य पैटर्न के साथ हासिल किया जा सकता है जो लॉगिंग-विशिष्ट कोड के साथ व्यवसाय और बुनियादी ढांचे कोड को 'लपेटने' की अनुमति देता है।
हालाँकि, यह हमेशा संभव नहीं हो सकता है, और कभी-कभी, व्यवसाय और बुनियादी ढांचे के कोड में ICustomLogEnricherSource
जैसे मध्यस्थ अमूर्त को इंजेक्ट करना और इसे लॉगिंग के लिए कस्टम डेटा पंजीकृत करने देना अधिक सुविधाजनक हो सकता है।
इस तरह, व्यवसाय और बुनियादी ढांचे के कोड को वास्तविक लॉगिंग के बारे में कुछ भी जानने की आवश्यकता नहीं है, जबकि इसे अभी भी कुछ लॉगिंग-जागरूक कोड के टुकड़ों के साथ मिलाया जाता है।
वैसे भी, यह कोड बहुत कम युग्मित और अधिक परीक्षण योग्य होगा। हम नो-ऑप कार्यान्वयन के लिए NullLogEnricherSource
जैसा कुछ भी पेश कर सकते हैं जो कुछ बहुत-हॉट-पाथ परिदृश्यों के लिए कोई प्रदर्शन और मेमोरी नहीं लेगा।
जैसा कि माइक्रोसॉफ्ट कहता है,
लॉगिंग इतनी तेज़ होनी चाहिए कि यह एसिंक्रोनस कोड की प्रदर्शन लागत के लायक न हो।
इसलिए, जब भी हम अपने लॉग संदेशों को अतिरिक्त संदर्भ के साथ समृद्ध करते हैं, तो हमें प्रदर्शन निहितार्थों के बारे में पता होना चाहिए। सामान्यतया, सेरिलॉग लॉग संवर्धन कार्यान्वयन बहुत तेज़ है, कम से कम यह Microsoft लॉगिंग स्कोप के बराबर है, लेकिन यदि हम उनमें अधिक डेटा संलग्न करते हैं तो लॉग संदेश बनाने में अधिक समय लगेगा।
हम जितने अधिक कस्टम गुण संलग्न करेंगे, उनमें वस्तुएं उतनी ही अधिक जटिल होंगी, और लॉग संदेश तैयार करने में उतना ही अधिक समय और मेमोरी लगेगी। इसलिए, एक डेवलपर को इस बारे में बहुत विचारशील होना चाहिए कि लॉग संदेशों में कौन सा डेटा संलग्न करना है, इसे कब संलग्न करना है और इसका जीवनकाल क्या होना चाहिए।
नीचे एक छोटी बेंचमार्क परिणाम तालिका है जो सेरिलॉग लॉग संवर्धन और माइक्रोसॉफ्ट लॉगिंग स्कोप दोनों के लिए प्रदर्शन और मेमोरी खपत को दर्शाती है। प्रत्येक बेंचमार्क विधि कस्टम गुणों के साथ उन्हें समृद्ध करने के विभिन्न तरीकों के साथ 20 लॉग संदेश तैयार करेगी।
कोई कस्टम गुण संलग्न नहीं:
| Method | Mean | Error | StdDev | Gen0 | Allocated | | SerilogLoggingNoEnrichment | 74.22 us | 1.194 us | 1.116 us | 1.2207 | 21.25 KB | | MicrosoftLoggingNoEnrichment | 72.58 us | 0.733 us | 0.685 us | 1.2207 | 21.25 KB |
प्रत्येक लॉग संदेश में तीन तार जुड़े होते हैं:
| Method | Mean | Error | StdDev | Gen0 | Allocated | | SerilogLoggingWithContext | 77.47 us | 0.551 us | 0.516 us | 1.7090 | 28.6 KB | | SerilogLoggingWithEnrichment | 79.89 us | 1.482 us | 2.028 us | 1.7090 | 29.75 KB | | MicrosoftLoggingWithEnrichment | 81.86 us | 1.209 us | 1.131 us | 1.8311 | 31.22 KB |
प्रत्येक लॉग संदेश में तीन जटिल ऑब्जेक्ट (रिकॉर्ड) संलग्न होते हैं:
| Method | Mean | Error | StdDev | Gen0 | Allocated | | SerilogLoggingWithObjectContext | 108.49 us | 1.193 us | 1.058 us | 5.3711 | 88.18 KB | | SerilogLoggingWithObjectEnrichment | 106.07 us | 0.489 us | 0.409 us | 5.3711 | 89.33 KB | | MicrosoftLoggingWithObjectEnrichment | 99.63 us | 1.655 us | 1.468 us | 6.1035 | 100.28 KB |The
Serilog*Context
विधियां LogContext.PushProperty
का उपयोग करती हैं, Serilog*Enrichment
विधियां लेख में दिए गए कस्टम एनरिचमेंट और स्रोत कार्यान्वयन का उपयोग करती हैं, जबकि Microsoft*
विधियां लॉगिंग स्कोप का उपयोग करती हैं।
हम देख सकते हैं कि ज्यादातर मामलों में प्रदर्शन और मेमोरी खपत बहुत समान हैं, और जटिल वस्तुओं के साथ समृद्ध करना सरल प्रकारों के साथ समृद्ध करने की तुलना में अधिक महंगा है। जब हम लॉग संदेशों में रिकॉर्ड संलग्न करते हैं तो एकमात्र अपवाद Microsoft कार्यान्वयन के साथ लॉगिंग है।
ऐसा इसलिए है क्योंकि लॉगिंग स्कोप जटिल वस्तुओं को नष्ट नहीं करते हैं, और उन्हें लॉग संदेशों में संलग्न करते समय क्रमबद्धता के लिए ToString
विधि का उपयोग करते हैं। यह Microsoft कार्यान्वयन को थोड़ा तेज़ बनाता है लेकिन अधिक मेमोरी की खपत करता है।
यदि हम डिफ़ॉल्ट ToString
कार्यान्वयन के साथ कक्षाओं का उपयोग करेंगे, तो मेमोरी खपत बहुत कम होगी, लेकिन इन कस्टम ऑब्जेक्ट्स को पूरी तरह से योग्य प्रकार के नामों के रूप में लॉग किया जाएगा - पूरी तरह से बेकार।
और किसी भी स्थिति में, हम Microsoft कार्यान्वयन के साथ इन ऑब्जेक्ट्स के गुणों के आधार पर लॉग संदेशों को खोजने और फ़िल्टर करने में सक्षम नहीं होंगे क्योंकि उन्हें नष्ट नहीं किया जाएगा।
तो, यह Microsoft लॉगिंग स्कोप की सीमा है जिसके बारे में हमें अवगत होना चाहिए - जटिल वस्तुओं का कोई विनाश नहीं होता है जबकि सरल प्रकार लॉग ठीक होते हैं और खोजने योग्य और फ़िल्टर करने योग्य हो सकते हैं।
नोट: इस आलेख के लिए बेंचमार्क स्रोत कोड और कोड नमूने GitHub रिपॉजिटरी में पाए जा सकते हैं
कोड के विकास और रखरखाव के लिए उचित लॉग संवर्धन 'जीवन की गुणवत्ता में सुधार' है। यह लॉग संदेशों में अतिरिक्त संदर्भ संलग्न करने की क्षमता की अनुमति देता है जो लॉग संदेश निर्माण के समय ज्ञात नहीं हैं।
यह संदर्भ कुछ भी हो सकता है - एक साधारण स्ट्रिंग से लेकर एक जटिल वस्तु तक, और यह बुनियादी ढांचे या व्यवसाय कोड के भीतर किसी क्षण निर्मित या ज्ञात हो जाता है।
यह लॉगिंग और ऑब्जर्वेबिलिटी कोड को इंफ्रास्ट्रक्चर और बिजनेस कोड से अलग करने और कोड को अधिक रखरखाव योग्य, परीक्षण योग्य और पठनीय बनाने का एक तरीका है।