零基础写框架(3): Serilog.NET 中的日志使用技巧
.NET 中的日志使用技巧
Serilog
Serilog 是 .NET 社区中使用最广泛的日志框架,所以笔者使用一个小节单独讲解使用方法。
示例项目在 Demo2.Console 中。
创建一个控制台程序,引入两个包:
Serilog.Sinks.Console
Serilog.Sinks.File
除此之外,还有
Serilog.Sinks.Elasticsearch
、Serilog.Sinks.RabbitMQ
等。Serilog 提供了用于将日志事件以各种格式写入存储的接收器。下面列出的许多接收器都是由更广泛的 Serilog 社区开发和支持的;https://github.com/serilog/serilog/wiki/Provided-Sinks
可以直接使用代码配置 Serilog:
private static Serilog.ILogger GetLogger()
{
const string LogTemplate = "{SourceContext} {Scope} {Timestamp:HH:mm} [{Level}] {Message:lj} {Properties:j} {NewLine}{Exception}";
var logger = new LoggerConfiguration()
.Enrich.WithMachineName()
.Enrich.WithThreadId()
.Enrich.FromLogContext()
#if DEBUG
.MinimumLevel.Debug()
#else
.MinimumLevel.Information()
#endif
.WriteTo.Console(outputTemplate: LogTemplate)
.WriteTo.File("log.txt", rollingInterval: RollingInterval.Day, outputTemplate: LogTemplate)
.CreateLogger();
return logger;
}
如果想从配置文件中加载,添加 Serilog.Settings.Configuration:
private static Serilog.ILogger GetJsonLogger()
{
IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile(path: "serilog.json", optional: true, reloadOnChange: true)
.Build();
if (configuration == null)
{
throw new ArgumentNullException($"未能找到 serilog.json 日志配置文件");
}
var logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
return logger;
}
serilog.json 配置文件示例:
{
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": {
"Default": "Debug"
},
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "{SourceContext} {Scope} {Timestamp:HH:mm} [{Level}] {Message:lj} {Properties:j} {NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/log-.txt",
"rollingInterval": "Day",
"outputTemplate": "{SourceContext} {Scope} {Timestamp:HH:mm} [{Level}] {Message:lj} {Properties:j} {NewLine}{Exception}"
}
}
]
}
}
依赖注入 Serilog。
引入 Serilog.Extensions.Logging
包。
private static Microsoft.Extensions.Logging.ILogger InjectLogger()
{
var logger = GetJsonLogger();
var ioc = new ServiceCollection();
ioc.AddLogging(builder => builder.AddSerilog(logger: logger, dispose: true));
var loggerProvider = ioc.BuildServiceProvider().GetRequiredService<ILoggerProvider>();
return loggerProvider.CreateLogger("Program");
}
最后,使用不同方式配置 Serilog 日志,然后启动程序打印日志。
static void Main()
{
var log1 = GetLogger();
log1.Debug("溪源More、痴者工良");
var log2 = GetJsonLogger();
log2.Debug("溪源More、痴者工良");
var log3 = InjectLogger();
log3.LogDebug("溪源More、痴者工良");
}
20:50 [Debug] 溪源More、痴者工良 {"MachineName": "WIN-KQDULADM5LA", "ThreadId": 1}
20:50 [Debug] 溪源More、痴者工良 {"MachineName": "WIN-KQDULADM5LA", "ThreadId": 1}
20:50 [Debug] 溪源More、痴者工良 {"MachineName": "WIN-KQDULADM5LA", "ThreadId": 1}
在 ASP.NET Core 中使用日志
示例项目在 Demo2.Api 中。
新建一个 ASP.NET Core API 新项目,引入 Serilog.AspNetCore
包。
在 Program 中添加代码注入 Serilog 。
var builder = WebApplication.CreateBuilder(args);
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.CreateLogger();
builder.Host.UseSerilog(Log.Logger);
//builder.Host.UseSerilog();
将前面示例中的 serilog.json
文件内容复制到 appsettings.json 中。
启动程序后,尝试访问 API 接口,会打印示例如下的日志:
Microsoft.AspNetCore.Hosting.Diagnostics 20:32 [Information] Request finished HTTP/1.1 GET http://localhost:5148/WeatherForecast - - - 200 - application/json;+charset=utf-8 1029.4319ms {"ElapsedMilliseconds": 1029.4319, "StatusCode": 200, "ContentType": "application/json; charset=utf-8", "ContentLength": null, "Protocol": "HTTP/1.1", "Method": "GET", "Scheme": "http", "Host": "localhost:5148", "PathBase": "", "Path": "/WeatherForecast", "QueryString": "", "EventId": {"Id": 2}, "RequestId": "0HMOONQO5ONKU:00000003", "RequestPath": "/WeatherForecast", "ConnectionId": "0HMOONQO5ONKU"}
如果需要为请求上下文添加一些属性信息,可以添加一个中间件,示例如下:
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("TraceId", httpContext.TraceIdentifier);
};
});
HTTP GET /WeatherForecast responded 200 in 181.9992 ms {"TraceId": "0HMSD1OUG2DHG:00000003" ... ...
对请求上下文添加属性信息,比如当前请求的用户信息,在本次请求作用域中使用日志打印信息时,日志会包含这些上下文信息,这对于分析日志还有帮助,可以很容易分析日志中那些条目是同一个上下文。在微服务场景下,会使用 ElasticSearch 等日志存储引擎查询分析日志,如果在日志中添加了相关的上下文属性,那么在分析日志时可以通过对应的属性查询出来,分析日志时可以帮助排除故障。
如果需要打印 http 的请求和响应日志,我们可以使用 ASP.NET Core 自带的 HttpLoggingMiddleware 中间件。
首先注入请求日志拦截服务。
builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.All;
// 避免打印大量的请求和响应内容,只打印 4kb
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;
});
通过组合 HttpLoggingFields 枚举,可以配置中间件打印 Request、Query、HttpMethod、Header、Response 等信息。
可以将HttpLogging 中间件放在 Swagger、Static 之后,这样的话可以避免打印哪些用处不大的请求,只保留 API 请求相关的日志。
app.UseHttpLogging();
HttpLoggingMiddleware 中的日志模式是以 Information 级别打印的,在项目上线之后,如果每个请求都被打印信息的话,会降低系统性能,因此我们可以在配置文件中覆盖配置,避免打印普通的日志。
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
上下文属性和作用域
示例项目在 Demo2.ScopeLog 中。
日志范围注意事项
Microsoft.Extensions.Logging.Abstractions 提供 BeginScopeAPI,可用于添加任意属性以记录特定代码区域内的事件。
解释其作用
API 有两种形式:
IDisposable BeginScope<TState>(TState state)
IDisposable BeginScope(this ILogger logger, string messageFormat, params object[] args)
使用如下的模板:
{SourceContext} {Timestamp:HH:mm} [{Level}] (ThreadId:{ThreadId}) {Message}{NewLine}{Exception} {Scope}
使用示例:
static void Main()
{
var logger = GetLogger();
using (logger.BeginScope("Checking mail"))
{
// Scope is "Checking mail"
logger.LogInformation("Opening SMTP connection");
using (logger.BeginScope("Downloading messages"))
{
// Scope is "Checking mail" -> "Downloading messages"
logger.LogError("Connection interrupted");
}
}
}
而在 Serilog 中,除了支持上述接口外,还通过 LogContext 提供了在日志中注入上下文属性的方法。其作用是添加属性之后,使得在其作用域之内打印日志时,日志会携带这些上下文属性信息。
using (LogContext.PushProperty("Test", 1))
{
// Process request; all logged events will carry `RequestId`
Log.Information("{Test} Adding {Item} to cart {CartId}", 1,1);
}
嵌套复杂一些:
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");
}
当需要设置大量属性时,下面的方式会比较麻烦;
using (LogContext.PushProperty("Test1", 1))
using (LogContext.PushProperty("Test2", 2))
{
}
例如在 ASP.NET Core 中间件中,我们可以批量添加:
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var enrichers = new List<ILogEventEnricher>();
if (!string.IsNullOrEmpty(correlationId))
{
enrichers.Add(new PropertyEnricher(_options.EnricherPropertyNames.CorrelationId, correlationId));
}
using (LogContext.Push(enrichers.ToArray()))
{
await next(context);
}
}
在业务系统中,可以通过在中间件获取 Token 中的用户信息,然后注入到日志上下文中,这样打印出来的日志,会携带用户信息。
非侵入式日志
非侵入式的日志有多种方法,比如 ASP.NET Core 中间件管道,或者使用 AOP 框架。
这里可以使用笔者开源的 CZGL.AOP 框架,Nuget 中可以搜索到。
示例项目在 Demo2.AopLog 中。
有一个类型,我们需要在执行 SayHello 之前和之后打印日志,将参数和返回值记录下来。
public class Hello
{
public virtual string SayHello(string content)
{
var str = $"Hello,{content}";
return str;
}
}
编写统一的切入代码,这些代码将在函数被调用时执行。
Before
会在被代理的方法执行前或被代理的属性调用时生效,你可以通过 AspectContext
上下文,获取、修改传递的参数。
After 在方法执行后或属性调用时生效,你可以通过上下文获取、修改返回值。
public class LogAttribute : ActionAttribute
{
public override void Before(AspectContext context)
{
Console.WriteLine($"{context.MethodInfo.Name} 函数被执行前");
foreach (var item in context.MethodValues)
Console.WriteLine(item.ToString());
}
public override object After(AspectContext context)
{
Console.WriteLine($"{context.MethodInfo.Name} 函数被执行后");
Console.WriteLine(context.MethodResult.ToString());
return context.MethodResult;
}
}
改造 Hello 类,代码如下:
[Interceptor]
public class Hello
{
[Log]
public virtual string SayHello(string content)
{
var str = $"Hello,{content}";
return str;
}
}
然后创建代理类型:
static void Main(string[] args)
{
Hello hello = AopInterceptor.CreateProxyOfClass<Hello>();
hello.SayHello("any one");
Console.Read();
}
启动程序,会输出:
SayHello 函数被执行前
any one
SayHello 函数被执行后
Hello,any one
你完全不需要担心 AOP 框架会给你的程序带来性能问题,因为 CZGL.AOP 框架采用 EMIT 编写,并且自带缓存,当一个类型被代理过,之后无需重复生成。
CZGL.AOP 可以通过 .NET Core 自带的依赖注入框架和 Autofac 结合使用,自动代理 CI 容器中的服务。这样不需要 AopInterceptor.CreateProxyOfClass
手动调用代理接口。
CZGL.AOP 代码是开源的,可以参考笔者另一篇博文:
https://www.cnblogs.com/whuanle/p/13160139.html
零基础写框架(3): Serilog.NET 中的日志使用技巧的更多相关文章
- 【JAVA零基础入门系列】Day5 Java中的运算符
运算符,顾名思义就是用于运算的符号,比如最简单的+-*/,这些运算符可以用来进行数学运算,举个最简单的栗子: 已知长方形的长为3cm,高为4cm,求长方形的面积. 好,我们先新建一个项目,命名为Rec ...
- 【JAVA零基础入门系列】Day11 Java中的类和对象
今天要说的是Java中两个非常重要的概念--类和对象. 什么是类,什么又是对象呢?类是对特定集合的概括描述,比如,人,这个类,外观特征上,有名字,有年龄,能说话,能吃饭等等,这是我们作为人类的相同特征 ...
- 【JAVA零基础入门系列】Day9 Java中的那个大数值
什么是大数值?用脚趾头想也知道,当然是"大"的数值(233).Java中有两个用于表示大数值的类,BigInteger和BigDecimal,那到底能表示多大的数值呢?理论上,可以 ...
- 【JAVA零基础入门系列】Day10 Java中的数组
什么是数组?顾名思义,就是数据的组合,把一些相同类型的数放到一组里去. 那为什么要用数组呢?比如需要统计全班同学的成绩的时候,如果给班上50个同学的成绩信息都命名一个变量进行存储,显然不方便,而且在做 ...
- 零基础写python爬虫之使用Scrapy框架编写爬虫
网络爬虫,是在网上进行数据抓取的程序,使用它能够抓取特定网页的HTML数据.虽然我们利用一些库开发一个爬虫程序,但是使用框架可以大大提高效率,缩短开发时间.Scrapy是一个使用Python编写的,轻 ...
- 【转】零基础写Java知乎爬虫之进阶篇
转自:脚本之家 说到爬虫,使用Java本身自带的URLConnection可以实现一些基本的抓取页面的功能,但是对于一些比较高级的功能,比如重定向的处理,HTML标记的去除,仅仅使用URLConnec ...
- salesforce零基础学习(九十)项目中的零碎知识点小总结(三)
本次的内容其实大部分人都遇到过,也知道解决方案.但是因为没有牢记于心,导致问题再次出现还是花费了一点时间去排查了原因.在此记录下来,好记性不如烂笔头,争取下次发现类似的现象可以直接就知道原因.废话少说 ...
- java零基础之--JDK安装篇
---恢复内容开始--- 很多零基础学习者在开始学习java中很难理解JDK的安装和配置,以下是基于Windows 7 的安装配置流程(Windows 10类似) 1. 在安装之前我们先了解几个名词: ...
- Java秘诀!零基础怎样快速学习Java?
对于零基础想学Java的朋友,其实一开始最应该做的就是定好学习目标和端正学习态度,切记不要三天打鱼两天晒网! 首先你是零基础,现在急需把Java学好,在保证学习质量的同时,用最短的时间学好Java应该 ...
- [零基础学IoT Pwn] 环境搭建
[零基础学IoT Pwn] 环境搭建 0x00 前言 这里指的零基础其实是我们在实战中遇到一些基础问题,再相应的去补充学习理论知识,这样起码不会枯燥. 本系列主要是利用网上已知的IoT设备(路由器)漏 ...
随机推荐
- AI圈内卷?天池团聚请来专家集体“问诊”
简介: 近期杭州云栖大会上出现了一个"数据博物馆",最吸引眼球的"展品",竟是行业大规模开源数据集.不仅数量多达上百个,还覆盖零售.文娱.工业.医疗.自然科学 ...
- Spring Boot参数校验以及分组校验的使用
简介: 做web开发基本上每个接口都要对参数进行校验,如果参数比较少,还比较容易处理,一但参数比较多了的话代码中就会出现大量的if-else语句.虽然这种方式简单直接,但会大大降低开发效率和代码可读性 ...
- [Go] 浅谈 Golang struct 与 PHP class 的相似
Golang 中的 struct 与 PHP 的 class 在使用方式上差不多. struct 中的成员可以类比 class 中的属性,struct 中的成员函数可以类比 class 中的方法. 对 ...
- linux文本三剑客之grep及正则表达式详解
linux文本三剑客之grep及正则表达式详解 目录 linux文本三剑客之grep及正则表达式详解 1. grep命令详解 2. 正则表达式 2.1 基本正则表达式 2.2 扩展正则表达式 1. g ...
- C#.Net筑基-运算符🔣Family
C#运算符 内置了丰富的运算符操作类型,使用方便,极大的简化了编码,同时还支持多种运算符重载机制,让自定义的类型也能支持运算符行为. 01.运算符概览 运算符分类 描述 数学运算 基础的加减乘除,及+ ...
- margin-bottom:-1px无效的问题
在实现tab的时候,margin-bottom:-1px无效的问题 active的tab项,要指定他的border-top, 如: border-top: 1px solid #fff; border ...
- CF933-Div3 大致思路+题解
\(Rank\) A - Rudolf and the Ticket 纯水题 暴力枚举直接过 $code$ #include<bits/stdc++.h> #define fo(x,y,z ...
- 2D空间中比较两三角形相交与包含
在处理UV重叠.CPU的ZFighting检测时会遇到2D空间中的三角形相交问题, 网上普遍是3D空间的相交解法,因此写本文研究下,不过虽然实现了需求, 但用的方法比较暴力. 效果如图: (鼠标拖动区 ...
- NFS共享文件
NFS共享文件 服务端 安装NFS [root@localhost www] yum -y install nfs-utils rpcbind 创建需要共享的文件夹share [root@localh ...
- opensips开启lua支持
操作系统 :CentOS 7.6_x64 opensips版本 :2.4.9 lua版本:5.1 今天整理下CentOS7环境下opensips2.4.9的lua模块笔记及使用示例,并提供运行效果截图 ...