這是《
ABP大型項目實戰》系列文章的一篇。
項目發布到生產環境后難免會有錯誤。
那么如何進行調試和排錯呢?
我看到俱樂部里有人是直接登陸生產服務器把數據庫下載到開發機器進行調試排錯。
這種辦法是不適用於大型項目的:
-
首先,大型項目(特別是全球都有分公司的大型項目)很有可能24小時都有人在使用。所以盡量避免直接登錄生產服務器操作,就算部署,也應該用DevOps、藍綠部署等辦法。
-
另外,如果大型項目有采用金絲雀發布和A/B測試,那么把數據庫下載到開發機器這種方法是很不適用的。
-
即使大型項目沒有采用金絲雀發布和A/B測試,也不適合把數據庫下載到開發機器進行調試排錯。因為數據庫有可能很大,網絡傳輸需要時間,特別是連VPN的時候,甚至有可能要從歐洲傳到中國,又要從中國回傳到歐洲。
-
生產環境數據庫下載后為了安全還需要脫敏,這也需要時間。
還有其他方法,但是這些方法都存在一個問題,就是時光不能倒流,很多時候你是不可能叫客戶回來重現一遍操作流程來復現bug的。
那么有什么好辦法呢?
有的,通過查看日志來調試與排錯。
ABP在這方面做得不錯,內置了審計日志,提供了詳細日志基類。嗯,這是兩塊內容了,一篇文章是講不完的,所以我分開多篇文章來講。
先從審計日志開始吧。
不得不說,ABP在這方面做得很好,審計日志是透明的,你要關閉審計日志反而要寫代碼控制。當然,這里不建議關閉審計日志。
然而,ABP的審計日志只提供了寫,沒有提供讀的UI,這樣的話,要看審計日志就必須要打開數據庫查看了。
從前面的描述看到,這種做法在大型項目肯定是行不通的啦。我們必須要提供讀取審計日志的UI。這就是這篇文章的主題。
在我使用的ABP 3.4版本里面,並沒有提供審計日志的讀取AppService, 但是提供了審計日志的Entity class。(注意: ABP更新很頻繁,所以你目前使用的版本有可能新增甚至刪除了部分interface或class)
所以我們第一步是先圍繞ABP提供的審計日志Entity class(AuditLog)來建立AppService class和相關讀取Mehtod. 以下是ABP 3.4版本的示例代碼:
注意:這是示例代碼,直接復制到你的項目里不經任何修改很大概率是編譯不通過的,你必須要根據你的項目實際情況進行修改,你最起碼要改命名空間吧。
注意:ABP自己已經提供了AuditLog實體類,我們不需要另外再新建實體類了。
IAuditLogAppService.cs
public interface IAuditLogAppService : IApplicationService { /// <summary> /// 大型項目的審計日志量會十分大,所以最起碼要分頁 /// </summary> /// <param name="input"></param> /// <returns></returns> Task<PagedResultDto<AuditLogListDto>> GetAuditLogs(GetAuditLogsInput input); /// <summary> /// 一定要提供Excel下載功能,一般建議是按照時間段選取 /// </summary> /// <param name="input"></param> /// <returns></returns> Task<FileDto> GetAuditLogsToExcel(GetAuditLogsInput input); /// <summary> /// 提供全部審計日志的Excel下載,因為數據量會比較大,需要在服務器先壓縮好,再提供給客戶端下載。 /// </summary> /// <returns></returns> Task<FileDto> GetAuditLogsToExcel(); //List<AuditLogListDto> GetAllAuditLogs(); //錯誤案例示范,大型項目的審計日志量會十分大,所以最起碼要分頁 }
AuditLogListDto.cs
using System; using Abp.Application.Services.Dto; using Abp.Auditing; using Abp.AutoMapper; [AutoMapFrom(typeof(AuditLog))] public class AuditLogListDto : EntityDto<long> { public long? UserId { get; set; } public string UserName { get; set; } public int? ImpersonatorTenantId { get; set; } public long? ImpersonatorUserId { get; set; } public string ServiceName { get; set; } public string MethodName { get; set; } public string Parameters { get; set; } public DateTime ExecutionTime { get; set; } public int ExecutionDuration { get; set; } public string ClientIpAddress { get; set; } public string ClientName { get; set; } public string BrowserInfo { get; set; } public string Exception { get; set; } public string CustomData { get; set; } }
AuditLogAppService.cs
[DisableAuditing] //屏蔽這個AppService的審計功能 [AbpAuthorize(AppPermissions.Pages_Administration_AuditLogs)] public class AuditLogAppService : DemoAppServiceBase, IAuditLogAppService { private readonly IRepository<AuditLog, long> _auditLogRepository; private readonly IRepository<User, long> _userRepository; private readonly IAuditLogListExcelExporter _auditLogListExcelExporter; private readonly INamespaceStripper _namespaceStripper; public AuditLogAppService( IRepository<AuditLog, long> auditLogRepository, IRepository<User, long> userRepository, IAuditLogListExcelExporter auditLogListExcelExporter, INamespaceStripper namespaceStripper) { _auditLogRepository = auditLogRepository; _userRepository = userRepository; _auditLogListExcelExporter = auditLogListExcelExporter; _namespaceStripper = namespaceStripper; } // 下面視具體業務情況實現接口的方法 }
以下是使用EF來查詢Auditlog關聯User表數據的示例代碼:
1 IQueryable<AuditLogAndUser> query = from auditLog in _auditLogRepository.GetAll() 2 join user in _userRepository.GetAll() on auditLog.UserId equals user.Id into userJoin 3 from joinedUser in userJoin.DefaultIfEmpty() 4 where auditLog.ExecutionTime >= input.StartDate && auditLog.ExecutionTime <= input.EndDate 5 select new AuditLogAndUser { AuditLog = auditLog, User = joinedUser }; 6 7 query = query 8 //.WhereIf(!input.UserName.IsNullOrWhiteSpace(), item => item.User.UserName.Contains(input.UserName))// 以前的寫法,不支持多個用戶名查詢 9 .WhereIf(usernamelist != null, item => usernamelist.Contains(item.User.UserName)) 10 //.WhereIf(!input.RealUserName.IsNullOrWhiteSpace(), item => item.User.Name.Contains(input.RealUserName))// 以前的寫法,不支持多個用戶名查詢 11 .WhereIf(realusernamelist != null, item => realusernamelist.Contains(item.User.Name)) 12 .WhereIf(!input.ServiceName.IsNullOrWhiteSpace(), item => item.AuditLog.ServiceName.Contains(input.ServiceName)) 13 .WhereIf(!input.MethodName.IsNullOrWhiteSpace(), item => item.AuditLog.MethodName.Contains(input.MethodName)) 14 .WhereIf(!input.BrowserInfo.IsNullOrWhiteSpace(), item => item.AuditLog.BrowserInfo.Contains(input.BrowserInfo)) 15 .WhereIf(input.MinExecutionDuration.HasValue && input.MinExecutionDuration > 0, item => item.AuditLog.ExecutionDuration >= input.MinExecutionDuration.Value) 16 .WhereIf(input.MaxExecutionDuration.HasValue && input.MaxExecutionDuration < int.MaxValue, item => item.AuditLog.ExecutionDuration <= input.MaxExecutionDuration.Value) 17 .WhereIf(input.HasException == true, item => item.AuditLog.Exception != null && item.AuditLog.Exception != "") 18 .WhereIf(input.HasException == false, item => item.AuditLog.Exception == null || item.AuditLog.Exception == "");
這里可以看到,既有大量數據又有多表關聯查詢哦,並且純是使用EF去做,實踐證明EF性能並不差,順便推廣一下Edi的另一篇文章《Entity Framework 的一些性能建議》
然而還是有同學強烈要求提供SQL版本,好吧,以下是最簡單的sql版本:
select * from AbpAuditLogs left join AbpUsers on (AbpAuditLogs.UserId = AbpUsers.Id) where AbpAuditLogs .ExecutionTime >= '2019/2/18' and AbpAuditLogs.ExecutionTime <= '2019/2/19'
這里還有個小技巧可以節省時間:
- 先手動建立一個AuditLog類
public class AuditLog { /// <summary> /// TenantId. /// </summary> public virtual int? TenantId { get; set; } /// <summary> /// UserId. /// </summary> public virtual long? UserId { get; set; } /// <summary> /// Service (class/interface) name. /// </summary> public virtual string ServiceName { get; set; } /// <summary> /// Executed method name. /// </summary> public virtual string MethodName { get; set; } /// <summary> /// Calling parameters. /// </summary> public virtual string Parameters { get; set; } /// <summary> /// Return values. /// </summary> public virtual string ReturnValue { get; set; } /// <summary> /// Start time of the method execution. /// </summary> public virtual DateTime ExecutionTime { get; set; } /// <summary> /// Total duration of the method call as milliseconds. /// </summary> public virtual int ExecutionDuration { get; set; } /// <summary> /// IP address of the client. /// </summary> public virtual string ClientIpAddress { get; set; } /// <summary> /// Name (generally computer name) of the client. /// </summary> public virtual string ClientName { get; set; } /// <summary> /// Browser information if this method is called in a web request. /// </summary> public virtual string BrowserInfo { get; set; } /// <summary> /// Exception object, if an exception occured during execution of the method. /// </summary> public virtual string Exception { get; set; } /// <summary> /// <see cref="AuditInfo.ImpersonatorUserId"/>. /// </summary> public virtual long? ImpersonatorUserId { get; set; } /// <summary> /// <see cref="AuditInfo.ImpersonatorTenantId"/>. /// </summary> public virtual int? ImpersonatorTenantId { get; set; } /// <summary> /// <see cref="AuditInfo.CustomData"/>. /// </summary> public virtual string CustomData { get; set; } }
- 選中這個類然后鼠標右鍵52abp代碼生成器
- 生成后再把AppService接口和類改成上面的代碼
- 刪除第一步建立的AuditLog類。把引用修正為“Abp.Auditing”
這個小技巧大概可以節省你三十分鍾時間吧。
第二步就是UI層面,因為審計日志數量會很大,查閱基本要靠搜索,所以要選一個查閱功能強大的Grid組件,根據我個人經驗,推薦使用primeng的table組件。具體代碼因為沒有什么技術難點,我就不貼了。
關於其他成熟框架組件庫和如何在angular中使用多個成熟控件框架,請參考我的另外一篇文章《
如何用ABP框架快速完成項目(6) - 用ABP一個人快速完成項目(2) - 使用多個成熟控件框架》
對了,前端一定要完成導出Excel格式功能哦,你會發覺這個功能實在是太贊了!
現在終於可以不用登陸生產服務器就可以查看審計日志了。
但是這只是開始!
因為一個大型項目里,審計日志的增長速度是驚人的,如果審計日志表和主數據庫放在一起,是十分不科學的。
那么我們如何解決這個問題呢?敬請期待下一篇文章《ABP大型項目實戰(2) - 調試與排錯 - 日志 - 單獨存儲審計日志》
Q&A:
- 如何禁用具體某個接口的審計功能?
答:在類頭加上如下屬性[DisableAuditing] //屏蔽這個AppService的審計功能 [AbpAuthorize(AppPermissions.Pages_Administration_AuditLogs)] public class AuditLogAppService : GHITAssetAppServiceBase, IAuditLogAppService