🍓 refactor(logging): 重构日志记录逻辑

This commit is contained in:
喵你个汪呀 2025-08-24 13:20:20 +08:00
parent 086b6715cd
commit b6381a0660
5 changed files with 476 additions and 116 deletions

View File

@ -12,7 +12,7 @@ namespace Admin.NET.Core;
[SugarTable(null, "系统差异日志表")]
[SysTable]
[LogTable]
public partial class SysLogDiff : EntityTenant
public partial class SysLogDiff : EntityTenantId
{
/// <summary>
/// 操作前记录
@ -55,4 +55,23 @@ public partial class SysLogDiff : EntityTenant
/// </summary>
[SugarColumn(ColumnDescription = "耗时")]
public long? Elapsed { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[SugarColumn(ColumnDescription = "创建时间", IsOnlyIgnoreUpdate = true)]
public virtual DateTime CreateTime { get; set; }
/// <summary>
/// 创建者Id
/// </summary>
[OwnerUser]
[SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true)]
public virtual long? CreateUserId { get; set; }
/// <summary>
/// 创建者姓名
/// </summary>
[SugarColumn(ColumnDescription = "创建者姓名", Length = 64, IsOnlyIgnoreUpdate = true)]
public virtual string? CreateUserName { get; set; }
}

View File

@ -12,7 +12,7 @@ namespace Admin.NET.Core;
[SugarTable(null, "系统消息日志表")]
[SysTable]
[LogTable]
public partial class SysLogMsg : EntityTenant
public partial class SysLogMsg : EntityTenantId
{
/// <summary>
/// 消息类型
@ -125,4 +125,23 @@ public partial class SysLogMsg : EntityTenant
[SugarColumn(ColumnDescription = "发送者设备", Length = 256)]
[MaxLength(256)]
public string? SendDevice { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[SugarColumn(ColumnDescription = "创建时间", IsNullable = true, IsOnlyIgnoreUpdate = true)]
public virtual DateTime CreateTime { get; set; }
/// <summary>
/// 创建者Id
/// </summary>
[OwnerUser]
[SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true)]
public virtual long? CreateUserId { get; set; }
/// <summary>
/// 创建者姓名
/// </summary>
[SugarColumn(ColumnDescription = "创建者姓名", Length = 64, IsOnlyIgnoreUpdate = true)]
public virtual string? CreateUserName { get; set; }
}

View File

@ -12,7 +12,7 @@ namespace Admin.NET.Core;
[SugarTable(null, "系统访问日志表")]
[SysTable]
[LogTable]
public partial class SysLogVis : EntityTenant
public partial class SysLogVis : EntityTenantId
{
/// <summary>
/// 模块名称
@ -113,4 +113,24 @@ public partial class SysLogVis : EntityTenant
[SugarColumn(ColumnDescription = "真实姓名", Length = 32)]
[MaxLength(32)]
public string? RealName { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[SugarColumn(ColumnDescription = "创建时间", IsOnlyIgnoreUpdate = true)]
public virtual DateTime CreateTime { get; set; }
/// <summary>
/// 创建者Id
/// </summary>
[OwnerUser]
[SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true)]
public virtual long? CreateUserId { get; set; }
/// <summary>
/// 创建者姓名
/// </summary>
[SugarColumn(ColumnDescription = "创建者姓名", Length = 64, IsOnlyIgnoreUpdate = true)]
public virtual string? CreateUserName { get; set; }
}

View File

