Serilog 最佳实践

概述

Serilog[1]是 Microsoft .NET 的结构化日志记录库,并已成为Checkout.com 上NET 的首选日志记录库。它支持各种日志记录目的地(称为接收器[2])包从标准控制台和基于文件的接收器到日志服务,如 Datadog。

本指南最初是我们工程手册中的一篇文章,在收到内部积极反馈后,我决定在我的博客上发布它。

内容

1.标准日志属性2.日志记录基础知识1.记录一切2.选择合适的日志记录级别3.定时操作源4.上下文5.HTTP 日志记录6.日志的职责3.日志内容的采集1.Serilog标准采集方法2.通过全局属性采集日志4.关联日志5.消息模板1.消息模板推荐6.日志和诊断上下文1.日志上下文2.诊断上下文7.配置8.生产日志1.当日志变得不仅仅是日志9.其他工具和实用程序1.在本地使用 Seq2.按日志类型记录日志3.按属性块记录日志4.按请求记录日志

标准日志属性

标准化日志事件属性使您能够充分利用日志搜索和分析工具。在适用的情况下使用以下属性:

ApplicationName 生成日志事件的应用程序的名称
ClientIP 发出请求的客户端的 IP 地址
CorrelationId 可用于跨多个应用程序边界跟踪请求的 ID
Elapsed 操作完成所用的时间(以毫秒为单位)
EventType 用于确定消息类型的消息模板的哈希值
MachineName 运行应用程序的机器的名称
Outcome 手术的结果
RequestMethod HTTP 请求方法,例如 POST
RequestPath HTTP 请求路径
SourceContext 日志源自的组件/类的名称
StatusCode HTTP 响应状态码
UserAgent HTTP 用户代理
Version 正在运行的应用程序的版本

上面的很多属性都来自于 Serilog 自己的扩展,例如Serilog Timings[3](用于计时操作)和Serilog 请求日志记录[4]

日志记录基础知识

记录一切

通常,记录所有可以深入了解您的应用程序和用户行为的内容,例如:

•代码中的主要分支点•遇到错误或意外值时•任何 IO 或资源密集型操作•重大领域事件•请求失败和重试•耗时的批处理操作的开始和结束

选择合适的日志记录级别

对您的日志记录要慷慨,但对您的日志记录级别要严格。在几乎所有情况下,您的日志级别都应该是Debug. 使用Information的日志事件,将在生产中需要确定运行状态或应用程序的正确性,WarningError突发事件,如异常。

请注意,该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

将以下内容添加到您的应用程序启动中以添加中间件:

public void Configure(IApplicationBuilder app)
{app.UseHealthAndMetricsMiddleware();app.UseSerilogRequestLogging();app.UseAuthentication();app.UseMvc();
}

请注意,在health 和 metrics 中间件之后添加了 Serilog中间件。这是为了避免每次 AWS 负载均衡器命中您的健康检查端点时生成日志。

记录 HTTP 资源

Serilog 中间件默认记录请求路径。如果您确实需要查看对应用程序中特定端点的所有请求,如果路径包含标识符等动态参数,您可能会遇到挑战。

为了解决这个问题,记录资源名称,在我们的应用程序中,按照惯例,它是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
}

日志的职责

过度全面的日志记录不仅会对您的应用程序产生性能影响,而且还会使诊断问题变得更加困难,并增加暴露敏感信息的风险。

Serilog 支持结构化对象输出,允许将复杂对象作为日志中的参数传递。这应该谨慎使用,如果您的主要目标是对相关属性进行分组,您最好初始化一个新的匿名对象,这样您就可以明确哪些信息被推送到日志中。

倾向于使用 Serilog 的诊断上下文功能(下面讨论)将日志折叠为单个日志条目。

收集日志

将附加信息推送到您的日志中有助于提供有关特定事件的附加上下文。

标准 Serilog 收集器

您可以使用收集器来丰富应用程序生成的所有日志事件。我们建议使用以下 Serilog 浓缩器:

•日志上下文收集器 - 内置于 Serilog,此丰富器可确保添加到日志上下文的[7]任何属性都被推送到日志事件中•环境收集器[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>());

关联日志

为了关联属于同一请求的日志,甚至跨多个应用程序,请CorrelationId向日志添加一个属性。

在 HTTP 应用程序中,我们通常从HttpContext.TraceIdentifier属性映射它。这是使用Cko-Correlation-Id标头在内部 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 标头。

为了确保将关联 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);}}
}

消息模板

日志消息应提供事件的简短描述。我们通常看到开发人员创建过于冗长的消息作为在事件中包含额外数据的手段,例如:

_logger.Information("Storing payment state in Couchbase for Payment ID {PaymentId} and current state {State}", paymentId, state);

相反,您可以使用ForContext(或本文底部的属性包丰富器)仍然包含数据但具有更简洁的消息:

