using Newtonsoft.Json.Linq; using Serilog.Core; using Serilog.Events; using Serilog; using System.Security.Claims; using System.Text; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Newtonsoft.Json; using System.IO; using ServiceStack; using System.Globalization; namespace BZPT.Api.Middleware { public static class LogExt { public static void UseMySerilog(this WebApplicationBuilder builder, string connection, string serviceName) { builder.Services.AddHttpContextAccessor(); // 配置日志 //var log = builder.Logging.AddLog4Net(); //log.AddConsole(); //log.AddDebug(); //log.SetMinimumLevel(LogLevel.Debug); // 配置 Serilog var httpContextAccessor = builder.Services.BuildServiceProvider().GetRequiredService(); // 配置 Serilog Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Hangfire", LogEventLevel.Warning) .MinimumLevel.Override("IdentityServer4", LogEventLevel.Debug) .Enrich.With(new SensitiveDataFilterEnricher()) // 敏感词过滤 .Enrich.With(new RequestContextEnricher(httpContextAccessor)) .Enrich.With(new OperationObjectEnricher(httpContextAccessor)) //.Enrich.With(new ParamsEnricher(httpContextAccessor)) .Enrich.With(new ModuleTypeEnricher(httpContextAccessor)) .Enrich.FromLogContext() .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3} {EventId:l}] " + "{CategoryName}: {Message:lj}{NewLine}{Exception}", formatProvider: CultureInfo.InvariantCulture ) .WriteTo.Async(a => a.Sink(new LoggingWithDMSink(connection, serviceName, httpContextAccessor, batchSizeLimit: 200, period: TimeSpan.FromSeconds(30) )) ) // 信息日志批量写入文件(异步) .WriteTo.Logger(lc => lc .Filter.ByIncludingOnly(e => e.Level <= LogEventLevel.Information) .WriteTo.Async(a => a.File( path: "logs/info-.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}", buffered: true, // 启用缓冲 flushToDiskInterval: TimeSpan.FromSeconds(5) // 5秒刷盘 )) ) // 其他日志处理(可选) .CreateLogger(); builder.Host.UseSerilog(); } } public class RequestContextEnricher : ILogEventEnricher { private readonly IHttpContextAccessor _httpContextAccessor; public RequestContextEnricher(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory) { var context = _httpContextAccessor.HttpContext; if (context == null) return; var forwardedIp = context.Request.Headers["X-Forwarded-For"].FirstOrDefault()?.Split(',')[0].Trim(); var clientIp = forwardedIp ?? context.Connection.RemoteIpAddress?.ToString(); // 基础信息 logEvent.AddOrUpdateProperty(factory.CreateProperty("RequestID", context.Items["X-Request-ID"])); logEvent.AddOrUpdateProperty(factory.CreateProperty("UserID", context.User.FindFirstValue(ClaimTypes.NameIdentifier))); logEvent.AddOrUpdateProperty(factory.CreateProperty("UserName", context.User.FindFirstValue(ClaimTypes.Name))); logEvent.AddOrUpdateProperty(factory.CreateProperty("IP", clientIp)); logEvent.AddOrUpdateProperty(factory.CreateProperty("UserAgent", context.Request.Headers["User-Agent"].ToString())); } } public class OperationObjectEnricher : ILogEventEnricher { private readonly IHttpContextAccessor _httpContextAccessor; public OperationObjectEnricher(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory) { var context = _httpContextAccessor.HttpContext; if (context == null) return; // 从路由参数或请求体中提取对象ID/类型 var objId = context.Request.RouteValues["id"]?.ToString() ?? GetFromBody(context, "id"); var objType = context.Request.RouteValues["controller"]?.ToString(); logEvent.AddOrUpdateProperty(factory.CreateProperty("ObjID", objId)); logEvent.AddOrUpdateProperty(factory.CreateProperty("ObjType", objType)); } private string GetFromBody(HttpContext context, string key) { try { if (context.Request.HasJsonContentType()) { context.Request.Body.Seek(0, SeekOrigin.Begin); using var reader = new StreamReader(context.Request.Body); var json = reader.ReadToEndAsync().Result; var jObject = JObject.Parse(json); return jObject[key]?.ToString(); } } catch { /* 安全忽略解析错误 */ } return string.Empty; } } public class ModuleTypeEnricher : ILogEventEnricher { private readonly IHttpContextAccessor _httpContextAccessor; public ModuleTypeEnricher(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory) { var context = _httpContextAccessor.HttpContext; if (context == null) return; // 示例路径: /api/v1/orders/create -> Module=Orders, Type=Create var path = context.Request.Path.Value?.Split('/'); var module = path?.Length >= 2 ? path[path.Length-2] : "Unknown"; var operationType = path?.LastOrDefault()?.ToUpper(); logEvent.AddOrUpdateProperty(factory.CreateProperty("ModuleName", module)); logEvent.AddOrUpdateProperty(factory.CreateProperty("OperationType", operationType)); } } // SensitiveDataFilterEnricher.cs public class SensitiveDataFilterEnricher : ILogEventEnricher { private readonly Regex _sensitivePattern; private readonly string _replacement; public SensitiveDataFilterEnricher(string pattern = @"\b(?:password|creditcard|cvv|token)\b", string replacement = "***REDACTED***") { _sensitivePattern = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); _replacement = replacement; } public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory) { // 过滤所有字符串类型属性 foreach (var prop in logEvent.Properties.ToList()) { if (prop.Value is ScalarValue scalar && scalar.Value is string strVal) { var cleanVal = _sensitivePattern.Replace(strVal, _replacement); logEvent.AddOrUpdateProperty(factory.CreateProperty(prop.Key, cleanVal)); } } } } }