@ -14,13 +14,17 @@ namespace Admin.NET.Core;
public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable
{
private static readonly Lazy<UserManager> _userManager = new(() => App.GetService<UserManager>());
private readonly IServiceScope _serviceScope;
private readonly IEventPublisher _eventPublisher;
private static readonly Lazy<SqlSugarRepository<SysUser>> _sysUserRep = new(() => App.GetService<SqlSugarRepository<SysUser>>());
private readonly ILogger<DatabaseLoggingWriter> _logger;
private readonly SysConfigService _sysConfigService;
private readonly IEventPublisher _eventPublisher;
private readonly IServiceScope _serviceScope;
private readonly SqlSugarScopeProvider _db;
public DatabaseLoggingWriter(IServiceScopeFactory serviceScopeFactory, IEventPublisher eventPublisher, ILogger<DatabaseLoggingWriter> logger)
public DatabaseLoggingWriter(
IServiceScopeFactory serviceScopeFactory,
ILogger<DatabaseLoggingWriter> logger,
IEventPublisher eventPublisher)
{
_serviceScope = serviceScopeFactory.CreateScope();
_sysConfigService = _serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
@ -95,34 +99,24 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable
return;
}
var loggingMonitor = JSON.Deserialize<dynamic>(jsonStr);
// 获取当前操作者
string account = "", realName = "", userId = "", tenantId = "";
if (loggingMonitor.authorizationClaims != null)
{
var authDict = (loggingMonitor.authorizationClaims as IEnumerable<dynamic>)!.ToDictionary(u => u.type.ToString(), u => u.value.ToString());
userId = authDict.GetValueOrDefault(ClaimConst.UserId);
var userSession = _userManager.Value.GetSession(userId);
account = userSession?.Account;
realName = userSession?.RealName;
tenantId = userSession?.TenantId?.ToString();
}
var loggingMonitor = JSON.Deserialize<LoggingMonitorDto>(jsonStr);
var userInfo = GetUserInfo(loggingMonitor);
// 优先获取 X-Forwarded-For 头部信息携带的IP地址如nginx代理配置转发
var remoteIPv4 = ((JArray)loggingMonitor.requestHeaders).OfType<JObject>()
.FirstOrDefault(header => (string)header["key"] == "X-Forwarded-For")?["value"]?.ToString();
var reqHeaders = loggingMonitor.RequestHeaders.ToDictionary(u => u.Key, u => u.Value);
var remoteIPv4 = reqHeaders.GetValueOrDefault("X-Forwarded-For")?.ToString();
// 获取IP地理位置
if (string.IsNullOrEmpty(remoteIPv4))
remoteIPv4 = loggingMonitor.remoteIPv4;
if (string.IsNullOrEmpty(remoteIPv4)) remoteIPv4 = reqHeaders.GetValueOrDefault("remoteIPv4")?.ToString();
(string ipLocation, double? longitude, double? latitude) = CommonHelper.GetIpAddress(remoteIPv4);
// 获取设备信息
var browser = "";
var os = "";
if (loggingMonitor.userAgent != null)
var browser = "";
if (loggingMonitor.UserAgent != null)
{
var client = Parser.GetDefault().Parse(loggingMonitor.userAgent.ToString());
var client = Parser.GetDefault().Parse(loggingMonitor.UserAgent);
browser = $"{client.UA.Family} {client.UA.Major}.{client.UA.Minor} / {client.Device.Family}";
os = $"{client.OS.Family} {client.OS.Major} {client.OS.Minor}";
}
@ -130,120 +124,119 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable
// 捕捉异常,否则会由于 unhandled exception 导致程序崩溃
try
{
// 记录异常日志-发送邮件
if (logMsg.Exception != null || loggingMonitor.exception != null)
var logEntity = new SysLogOp
{
await _db.Insertable(new SysLogEx
{
ControllerName = loggingMonitor.controllerName,
ActionName = loggingMonitor.actionTypeName,
DisplayTitle = loggingMonitor.displayTitle,
Status = loggingMonitor.returnInformation?.httpStatusCode,
RemoteIp = remoteIPv4,
Location = ipLocation,
Longitude = longitude,
Latitude = latitude,
Browser = browser, // loggingMonitor.userAgent,
Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture,
Elapsed = loggingMonitor.timeOperationElapsedMilliseconds,
LogDateTime = logMsg.LogDateTime,
Account = account,
RealName = realName,
HttpMethod = loggingMonitor.httpMethod,
RequestUrl = loggingMonitor.requestUrl,
RequestParam = (loggingMonitor.parameters == null || loggingMonitor.parameters.Count == 0) ? null : JSON.Serialize(loggingMonitor.parameters[0].value),
ReturnResult = loggingMonitor.returnInformation == null ? null : JSON.Serialize(loggingMonitor.returnInformation),
EventId = logMsg.EventId.Id,
ThreadId = logMsg.ThreadId,
TraceId = logMsg.TraceId,
Exception = JSON.Serialize(loggingMonitor.exception),
Message = logMsg.Message,
CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId),
TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId),
LogLevel = logMsg.LogLevel
}).ExecuteCommandAsync();
ControllerName = loggingMonitor.DisplayName,
ActionName = loggingMonitor.ActionTypeName,
DisplayTitle = loggingMonitor.DisplayTitle,
Status = loggingMonitor.ReturnInformation?.HttpStatusCode?.ToString(),
RemoteIp = remoteIPv4,
Location = ipLocation,
Longitude = longitude,
Latitude = latitude,
Browser = browser,
Os = os,
Elapsed = loggingMonitor.TimeOperationElapsedMilliseconds,
Message = logMsg.Message,
HttpMethod = loggingMonitor.HttpMethod,
RequestUrl = loggingMonitor.RequestUrl,
RequestParam = loggingMonitor.Parameters is { Count: > 0 } ? JSON.Serialize(loggingMonitor.Parameters[0].Value) : null,
ReturnResult = loggingMonitor.ReturnInformation?.Value != null ? JSON.Serialize(loggingMonitor.ReturnInformation?.Value) : null,
Exception = loggingMonitor.Exception == null ? JSON.Serialize(logMsg.Exception) : JSON.Serialize(loggingMonitor.Exception),
LogDateTime = logMsg.LogDateTime,
EventId = logMsg.EventId.Id,
ThreadId = logMsg.ThreadId,
TraceId = logMsg.TraceId,
Account = userInfo.Account,
RealName = userInfo.RealName,
CreateUserId = userInfo.UserId,
CreateUserName = userInfo.RealName,
TenantId = userInfo.TenantId,
LogLevel = logMsg.LogLevel,
};
// 记录异常日志-发送邮件
if (logMsg.Exception != null || loggingMonitor.Exception != null)
{
await _db.Insertable(logEntity.Adapt<SysLogEx>()).ExecuteCommandAsync();
// 将异常日志发送到邮件
await _eventPublisher.PublishAsync(CommonConst.SendErrorMail, logMsg.Exception ?? loggingMonitor.exception);
await _eventPublisher.PublishAsync(CommonConst.SendErrorMail, logMsg.Exception ?? loggingMonitor.Exception);
return;
}
// 记录访问日志-登录退出
if (loggingMonitor.actionName == "login" || loggingMonitor.actionName == "loginPhone" || loggingMonitor.actionName == "logout")
if (loggingMonitor.ActionName == "login" || loggingMonitor.ActionName == "loginPhone" || loggingMonitor.ActionName == "logout")
{
if (loggingMonitor.actionName != "logout")
{
dynamic para = Clay.Parse((loggingMonitor.parameters == null) ? null : JSON.Serialize(loggingMonitor.parameters[0].value));
if (loggingMonitor.actionName == "login")
account = para.account;
else if (loggingMonitor.actionName == "loginPhone")
account = para.phone;
}
await _db.Insertable(new SysLogVis
{
ControllerName = loggingMonitor.displayName,
ActionName = loggingMonitor.actionTypeName,
DisplayTitle = loggingMonitor.displayTitle,
Status = loggingMonitor.returnInformation?.httpStatusCode,
RemoteIp = remoteIPv4,
Location = ipLocation,
Longitude = longitude,
Latitude = latitude,
Browser = browser, // loggingMonitor.userAgent,
Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture,
Elapsed = loggingMonitor.timeOperationElapsedMilliseconds,
LogDateTime = logMsg.LogDateTime,
Account = account,
RealName = realName,
CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId),
TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId),
LogLevel = logMsg.LogLevel
}).ExecuteCommandAsync();
await _db.Insertable(logEntity.Adapt<SysLogVis>()).ExecuteCommandAsync();
return;
}
// 记录操作日志
if (!await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysOpLog)) return;
await _db.Insertable(new SysLogOp
{
ControllerName = loggingMonitor.controllerName,
ActionName = loggingMonitor.actionTypeName,
DisplayTitle = loggingMonitor.displayTitle,
Status = loggingMonitor.returnInformation?.httpStatusCode,
RemoteIp = remoteIPv4,
Location = ipLocation,
Longitude = longitude,
Latitude = latitude,
Browser = browser, // loggingMonitor.userAgent,
Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture,
Elapsed = loggingMonitor.timeOperationElapsedMilliseconds,
LogDateTime = logMsg.LogDateTime,
Account = account,
RealName = realName,
HttpMethod = loggingMonitor.httpMethod,
RequestUrl = loggingMonitor.requestUrl,
RequestParam = (loggingMonitor.parameters == null || loggingMonitor.parameters.Count == 0) ? null : JSON.Serialize(loggingMonitor.parameters[0].value),
ReturnResult = loggingMonitor.returnInformation == null ? null : JSON.Serialize(loggingMonitor.returnInformation),
EventId = logMsg.EventId.Id,
ThreadId = logMsg.ThreadId,
TraceId = logMsg.TraceId,
Exception = loggingMonitor.exception == null ? null : JSON.Serialize(loggingMonitor.exception),
Message = logMsg.Message,
CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId),
TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId),
LogLevel = logMsg.LogLevel
}).ExecuteCommandAsync();
await _db.Insertable(logEntity.Adapt<SysLogOp>()).ExecuteCommandAsync();
await Task.Delay(50); // 延迟 0.05 秒写入数据库,有效减少高频写入数据库导致死锁问题
}
catch (Exception ex)
{
_logger.LogError(ex, "操作日志入库");
// 将异常日志发送到邮件
await _eventPublisher.PublishAsync(CommonConst.SendErrorMail, ex);
}
}
/// <summary>
/// 从日志消息中获取用户信息
/// </summary>
/// <param name="loggingMonitor"></param>
/// <returns></returns>
private LoggingUserInfo GetUserInfo(LoggingMonitorDto loggingMonitor)
{
LoggingUserInfo result = new();
if (loggingMonitor.AuthorizationClaims != null)
{
result.UserId = long.TryParse(loggingMonitor.AuthorizationClaims
.ToDictionary(u => u.Type, u => u.Value)
.GetValueOrDefault(ClaimConst.UserId) ?? ""
, out var temp) ? temp : null;
var userSession = _userManager.Value.GetSession(result.UserId);
result.TenantId = long.TryParse(userSession?.TenantId?.ToString() ?? "", out temp) ? temp : null;
result.RealName = userSession?.RealName;
result.Account = userSession?.Account;
}
// 退出登陆时没有session尝试从数据库中获取
if (string.IsNullOrWhiteSpace(result.Account) && result.UserId != null)
{
var user = _sysUserRep.Value.GetById(result.UserId);
result.Account = user?.TenantId?.ToString();
result.RealName = user?.RealName;
result.Account = user?.Account;
}
// 用户登陆时没有userId需要根据入参获取
if (loggingMonitor.ActionName == "login" && loggingMonitor.Parameters is { Count: > 0 })
{
try
{
result.Account = (loggingMonitor.Parameters[0].Value as JObject)!.GetValue("account")?.ToString();
if (!string.IsNullOrEmpty(result.Account))
{
var user = _sysUserRep.Value.AsQueryable().First(u => u.Account == result.Account);
result.TenantId = user?.TenantId;
result.RealName = user?.RealName;
result.UserId = user?.Id;
}
}
catch
{
// ignored
}
}
return result;
}
/// <summary>
/// 释放服务作用域
/// </summary>