_logger.ForContext("PaymentId", paymentId).ForContext("State", state).Information("Storing payment state in Couchbase");

消息模板推荐

Fluent风格指南

好的 Serilog 事件使用属性名称作为消息中的内容来提高可读性并使事件更紧凑,例如:

_logger.Information("Processed {@Position} in {Elapsed:000} ms.", position, elapsedMs);

句子与片段

日志事件消息是片段,而不是句子;为了与使用 Serilog 的其他库保持一致,请尽可能避免尾随句点/句号。

模板与消息

Serilog 事件具有关联的消息模板,而不是消息。在内部,Serilog 解析和缓存每个模板(最多固定大小限制)。将日志方法的字符串参数视为消息,如下例所示,会降低性能并消耗缓存内存。例如,避免:

Log.Information("The time is " + DateTime.Now);

而是使用消息属性:

Log.Information("The time is {Now}", DateTime.Now);

除了在日志消息中使用字符串连接/插值的性能开销之外,它还意味着无法计算一致的事件类型(请参阅事件类型丰富器),从而无法找到特定类型的所有日志。

日志和诊断上下文

Serilog 支持两种可用于增强日志的上下文感知功能。

日志上下文

LogContext可用于动态地添加和移除来自周围“执行上下文”性能; 例如,在事务期间写入的所有消息都可能带有该事务的 id,等等。

RequestLogContextMiddleware上面的介绍演示了如何推动CorrelationId请求到LogContext在请求的开始。这可确保该请求中的所有日志都包含该属性。

更多信息可以在Serilog wiki[9]上找到。

诊断上下文

日志记录的一个挑战是上下文并不总是预先知道。例如,在处理 HTTP 请求的过程中,随着我们通过 HTTP 管道(例如了解用户的身份)

获得

额外的上下文。虽然LogContext我们所有人都会在附加信息可用时创建新上下文,但此信息仅在 _subsequent_log 条目中可用。这通常会导致日志数量增加,只是为了捕获有关整个请求或操作的所有信息。

诊断上下文提供了一个执行上下文(类似于LogContext),其优点是可以在其整个生命周期中进行丰富。请求日志中间件然后使用它来丰富最终的“日志完成事件”。这允许我们将许多不同的日志操作折叠为一个日志条目,其中包含来自请求管道中许多点的信息,例如:

SEQ 中的丰富属性

在这里您可以看到,不仅有中间件发出的 HTTP 属性,还有应用程序数据,例如AcquirerIdMerchantNameResponseCode。这些数据点来自请求中的不同点,但通过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 配置系统进行配置。我们建议使用配置系统,因为可以在不发布应用程序新版本的情况下更改日志配置。

为此,添加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>();  });

您现在可以通过任何支持的配置提供程序配置 Serilog。通常我们appsettings.json用于全局设置并通过生产中的环境变量配置实际接收器(因为我们不想在本地运行时使用我们的远程日志服务):

{"Serilog": {"Using": ["Serilog.Sinks.Console","Serilog.Sinks.Datadog.Logs"],"MinimumLevel": {"Default": "Information"},"WriteTo": [{"Name": "Console"}],"Enrich": ["FromLogContext","WithMachineName"],"Properties": {"ApplicationName": "Flow API"}}
}

生产环境下使用日志

在生产中部署应用程序时,请确保相应地配置日志记录:

•控制台日志记录应限于Error. 在 .NET 中,写入控制台是一个阻塞调用,会对性能产生重大影响[11]。•应为Information及以上配置全局日志记录。

据了解,在新项目发布期间,您可能需要更多信息来建立对解决方案的信心或诊断任何预期的初期问题。与其Information为此升级您的日志条目,不如考虑Debug在有限的时间内启用级别。

从开发人员那里听到的一个常见问题是他们如何在运行时动态切换日志级别。虽然这是可能的,但也可以使用蓝/绿部署来实现。使用降低的日志级别配置和部署非活动环境,然后通过加权目标组切换部分或全部流量。

当日志变得不仅仅是日志

日志可以提供对应用程序的大量洞察,并且在许多情况下足以处理日常支持请求或故障排除。然而,在某些情况下,日志可能不能确保你的工作正确,有许多警告信号:

•您发现自己向非技术用户开放应用程序日志•日志用于生成应用程序指标•更多信息被“塞进”日志以满足常见的支持请求或报告要求

在这些情况下,您可能需要为您的产品考虑专用工具。许多团队开发了类似“Inspector”的应用程序,将关键系统和业务数据聚合在一起,以处理可以提供给非技术利益相关者的 BAU 请求。此外,您可能会发现需要将应用程序中的数据推送到报告和分析工具中。

日志的有效性取决于您记录的内容和未记录的内容。

其他工具和实用程序

在本地使用 Seq

