A:基础实现

This commit is contained in:
jimmy committed 2025-11-13 19:12:55 +08:00
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>();
}
}
+52
View File
@@ -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;
}