-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature: Structured Logging Support #141
base: develop/9.0.0
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
using Arc4u.Diagnostics.Logging; | ||
using Microsoft.AspNetCore.Builder; | ||
|
||
namespace Arc4u.Diagnostics; | ||
public static class ApplicationBuilderExtensions | ||
{ | ||
public static IApplicationBuilder UseArc4uLogging(this IApplicationBuilder app) | ||
{ | ||
LoggerWrapperContext.Initialize(app.ApplicationServices); | ||
return app; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
using Microsoft.Extensions.Hosting; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Arc4u.Diagnostics.Logging; | ||
|
||
public static class HostBuilderExtensions | ||
{ | ||
public static IHostBuilder UseArc4uLogging( | ||
this IHostBuilder builder, | ||
Action<HostBuilderContext, ILoggingBuilder>? configure = null) | ||
{ | ||
builder.ConfigureLogging((context, loggingBuilder) => | ||
{ | ||
var services = loggingBuilder.Services; | ||
configure?.Invoke(context, loggingBuilder); | ||
}); | ||
|
||
return builder; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
using System.Runtime.CompilerServices; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Arc4u.Diagnostics.Logging; | ||
public static class ILoggerExtensions | ||
{ | ||
public static LoggerWrapper<T> Technical<T>(this ILogger logger, [CallerMemberName] string methodName = "") => | ||
new(logger, MessageCategory.Technical, typeof(T), methodName); | ||
|
||
public static LoggerWrapper<T> Business<T>(this ILogger logger, [CallerMemberName] string methodName = "") => | ||
new(logger, MessageCategory.Business, typeof(T), methodName); | ||
|
||
public static LoggerWrapper<T> Monitoring<T>(this ILogger logger, [CallerMemberName] string methodName = "") => | ||
new(logger, MessageCategory.Monitoring, typeof(T), methodName); | ||
|
||
public static LoggerWrapper<T> Technical<T>(this ILogger<T> logger, [CallerMemberName] string methodName = "") => | ||
new(logger, MessageCategory.Technical, typeof(T), methodName); | ||
|
||
public static LoggerWrapper<T> Business<T>(this ILogger<T> logger, [CallerMemberName] string methodName = "") => | ||
new(logger, MessageCategory.Business, typeof(T), methodName); | ||
|
||
public static LoggerWrapper<T> Monitoring<T>(this ILogger<T> logger, [CallerMemberName] string methodName = "") => | ||
new(logger, MessageCategory.Monitoring, typeof(T), methodName); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
using System.Reflection; | ||
using System.Text; | ||
using Arc4u.Diagnostics; | ||
using Arc4u.Diagnostics.Logging; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Microsoft.Extensions.DependencyInjection; | ||
|
||
public sealed class LoggerWrapper<T> : ILogger<T> | ||
{ | ||
private readonly ILogger _logger; | ||
private readonly MessageCategory _category; | ||
private readonly Dictionary<string, object?> _additionalFields = []; | ||
private readonly Type? _contextType; | ||
private readonly string _caller = string.Empty; | ||
private bool _disposed; | ||
private bool _includeStackTrace; | ||
private readonly Lazy<IReadOnlyList<IAddPropertiesToLog>> _providers = new(Enumerable.Empty<IAddPropertiesToLog>().ToList()); | ||
|
||
internal Dictionary<string, object?> AdditionalFields => _additionalFields; | ||
internal bool IncludeStackTrace { get => _includeStackTrace; set => _includeStackTrace = value; } | ||
|
||
internal static int ProcessId | ||
{ | ||
get | ||
{ | ||
try | ||
{ | ||
return Environment.ProcessId; | ||
} | ||
catch (PlatformNotSupportedException) | ||
{ | ||
return -1; | ||
} | ||
} | ||
} | ||
|
||
public LoggerWrapper(ILoggerFactory loggerFactory) => | ||
_logger = loggerFactory.CreateLogger<T>(); | ||
|
||
internal LoggerWrapper( | ||
ILogger logger, | ||
MessageCategory category, | ||
Type? contextType = null, | ||
string caller = "") | ||
{ | ||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||
_category = category; | ||
_contextType = contextType; | ||
_caller = caller; | ||
|
||
|
||
_providers = new Lazy<IReadOnlyList<IAddPropertiesToLog>>(() => | ||
{ | ||
using var scope = LoggerWrapperContext.CreateScope(); | ||
return scope.ServiceProvider.GetServices<IAddPropertiesToLog>().ToList(); | ||
}); | ||
} | ||
|
||
private static class LoggerMessages | ||
{ | ||
private static readonly EventId TechnicalEventId = new(1000, "Technical"); | ||
private static readonly EventId BusinessEventId = new(2000, "Business"); | ||
private static readonly EventId MonitoringEventId = new(3000, "Monitoring"); | ||
|
||
public static readonly Func<LogLevel, Action<ILogger, string, string, object, string, Exception?>> TechnicalLog = | ||
level => Logging.LoggerMessage.Define<string, string, object, string>( | ||
level, | ||
TechnicalEventId, | ||
"{Category} [{Context}] [{@State}] {Message}"); | ||
|
||
public static readonly Func<LogLevel, Action<ILogger, string, string, object, string, Exception?>> BusinessLog = | ||
level => Logging.LoggerMessage.Define<string, string, object, string>( | ||
level, | ||
BusinessEventId, | ||
"{Category} [{Context}] [{@State}] {Message}"); | ||
|
||
public static readonly Func<LogLevel, Action<ILogger, string, string, object, string, Exception?>> MonitoringLog = | ||
level => Logging.LoggerMessage.Define<string, string, object, string>( | ||
level, | ||
MonitoringEventId, | ||
"{Category} [{Context}] [{@State}] {Message}"); | ||
} | ||
|
||
private void Log(LogLevel level, string? message, Exception? exception = null) | ||
{ | ||
ThrowIfDisposed(); | ||
|
||
if (!IsEnabled(level)) | ||
{ | ||
return; | ||
} | ||
|
||
try | ||
{ | ||
var properties = AddAdditionalProperties(); | ||
if (IncludeStackTrace) | ||
{ | ||
properties.AddIfNotExist(LoggingConstants.Stacktrace, exception?.StackTrace ?? Environment.StackTrace); | ||
} | ||
|
||
if (exception is not null) | ||
{ | ||
properties.AddIfNotExist(LoggingConstants.UnwrappedException, exception.ToFormattedstring()); | ||
} | ||
|
||
properties.AddIfNotExist(LoggingConstants.MethodName, _caller); | ||
properties.AddIfNotExist(LoggingConstants.Class, _contextType?.FullName ?? nameof(_contextType)); | ||
properties.AddIfNotExist(LoggingConstants.Category, (short)_category); | ||
properties.AddIfNotExist(LoggingConstants.Application, Assembly.GetEntryAssembly()?.GetName().Name ?? "Unknown App"); | ||
properties.AddIfNotExist(LoggingConstants.ThreadId, Environment.CurrentManagedThreadId); | ||
properties.AddIfNotExist(LoggingConstants.ProcessId, ProcessId); | ||
|
||
var enrichedState = new | ||
{ | ||
Caller = _caller, | ||
TimeStamp = DateTime.UtcNow, | ||
AdditionalFields = properties, | ||
Level = level | ||
}; | ||
|
||
var logAction = _category switch | ||
{ | ||
MessageCategory.Technical => LoggerMessages.TechnicalLog(level), | ||
MessageCategory.Business => LoggerMessages.BusinessLog(level), | ||
MessageCategory.Monitoring => LoggerMessages.MonitoringLog(level), | ||
_ => LoggerMessages.TechnicalLog(level) | ||
}; | ||
|
||
logAction( | ||
_logger, | ||
_category.ToString(), | ||
_contextType?.Name ?? "General", | ||
enrichedState, | ||
message ?? string.Empty, | ||
exception); | ||
} | ||
catch (Exception ex) | ||
{ | ||
_logger.LogError(ex, "Failed to log message with property providers"); | ||
_logger.Log(level, exception, message ?? string.Empty); | ||
} | ||
} | ||
|
||
internal void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); | ||
|
||
public void Dispose() | ||
{ | ||
if (_disposed) | ||
{ | ||
return; | ||
} | ||
|
||
_disposed = true; | ||
} | ||
|
||
private Dictionary<string, object?> AddAdditionalProperties() | ||
{ | ||
Dictionary<string, object?> properties; | ||
|
||
try | ||
{ | ||
properties = new Dictionary<string, object?>(AdditionalFields); | ||
var definedProperties = _providers.Value.Select(x => x.GetProperties()).SelectMany(x => x); | ||
if (definedProperties != null) | ||
{ | ||
foreach (var property in definedProperties) | ||
{ | ||
if (property.Value != null) | ||
{ | ||
this.AddIfNotExist(property.Key, property.Value); | ||
} | ||
} | ||
|
||
return properties; | ||
} | ||
|
||
return new Dictionary<string, object?>(AdditionalFields); | ||
} | ||
catch (Exception ex) | ||
{ | ||
_logger.LogWarning(ex, "Error getting property providers. Logging without additional properties."); | ||
return new Dictionary<string, object?>(AdditionalFields); | ||
} | ||
} | ||
|
||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) | ||
{ | ||
Console.WriteLine("Log<TState> is called."); | ||
|
||
if (state is IEnumerable<KeyValuePair<string, object>> pairs) | ||
{ | ||
foreach (var pair in pairs) | ||
{ | ||
AdditionalFields.AddOrReplace(pair.Key, pair.Value); | ||
} | ||
} | ||
|
||
Log(logLevel, formatter(state, exception), exception); | ||
} | ||
|
||
public bool IsEnabled(LogLevel logLevel) => _logger.IsEnabled(logLevel); | ||
|
||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => _logger.BeginScope(state); | ||
} | ||
|
||
internal static class DumpException | ||
{ | ||
internal static string ToFormattedstring(this Exception exception) | ||
{ | ||
var messages = exception | ||
.GetAllExceptions() | ||
.Where(e => !string.IsNullOrWhiteSpace(e.Message)) | ||
.Select(e => e.GetType().FullName + " : " + e.Message.Trim()); | ||
var sb = new StringBuilder(); | ||
var i = 0; | ||
foreach (var message in messages) | ||
{ | ||
sb.Append("".PadLeft(i * 4)); | ||
sb.Append("|---".PadLeft(i++ > 0 ? 4 : 0)); | ||
sb.AppendLine(message); | ||
} | ||
|
||
return sb.ToString(); | ||
} | ||
|
||
private static IEnumerable<Exception> GetAllExceptions(this Exception exception) | ||
{ | ||
yield return exception; | ||
|
||
if (exception is AggregateException aggrEx) | ||
{ | ||
foreach (var innerEx in aggrEx.InnerExceptions.SelectMany(e => e.GetAllExceptions())) | ||
{ | ||
yield return innerEx; | ||
} | ||
} | ||
else if (exception.InnerException != null) | ||
{ | ||
foreach (var innerEx in exception.InnerException.GetAllExceptions()) | ||
{ | ||
yield return innerEx; | ||
} | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,29 @@ | ||||||||||||||||||
using Microsoft.Extensions.DependencyInjection; | ||||||||||||||||||
|
||||||||||||||||||
namespace Arc4u.Diagnostics.Logging; | ||||||||||||||||||
internal class LoggerWrapperContext | ||||||||||||||||||
{ | ||||||||||||||||||
private static Lazy<IServiceScopeFactory> _scopeFactory = default!; | ||||||||||||||||||
|
||||||||||||||||||
/// <summary> | ||||||||||||||||||
/// Initializes the specified service provider. | ||||||||||||||||||
/// </summary> | ||||||||||||||||||
/// <param name="serviceProvider">The service provider.</param> | ||||||||||||||||||
public static void Initialize(IServiceProvider serviceProvider) | ||||||||||||||||||
=> _scopeFactory = new Lazy<IServiceScopeFactory>(serviceProvider.GetRequiredService<IServiceScopeFactory>()); | ||||||||||||||||||
|
||||||||||||||||||
/// <summary> | ||||||||||||||||||
/// Creates the scope. | ||||||||||||||||||
/// </summary> | ||||||||||||||||||
/// <returns></returns> | ||||||||||||||||||
/// <exception cref="InvalidOperationException">LoggerContext not initialized</exception> | ||||||||||||||||||
public static IServiceScope CreateScope() | ||||||||||||||||||
{ | ||||||||||||||||||
if (_scopeFactory.IsValueCreated) | ||||||||||||||||||
{ | ||||||||||||||||||
throw new InvalidOperationException("LoggerContext not initialized"); | ||||||||||||||||||
} | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Logic error in CreateScope method The condition is reversed. The method throws when the factory IS created, but it should throw when it's NOT created. Apply this fix: - if (_scopeFactory.IsValueCreated)
+ if (!_scopeFactory.IsValueCreated)
{
throw new InvalidOperationException("LoggerContext not initialized");
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||
|
||||||||||||||||||
return _scopeFactory.Value.CreateScope(); | ||||||||||||||||||
} | ||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove debug Console.WriteLine statement
Debug statements should not be present in production code.
- Console.WriteLine("Log<TState> is called.");