ASP.NET Core Logging with Serilog and SQL Server

    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.

    1. public class Program
    2. {
    3. public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
    4. .SetBasePath(Directory.GetCurrentDirectory())
    5. .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    6. .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
    7. .Build();
    8. public static void Main(string[] args)
    9. {
    10. Log.Logger = new LoggerConfiguration()
    11. .ReadFrom.Configuration(Configuration)
    12. .CreateLogger();
    13. try
    14. {
    15. Log.Information("Getting the motors running...");
    16. BuildWebHost(args).Run();
    17. }
    18. catch (Exception ex)
    19. {
    20. Log.Fatal(ex, "Host terminated unexpectedly");
    21. }
    22. finally
    23. {
    24. Log.CloseAndFlush();
    25. }
    26. }
    27. public static IWebHost BuildWebHost(string[] args) =>
    28. WebHost.CreateDefaultBuilder(args)
    29. .UseStartup<Startup>()
    30. .UseConfiguration(Configuration)
    31. .UseSerilog()
    32. .Build();
    33. }

    然后可以将 Serilog 配置为使用 SQL Server 作为 中的目标appSettings.json,以及最低日志记录级别。我们需要指定日志数据库的连接字符串和将数据记录到的表名。

    1. {
    2. ...
    3. "Serilog": {
    4. "MinimumLevel": "Information",
    5. "WriteTo": [
    6. {
    7. "Name": "MSSqlServer",
    8. "Args": {
    9. "connectionString": "<our connection string>",
    10. "tableName": "Log"
    11. }
    12. }
    13. ]
    14. },
    15. ...
    16. }

    我们可以让 SerilogLog自动创建我们的表,但让我们自己做,这样我们就可以控制我们的模式。例如,我们希望Properties列基于xml数据类型,以便我们可以查询它(serilog 将其创建为nvarchar)。
    下面是创建Log表的TSQL 脚本:

    1. CREATE TABLE [Log] (
    2. [Id] int IDENTITY(1,1) NOT NULL,
    3. [Message] nvarchar(max) NULL,
    4. [MessageTemplate] nvarchar(max) NULL,
    5. [Level] nvarchar(128) NULL,
    6. [TimeStamp] datetimeoffset(7) NOT NULL,
    7. [Exception] nvarchar(max) NULL,
    8. [Properties] xml NULL,
    9. [LogEvent] nvarchar(max) NULL
    10. CONSTRAINT [PK_Log]
    11. PRIMARY KEY CLUSTERED ([Id] ASC)
    12. )

    然后我们可以在我们的代码中写入日志,记录{@object}在消息模板中使用的结构化对象。下面是用于获取和返回记录(在本例中为联系人)的 Web API 操作方法。我们记录何时从缓存/数据库中获取联系人以及何时在缓存中设置联系人。

    1. [HttpGet("{contactId}")]
    2. public async Task GetById(Guid contactId)
    3. {
    4. // Initialise the contact that is going to be returned
    5. Contact contact = null;
    6. // Get the requested ETag
    7. string requestETag = "";
    8. if (Request.Headers.ContainsKey("If-None-Match"))
    9. {
    10. requestETag = Request.Headers["If-None-Match"].First();
    11. if (!string.IsNullOrEmpty(requestETag))
    12. {
    13. // The client has supplied an ETag, so, get this version of the contact from our cache
    14. // 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)
    15. string oldCacheKey = $"contact-{contactId}-{requestETag}";
    16. // Get the cached item
    17. string cachedContactJson = await cache.GetStringAsync(oldCacheKey);
    18. // If there was a cached item then deserialise this into our contact object
    19. if (!string.IsNullOrEmpty(cachedContactJson))
    20. {
    21. contact = JsonConvert.DeserializeObject(cachedContactJson);
    22. Log.Information("Contact {@contact} retrieved from cache", contact);
    23. }
    24. }
    25. }
    26. // We have no cached contact, then get the contact from the database
    27. if (contact == null)
    28. {
    29. contact = await dataRepository.GetContactByIdAsync(contactId);
    30. Log.Information("Contact {@contact} retrieved from database", contact);
    31. }
    32. // If no contact was found in the cache or the database, then return a 404
    33. if (contact == null)
    34. {
    35. Log.Information("Contact {@contactId} not found", contactId);
    36. return NotFound();
    37. }
    38. // Construct the new ETag
    39. string responseETag = Convert.ToBase64String(contact.RowVersion);
    40. // Return a 304 if the ETag of the current record matches the ETag in the "If-None-Match" HTTP header
    41. if (Request.Headers.ContainsKey("If-None-Match") && responseETag == requestETag)
    42. {
    43. return StatusCode((int)HttpStatusCode.NotModified);
    44. }
    45. // Add the contact to the cache for 30 mins
    46. string cacheKey = $"contact-{contactId}-{responseETag}";
    47. await cache.SetStringAsync(cacheKey, JsonConvert.SerializeObject(contact), new DistributedCacheEntryOptions() { AbsoluteExpiration = DateTime.Now.AddMinutes(30) });
    48. Log.Information("Contact {@contact} added to cache with key {@cacheKey}", contact, cacheKey);
    49. // Add the current ETag to the HTTP header
    50. Response.Headers.Add("ETag", responseETag);
    51. return Ok(contact);
    52. }

    现在 Serilog 和 SQL Server 已设置、连接在一起并且我们有一些日志记录代码,我们应该将日志输出到我们的 SQL Server 表。
    使用 Serilog 和 SQL Server 进行 ASP.NET Core 日志记录 - 图1
    请注意,如果我们在将日志写入 SQL Server 时遇到问题,我们可以使用Serilog.Debugging.SelfLog.Enable()inProgram.Main()从 Serilog 中显示错误。

    1. public class Program
    2. {
    3. public static int Main(string[] args)
    4. {
    5. Log.Logger = new LoggerConfiguration()
    6. .ReadFrom.Configuration(Configuration)
    7. .CreateLogger();
    8. Serilog.Debugging.SelfLog.Enable(msg =>
    9. {
    10. Debug.Print(msg);
    11. Debugger.Break();
    12. });
    13. ...
    14. }
    15. ...
    16. }

    该Properties列包含有用的附加信息,包括我们在将联系人添加到缓存后在结构化日志中指定的内容:
    使用 Serilog 和 SQL Server 进行 ASP.NET Core 日志记录 - 图2
    我们可以Properties使用XQuery语法提取列中的特定数据。我们甚至可以过滤Properties列中的值。
    例如,如果我们想查找特定联系人何时添加到缓存中,我们可以使用类似的方法:

    1. SELECT
    2. Properties.value('(/properties/property[@key="contact"]/structure[@type="Contact"]/property[@key="ContactId"])[1]', 'nvarchar(max)') AS ContactId,
    3. Properties.value('(/properties/property[@key="contact"]/structure[@type="Contact"]/property[@key="FirstName"])[1]', 'nvarchar(50)') AS FirstName,
    4. Properties.value('(/properties/property[@key="contact"]/structure[@type="Contact"]/property[@key="Surname"])[1]', 'nvarchar(100)') AS Surname,
    5. Properties.value('(/properties/property[@key="cacheKey"])[1]', 'nvarchar(100)') AS CacheKey,
    6. *
    7. FROM Log
    8. WHERE MessageTemplate = 'Contact {@contact} added to cache with key {@cacheKey}'
    9. AND Properties.value('(/properties/property[@key="contact"]/structure[@type="Contact"]/property[@key="ContactId"])[1]', 'nvarchar(max)') = 'f7d10f53-4c11-44f4-8dce-d0e0e22cb6ab'

    使用 Serilog 和 SQL Server 进行 ASP.NET Core 日志记录 - 图3
    蛮好用的!