Trong bài viết này, chúng tôi sẽ kết thúc việc xem xét cách có thể tách và tách mối quan tâm về ghi nhật ký và mã khỏi cơ sở hạ tầng và mã doanh nghiệp. Trong bài viết trước , chúng ta đã xem xét cách sử dụng DiagnosticSource và DiagnosticListener để đạt được điều đó cho các hoạt động cơ sở hạ tầng.
Bây giờ, chúng ta sẽ xem xét cách làm phong phú dữ liệu đã ghi bằng ngữ cảnh bổ sung.
Về cơ bản, khái niệm làm phong phú dữ liệu được ghi lại xoay quanh việc đăng ký ngữ cảnh hoặc dữ liệu bổ sung phải đi cùng với thông điệp tường trình. Ngữ cảnh này có thể là bất cứ thứ gì - từ một chuỗi đơn giản đến một đối tượng phức tạp và nó được tạo ra hoặc được biết đến tại một thời điểm nào đó trong cơ sở hạ tầng hoặc mã doanh nghiệp, chứ không phải tại thời điểm thông điệp tường trình được viết.
Vì vậy, chúng tôi không thể chỉ thêm một thuộc tính khác vào thông điệp tường trình, vì mã này không tạo ra bất kỳ đầu ra ghi nhật ký nào hoặc chúng tôi muốn ngữ cảnh được gắn vào nhiều thông điệp tường trình có thể được hoặc không được tạo ra trong chuỗi thực thi.
Ngữ cảnh này thậm chí có thể có điều kiện - chẳng hạn như chỉ thêm dữ liệu cụ thể vào thông báo nhật ký lỗi trong khi tất cả các thông báo khác không cần đến nó.
Chúng tôi sẽ sử dụng Serilog và tính năng phong phú của nó vì Serilog đã làm cho nó rất linh hoạt và mạnh mẽ. Các giải pháp khác cũng có các tính năng tương tự ở các mức độ hoàn thiện khác nhau và chúng tôi sẽ so sánh mức độ phong phú của Serilog với những gì Microsoft.Extensions.Logging cung cấp ngay lập tức.
Serilog đi kèm với nhiều công cụ làm giàu hữu ích có thể rất hữu ích trong một số trường hợp. Bạn có thể truy cập trang này - https://github.com/serilog/serilog/wiki/Enrichment - để xem danh sách đầy đủ các chất làm giàu và mô tả của chúng.
Ví dụ: có các bộ làm giàu LogContext và GlobalLogContext cho phép đẩy dữ liệu bổ sung để ghi nhật ký bằng thông điệp tường trình nếu chúng được ghi trong phạm vi phù hợp.
Trình làm phong phú LogContext rất giống với khái niệm phạm vi ghi nhật ký trong Microsoft.Extensions.Logging. Về cơ bản, cả hai đều đẩy một số dữ liệu tùy chỉnh và cung cấp một đối tượng IDisposable cần được xử lý để xóa dữ liệu khỏi ngữ cảnh nhật ký.
Nghĩa là, miễn là đối tượng IDisposable nằm trong phạm vi, dữ liệu sẽ được đính kèm vào tất cả thông điệp tường trình được viết trong phạm vi đó. Khi nó được xử lý, dữ liệu sẽ không còn được đính kèm nữa.
Tài liệu Serilog và Microsoft cung cấp các ví dụ sau:
// For Serilog log.Information("No contextual properties"); 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"); } // For Microsoft.Extensions.Logging scopes using (logger.BeginScope(new List<KeyValuePair<string, object>> { new KeyValuePair<string, object>("TransactionId", transactionId), })) { _logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}", id); todoItem = await _context.TodoItems.FindAsync(id); if (todoItem == null) { _logger.LogWarning(MyLogEvents.GetItemNotFound, "Get({Id}) NOT FOUND", id); return NotFound(); } }
Mặc dù hữu ích trong một số trường hợp nhưng chúng khá hạn chế. Cách sử dụng tốt nhất cho kiểu làm giàu nhật ký này là trong loại triển khai phần mềm trung gian hoặc trang trí, trong đó phạm vi được xác định rõ và dữ liệu được biết tại thời điểm tạo phạm vi và chúng tôi chắc chắn rằng dữ liệu không có giá trị bên ngoài phạm vi.
Ví dụ: chúng tôi có thể sử dụng nó để đính kèm ID tương quan vào tất cả thông điệp tường trình trong một phạm vi xử lý yêu cầu HTTP duy nhất.
Công cụ làm giàu GlobalLogContext tương tự như LogContext, nhưng nó mang tính toàn cầu - nó đẩy dữ liệu tới tất cả các thông điệp tường trình được viết trong một ứng dụng. Tuy nhiên, các trường hợp sử dụng thực sự của loại hình làm giàu này rất hạn chế.
Trên thực tế, việc triển khai trình làm phong phú nhật ký tùy chỉnh cho Serilog rất dễ dàng - chỉ cần triển khai giao diện ILogEventEnricher
và đăng ký trình làm phong phú nhật ký với cấu hình trình ghi nhật ký. Giao diện chỉ có một phương thức để triển khai - Enrich
- chấp nhận sự kiện nhật ký và làm phong phú nó bằng dữ liệu bạn muốn.
Hãy xem lại cách triển khai mẫu cho trình bổ sung nhật ký tùy chỉnh.
public sealed class CustomLogEnricher : ILogEventEnricher { private readonly IHttpContextAccessor contextAccessor; public CustomLogEnricher(IHttpContextAccessor contextAccessor) { this.contextAccessor = contextAccessor; } public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var source = contextAccessor.HttpContext?.RequestServices.GetService<ICustomLogEnricherSource>(); if (source is null) { return; } var loggedProperties = source.GetCustomProperties(logEvent.Level); foreach (var item in loggedProperties) { var property = item.ProduceProperty(propertyFactory); logEvent.AddOrUpdateProperty(property); } } }
Như chúng ta thấy, trình làm phong phú này dựa vào ICustomLogEnricherSource
để lấy các thuộc tính tùy chỉnh nhằm làm phong phú thêm sự kiện nhật ký. Thông thường, trình làm giàu nhật ký là các phiên bản tồn tại lâu dài và thậm chí tuân theo mẫu Singleton - chúng được đăng ký một lần khi khởi động ứng dụng và tồn tại trong suốt vòng đời của ứng dụng.
Vì vậy, chúng ta cần tách bộ làm phong phú khỏi nguồn của các thuộc tính tùy chỉnh và nguồn có thể là một phiên bản có phạm vi với thời gian tồn tại giới hạn.
public sealed class CustomLogEnricherSource : ICustomLogEnricherSource { private readonly Dictionary<string, CustomLogEventProperty> properties = new(); public ICustomLogEnricherSource With<T>(string property, T? value, LogEventLevel logLevel) where T : class { if (value is null) { return this; } properties[property] = new CustomLogEventProperty(property, value, logLevel); return this; } public void Remove(string property) { properties.Remove(property); } public IEnumerable<CustomLogEventProperty> GetCustomProperties(LogEventLevel logLevel) { foreach (var item in properties.Values) { if (item.Level <= logLevel) { yield return item; } } } } // And CustomLogEventProperty is a simple struct (you can make it a class, too): public struct CustomLogEventProperty { private LogEventProperty? propertyValue; public CustomLogEventProperty(string property, object value, LogEventLevel level) { Name = property; Value = value; Level = level; } public string Name { get; } public object Value { get; } public LogEventLevel Level { get; } public LogEventProperty ProduceProperty(ILogEventPropertyFactory propertyFactory) { propertyValue ??= propertyFactory.CreateProperty(Name, Value, destructureObjects: true); return propertyValue; } }
Việc triển khai này có một số chi tiết đơn giản:
ICustomLogEnricherSource
, vì các thành phần ứng dụng khác nhau có thể tạo ra các thuộc tính tùy chỉnh khác nhau.
ICustomLogEnricherSource
) có thể được đưa vào bất kỳ thành phần nào cần làm phong phú thông điệp tường trình bằng các thuộc tính tùy chỉnh. Lý tưởng nhất là nó phải là một phiên bản có phạm vi, vì nó không cung cấp một cách thuận tiện để hết hạn các thuộc tính tùy chỉnh.
LogEvent
khác hoặc thậm chí vào trạng thái ứng dụng.
CustomLogEventProperty
lưu trữ phiên bản LogEventProperty
đã tạo để tránh tạo phiên bản đó nhiều lần cho cùng một thuộc tính tùy chỉnh vì phiên bản này có thể được đính kèm vào nhiều thông điệp tường trình.
Một chi tiết quan trọng khác nằm trong dòng mã này, tham số destructureObjects
:
propertyFactory.CreateProperty(Name, Value, destructureObjects: true);
Khi chúng ta đính kèm các đối tượng phức tạp - các lớp, bản ghi, cấu trúc, bộ sưu tập, v.v. - và tham số này không được đặt thành true
, Serilog sẽ gọi ToString
trên đối tượng và đính kèm kết quả vào thông điệp tường trình. Mặc dù các bản ghi có triển khai ToString
sẽ xuất ra tất cả các thuộc tính công khai và chúng ta có thể ghi đè ToString
cho các lớp và cấu trúc của mình, nhưng điều này không phải lúc nào cũng đúng.
Ngoài ra, đầu ra như vậy không phù hợp với việc ghi nhật ký có cấu trúc, vì nó sẽ là một chuỗi đơn giản và chúng tôi sẽ không thể tìm kiếm và lọc thông báo tường trình dựa trên các thuộc tính của đối tượng đính kèm. Vì vậy, chúng tôi đặt tham số này thành true
ở đây. Các loại đơn giản (loại giá trị và chuỗi) sẽ hoạt động tốt với một trong hai giá trị của tham số này.
Một lợi ích khác của việc triển khai này là sau khi một thuộc tính tùy chỉnh được đăng ký trong nguồn, thuộc tính đó vẫn ở đó cho tất cả các thông điệp tường trình cho đến khi nó bị xóa hoặc nguồn bị loại bỏ (hãy nhớ rằng nó phải là một phiên bản có phạm vi). Một số triển khai có thể cần kiểm soát tốt hơn trong suốt thời gian tồn tại của thuộc tính tùy chỉnh và điều này có thể đạt được theo nhiều cách khác nhau.
Ví dụ: bằng cách cung cấp các triển khai CustomLogEventProperty
cụ thể hoặc bằng cách cung cấp lệnh gọi lại để kiểm tra xem có nên xóa thuộc tính tùy chỉnh hay không.
Logic hết hạn này có thể được sử dụng trong phương thức GetCustomProperties
để kiểm tra trước một thuộc tính tùy chỉnh và xóa nó khỏi nguồn nếu nó đã hết hạn.
Chà, lý tưởng nhất là chúng tôi không muốn trộn lẫn mối quan tâm về việc ghi nhật ký với mã cơ sở hạ tầng và kinh doanh. Điều này có thể đạt được bằng nhiều công cụ trang trí, phần mềm trung gian và các mẫu khác cho phép 'bọc' mã cơ sở hạ tầng và doanh nghiệp bằng mã ghi nhật ký cụ thể.
Tuy nhiên, điều này không phải lúc nào cũng thực hiện được và đôi khi, có thể thuận tiện hơn khi đưa một phần trừu tượng trung gian như ICustomLogEnricherSource
vào mã cơ sở hạ tầng và doanh nghiệp, đồng thời để nó đăng ký dữ liệu tùy chỉnh để ghi nhật ký.
Bằng cách này, mã cơ sở hạ tầng và doanh nghiệp không cần biết bất cứ điều gì về việc ghi nhật ký thực tế, trong khi vẫn trộn lẫn nó với một số đoạn mã nhận biết ghi nhật ký.
Dù sao đi nữa, mã này sẽ ít được ghép đôi hơn và dễ kiểm tra hơn nhiều. Chúng tôi thậm chí có thể giới thiệu thứ gì đó như NullLogEnricherSource
để triển khai không hoạt động, không chiếm bất kỳ hiệu suất và bộ nhớ nào đối với một số trường hợp rất nóng.
Như Microsoft tuyên bố,
Việc ghi nhật ký phải nhanh đến mức không đáng với chi phí hiệu năng của mã không đồng bộ.
Vì vậy, bất cứ khi nào chúng tôi làm phong phú thông điệp tường trình của mình bằng ngữ cảnh bổ sung, chúng tôi nên lưu ý đến những tác động về hiệu suất. Nói chung, quá trình triển khai làm giàu nhật ký Serilog diễn ra rất nhanh, ít nhất là ngang bằng với phạm vi ghi nhật ký của Microsoft, nhưng việc tạo thông điệp nhật ký sẽ mất nhiều thời gian hơn nếu chúng ta đính kèm nhiều dữ liệu hơn vào chúng.
Càng đính kèm nhiều thuộc tính tùy chỉnh thì các đối tượng trong số đó càng phức tạp hơn và càng mất nhiều thời gian và bộ nhớ để tạo thông điệp tường trình. Vì vậy, nhà phát triển phải hết sức suy nghĩ về việc nên đính kèm dữ liệu nào vào thông điệp tường trình, thời điểm đính kèm và thời gian tồn tại của dữ liệu đó.
Dưới đây là bảng kết quả điểm chuẩn nhỏ hiển thị hiệu suất và mức tiêu thụ bộ nhớ cho cả phạm vi ghi nhật ký Serilog và phạm vi ghi nhật ký của Microsoft. Mỗi phương pháp đo điểm chuẩn sẽ tạo ra 20 thông điệp tường trình với các cách khác nhau để làm phong phú chúng bằng các thuộc tính tùy chỉnh.
Không có thuộc tính tùy chỉnh nào được đính kèm:
| Method | Mean | Error | StdDev | Gen0 | Allocated | | SerilogLoggingNoEnrichment | 74.22 us | 1.194 us | 1.116 us | 1.2207 | 21.25 KB | | MicrosoftLoggingNoEnrichment | 72.58 us | 0.733 us | 0.685 us | 1.2207 | 21.25 KB |
Mỗi thông điệp tường trình có ba chuỗi đính kèm:
| Method | Mean | Error | StdDev | Gen0 | Allocated | | SerilogLoggingWithContext | 77.47 us | 0.551 us | 0.516 us | 1.7090 | 28.6 KB | | SerilogLoggingWithEnrichment | 79.89 us | 1.482 us | 2.028 us | 1.7090 | 29.75 KB | | MicrosoftLoggingWithEnrichment | 81.86 us | 1.209 us | 1.131 us | 1.8311 | 31.22 KB |
Mỗi thông điệp tường trình có ba đối tượng (bản ghi) phức tạp được đính kèm:
| Method | Mean | Error | StdDev | Gen0 | Allocated | | SerilogLoggingWithObjectContext | 108.49 us | 1.193 us | 1.058 us | 5.3711 | 88.18 KB | | SerilogLoggingWithObjectEnrichment | 106.07 us | 0.489 us | 0.409 us | 5.3711 | 89.33 KB | | MicrosoftLoggingWithObjectEnrichment | 99.63 us | 1.655 us | 1.468 us | 6.1035 | 100.28 KB |The
Các phương thức Serilog*Context
sử dụng LogContext.PushProperty
, các phương thức Serilog*Enrichment
sử dụng trình bổ sung nguồn và trình bổ sung tùy chỉnh được đưa ra trong bài viết, trong khi các phương thức Microsoft*
sử dụng phạm vi ghi nhật ký.
Chúng ta có thể thấy rằng hiệu suất và mức tiêu thụ bộ nhớ rất giống nhau trong hầu hết các trường hợp và việc làm giàu bằng các đối tượng phức tạp sẽ tốn kém hơn so với việc làm giàu bằng các kiểu đơn giản. Ngoại lệ duy nhất là việc ghi nhật ký bằng cách triển khai của Microsoft khi chúng tôi đính kèm bản ghi vào thông điệp tường trình.
Điều này là do phạm vi ghi nhật ký không phá hủy các đối tượng phức tạp và sử dụng phương thức ToString
để tuần tự hóa khi đính kèm chúng vào thông điệp tường trình. Điều này làm cho việc triển khai của Microsoft nhanh hơn một chút nhưng tiêu tốn nhiều bộ nhớ hơn.
Nếu chúng ta sử dụng các lớp có triển khai ToString
mặc định, mức tiêu thụ bộ nhớ sẽ nhỏ hơn nhiều, nhưng các đối tượng tùy chỉnh này sẽ được ghi lại dưới dạng tên loại đủ điều kiện - hoàn toàn vô dụng.
Và trong mọi trường hợp, chúng tôi sẽ không thể tìm kiếm và lọc thông báo tường trình dựa trên thuộc tính của các đối tượng này khi triển khai Microsoft do không phá hủy chúng.
Vì vậy, đây là hạn chế của phạm vi ghi nhật ký của Microsoft mà chúng ta nên biết - không phá hủy các đối tượng phức tạp trong khi các loại đơn giản được ghi lại tốt và có thể tìm kiếm và lọc được.
Lưu ý: bạn có thể tìm thấy mã nguồn điểm chuẩn và mẫu mã cho bài viết này trong kho GitHub
Làm giàu nhật ký phù hợp là 'cải thiện chất lượng cuộc sống' cho việc phát triển và duy trì mã. Nó cho phép khả năng đính kèm ngữ cảnh bổ sung vào thông điệp tường trình chưa được biết tại thời điểm tạo thông điệp tường trình.
Ngữ cảnh này có thể là bất cứ thứ gì - từ một chuỗi đơn giản đến một đối tượng phức tạp và nó được tạo ra hoặc được biết đến tại một thời điểm nào đó trong cơ sở hạ tầng hoặc mã doanh nghiệp.
Đó là một cách để tách mã ghi nhật ký và khả năng quan sát khỏi cơ sở hạ tầng và mã doanh nghiệp, đồng thời làm cho mã dễ bảo trì, dễ kiểm tra và dễ đọc hơn.