Serilog 最佳实践
Serilog 最佳實踐
概述
Serilog[1]是 Microsoft .NET 的結(jié)構(gòu)化日志記錄庫,并已成為Checkout.com 上NET 的首選日志記錄庫。它支持各種日志記錄目的地(稱為接收器[2])包從標準控制臺和基于文件的接收器到日志服務,如 Datadog。
本指南最初是我們工程手冊中的一篇文章,在收到內(nèi)部積極反饋后,我決定在我的博客上發(fā)布它。
內(nèi)容
1.標準日志屬性2.日志記錄基礎知識1.記錄一切2.選擇合適的日志記錄級別3.定時操作源4.上下文5.HTTP 日志記錄6.日志的職責3.日志內(nèi)容的采集1.Serilog標準采集方法2.通過全局屬性采集日志4.關聯(lián)日志5.消息模板1.消息模板推薦6.日志和診斷上下文1.日志上下文2.診斷上下文7.配置8.生產(chǎn)日志1.當日志變得不僅僅是日志9.其他工具和實用程序1.在本地使用 Seq2.按日志類型記錄日志3.按屬性塊記錄日志4.按請求記錄日志
標準日志屬性
標準化日志事件屬性使您能夠充分利用日志搜索和分析工具。在適用的情況下使用以下屬性:
| ApplicationName | 生成日志事件的應用程序的名稱 |
| ClientIP | 發(fā)出請求的客戶端的 IP 地址 |
| CorrelationId | 可用于跨多個應用程序邊界跟蹤請求的 ID |
| Elapsed | 操作完成所用的時間(以毫秒為單位) |
| EventType | 用于確定消息類型的消息模板的哈希值 |
| MachineName | 運行應用程序的機器的名稱 |
| Outcome | 手術的結(jié)果 |
| RequestMethod | HTTP 請求方法,例如?POST |
| RequestPath | HTTP 請求路徑 |
| SourceContext | 日志源自的組件/類的名稱 |
| StatusCode | HTTP 響應狀態(tài)碼 |
| UserAgent | HTTP 用戶代理 |
| Version | 正在運行的應用程序的版本 |
上面的很多屬性都來自于 Serilog 自己的擴展,例如Serilog Timings[3](用于計時操作)和Serilog 請求日志記錄[4]。
日志記錄基礎知識
記錄一切
通常,記錄所有可以深入了解您的應用程序和用戶行為的內(nèi)容,例如:
?代碼中的主要分支點?遇到錯誤或意外值時?任何 IO 或資源密集型操作?重大領域事件?請求失敗和重試?耗時的批處理操作的開始和結(jié)束
選擇合適的日志記錄級別
對您的日志記錄要慷慨,但對您的日志記錄級別要嚴格。在幾乎所有情況下,您的日志級別都應該是Debug. 使用Information的日志事件,將在生產(chǎn)中需要確定運行狀態(tài)或應用程序的正確性,Warning或Error突發(fā)事件,如異常。
請注意,該Error級別應保留用于您打算對其采取行動的事件。如果某些事情成為正常的應用程序行為(例如,請求輸入驗證失敗),您應該降級日志級別以減少日志“噪音”。
定時操作
將應用程序中的每個資源密集型操作(例如 IO)與指標代碼一起記錄下來。這在本地運行應用程序以查看應用程序瓶頸或響應時間消耗的情況時非常有用。該Serilog時序庫[5]提供了一個方便的方式來做到這一點:
using (_logger.TimeDebug("Sending notification to Slack channel {Channel} with {WebhookUrl}", _slackOptions.Channel, _slackOptions.WebhookUrl)) using (_metrics.TimeIO("http", "slack", "send_message")) {}源上下文
該SourceContext屬性用于跟蹤日志事件的來源,通常是使用記錄器的 C# 類。ILogger使用依賴注入將 Serilog 注入到類中是很常見的。為確保SourceContext正確設置,請使用ForContext擴展名:
public TheThing(ILogger logger) {_logger = logger?.ForContext<TheThing>() ?? throw new ArgumentNullException(nameof(_logger)); }HTTP 日志記錄
使用Serilog 請求日志記錄中間件[6]來記錄 HTTP 請求。這會自動包含上面列出的許多 HTTP 屬性并生成以下日志消息:
HTTP POST /payments responded 201 in 1348.6188 ms將以下內(nèi)容添加到您的應用程序啟動中以添加中間件:
public void Configure(IApplicationBuilder app) {app.UseHealthAndMetricsMiddleware();app.UseSerilogRequestLogging();app.UseAuthentication();app.UseMvc(); }請注意,在health 和 metrics 中間件之后添加了 Serilog中間件。這是為了避免每次 AWS 負載均衡器命中您的健康檢查端點時生成日志。
記錄 HTTP 資源
Serilog 中間件默認記錄請求路徑。如果您確實需要查看對應用程序中特定端點的所有請求,如果路徑包含標識符等動態(tài)參數(shù),您可能會遇到挑戰(zhàn)。
為了解決這個問題,記錄資源名稱,在我們的應用程序中,按照慣例,它是Name賦予相應路由的屬性。這是這樣檢索的:
public static string GetMetricsCurrentResourceName(this HttpContext httpContext) {if (httpContext == null)throw new ArgumentNullException(nameof(httpContext));Endpoint endpoint = httpContext.Features.Get<IEndpointFeature>()?.Endpoint;#if NETCOREAPP3_1return endpoint?.Metadata.GetMetadata<EndpointNameMetadata>()?.EndpointName; #elsereturn endpoint?.Metadata.GetMetadata<IRouteValuesAddressMetadata>()?.RouteName; #endif }日志的職責
過度全面的日志記錄不僅會對您的應用程序產(chǎn)生性能影響,而且還會使診斷問題變得更加困難,并增加暴露敏感信息的風險。
Serilog 支持結(jié)構(gòu)化對象輸出,允許將復雜對象作為日志中的參數(shù)傳遞。這應該謹慎使用,如果您的主要目標是對相關屬性進行分組,您最好初始化一個新的匿名對象,這樣您就可以明確哪些信息被推送到日志中。
傾向于使用 Serilog 的診斷上下文功能(下面討論)將日志折疊為單個日志條目。
收集日志
將附加信息推送到您的日志中有助于提供有關特定事件的附加上下文。
標準 Serilog 收集器
您可以使用收集器來豐富應用程序生成的所有日志事件。我們建議使用以下 Serilog 濃縮器:
?日志上下文收集器 - 內(nèi)置于 Serilog,此豐富器可確保添加到日志上下文的[7]任何屬性都被推送到日志事件中?環(huán)境收集器[8]- 使用機器或當前用戶名采集日志
可以使用Enrich.WithSerilog的fluent APILoggerConfiguration或通過您的appsettings.json文件(推薦)指定增強器:
{"Serilog": {"Using": ["Serilog.Sinks.Console"],"MinimumLevel": {"Default": "Information"},"WriteTo": [{"Name": "Console"}],"Enrich": ["FromLogContext","WithMachineName"],"Properties": {"ApplicationName": "Gateway API"}} }全局屬性采集
您還可以全局指定屬性。上面的片段appsettings.json演示了我們通常如何設置ApplicationName屬性。在某些情況下,我們需要在啟動時計算屬性,這可以使用 Fluent API 來完成:
loggerConfiguration.ReadFrom.Configuration(hostContext.Configuration).EnrichWithEventType().Enrich.WithProperty("Version", ReflectionUtils.GetAssemblyVersion<Program>());關聯(lián)日志
為了關聯(lián)屬于同一請求的日志,甚至跨多個應用程序,請CorrelationId向日志添加一個屬性。
在 HTTP 應用程序中,我們通常從HttpContext.TraceIdentifier屬性映射它。這是使用Cko-Correlation-Id標頭在內(nèi)部 API 之間傳遞的。我們使用以下擴展來獲取 _current_correlation ID:
public static string GetCorrelationId(this HttpContext httpContext) {httpContext.Request.Headers.TryGetValue("Cko-Correlation-Id", out StringValues correlationId);return correlationId.FirstOrDefault() ?? httpContext.TraceIdentifier; }請注意,如果應用程序面向用戶,則不應依賴提供的相關 ID 標頭。
為了確保將關聯(lián) ID 推送到每個日志事件中,我們使用以下使用 Serilog 的中間件LogContext(本文稍后將詳細討論):
public class RequestLogContextMiddleware {private readonly RequestDelegate _next;public RequestLogContextMiddleware(RequestDelegate next){_next = next;}public Task Invoke(HttpContext context){using (LogContext.PushProperty("CorrelationId", context.GetCorrelationId())){return _next.Invoke(context);}} }消息模板
日志消息應提供事件的簡短描述。我們通??吹介_發(fā)人員創(chuàng)建過于冗長的消息作為在事件中包含額外數(shù)據(jù)的手段,例如:
_logger.Information("Storing payment state in Couchbase for Payment ID {PaymentId} and current state {State}", paymentId, state);相反,您可以使用ForContext(或本文底部的屬性包豐富器)仍然包含數(shù)據(jù)但具有更簡潔的消息:
_logger.ForContext("PaymentId", paymentId).ForContext("State", state).Information("Storing payment state in Couchbase");消息模板推薦
Fluent風格指南
好的 Serilog 事件使用屬性名稱作為消息中的內(nèi)容來提高可讀性并使事件更緊湊,例如:
_logger.Information("Processed {@Position} in {Elapsed:000} ms.", position, elapsedMs);句子與片段
日志事件消息是片段,而不是句子;為了與使用 Serilog 的其他庫保持一致,請盡可能避免尾隨句點/句號。
模板與消息
Serilog 事件具有關聯(lián)的消息模板,而不是消息。在內(nèi)部,Serilog 解析和緩存每個模板(最多固定大小限制)。將日志方法的字符串參數(shù)視為消息,如下例所示,會降低性能并消耗緩存內(nèi)存。例如,避免:
Log.Information("The time is " + DateTime.Now);而是使用消息屬性:
Log.Information("The time is {Now}", DateTime.Now);除了在日志消息中使用字符串連接/插值的性能開銷之外,它還意味著無法計算一致的事件類型(請參閱事件類型豐富器),從而無法找到特定類型的所有日志。
日志和診斷上下文
Serilog 支持兩種可用于增強日志的上下文感知功能。
日志上下文
LogContext可用于動態(tài)地添加和移除來自周圍“執(zhí)行上下文”性能; 例如,在事務期間寫入的所有消息都可能帶有該事務的 id,等等。
在RequestLogContextMiddleware上面的介紹演示了如何推動CorrelationId請求到LogContext在請求的開始。這可確保該請求中的所有日志都包含該屬性。
更多信息可以在Serilog wiki[9]上找到。
診斷上下文
日志記錄的一個挑戰(zhàn)是上下文并不總是預先知道。例如,在處理 HTTP 請求的過程中,隨著我們通過 HTTP 管道(例如了解用戶的身份)
獲得
額外的上下文。雖然LogContext我們所有人都會在附加信息可用時創(chuàng)建新上下文,但此信息僅在 _subsequent_log 條目中可用。這通常會導致日志數(shù)量增加,只是為了捕獲有關整個請求或操作的所有信息。
診斷上下文提供了一個執(zhí)行上下文(類似于LogContext),其優(yōu)點是可以在其整個生命周期中進行豐富。請求日志中間件然后使用它來豐富最終的“日志完成事件”。這允許我們將許多不同的日志操作折疊為一個日志條目,其中包含來自請求管道中許多點的信息,例如:
SEQ 中的豐富屬性在這里您可以看到,不僅有中間件發(fā)出的 HTTP 屬性,還有應用程序數(shù)據(jù),例如AcquirerId、MerchantName和ResponseCode。這些數(shù)據(jù)點來自請求中的不同點,但通過IDiagnosticContext接口推送到診斷上下文中:
public class HomeController : Controller {readonly IDiagnosticContext _diagnosticContext;public HomeController(IDiagnosticContext diagnosticContext){_diagnosticContext = diagnosticContext ?? throw new ArgumentNullException(nameof(diagnosticContext));}public IActionResult Index(){// The request completion event will carry this property_diagnosticContext.Set("CatalogLoadTime", 1423);return View();}在非 HTTP 應用程序中使用診斷上下文
診斷上下文不限于在 ASP.NET Core 中使用。它也可以以與請求日志中間件非常相似的方式在非 HTTP 應用程序中使用。例如,我們使用它在 SQS 使用者中生成完成日志事件。
配置
Serilog 可以使用 Fluent API 或通過 Microsoft 配置系統(tǒng)進行配置。我們建議使用配置系統(tǒng),因為可以在不發(fā)布應用程序新版本的情況下更改日志配置。
為此,添加Serilog.Settings.Configuration[10]包并按如下方式配置 Serilog:
public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).UseSerilog((hostContext, loggerConfiguration) =>{loggerConfiguration.ReadFrom.Configuration(hostContext.Configuration);}).ConfigureAppConfiguration((hostingContext, config) =>{config.AddEnvironmentVariables(prefix: "FLOW_").AddAwsSecrets();}).ConfigureWebHostDefaults(webBuilder =>{webBuilder.UseStartup<Startup>(); });您現(xiàn)在可以通過任何支持的配置提供程序配置 Serilog。通常我們appsettings.json用于全局設置并通過生產(chǎn)中的環(huán)境變量配置實際接收器(因為我們不想在本地運行時使用我們的遠程日志服務):
{"Serilog": {"Using": ["Serilog.Sinks.Console","Serilog.Sinks.Datadog.Logs"],"MinimumLevel": {"Default": "Information"},"WriteTo": [{"Name": "Console"}],"Enrich": ["FromLogContext","WithMachineName"],"Properties": {"ApplicationName": "Flow API"}} }生產(chǎn)環(huán)境下使用日志
在生產(chǎn)中部署應用程序時,請確保相應地配置日志記錄:
?控制臺日志記錄應限于Error. 在 .NET 中,寫入控制臺是一個阻塞調(diào)用,會對性能產(chǎn)生重大影響[11]。?應為Information及以上配置全局日志記錄。
據(jù)了解,在新項目發(fā)布期間,您可能需要更多信息來建立對解決方案的信心或診斷任何預期的初期問題。與其Information為此升級您的日志條目,不如考慮Debug在有限的時間內(nèi)啟用級別。
從開發(fā)人員那里聽到的一個常見問題是他們?nèi)绾卧谶\行時動態(tài)切換日志級別。雖然這是可能的,但也可以使用藍/綠部署來實現(xiàn)。使用降低的日志級別配置和部署非活動環(huán)境,然后通過加權目標組切換部分或全部流量。
當日志變得不僅僅是日志
日志可以提供對應用程序的大量洞察,并且在許多情況下足以處理日常支持請求或故障排除。然而,在某些情況下,日志可能不能確保你的工作正確,有許多警告信號:
?您發(fā)現(xiàn)自己向非技術用戶開放應用程序日志?日志用于生成應用程序指標?更多信息被“塞進”日志以滿足常見的支持請求或報告要求
在這些情況下,您可能需要為您的產(chǎn)品考慮專用工具。許多團隊開發(fā)了類似“Inspector”的應用程序,將關鍵系統(tǒng)和業(yè)務數(shù)據(jù)聚合在一起,以處理可以提供給非技術利益相關者的 BAU 請求。此外,您可能會發(fā)現(xiàn)需要將應用程序中的數(shù)據(jù)推送到報告和分析工具中。
日志的有效性取決于您記錄的內(nèi)容和未記錄的內(nèi)容。
其他工具和實用程序
在本地使用 Seq
Seq[12]是由 Serilog 的作者創(chuàng)建的免費(供本地使用)日志記錄工具。它提供高級搜索和過濾功能以及對結(jié)構(gòu)化日志數(shù)據(jù)的完全訪問。雖然我們的日志記錄要求現(xiàn)在超出了 Seq 所能提供的范圍,但它仍然是本地測試的一個很好的選擇。
我們通常在 docker 中啟動 Seq 作為單獨的 docker-compose 文件 (?docker-compose-logging.hml) 的一部分:
version: "3.5"services:seq:image: datalust/seq:latestcontainer_name: seqports:- '5341:80'environment:- ACCEPT_EULA=Ynetworks:- gateway-networknetworks: gateway-network:name: gateway-network并配置我們的appsettings.Development.json文件以使用 Seq 接收器:
{"Serilog": {"Using": ["Serilog.Sinks.Console","Serilog.Sinks.Seq"],"MinimumLevel": {"Default": "Debug","Override": {"Microsoft": "Warning"}},"WriteTo": [{"Name": "Console"},{"Name": "Seq","Args": {"serverUrl": "http://localhost:5341","apiKey": "none"}}]} }事件類型收集器
通常我們需要唯一標識相同類型的日志。一些接收器(例如Seq)[13]通過散列消息模板來自動執(zhí)行此操作。為了在其他接收器中復制相同的行為,我們創(chuàng)建了以下使用Murmerhash 算法[14]的收集器[15]:
internal class EventTypeEnricher : ILogEventEnricher {public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory){if (logEvent is null)throw new ArgumentNullException(nameof(logEvent));if (propertyFactory is null)throw new ArgumentNullException(nameof(propertyFactory));Murmur32 murmur = MurmurHash.Create32();byte[] bytes = Encoding.UTF8.GetBytes(logEvent.MessageTemplate.Text);byte[] hash = murmur.ComputeHash(bytes);string hexadecimalHash = BitConverter.ToString(hash).Replace("-", "");LogEventProperty eventId = propertyFactory.CreateProperty("EventType", hexadecimalHash);logEvent.AddPropertyIfAbsent(eventId);} }屬性包收集器
如果您想向日志事件添加多個屬性,請使用PropertyBagEnricher:
public class PropertyBagEnricher : ILogEventEnricher {private readonly Dictionary<string, Tuple<object, bool>> _properties;/// <summary>/// Creates a new <see cref="PropertyBagEnricher" /> instance./// </summary>public PropertyBagEnricher(){_properties = new Dictionary<string, Tuple<object, bool>>(StringComparer.OrdinalIgnoreCase);}/// <summary>/// Enriches the <paramref name="logEvent" /> using the values from the property bag./// </summary>/// <param name="logEvent">The log event to enrich.</param>/// <param name="propertyFactory">The factory used to create the property.</param>public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory){foreach (KeyValuePair<string, Tuple<object, bool>> prop in _properties){logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(prop.Key, prop.Value.Item1, prop.Value.Item2));}}/// <summary>/// Add a property that will be added to all log events enriched by this enricher./// </summary>/// <param name="key">The property key.</param>/// <param name="value">The property value.</param>/// <param name="destructureObject">/// Whether to destructure the value. See https://github.com/serilog/serilog/wiki/Structured-Data/// </param>/// <returns>The enricher instance, for chaining Add operations together.</returns>public PropertyBagEnricher Add(string key, object value, bool destructureObject = false){if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key));if (!_properties.ContainsKey(key)) _properties.Add(key, Tuple.Create(value, destructureObject));return this;} }用法:
_logger.ForContext(new PropertyBagEnricher().Add("ResponseCode", response?.ResponseCode).Add("EnrollmentStatus", response?.Enrolled)).Warning("Malfunction when processing 3DS enrollment verification");收集請求日志
Serilog 請求日志記錄中間件允許提供一個函數(shù),該函數(shù)可用于將來自 HTTP 請求的附加信息添加到完成日志事件。我們使用它來記錄ClientIP,UserAgent和Resource屬性:
public static class LogEnricher {/// <summary>/// Enriches the HTTP request log with additional data via the Diagnostic Context/// </summary>/// <param name="diagnosticContext">The Serilog diagnostic context</param>/// <param name="httpContext">The current HTTP Context</param>public static void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpContext httpContext){diagnosticContext.Set("ClientIP", httpContext.Connection.RemoteIpAddress.ToString());diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"].FirstOrDefault());diagnosticContext.Set("Resource", httpContext.GetMetricsCurrentResourceName());} }用法
app.UseSerilogRequestLogging(opts=> opts.EnrichDiagnosticContext = LogEnricher.EnrichFromRequest);? 2021 Ben Foster https://benfoster.io/
References
[1]?Serilog:?https://serilog.net/
[2]?接收器:?https://github.com/serilog?q=sinks&type=&language=
[3]?Serilog Timings:?https://github.com/nblumhardt/serilog-timings
[4]?Serilog 請求日志記錄:?https://github.com/serilog/serilog-aspnetcore
[5]?Serilog時序庫:?https://github.com/nblumhardt/serilog-timings
[6]?Serilog 請求日志記錄中間件:?https://github.com/serilog/serilog-aspnetcore
[7]?日志上下文的:?https://github.com/serilog/serilog/wiki/Enrichment
[8]?環(huán)境收集器:?https://github.com/serilog/serilog-enrichers-environment
[9]?Serilog wiki:?https://github.com/serilog/serilog/wiki/Enrichment#the-logcontext
[10]?Serilog.Settings.Configuration:?https://github.com/serilog/serilog-settings-configuration
[11]?性能產(chǎn)生重大影響:?https://weblog.west-wind.com/posts/2018/Dec/31/Dont-let-ASPNET-Core-Default-Console-Logging-Slow-your-App-down
[12]?Seq:?https://datalust.co/seq
[13]?Seq):?https://datalust.co/seq
[14]?Murmerhash 算法:?https://github.com/darrenkopp/murmurhash-net
[15]?收集器:?https://github.com/darrenkopp/murmurhash-net
總結(jié)
以上是生活随笔為你收集整理的Serilog 最佳实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET 6 Preview 6 Rel
- 下一篇: TIOBE 发布 8 月编程语言榜单:C