A:基础实现
This commit is contained in:
1 parent
c8fde9759f
commit
c73c6f1380
5 files changed
+355
No files matched your search
@@ -0,0 +1,72 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Serilog.Extensions.Logging;
|
||||||
|
using Sharp.Log.Internal;
|
||||||
|
using Sharp.Log.Options;
|
||||||
|
|
||||||
|
namespace Sharp.Log.Extensions;
|
||||||
|
|
||||||
|
public static class SharpLogBuilderExtensions
|
||||||
|
{
|
||||||
|
public static ILoggingBuilder AddSharpLog(this ILoggingBuilder builder, Action<SharpLogOptions>? configure = null)
|
||||||
|
{
|
||||||
|
// Options binding from configuration section "SharpLog"
|
||||||
|
builder.Services.AddOptions<SharpLogOptions>()
|
||||||
|
.BindConfiguration("SharpLog")
|
||||||
|
.PostConfigure(opt => configure?.Invoke(opt));
|
||||||
|
|
||||||
|
// Configure LoggerFilterOptions according to our options (min level and overrides)
|
||||||
|
builder.Services.AddSingleton<IConfigureOptions<LoggerFilterOptions>>(sp =>
|
||||||
|
new ConfigureNamedOptions<LoggerFilterOptions>(Microsoft.Extensions.Options.Options.DefaultName, o =>
|
||||||
|
{
|
||||||
|
var opt = sp.GetRequiredService<IOptions<SharpLogOptions>>().Value;
|
||||||
|
o.MinLevel = opt.MinLevel;
|
||||||
|
foreach (var kv in opt.Overrides)
|
||||||
|
{
|
||||||
|
// category-specific rule
|
||||||
|
o.Rules.Add(new LoggerFilterRule(null, kv.Key, kv.Value, null));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Register Serilog provider via DI so it participates in logging pipeline
|
||||||
|
builder.Services.AddSingleton<ILoggerProvider>(sp =>
|
||||||
|
{
|
||||||
|
var opt = sp.GetRequiredService<IOptions<SharpLogOptions>>().Value;
|
||||||
|
|
||||||
|
if (!opt.UseSerilog)
|
||||||
|
{
|
||||||
|
// If not using Serilog, don't inject a provider; return a no-op stub
|
||||||
|
return new NoopLoggerProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
var env = sp.GetService<IHostEnvironment>();
|
||||||
|
var cfg = sp.GetService<IConfiguration>();
|
||||||
|
var serilog = SerilogConfigurator.CreateLogger(opt, env, cfg);
|
||||||
|
return new SerilogLoggerProvider(serilog, dispose: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoopLoggerProvider : ILoggerProvider
|
||||||
|
{
|
||||||
|
private sealed class NoopLogger : ILogger
|
||||||
|
{
|
||||||
|
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
||||||
|
public bool IsEnabled(LogLevel logLevel) => false;
|
||||||
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NullScope : IDisposable
|
||||||
|
{
|
||||||
|
public static readonly NullScope Instance = new();
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogger CreateLogger(string categoryName) => new NoopLogger();
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace Sharp.Log.Internal;
|
||||||
|
|
||||||
|
internal sealed class ActivityEnricher : ILogEventEnricher
|
||||||
|
{
|
||||||
|
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
||||||
|
{
|
||||||
|
var activity = Activity.Current;
|
||||||
|
if (activity == null) return;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(activity.TraceId.ToString()))
|
||||||
|
{
|
||||||
|
var traceIdProp = propertyFactory.CreateProperty("TraceId", activity.TraceId.ToString());
|
||||||
|
logEvent.AddOrUpdateProperty(traceIdProp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(activity.SpanId.ToString()))
|
||||||
|
{
|
||||||
|
var spanIdProp = propertyFactory.CreateProperty("SpanId", activity.SpanId.ToString());
|
||||||
|
logEvent.AddOrUpdateProperty(spanIdProp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
using Serilog.Formatting.Json;
|
||||||
|
using Sharp.Log.Options;
|
||||||
|
|
||||||
|
namespace Sharp.Log.Internal;
|
||||||
|
|
||||||
|
internal static class SerilogConfigurator
|
||||||
|
{
|
||||||
|
public static Serilog.ILogger CreateLogger(
|
||||||
|
SharpLogOptions options,
|
||||||
|
IHostEnvironment? env,
|
||||||
|
IConfiguration? configuration)
|
||||||
|
{
|
||||||
|
var cfg = new LoggerConfiguration();
|
||||||
|
|
||||||
|
// Minimum levels
|
||||||
|
cfg = cfg.MinimumLevel.Is(MapLevel(options.MinLevel));
|
||||||
|
foreach (var kv in options.Overrides)
|
||||||
|
{
|
||||||
|
cfg = cfg.MinimumLevel.Override(kv.Key, MapLevel(kv.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrichers
|
||||||
|
cfg = cfg.Enrich.FromLogContext();
|
||||||
|
if (options.Enrich.Enabled)
|
||||||
|
{
|
||||||
|
if (options.Enrich.WithMachineName)
|
||||||
|
cfg = cfg.Enrich.WithMachineName();
|
||||||
|
if (options.Enrich.WithProcessId)
|
||||||
|
cfg = cfg.Enrich.WithProcessId();
|
||||||
|
if (options.Enrich.WithThreadId)
|
||||||
|
cfg = cfg.Enrich.WithThreadId();
|
||||||
|
if (options.Enrich.WithEnvironmentName)
|
||||||
|
cfg = cfg.Enrich.WithEnvironmentName();
|
||||||
|
|
||||||
|
var appName = options.Enrich.Application
|
||||||
|
?? TryGetAppName()
|
||||||
|
?? AppDomain.CurrentDomain.FriendlyName;
|
||||||
|
cfg = cfg.Enrich.WithProperty("Application", appName);
|
||||||
|
|
||||||
|
if (env != null)
|
||||||
|
{
|
||||||
|
cfg = cfg.Enrich.WithProperty("Environment", env.EnvironmentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.Enrich.WithActivityTrace)
|
||||||
|
{
|
||||||
|
cfg = cfg.Enrich.With(new ActivityEnricher());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sinks
|
||||||
|
if (options.UseConsole)
|
||||||
|
{
|
||||||
|
if (options.ConsoleJson)
|
||||||
|
{
|
||||||
|
cfg = cfg.WriteTo.Async(a => a.Console(new JsonFormatter(renderMessage: true)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const string template = "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}";
|
||||||
|
cfg = cfg.WriteTo.Async(a => a.Console(outputTemplate: template));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.File.Enabled)
|
||||||
|
{
|
||||||
|
var path = options.File.Path;
|
||||||
|
var rolling = MapRolling(options.File.Rolling);
|
||||||
|
var retained = options.File.RetainedFileCountLimit;
|
||||||
|
var sizeLimit = options.File.FileSizeLimitBytes;
|
||||||
|
var shared = options.File.Shared;
|
||||||
|
var buffered = options.File.Buffered;
|
||||||
|
|
||||||
|
if (options.File.Json)
|
||||||
|
{
|
||||||
|
cfg = cfg.WriteTo.Async(a => a.File(new JsonFormatter(renderMessage: true), path,
|
||||||
|
rollingInterval: rolling,
|
||||||
|
retainedFileCountLimit: retained,
|
||||||
|
fileSizeLimitBytes: sizeLimit,
|
||||||
|
shared: shared,
|
||||||
|
buffered: buffered));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const string template = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}";
|
||||||
|
cfg = cfg.WriteTo.Async(a => a.File(path,
|
||||||
|
rollingInterval: rolling,
|
||||||
|
retainedFileCountLimit: retained,
|
||||||
|
fileSizeLimitBytes: sizeLimit,
|
||||||
|
shared: shared,
|
||||||
|
buffered: buffered,
|
||||||
|
outputTemplate: template));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg.CreateLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LogEventLevel MapLevel(LogLevel level) => level switch
|
||||||
|
{
|
||||||
|
LogLevel.Trace => LogEventLevel.Verbose,
|
||||||
|
LogLevel.Debug => LogEventLevel.Debug,
|
||||||
|
LogLevel.Information => LogEventLevel.Information,
|
||||||
|
LogLevel.Warning => LogEventLevel.Warning,
|
||||||
|
LogLevel.Error => LogEventLevel.Error,
|
||||||
|
LogLevel.Critical => LogEventLevel.Fatal,
|
||||||
|
_ => LogEventLevel.Information
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Serilog.RollingInterval MapRolling(Sharp.Log.Options.RollingInterval rolling) => rolling switch
|
||||||
|
{
|
||||||
|
Sharp.Log.Options.RollingInterval.Infinite => Serilog.RollingInterval.Infinite,
|
||||||
|
Sharp.Log.Options.RollingInterval.Year => Serilog.RollingInterval.Year,
|
||||||
|
Sharp.Log.Options.RollingInterval.Month => Serilog.RollingInterval.Month,
|
||||||
|
Sharp.Log.Options.RollingInterval.Day => Serilog.RollingInterval.Day,
|
||||||
|
Sharp.Log.Options.RollingInterval.Hour => Serilog.RollingInterval.Hour,
|
||||||
|
Sharp.Log.Options.RollingInterval.Minute => Serilog.RollingInterval.Minute,
|
||||||
|
_ => Serilog.RollingInterval.Day
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string? TryGetAppName()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Sharp.Log.Middleware;
|
||||||
|
|
||||||
|
public sealed class SharpRequestLoggingMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<SharpRequestLoggingMiddleware> _logger;
|
||||||
|
|
||||||
|
public SharpRequestLoggingMiddleware(RequestDelegate next, ILogger<SharpRequestLoggingMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Invoke(HttpContext context)
|
||||||
|
{
|
||||||
|
var sw = ValueStopwatch.StartNew();
|
||||||
|
var request = context.Request;
|
||||||
|
|
||||||
|
using var scope = _logger.BeginScope(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["TraceId"] = Activity.Current?.TraceId.ToString() ?? string.Empty,
|
||||||
|
["SpanId"] = Activity.Current?.SpanId.ToString() ?? string.Empty
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("HTTP {Method} {Path} started", request.Method, request.Path);
|
||||||
|
await _next(context);
|
||||||
|
var elapsed = sw.GetElapsedMilliseconds();
|
||||||
|
var status = context.Response?.StatusCode;
|
||||||
|
|
||||||
|
var level = status >= 500 ? LogLevel.Error : LogLevel.Information;
|
||||||
|
_logger.Log(level, "HTTP {Method} {Path} responded {StatusCode} in {Elapsed} ms",
|
||||||
|
request.Method, request.Path, status, elapsed);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var elapsed = sw.GetElapsedMilliseconds();
|
||||||
|
_logger.LogError(ex, "HTTP {Method} {Path} failed in {Elapsed} ms", request.Method, request.Path, elapsed);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct ValueStopwatch
|
||||||
|
{
|
||||||
|
private static readonly double TimestampToMilliseconds = 1000.0 / Stopwatch.Frequency;
|
||||||
|
private readonly long _start;
|
||||||
|
|
||||||
|
private ValueStopwatch(long start) => _start = start;
|
||||||
|
public static ValueStopwatch StartNew() => new(Stopwatch.GetTimestamp());
|
||||||
|
public long GetElapsedMilliseconds() => (long)((Stopwatch.GetTimestamp() - _start) * TimestampToMilliseconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SharpRequestLoggingExtensions
|
||||||
|
{
|
||||||
|
public static IApplicationBuilder UseSharpRequestLogging(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
return app.UseMiddleware<SharpRequestLoggingMiddleware>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Sharp.Log.Options;
|
||||||
|
|
||||||
|
public sealed class SharpLogOptions
|
||||||
|
{
|
||||||
|
public bool UseSerilog { get; set; } = true;
|
||||||
|
public bool UseConsole { get; set; } = true;
|
||||||
|
public bool ConsoleJson { get; set; } = false;
|
||||||
|
|
||||||
|
public LogLevel MinLevel { get; set; } = LogLevel.Information;
|
||||||
|
public Dictionary<string, LogLevel> Overrides { get; set; } = new();
|
||||||
|
|
||||||
|
public FileOptions File { get; set; } = new();
|
||||||
|
public EnrichOptions Enrich { get; set; } = new();
|
||||||
|
|
||||||
|
public bool ClearDefaultProviders { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FileOptions
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public string Path { get; set; } = System.IO.Path.Combine("logs", "app-.log");
|
||||||
|
public RollingInterval Rolling { get; set; } = RollingInterval.Day;
|
||||||
|
public int? RetainedFileCountLimit { get; set; } = 30;
|
||||||
|
public long? FileSizeLimitBytes { get; set; } = null; // default Serilog
|
||||||
|
public bool Buffered { get; set; } = true;
|
||||||
|
public bool Shared { get; set; } = false;
|
||||||
|
public bool Json { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RollingInterval
|
||||||
|
{
|
||||||
|
Infinite = 0,
|
||||||
|
Year = 1,
|
||||||
|
Month = 2,
|
||||||
|
Day = 3,
|
||||||
|
Hour = 4,
|
||||||
|
Minute = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EnrichOptions
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public bool WithThreadId { get; set; } = true;
|
||||||
|
public bool WithProcessId { get; set; } = true;
|
||||||
|
public bool WithMachineName { get; set; } = true;
|
||||||
|
public bool WithEnvironmentName { get; set; } = true;
|
||||||
|
public string? Application { get; set; } = null; // default to entry assembly name
|
||||||
|
public bool WithActivityTrace { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user