Seq[12]是由 Serilog 的作者创建的免费(供本地使用)日志记录工具。它提供高级搜索和过滤功能以及对结构化日志数据的完全访问。虽然我们的日志记录要求现在超出了 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]通过散列消息模板来自动执行此操作。为了在其他接收器中复制相同的行为,我们创建了以下使用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 请求日志记录中间件允许提供一个函数,该函数可用于将来自 HTTP 请求的附加信息添加到完成日志事件。我们使用它来记录ClientIP,UserAgentResource属性:

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] 环境收集器: 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] 性能产生重大影响: 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

Serilog 最佳实践相关推荐

  1. ABP vnext模块化架构的最佳实践的实现

    在上一篇文章<手把手教你用Abp vnext构建API接口服务>中,我们用ABP vnext实现了WebAPI接口服务,但是并非ABP模块化架构的最佳实践.我本身也在学习ABP,我认为AB ...

  2. ASP.NET Core Web API 最佳实践指南

    原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide 介绍 当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求. 但是,你难 ...

  3. 编写高性能Java代码的最佳实践

    编写高性能Java代码的最佳实践 摘要:本文首先介绍了负载测试.基于APM工具的应用程序和服务器监控,随后介绍了编写高性能Java代码的一些最佳实践.最后研究了JVM特定的调优技巧.数据库端的优化和架 ...

  4. 提示和技巧:光线跟踪最佳实践

    提示和技巧:光线跟踪最佳实践 Tips and Tricks: Ray Tracing Best Practices 本文介绍了在游戏和其他实时图形应用程序中实现光线跟踪的最佳实践.我们尽可能简短地介 ...

  5. SQL Server 最佳实践分析器使用小结

    Best Practices Analyzer Tool for Microsoft SQL Server 2000是Microsoft SQL Server开发团队开发的一个数据库管理工具,可以让你 ...

  6. 使用ADO.NET 的最佳实践(zz)

    数据访问:使用 ADO.NET 的最佳实践(ADO.NET 技术文档) 发布日期: 4/1/2004 | 更新日期: 4/1/2004 摘要:编写 Microsoft ADO.NET 代码的最佳实践, ...

  7. 何崚谈阿里巴巴前端性能优化最佳实践

    转载:http://www.infoq.com/cn/interviews/hl-alibaba-front-end-performance-optimization 大家好,我现在在阿里巴巴园区采访 ...

  8. 卡片式设计的最佳实践分享

    2017-02-17 三达不留点gpj CocoaChina 卡片本质上是一个简单的信息容器,信息量有限,但设计干净整洁.现如今,在保证界面具有优秀可用性的同时,卡片式的设计甚至成为了平衡界面美学的默 ...

  9. GMTC 大前端时代前端监控的最佳实践

    摘要: 今天我分享的内容分成三个部分: 第一部分是"大前端时代前端监控新的变化", 讲述这些年来,前端监控一些新的视角以及最前沿的一些思考. 第二部分"前端监控的最佳实践 ...

最新文章

  1. 逆向工程 sql_mybatis逆向生成工具,真的很好用!
  2. Java黑皮书课后题第10章:10.2(BMI类)将下面的新构造方法加入BMI类中
  3. JavaFX 学习笔记——窗口与控件
  4. cf1552F. Telepanting
  5. 2020年中国网络安全市场全景图
  6. K-th Closest Distance
  7. java实现中文大写金额转小写数字
  8. 纵横公路造价软件学习_纵横公路造价软件教程
  9. 差异表达基因变化倍数_【其他】差异基因筛选方法
  10. linux 加入域 命令,Linux 加入域的最终完整版修改
  11. 主机无法复制文件到ubuntu虚拟机的解决方法
  12. 全场景效能平台猪齿鱼 VS Jira
  13. Diff 算法的执行过程数据响应式模拟routerSnabbdom
  14. web渗透学习目录-新手打开思路
  15. 智能人物画像综合分析系统——Day6
  16. LINUX进程内存占用情况如何查看的方法
  17. eoLinker-AMS接口管理系统 项目管理教程
  18. 如何获取系统当前时间
  19. 友元函数,友元类,类模板
  20. python爬虫能赚钱吗-个人利用Python爬虫技术怎么挣钱-10万被动收入

热门文章

  1. c# 获取word表格中的内容_Java 获取、删除Word文本框中的表格
  2. html-盒子模型及pading和margin相关
  3. 01: 实现注册登录功能
  4. POM思想__首页页面元素查找、功能点实现进行封装
  5. sublime-text3按tab跳出括号
  6. FFT算法的完整DSP实现(转)
  7. 隐藏UITableView当没有数据或数据不够的时候出现的分割线.
  8. 轻松搭建Google ADK开发环境
  9. Sharepoint学习笔记---如何在Sharepoint2010网站中整合Crystal Report水晶报表(显示数据 二)...
  10. SQL进阶提升(疑惑篇order by)-学习sql server2005 step by step(十一)