TemplatePro/BZPT.Api/Middleware/Enricher.cs

187 lines
7.9 KiB
C#

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<IHttpContextAccessor>();
// 配置 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));
}
}
}
}
}