在做开发中,审计日志应该是后续才会考虑的事情,前期可能会考虑下就是哪些数据要做日志记录,往往丢一个记录表,这个实现不能说不行,只是有点鸡肋!一起来看看我是如何改造官方的审计日志模块的!
先说下我的理解
Auditing审计日志,主要起作用的是中间件!
app.UseAuditing();//官方调用
也就是上面这段代码,一起看看他里面说了啥
public static IApplicationBuilder UseAuditing(this IApplicationBuilder app)
{
return app.UseMiddleware<AbpAuditingMiddleware>(Array.Empty<object>());
}
看到了吧,调用了中间件AbpAuditingMiddleware
再看看这个里面干了啥
public class AbpAuditingMiddleware : IMiddleware, ITransientDependency
{
private readonly IAuditingManager _auditingManager;
protected AbpAuditingOptions AuditingOptions { get; }
protected AbpAspNetCoreAuditingOptions AspNetCoreAuditingOptions { get; }
protected ICurrentUser CurrentUser { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
public AbpAuditingMiddleware(IAuditingManager auditingManager, ICurrentUser currentUser, IOptions<AbpAuditingOptions> auditingOptions, IOptions<AbpAspNetCoreAuditingOptions> aspNetCoreAuditingOptions, IUnitOfWorkManager unitOfWorkManager)
{
_auditingManager = auditingManager;
CurrentUser = currentUser;
UnitOfWorkManager = unitOfWorkManager;
AuditingOptions = auditingOptions.Value;
AspNetCoreAuditingOptions = aspNetCoreAuditingOptions.Value;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!AuditingOptions.IsEnabled || IsIgnoredUrl(context))
{
await next(context).ConfigureAwait(continueOnCapturedContext: false);
return;
}
bool hasError = false;
using IAuditLogSaveHandle saveHandle = _auditingManager.BeginScope();
try
{
_ = 1;
try
{
await next(context).ConfigureAwait(continueOnCapturedContext: false);
if (_auditingManager.Current.Log.Exceptions.Any())
{
hasError = true;
}
}
catch (Exception item)
{
hasError = true;
if (!_auditingManager.Current.Log.Exceptions.Contains(item))
{
_auditingManager.Current.Log.Exceptions.Add(item);
}
throw;
}
}
finally
{
if (await ShouldWriteAuditLogAsync(_auditingManager.Current.Log, context, hasError).ConfigureAwait(continueOnCapturedContext: false))
{
if (UnitOfWorkManager.Current != null)
{
try
{
await UnitOfWorkManager.Current.SaveChangesAsync().ConfigureAwait(continueOnCapturedContext: false);
}
catch (Exception item2)
{
if (!_auditingManager.Current.Log.Exceptions.Contains(item2))
{
_auditingManager.Current.Log.Exceptions.Add(item2);
}
}
}
await saveHandle.SaveAsync().ConfigureAwait(continueOnCapturedContext: false);
}
}
}
private bool IsIgnoredUrl(HttpContext context)
{
if (context.Request.Path.Value != null)
{
return AspNetCoreAuditingOptions.IgnoredUrls.Any((string x) => context.Request.Path.Value.StartsWith(x));
}
return false;
}
private async Task<bool> ShouldWriteAuditLogAsync(AuditLogInfo auditLogInfo, HttpContext httpContext, bool hasError)
{
foreach (Func<AuditLogInfo, Task<bool>> alwaysLogSelector in AuditingOptions.AlwaysLogSelectors)
{
if (await alwaysLogSelector(auditLogInfo).ConfigureAwait(continueOnCapturedContext: false))
{
return true;
}
}
if (AuditingOptions.AlwaysLogOnException && hasError)
{
return true;
}
if (!AuditingOptions.IsEnabledForAnonymousUsers && !CurrentUser.IsAuthenticated)
{
return false;
}
if (!AuditingOptions.IsEnabledForGetRequests && string.Equals(httpContext.Request.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
}
上面的代码,怎么说呢,我这个版本是5.3.4的
不管怎么说,反正我要改造的话,肯定是自己写一个中间件替换系统的,对吧!
为啥要改造呢,说白点就是官方的提供的太全了,全到我没这么多资源来处理它,有以下几个方面
查看官方的Entity你会发现存储的字段是真的多的,很多我感觉是不需要存储的,还有些字段长短的问题,据说可以通过配置修改字段的长短,我感觉就是不够直,绕来绕去太晕乎了!
有些字段是真的没必要的,比如用户姓名啥的,在我理解上,你要么从比如token中解析用户名,要么从缓存中获取用户对象获取,总之是一个子浪费!
你不觉得?举个例子,我之前有一个系统,一天的访问量PV大概在2000万,对你没看到后面有一个万字,所以官方的JWT啥的压根不敢用,因为太浪费资源了啊!!!所以后续我会出一篇改造官方的Auth模块的帖子!
里面很多字段的类型是约定的,特别让我奇怪的就是UserId,整个系统几乎都是使用Guid的,其实使用Guid不是不行,只是有没有这个必要,因为Guid的引入,会带来新的问题,并没有全方面碾压int/long,当然如果你说int/long有他的问题所在,这个就是互相博弈的结果了!总之有需求是非Guid的!
这里的不足不是数量不足,是需要的信息不一,比如我之前的系统中需要存储店铺ID,经办ID,等,上面说的很多冗余字段,这里又说不足,那干脆字段借用???
我个人是很反对借用这个事的,因为这个就比字段命名ABC更加迷糊,字段的名字本身会给你一个暗示,比如UserName结果你存储的是Address,就问你迷糊不迷糊,难不成你查看代码的时候,旁边还得放一个文档字段映射解说???
官方的默认实现,我感觉很多地方有改进的地方,比如存储,默认采用的是同步的方式(意思是同一个事务内,不是async的同步)!
如果要处理以上的问题,你会发觉大改了,而且改完后使用还是不顺手,那就干脆照着官方的代码,重新修改一份,入口处修改下即可!!!
我会从Entity开始改造,包括存储,中间件等,也可以引入自己的配置,比如你可以配置某些模块下的Entity做审计,某些不做,而不用一个一个的去标记审计的特性!
代码直接如下
/// <summary>
///
/// </summary>
[DisableAuditing]
public class MyAuditingLog : Entity<Guid>
{
/// <summary>
/// 用户ID 这里是一个Guid
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// 登陆者ID 这里是一个int
/// </summary>
public int LoginId { get; set; }
/// <summary>
/// 客户端ID 最大64字符
/// </summary>
[MaxLength(64)]
public string ClientId { get; set; }
/// <summary>
/// 程序名称 最大32字符
/// </summary>
[MaxLength(32)]
public string AppName { get; set; }
/// <summary>
/// 时间
/// </summary>
public DateTime ExecutionTime { get; set; }
/// <summary>
/// 持续
/// </summary>
public int ExecutionDuration { get; set; }
/// <summary>
/// 地址 最大16
/// </summary>
[MaxLength(16)]
public string ClientIpAddress { get; set; }
/// <summary>
/// 方法 最大8字符
/// </summary>
[MaxLength(8)]
public string HttpMethod { get; set; }
/// <summary>
/// 地址 最大128
/// </summary>
[MaxLength(128)]
public string Url { get; set; }
/// <summary>
/// 状态码
/// </summary>
public int? HttpStatusCode { get; set; }
/// <summary>
/// 异常 是否有异常信息
/// </summary>
public bool HasException { get; set; }
/// <summary>
/// 异常信息
/// </summary>
public ICollection<MyAuditingException> Exceptions { get; set; }
/// <summary>
/// 执行
/// </summary>
[NotMapped]
public ICollection<MyAuditingAction> Actions { get; set; }
/// <summary>
/// 变更
/// </summary>
[NotMapped]
public ICollection<MyEntityChange> EntityChanges { get; set; }
}
/// <summary>
///
/// </summary>
[DisableAuditing]
public class MyAuditingException:Entity<Guid>
{
/// <summary>
/// 错误信息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 代码块
/// </summary>
public string StackTrace { get; set; }
}
/// <summary>
///
/// </summary>
[DisableAuditing]
public class MyAuditingAction : Entity<Guid>
{
/// <summary>
///
/// </summary>
public Guid AuditLogId { get; set; }
/// <summary>
/// 最大64
/// </summary>
[MaxLength(64)]
public string ServiceName { get; set; }
/// <summary>
/// 函数名称 最大32字符
/// </summary>
[MaxLength(32)]
public string MethodName { get; set; }
/// <summary>
/// 最大128
/// </summary>
[MaxLength(128)]
public string Parameters { get; set; }
/// <summary>
/// 发生时间
/// </summary>
public DateTime ExecutionTime { get; set; }
/// <summary>
/// 持续时间
/// </summary>
public int ExecutionDuration { get; set; }
}
/// <summary>
///
/// </summary>
[DisableAuditing]
public class MyEntityChange : Entity<Guid>
{
/// <summary>
///
/// </summary>
public Guid AuditLogId { get; set; }
///// <summary>
/////
///// </summary>
//public Guid? TenantId { get; set; }
/// <summary>
/// 发生时间
/// </summary>
public DateTime ChangeTime { get; set; }
/// <summary>
/// 变更类型
/// </summary>
public EntityChangeType ChangeType { get; set; }
///// <summary>
/////
///// </summary>
//public Guid? EntityTenantId { get; set; }
/// <summary>
/// 最大64
/// </summary>
[MaxLength(64)]
public string EntityId { get; set; }
/// <summary>
/// 最大128
/// </summary>
[MaxLength(128)]
public string EntityTypeFullName { get; set; }
/// <summary>
/// 变更内容
/// </summary>
public ICollection<MyEntityPropertyChange> PropertyChanges { get; set; }
}
/// <summary>
///
/// </summary>
[DisableAuditing]
public class MyEntityPropertyChange : Entity<Guid>
{
///// <summary>
///// 变换ID
///// </summary>
//public Guid EntityChangeId { get; set; }
/// <summary>
/// 新值 长度不限
/// </summary>
public string NewValue { get; set; }
/// <summary>
/// 旧值 长度不限
/// </summary>
public string OriginalValue { get; set; }
/// <summary>
/// 最大32
/// </summary>
[MaxLength(32)]
public string PropertyName { get; set; }
/// <summary>
/// 最大64
/// </summary>
[MaxLength(64)]
public string PropertyTypeFullName { get; set; }
}
大致意思是保留要的,添加自己的,去除不需要的!
以下包含多个信息的改造,我全部放一起好找
/// <summary>
///
/// </summary>
public class MyAuditingHelper
{
/// <summary>
///
/// </summary>
public static MyAuditingLog BuildEntity(AuditLogInfo auditInfo)
{
try
{
var mylog = new MyAuditingLog
{
AppName = auditInfo.ApplicationName.AutoString(32),
ClientIpAddress = auditInfo.ClientIpAddress.AutoString(16),
ExecutionDuration = auditInfo.ExecutionDuration,
ExecutionTime = auditInfo.ExecutionTime,
HttpMethod = auditInfo.HttpMethod.AutoString(8),
ClientId = auditInfo.ClientId,
HttpStatusCode = auditInfo.HttpStatusCode,
Url = auditInfo.Url.AutoString(128),
UserId = auditInfo.UserId,
};
//_dbContext.Add(mylog);
//await _dbContext.SaveChangesAsync();
if (auditInfo.Actions?.Any() == true)
{
var actions = new List<MyAuditingAction>();
foreach (var item in auditInfo.Actions)
{
var one = new MyAuditingAction
{
AuditLogId = mylog.Id,
ExecutionDuration = item.ExecutionDuration,
ExecutionTime = item.ExecutionTime,
MethodName = item.MethodName.AutoString(32),
Parameters = item.Parameters.AutoString(128),
ServiceName = item.ServiceName.AutoString(64),
};
actions.Add(one);
}
mylog.Actions = actions;
//_dbContext.AddRange(actions);
//await _dbContext.SaveChangesAsync();
}
if (auditInfo.EntityChanges?.Any() == true)
{
var entitys = new List<MyEntityChange>();
foreach (var item in auditInfo.EntityChanges.ToList())
{
if (item.ChangeType != EntityChangeType.Created)
{
var one = new MyEntityChange
{
AuditLogId = mylog.Id,
ChangeTime = item.ChangeTime,
ChangeType = item.ChangeType,
EntityId = item.EntityId.AutoString(64),
EntityTypeFullName = item.EntityTypeFullName.AutoString(128),
};
//await _dbContext.SaveChangesAsync();
if (item.PropertyChanges?.Any() == true)
{
one.PropertyChanges = item.PropertyChanges
.Select(x => new MyEntityPropertyChange
{
NewValue = x.NewValue,
//EntityChangeId = one.Id,
OriginalValue = x.OriginalValue,
PropertyName = x.PropertyName.AutoString(32),
PropertyTypeFullName = x.PropertyTypeFullName.AutoString(64),
})
.ToList();
}
entitys.Add(one);
}
}
if (entitys.Any())
{
mylog.EntityChanges = entitys;
//_dbContext.AddRange(entitys);
//await _dbContext.SaveChangesAsync();
}
}
if (auditInfo.Exceptions?.Any() == true)
{
mylog.HasException = true;
mylog.Exceptions = auditInfo.Exceptions.Select(x => new MyAuditingException { Message = x.Message, StackTrace = x.StackTrace }).ToList();
}
//await _dbContext.SaveChangesAsync();
return mylog;
}
catch (Exception exl)
{
Log.Error(exl.ToString());
}
return null;
}
}
/// <summary>
///
/// </summary>
public class MyAuditLogContributor : AuditLogContributor
{
/// <summary>
///
/// </summary>
public MyAuditLogContributor(){}
/// <summary>
///
/// </summary>
/// <param name="context"></param>
public override void PostContribute(AuditLogContributionContext context)
{
base.PostContribute(context);
if (context.GetHttpContext().Request.Headers.TryGetValue(PublicString.TokenHeadName, out var val))
{
context.AuditInfo.ClientId = val.FirstOrDefault().AutoString(32);//这里要按照需求变更
//从这里解析出用户ID 可以通过解析token获取自己需要的数据
Console.WriteLine($"PostContribute.ClientId:{context.AuditInfo.ClientId}");
}
}
}
/// <summary>
///
/// </summary>
public class MyAuditingMiddleware : IMiddleware, ITransientDependency
{
/// <summary>
///
/// </summary>
private readonly IAuditingManager _auditingManager;
/// <summary>
///
/// </summary>
protected AbpAuditingOptions AuditingOptions { get; }
/// <summary>
///
/// </summary>
protected AbpAspNetCoreAuditingOptions AspNetCoreAuditingOptions { get; }
/// <summary>
///
/// </summary>
protected ICurrentUser CurrentUser { get; }
/// <summary>
///
/// </summary>
protected IUnitOfWorkManager UnitOfWorkManager { get; }
/// <summary>
///
/// </summary>
/// <param name="auditingManager"></param>
/// <param name="currentUser"></param>
/// <param name="auditingOptions"></param>
/// <param name="aspNetCoreAuditingOptions"></param>
/// <param name="unitOfWorkManager"></param>
public MyAuditingMiddleware(IAuditingManager auditingManager, ICurrentUser currentUser, IOptions<AbpAuditingOptions> auditingOptions, IOptions<AbpAspNetCoreAuditingOptions> aspNetCoreAuditingOptions, IUnitOfWorkManager unitOfWorkManager)
{
_auditingManager = auditingManager;
CurrentUser = currentUser;
UnitOfWorkManager = unitOfWorkManager;
AuditingOptions = auditingOptions.Value;
AspNetCoreAuditingOptions = aspNetCoreAuditingOptions.Value;
}
/// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <param name="next"></param>
/// <returns></returns>
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!AuditingOptions.IsEnabled || IsIgnoredUrl(context))
{
await next(context).ConfigureAwait(continueOnCapturedContext: false);
return;
}
//其实可以通过AspNetCoreAuditingOptions配置
bool hasError = false;
using IAuditLogSaveHandle saveHandle = _auditingManager.BeginScope();
try
{
try
{
await next(context).ConfigureAwait(continueOnCapturedContext: false);
if (_auditingManager.Current.Log.Exceptions.Any())
{
hasError = true;
}
}
catch (Exception item)
{
hasError = true;
if (!_auditingManager.Current.Log.Exceptions.Contains(item))
{
_auditingManager.Current.Log.Exceptions.Add(item);
}
throw;
}
}
finally
{
if (ShouldWriteAuditLogAsync(_auditingManager.Current.Log, context, hasError))
{
var currentUow = UnitOfWorkManager.Current; // 安全获取
if (currentUow != null)
{
try
{
await currentUow.SaveChangesAsync();
}
catch (Exception ex)
{
_auditingManager.Current.Log.Exceptions.Add(ex);
}
}
await saveHandle.SaveAsync().ConfigureAwait(continueOnCapturedContext: false);
}
}
}
/// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private bool IsIgnoredUrl(HttpContext context)
{
if (AspNetCoreAuditingOptions.IgnoredUrls != null && AspNetCoreAuditingOptions.IgnoredUrls.Any())
{
if (context.Request.Path.Value != null)
{
return AspNetCoreAuditingOptions.IgnoredUrls.Any((string x) => context.Request.Path.Value.StartsWith(x));
}
}
return false;
}
/// <summary>
///
/// </summary>
/// <param name="auditLogInfo"></param>
/// <param name="httpContext"></param>
/// <param name="hasError"></param>
/// <returns></returns>
private bool ShouldWriteAuditLogAsync(AuditLogInfo auditLogInfo, HttpContext httpContext, bool hasError)
{
if (AuditingOptions.AlwaysLogOnException && hasError)
{
return true;
}
if (auditLogInfo.HttpStatusCode.HasValue)
{
if (auditLogInfo.HttpStatusCode.Value == 204)
{
return false;
}
}
if (httpContext.Request != null)
{
if (httpContext.Request.Path.HasValue)
{
if (httpContext.Request.Path.Value.StartsWith("/api/cluster"))
{
return false;
}
if (httpContext.Request.Path.Value.StartsWith("/statushub/negotiate"))
{
return false;
}
}
}
//if (!AuditingOptions.IsEnabledForAnonymousUsers && !CurrentUser.IsAuthenticated)
//{
// return false;
//}
if (!AuditingOptions.IsEnabledForGetRequests && string.Equals(httpContext.Request.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase))
{
return false;
}
//当特性冲突的时候,最后的为准!
var _end = httpContext.GetEndpoint();
if (_end != null)
{
if (_end.Metadata != null)
{
var _size = _end.Metadata.Count;
if (_size > 0)
{
for (var k = _size - 1; k >= 0; k--)
{
var _item = _end.Metadata[k];
if (_item.GetType() == typeof(AuditedAttribute))
{
break;//跳出循环,由后续的判断接手
}
if (_item.GetType() == typeof(DisableAuditingAttribute))
{
return false;
}
}
}
}
}
return true;
}
}
/// <summary>
///
/// </summary>
public class MyAuditingStore : IAuditingStore, ITransientDependency
{
#region 下面这个是采用后台作业的模式进行
private readonly IBackgroundJobManager _jobManager;
/// <summary>
///
/// </summary>
/// <param name="job"></param>
public MyAuditingStore(IBackgroundJobManager job)
{
_jobManager = job;
}
/// <summary>
/// 新版本 采用后台作业模式
/// </summary>
/// <param name="auditInfo"></param>
/// <returns></returns>
public async Task SaveAsync(AuditLogInfo auditInfo)
{
var log = MyAuditingHelper.BuildEntity(auditInfo);
if (log != null)
{
await _jobManager.EnqueueAsync(new AuditLogQueueModel { LogInfo = log }, BackgroundJobPriority.Normal, delay: TimeSpan.Zero);
}
//var clonedInfo = JsonSerializer.Deserialize<AuditLogInfo>(
// JsonSerializer.Serialize(auditInfo, new JsonSerializerOptions
// {
// ReferenceHandler = ReferenceHandler.Preserve // 处理循环引用
// }),
// new JsonSerializerOptions
// {
// ReferenceHandler = ReferenceHandler.Preserve // 处理循环引用
// }
// );
}
#endregion
#region 以下是旧版本,同步模式,影响主程序
///// <summary>
/////
///// </summary>
//private readonly MyAuditingDb _dbContext;
///// <summary>
/////
///// </summary>
///// <param name="dbContext"></param>
//public MyAuditingStore(MyAuditingDb dbContext)
//{
// _dbContext = dbContext;
//}
///// <summary>
/////
///// </summary>
///// <param name="auditInfo"></param>
///// <returns></returns>
///// <exception cref="NotImplementedException"></exception>
//public async Task SaveAsync(AuditLogInfo auditInfo)
//{
// //下面后续要改成队列模式,提高吞吐!
// try
// {
// var mylog = new MyAuditingLog
// {
// AppName = auditInfo.ApplicationName.AutoString(32),
// ClientIpAddress = auditInfo.ClientIpAddress.AutoString(16),
// ExecutionDuration = auditInfo.ExecutionDuration,
// ExecutionTime = auditInfo.ExecutionTime,
// HttpMethod = auditInfo.HttpMethod.AutoString(8),
// ClientId = auditInfo.ClientId,
// HttpStatusCode = auditInfo.HttpStatusCode,
// Url = auditInfo.Url.AutoString(128),
// UserId = auditInfo.UserId,
// };
// _dbContext.Add(mylog);
// await _dbContext.SaveChangesAsync();
// if (auditInfo.Actions?.Any() == true)
// {
// var actions = new List<MyAuditingAction>();
// foreach (var item in auditInfo.Actions)
// {
// var one = new MyAuditingAction
// {
// AuditLogId = mylog.Id,
// ExecutionDuration = item.ExecutionDuration,
// ExecutionTime = item.ExecutionTime,
// MethodName = item.MethodName.AutoString(32),
// Parameters = item.Parameters.AutoString(128),
// ServiceName = item.ServiceName.AutoString(64),
// };
// actions.Add(one);
// }
// _dbContext.AddRange(actions);
// await _dbContext.SaveChangesAsync();
// }
// if (auditInfo.EntityChanges?.Any() == true)
// {
// var entitys = new List<MyEntityChange>();
// foreach (var item in auditInfo.EntityChanges.ToList())
// {
// if (item.ChangeType != EntityChangeType.Created)
// {
// var one = new MyEntityChange
// {
// AuditLogId = mylog.Id,
// ChangeTime = item.ChangeTime,
// ChangeType = item.ChangeType,
// EntityId = item.EntityId.AutoString(64),
// EntityTypeFullName = item.EntityTypeFullName.AutoString(128),
// };
// //await _dbContext.SaveChangesAsync();
// if (item.PropertyChanges?.Any() == true)
// {
// one.PropertyChanges = item.PropertyChanges
// .Select(x => new MyEntityPropertyChange
// {
// NewValue = x.NewValue,
// //EntityChangeId = one.Id,
// OriginalValue = x.OriginalValue,
// PropertyName = x.PropertyName.AutoString(32),
// PropertyTypeFullName = x.PropertyTypeFullName.AutoString(64),
// })
// .ToList();
// }
// entitys.Add(one);
// }
// }
// if (entitys.Any())
// {
// _dbContext.AddRange(entitys);
// await _dbContext.SaveChangesAsync();
// }
// }
// }
// catch (Exception exl)
// {
// Log.Error(exl.ToString());
// }
//}
#endregion
}
/// <summary>
///
/// </summary>
public class AuditLogQueueModel
{
/// <summary>
///
/// </summary>
public MyAuditingLog LogInfo { get; set; }
/// <summary>
///
/// </summary>
public DateTime CreateDate { get; set; } = DateTime.Now;
}
/// <summary>
/// 这个后续修改下,改成Channel的方式
/// </summary>
public class MyAuditJobHandler : AsyncBackgroundJob<AuditLogQueueModel>, ITransientDependency
{
private readonly MyAuditingDb _dbContext;
/// <summary>
///
/// </summary>
/// <param name="dbContext"></param>
public MyAuditJobHandler(MyAuditingDb dbContext)
{
_dbContext = dbContext;
}
/// <summary>
///
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public override async Task ExecuteAsync(AuditLogQueueModel args)
{
var mylog = args.LogInfo;
try
{
if (mylog.Exceptions?.Any() == true)
{
mylog.HasException = true;
}
_dbContext.Add(mylog);
await _dbContext.SaveChangesAsync();
if (mylog.Actions?.Any() == true)
{
foreach(var item in mylog.Actions)
{
item.AuditLogId = mylog.Id;
}
_dbContext.AddRange(mylog.Actions);
}
if (mylog.EntityChanges?.Any() == true)
{
foreach(var item in mylog.EntityChanges)
{
item.AuditLogId = mylog.Id;
}
_dbContext.AddRange(mylog.EntityChanges);
}
await _dbContext.SaveChangesAsync();
}
catch (Exception exl)
{
Log.Error(exl.ToString());
}
}
}
然后就是在入口处如何配置的问题了
context.Services.AddTransient<MyAuditLogContributor>();
// 注册你的中间件(如果是ASP.NET Core中间件)
context.Services.AddTransient<MyAuditingMiddleware>();
context.Services.Configure<AbpAuditingOptions>(options =>
{
options.IsEnabled = true;//启用日志记录
options.IsEnabledForGetRequests = false;//get的都忽略
options.IsEnabledForAnonymousUsers = true;
options.EntityHistorySelectors.Clear();//
var contributor = context.Services.GetRequiredService<MyAuditLogContributor>();
if (!options.Contributors.Contains(contributor))
{
options.Contributors.Add(contributor);
}
});
这里就是启用中间件
app.UseMiddleware<MyAuditingMiddleware>();
注意别忘了引用官方的模块
typeof(AbpAuditingModule)
上面弄了一通,少了数据库的配置,添加下如下代码
/// <summary>
///
/// </summary>
[ConnectionStringName(PasteTestDbProperties.AbpAuditingConnectionStringName)]
public class MyAuditingDb : AbpDbContext<MyAuditingDb>
{
/// <summary>
///
/// </summary>
/// <param name="options"></param>
public MyAuditingDb(DbContextOptions<MyAuditingDb> options) : base(options)
{
}
/// <summary>
///
/// </summary>
/// <param name="builder"></param>
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<MyAuditingLog>(b =>
{
b.ToTable("MyAuditingLog");
b.Property(x => x.Id).HasValueGenerator<SequentialGuidValueGenerator>().ValueGeneratedOnAdd();
b.ConfigureByConvention();
});
builder.Entity<MyAuditingAction>(b =>
{
b.ToTable("MyAuditingAction");
b.Property(x => x.Id).HasValueGenerator<SequentialGuidValueGenerator>().ValueGeneratedOnAdd();
b.ConfigureByConvention();
});
builder.Entity<MyEntityChange>(b =>
{
b.ToTable("MyEntityChange");
b.Property(x => x.Id).HasValueGenerator<SequentialGuidValueGenerator>().ValueGeneratedOnAdd();
b.ConfigureByConvention();
});
builder.Entity<MyEntityPropertyChange>(b =>
{
b.ToTable("MyEntityPropertyChange");
b.Property(x => x.Id).HasValueGenerator<SequentialGuidValueGenerator>().ValueGeneratedOnAdd();
b.ConfigureByConvention();
});
builder.Entity<MyAuditingException>(b =>
{
b.ToTable("MyAuditingException");
b.Property(x => x.Id).HasValueGenerator<SequentialGuidValueGenerator>().ValueGeneratedOnAdd();
b.ConfigureByConvention();
});
}
/// <summary>
///
/// </summary>
public DbSet<MyAuditingLog> MyAuditingLog { get; set; }
/// <summary>
///
/// </summary>
public DbSet<MyAuditingAction> MyAuditingAction { get; set; }
/// <summary>
///
/// </summary>
public DbSet<MyEntityChange> MyEntityChange { get; set; }
/// <summary>
///
/// </summary>
public DbSet<MyAuditingException> MyAuditingException { get; set; }
/// <summary>
///
/// </summary>
public DbSet<MyEntityPropertyChange> MyEntityPropertyChange { get; set; }
}
没看错,就是独立的数据库链接,不和业务的挂钩,这样可以做到切割,然后是日志的记录其实数据很大!
上面提到的自增有序Guid
/// <summary>
///
/// </summary>
public class SequentialGuidValueGenerator : ValueGenerator<Guid>
{
/// <summary>
/// 模块初始化的时候,需要进行赋值
/// </summary>
public static IGuidGenerator _guidGenerator;
//public SequentialGuidValueGenerator():this(new SequentialGuidGenerator())
//{
// //_guidGenerator = guidGenerator;
//}
/// <summary>
///
/// </summary>
public SequentialGuidValueGenerator()
{
//IGuidGenerator guidGenerator
//_guidGenerator = guidGenerator;
//_guidGenerator = ServiceLocator.Current.GetService<IGuidGenerator>();
}
/// <summary>
///
/// </summary>
/// <param name="entry"></param>
/// <returns></returns>
public override Guid Next(EntityEntry entry)
=> _guidGenerator.Create(); // 生成有序 GUID
/// <summary>
///
/// </summary>
public override bool GeneratesTemporaryValues => false;
}
启动项目后,就可以测试结果了,以下是我的记录
这样,是不是哪里不爽改哪里!
上面的代码其实还是有问题的,
1.比如我改成了异步方式写入日志,如果出现问题了,怎么一个处理方案,记录错误是肯定的,然后就是如何补回日志了
2.如果日志一直扩大也不是个办法,可能要采用按需删除过期日志,要基于不同纬度做不同处理
3.上面的BackgroundJob其实我感觉不好用,可能会改成本地的Channel模式,或者是改成RabbitMQ的模式以便应对集群部署
4.日志记录了,那就是剩下查询的问题了,比如我要查询某一个数据库表的某一个字段的变更记录!!!
更深层次的改造估计是AuditLogInfo的Property了
估计要使用到SetProperty和GetProperty,后续试试,是否可以自定义多一些字段!