Serilog是一个很棒的 3rd 方库,用于在我们的 ASP.NET 核心应用程序中进行结构化日志记录。结构化日志记录是生成易于阅读和过滤的日志的关键。
使用 SQL Server 作为日志目的地,允许我们利用 SQL 查询的强大功能进行日志过滤。如果我们的应用程序已经在使用 SQL Server,它可能是一个不错的选择。
那么,我们如何在 ASP.NET Core 2.0 中实现 Serilog SQL 日志……
首先我们需要引入以下nuget包:
- Serilog.AspNetCore
- Serilog.Settings.Configuration
- Serilog.Sinks.MSSqlServer
接下来,我们需要更改在Program.cs. 开头的 3 行Main()告诉程序使用 Serilog 作为记录器并从appsettings.json.
public class Program
{
public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
.Build();
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(Configuration)
.CreateLogger();
try
{
Log.Information("Getting the motors running...");
BuildWebHost(args).Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseConfiguration(Configuration)
.UseSerilog()
.Build();
}
然后可以将 Serilog 配置为使用 SQL Server 作为 中的目标appSettings.json,以及最低日志记录级别。我们需要指定日志数据库的连接字符串和将数据记录到的表名。
{
...
"Serilog": {
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "MSSqlServer",
"Args": {
"connectionString": "<our connection string>",
"tableName": "Log"
}
}
]
},
...
}
我们可以让 SerilogLog自动创建我们的表,但让我们自己做,这样我们就可以控制我们的模式。例如,我们希望Properties列基于xml数据类型,以便我们可以查询它(serilog 将其创建为nvarchar)。
下面是创建Log表的TSQL 脚本:
CREATE TABLE [Log] (
[Id] int IDENTITY(1,1) NOT NULL,
[Message] nvarchar(max) NULL,
[MessageTemplate] nvarchar(max) NULL,
[Level] nvarchar(128) NULL,
[TimeStamp] datetimeoffset(7) NOT NULL,
[Exception] nvarchar(max) NULL,
[Properties] xml NULL,
[LogEvent] nvarchar(max) NULL
CONSTRAINT [PK_Log]
PRIMARY KEY CLUSTERED ([Id] ASC)
)
然后我们可以在我们的代码中写入日志,记录{@object}在消息模板中使用的结构化对象。下面是用于获取和返回记录(在本例中为联系人)的 Web API 操作方法。我们记录何时从缓存/数据库中获取联系人以及何时在缓存中设置联系人。
[HttpGet("{contactId}")]
public async Task GetById(Guid contactId)
{
// Initialise the contact that is going to be returned
Contact contact = null;
// Get the requested ETag
string requestETag = "";
if (Request.Headers.ContainsKey("If-None-Match"))
{
requestETag = Request.Headers["If-None-Match"].First();
if (!string.IsNullOrEmpty(requestETag))
{
// The client has supplied an ETag, so, get this version of the contact from our cache
// Construct the key for the cache which includes the entity type (i.e. "contact"), the contact id and the version of the contact record (i.e. the ETag value)
string oldCacheKey = $"contact-{contactId}-{requestETag}";
// Get the cached item
string cachedContactJson = await cache.GetStringAsync(oldCacheKey);
// If there was a cached item then deserialise this into our contact object
if (!string.IsNullOrEmpty(cachedContactJson))
{
contact = JsonConvert.DeserializeObject(cachedContactJson);
Log.Information("Contact {@contact} retrieved from cache", contact);
}
}
}
// We have no cached contact, then get the contact from the database
if (contact == null)
{
contact = await dataRepository.GetContactByIdAsync(contactId);
Log.Information("Contact {@contact} retrieved from database", contact);
}
// If no contact was found in the cache or the database, then return a 404
if (contact == null)
{
Log.Information("Contact {@contactId} not found", contactId);
return NotFound();
}
// Construct the new ETag
string responseETag = Convert.ToBase64String(contact.RowVersion);
// Return a 304 if the ETag of the current record matches the ETag in the "If-None-Match" HTTP header
if (Request.Headers.ContainsKey("If-None-Match") && responseETag == requestETag)
{
return StatusCode((int)HttpStatusCode.NotModified);
}
// Add the contact to the cache for 30 mins
string cacheKey = $"contact-{contactId}-{responseETag}";
await cache.SetStringAsync(cacheKey, JsonConvert.SerializeObject(contact), new DistributedCacheEntryOptions() { AbsoluteExpiration = DateTime.Now.AddMinutes(30) });
Log.Information("Contact {@contact} added to cache with key {@cacheKey}", contact, cacheKey);
// Add the current ETag to the HTTP header
Response.Headers.Add("ETag", responseETag);
return Ok(contact);
}
现在 Serilog 和 SQL Server 已设置、连接在一起并且我们有一些日志记录代码,我们应该将日志输出到我们的 SQL Server 表。
请注意,如果我们在将日志写入 SQL Server 时遇到问题,我们可以使用Serilog.Debugging.SelfLog.Enable()inProgram.Main()从 Serilog 中显示错误。
public class Program
{
public static int Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(Configuration)
.CreateLogger();
Serilog.Debugging.SelfLog.Enable(msg =>
{
Debug.Print(msg);
Debugger.Break();
});
...
}
...
}
该Properties列包含有用的附加信息,包括我们在将联系人添加到缓存后在结构化日志中指定的内容:
我们可以Properties使用XQuery语法提取列中的特定数据。我们甚至可以过滤Properties列中的值。
例如,如果我们想查找特定联系人何时添加到缓存中,我们可以使用类似的方法:
SELECT
Properties.value('(/properties/property[@key="contact"]/structure[@type="Contact"]/property[@key="ContactId"])[1]', 'nvarchar(max)') AS ContactId,
Properties.value('(/properties/property[@key="contact"]/structure[@type="Contact"]/property[@key="FirstName"])[1]', 'nvarchar(50)') AS FirstName,
Properties.value('(/properties/property[@key="contact"]/structure[@type="Contact"]/property[@key="Surname"])[1]', 'nvarchar(100)') AS Surname,
Properties.value('(/properties/property[@key="cacheKey"])[1]', 'nvarchar(100)') AS CacheKey,
*
FROM Log
WHERE MessageTemplate = 'Contact {@contact} added to cache with key {@cacheKey}'
AND Properties.value('(/properties/property[@key="contact"]/structure[@type="Contact"]/property[@key="ContactId"])[1]', 'nvarchar(max)') = 'f7d10f53-4c11-44f4-8dce-d0e0e22cb6ab'