View File

@ -0,0 +1,309 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 日志监控信息输出参数
/// </summary>
public class LoggingMonitorDto
{
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 控制器名称
/// </summary>
public string ControllerName { get; set; }
/// <summary>
/// 控制器类型名称
/// </summary>
public string ControllerTypeName { get; set; }
/// <summary>
/// 操作方法名称
/// </summary>
public string ActionName { get; set; }
/// <summary>
/// 操作方法类型名称
/// </summary>
public string ActionTypeName { get; set; }
/// <summary>
/// 区域名称Area
/// </summary>
public string AreaName { get; set; }
/// <summary>
/// 显示名称(全路径)
/// </summary>
public string DisplayName { get; set; }
/// <summary>
/// 显示标题
/// </summary>
public string DisplayTitle { get; set; }
/// <summary>
/// 本地IPv4地址
/// </summary>
public string LocalIPv4 { get; set; }
/// <summary>
/// 本地端口
/// </summary>
public int? LocalPort { get; set; }
/// <summary>
/// 远程IPv4地址
/// </summary>
public string RemoteIPv4 { get; set; }
/// <summary>
/// 远程端口
/// </summary>
public int? RemotePort { get; set; }
/// <summary>
/// HTTP请求方法如GET、POST
/// </summary>
public string HttpMethod { get; set; }
/// <summary>
/// 分布式追踪IDTraceId
/// </summary>
public string TraceId { get; set; }
/// <summary>
/// 线程ID
/// </summary>
public int? ThreadId { get; set; }
/// <summary>
/// 请求URL
/// </summary>
public string RequestUrl { get; set; }
/// <summary>
/// 协议版本如HTTP/1.1
/// </summary>
public string Protocol { get; set; }
/// <summary>
/// 引用页面URLReferer
/// </summary>
public string RefererUrl { get; set; }
/// <summary>
/// 用户代理User-Agent
/// </summary>
public string UserAgent { get; set; }
/// <summary>
/// 接受的语言Accept-Language
/// </summary>
public string AcceptLanguage { get; set; }
/// <summary>
/// 请求来源client、server等
/// </summary>
public string RequestFrom { get; set; }
/// <summary>
/// 请求头中的Cookies
/// </summary>
public string RequestHeaderCookies { get; set; }
/// <summary>
/// 操作耗时(毫秒)
/// </summary>
public long? TimeOperationElapsedMilliseconds { get; set; }
/// <summary>
/// 访问令牌AccessToken
/// </summary>
public string AccessToken { get; set; }
/// <summary>
/// 响应头中的Cookies
/// </summary>
public string ResponseHeaderCookies { get; set; }
/// <summary>
/// 操作系统描述
/// </summary>
public string OsDescription { get; set; }
/// <summary>
/// 操作系统架构如X64
/// </summary>
public string OsArchitecture { get; set; }
/// <summary>
/// 框架描述(如.NET 8.0.18
/// </summary>
public string FrameworkDescription { get; set; }
/// <summary>
/// 基础框架名称如Furion.Pure
/// </summary>
public string BasicFramework { get; set; }
/// <summary>
/// 基础框架版本
/// </summary>
public string BasicFrameworkVersion { get; set; }
/// <summary>
/// 入口程序集名称
/// </summary>
public string EntryAssemblyName { get; set; }
/// <summary>
/// 进程名称
/// </summary>
public string ProcessName { get; set; }
/// <summary>
/// 部署服务器如Kestrel
/// </summary>
public string DeployServer { get; set; }
/// <summary>
/// 启动监听地址
/// </summary>
public string StartUrls { get; set; }
/// <summary>
/// 环境如Development、Production
/// </summary>
public string Environment { get; set; }
/// <summary>
/// 授权声明集合
/// </summary>
public List<LoggingAuthorizationClaimsDto> AuthorizationClaims { get; set; }
/// <summary>
/// 请求头集合
/// </summary>
public List<KeyValuePair<string, object>> RequestHeaders { get; set; }
/// <summary>
/// 请求参数集合
/// </summary>
public List<LoggingParametersDto> Parameters { get; set; }
/// <summary>
/// 返回信息
/// </summary>
public LoggingReturnInformationDto ReturnInformation { get; set; }
/// <summary>
/// 异常信息
/// </summary>
public object Exception { get; set; }
/// <summary>
/// 验证信息
/// </summary>
public object Validation { get; set; }
}
public class LoggingAuthorizationClaimsDto
{
/// <summary>
/// 类型名
/// </summary>
public string Type { get; set; }
/// <summary>
/// 值类型
/// </summary>
public string ValueType { get; set; }
/// <summary>
/// 值
/// </summary>
public string Value { get; set; }
}
/// <summary>
/// 输入参数
/// </summary>
public class LoggingParametersDto
{
/// <summary>
/// 输入类型
/// </summary>
public string Name { get; set; }
/// <summary>
/// 输入类型
/// </summary>
public string Type { get; set; }
/// <summary>
/// 实际输入数据
/// </summary>
public object Value { get; set; }
}
/// <summary>
/// 返回信息详情
/// </summary>
public class LoggingReturnInformationDto
{
/// <summary>
/// 返回类型如X.Core.XResult<System.Object>
/// </summary>
public string Type { get; set; }
/// <summary>
/// HTTP状态码
/// </summary>
public int? HttpStatusCode { get; set; }
/// <summary>
/// 实际返回类型如Task<List<T>>
/// </summary>
public string ActType { get; set; }
/// <summary>
/// 实际返回数据
/// </summary>
public object Value { get; set; }
}
/// <summary>
/// 用户信息
/// </summary>
public class LoggingUserInfo
{
/// <summary>
/// 用户Id
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 账号
/// </summary>
public string Account { get; set; }
/// <summary>
/// 真实姓名
/// </summary>
public string RealName { get; set; }
/// <summary>
/// 租户Id
/// </summary>
public long? TenantId { get; set; }
}