前情提要 筆者有幸參與到科內負責的大系統,不能單靠一個API站台就解決所有的需求,必須搭配Scheduler
、WindowsService
等服務,介接技術上,可能會用Socket
或是HttpClient
介接,再加上自己系統本身,分散多個專案,中間使用Message Queue
技術串聯起來,算是一個滿有挑戰性的系統了,挑戰主要是在串連服務的難度上,而非高流量。因此追蹤問題上,需要靠Loggin機制,由於各式各樣的服務,不能單靠底層Logging套件:NLog預設的紀錄欄位,針對不同種類的服務,做一個客製化屬性欄位的擴充才行。
內容 回到此篇主題,為什麼會有ILoggerManager
需求,依照前提情要中提到的情境,可以將大系統中的服務類型大致上可以分為API站台、Scheduler排程服務,Socket服務這三大類,試想可紀錄或紀錄關心的欄位有所不同。
以API站台來說,關心登入者與API Path,這樣才能快速找到對應的程式,並修復;Scheduler排程服務來說,關心的是Scheduler服務名稱,以此類推,依照需求面向不同,需要設計出不一樣的LoggerManager
。
以上述分類,筆者設計出三種不同的LoggerManager,分別為:
APILoggerManager
SchedulerLoggerManager
ExchangeLoggerManager
ILoggerManager 跟著筆者定義一下LoggerManager
1 2 3 4 5 6 7 public interface ILoggerManager { void LogInfo (string message ) ; void LogWarn (string message ) ; void LogDebug (string message ) ; void LogError (string message ) ; }
APILoggerManager NLog宣告語法 主要新增「UserId」、「APIPath」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 CREATE TABLE [dbo].[NLog]( [ID] [int ] IDENTITY (1 ,1 ) NOT NULL , [MachineName] [nvarchar](200 ) NULL , [UserId] [varchar ](10 ) NULL , [APIPath] [varchar ](400 ) NULL , [Logged] [datetime] NOT NULL , [Level] [varchar ](5 ) NOT NULL , [Message] [nvarchar](max) NOT NULL , [Logger] [nvarchar](300 ) NULL , [Properties] [nvarchar](max) NULL , [Callsite] [nvarchar](300 ) NULL , [Exception] [nvarchar](max) NULL , CONSTRAINT [PK_dbo.Log] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON , ALLOW_PAGE_LOCKS = ON ) ON [PRIMARY ] ) ON [PRIMARY ] TEXTIMAGE_ON [PRIMARY ]
APILoggerManager實作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class APILoggerManager : ILoggerManager { private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); private readonly IIdentityService _identityService; private string _apiUrl; public APILoggerManager (IIdentityService identityService, IHttpContextAccessor context ) { _identityService = identityService; _apiUrl = context?.HttpContext?.Request?.Path; } public void LogDebug (string message ) { _logger.WithProperty("UserId" , _identityService.GetUserName()) .WithProperty("APIPath" , _apiUrl) .Debug(message); } public void LogError (string message ) { _logger.WithProperty("UserId" , _identityService.GetUserName()) .WithProperty("APIPath" , _apiUrl) .Error(message); } public void LogInfo (string message ) { _logger.WithProperty("UserId" , _identityService.GetUserName()) .WithProperty("APIPath" , _apiUrl) .Info(message); } public void LogWarn (string message ) { _logger.WithProperty("UserId" , _identityService.GetUserName()) .WithProperty("APIPath" , _apiUrl) .Warn(message); } }
上述程式碼內容主要是使用IIdentityService
取得登入者相關資訊,NLog部份,主要使用WithProperty
來設定Custom Column對應的值。來看一下IIdentityService
的內容吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public interface IIdentityService { int GetUserId () ; string GetUserName () ; } public class IdentityService : IIdentityService { private readonly IHttpContextAccessor _context; public IdentityService (IHttpContextAccessor context ) { _context = context; } public int GetUserId () { var nameId = _context?.HttpContext?.User.FindFirst("id" ); return nameId != null ? Convert.ToInt32(nameId.Value) : 0 ; } public string GetUserName () { return _context?.HttpContext?.User.FindFirst("name" )?.Value; } }
上述程式碼中,主要使用注入IHttpContextAccessor,並取得登入者相關資訊,筆者使用的是最簡單的jwt token
的方式驗證登入者的合法性,後端在登入驗證成功後發出一條jwt token
,前端在每一個Request的header中帶入拿到的jwt token
,達到Authentitcation
的效果。因為關係到怎麼塞入Claim
資訊,就得用對應的key值做取得登入資訊。
NLog Config宣告
於NLog.config中宣告database紀錄相關設定,targets中加入database target
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <target name ="database" xsi:type ="Database" > <connectionString > ${var:connectionStringNlog}</connectionString > <commandText > insert into dbo.NLog ( MachineName, UserId, APIPath, Logged, Level, Message, Logger, Callsite, Exception ) values ( @MachineName, @UserId, @APIPath, @Logged, @Level, @Message, @Logger, @Callsite, @Exception ); </commandText > <parameter name ="@MachineName" layout ="${machinename}" /> <parameter name ="@UserId" layout ="${event-properties:UserId}" /> <parameter name ="@APIPath" layout ="${event-properties:APIPath}" /> <parameter name ="@Logged" layout ="${date}" /> <parameter name ="@Level" layout ="${level}" /> <parameter name ="@Message" layout ="${message}" /> <parameter name ="@Logger" layout ="${logger}" /> <parameter name ="@Callsite" layout ="${callsite}" /> <parameter name ="@Exception" layout ="${exception:tostring}" /> </target >
主要值得注意的是${event-properties:UserId}
及${event-properties:APIPath}
,這邊的key
值要對應到WithProperty
中的key
值。
rules區塊中加入database並且設定level為Error
1 <logger name ="*" minlevel ="Warn" writeTo ="database" />
最後要提醒一下,因為APILoggerManager中使用到IIDentityService,IDentityService中又使用到IHttpContextAccessor,因此需於Startup中注入其HttpContextAccessor。
1 2 3 4 5 6 7 8 9 10 public class Startup { public void ConfigureServices (IServiceCollection services ) { services.AddHttpContextAccessor(); } }
SchedulerLoggerManager NLogScheduler宣告語法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 CREATE TABLE [dbo].[NLogScheduler]( [ID] [bigint ] IDENTITY (1 ,1 ) NOT NULL , [JobID] [varchar ](200 ) NULL , [MachineName] [nvarchar](200 ) NULL , [Logged] [datetime] NOT NULL , [Level] [varchar ](5 ) NOT NULL , [Message] [nvarchar](max) NOT NULL , [Logger] [nvarchar](300 ) NULL , [Properties] [nvarchar](max) NULL , [Callsite] [nvarchar](300 ) NULL , [Exception] [nvarchar](max) NULL , CONSTRAINT [PK_NLogScheduler] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON , ALLOW_PAGE_LOCKS = ON ) ON [PRIMARY ] ) ON [PRIMARY ] TEXTIMAGE_ON [PRIMARY ] GO EXEC sys.sp_addextendedproperty @name = N'MS_Description' , @value = N'排程ID' , @level0type = N'SCHEMA' ,@level0name = N'dbo' , @level1type = N'TABLE' ,@level1name = N'NLogScheduler' , @level2type = N'COLUMN' ,@level2name = N'JobID' GO
SchedulerLoggerManager實作 筆者這邊環境是使用Hangfire實作相關排程程式,所以會有一些針對Hangfire做的一些客製化作業,先看一下SchedulerLoggerManager實作程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class SchedulerLoggerManager : ILoggerManager { private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); public void LogDebug (string message ) { _logger.WithProperty("JobID" , JobContext.JobId) .Debug(message); } public void LogError (string message ) { _logger.WithProperty("JobID" , JobContext.JobId) .Error(message); } public void LogInfo (string message ) { _logger.WithProperty("JobID" , JobContext.JobId) .Info(message); } public void LogWarn (string message ) { _logger.WithProperty("JobID" , JobContext.JobId) .Warn(message); } }
以上程式碼中所使用到的JobContext.JobId則是筆者另外寫的一個可以取得當次Scheduler
的JobId
名稱,不過筆者這邊是有導入動態排程機制,由資料庫去控制其排程設定值,可以透過JobInitial
程式可以重新宣告排程,因此取得JobId部份非常地客製化,參考其結構就可以。
IServerFilter實作 筆者這邊使用的是Hangfire有開放的IServerFilter實作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class JobContext : IServerFilter { [ThreadStatic ] private static string _jobId; public static string JobId { get { return _jobId; } set { _jobId = value ; } } public void OnPerformed (PerformedContext filterContext ) { } public void OnPerforming (PerformingContext context ) { } }
IServerFilter註冊 這個IServerFilter實作需要註冊,須於Startup中註冊才會生效
1 2 3 4 5 6 7 8 9 public class Startup { public void ConfigureServices (IServiceCollection services ) { GlobalConfiguration.Configuration.UseFilter(new JobContext()); } }
NLog.Config
部份參考其APILoggerManager
說明,只要將APILoggerManager
客製化屬性去除,新增SchedulerLoggerManager
客製化屬性即可。
ExchangeLoggerManager 該LoggerManager主要運用於Socket相關專案中,筆者這邊負責與五個系統使用Socket串接,對筆者來說主要是可以識別為哪個系統對應的專案log即可,因此只要多加一個欄位為「SystemId」,方便筆者去做一個Filter。
NLogExchange宣告語法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 CREATE TABLE [dbo].[NLogExchange]( [ID] [bigint ] IDENTITY (1 ,1 ) NOT NULL , [SystemId] [varchar ](20 ) NULL , [MachineName] [nvarchar](200 ) NULL , [Logged] [datetime] NOT NULL , [Level] [varchar ](5 ) NOT NULL , [Message] [nvarchar](max) NOT NULL , [Logger] [nvarchar](300 ) NULL , [Properties] [nvarchar](max) NULL , [Callsite] [nvarchar](300 ) NULL , [Exception] [nvarchar](max) NULL , CONSTRAINT [PK_NLogExchange] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON , ALLOW_PAGE_LOCKS = ON ) ON [PRIMARY ] ) ON [PRIMARY ] TEXTIMAGE_ON [PRIMARY ] GO
ExchangeLoggerManager實作 主要是使用筆者設計下的Options
參數取得系統名稱,筆者有包裝成一個AddSocketService
的Extension
,由外層Client
使用時方便的使用,但也多個系統共用,需要使用Options
參數來控制其對應的設定值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class ExchangeLoggerManager : ILoggerManager { private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private SocketSettingOption _socketSettingOption; public ExchangeLoggerManager (IOptions<SocketSettingOption> options ) { _socketSettingOption = options.Value; } public void LogDebug (string message ) { _logger.WithProperty("SystemId" , _socketSettingOption.AppId) .Debug(message); } public void LogError (string message ) { _logger.WithProperty("SystemId" , _socketSettingOption.AppId) .Error(message); } public void LogInfo (string message ) { _logger.WithProperty("SystemId" , _socketSettingOption.AppId) .Info(message); } public void LogWarn (string message ) { _logger.WithProperty("SystemId" , _socketSettingOption.AppId) .Warn(message); } }
筆者在另一個Socket文章系列中有提及其設計原理,可能要讀者對照著參考,才會理解其中的意義,不過至少在這邊列一下SocketSettinOption
物件,詳細說明參考其對應文章:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class SocketSettingOption { public string MainSocketSetting { get ; set ; } public string SubSocketSetting { get ; set ; } public string HubUrlSetting { get ; set ; } public Func<List<byte >, Tuple<List<byte >, List<Byte[]>>> ClientSliceByteFunc { get ; set ; } public Func<List<byte >, Tuple<List<byte >, List<Byte[]>>> ServerSliceByteFunc { get ; set ; } public bool DisconnectAfterClientReceive { get ; set ; } = false ; public string AppId { get ; set ; } public string AppName { get ; set ; } public string ServiceName { get ; set ; } public bool MonitorService { get ; set ; } = true ; #region ---ForLogging--- public Func<byte [], Type> GetHeaderResponseModelType { get ; set ; } = (data) => { return typeof (HeaderResponseModel); }; public string ConnectionStringKey { get ; set ; } = "FMTR" ; #endregion }
NLog.Config
部份參考其APILoggerManager
說明,只要將APILoggerManager
客製化屬性去除,新增SchedulerLoggerManager
客製化屬性即可。
Client套用 注入對應的ILoggerManager實作 1 2 3 4 5 6 7 8 9 10 11 12 public class Startup { public void ConfigureServices (IServiceCollection services ) { LogManager.Configuration.Variables["connectionStringNlog" ] = "{connectionString}" ; services.AddTransient<ILoggerManager, APILoggerManager>(); } }
Client的Service端中實際使用 主要是使用建構值注入的方式取得其ILoggerManager
實體
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public interface IFlowService { void DoProcess (FlowConditions conditions ) ; } public class FlowService : IFlowService { private readonly ILoggerManager _logger; public FlowService (ILoggerManager logger ) { _logger = logger; } public void DoProcess (FlowConditions conditions ) { _logger.LogInfo("Flow do process start!" ); _logger.LogInfo("Flow do process end!" ); } }
結論 筆者透過將不同需求分類的方式將NLog做一個有效的分類,Log資料量分開之外,查問題時更清楚自己要查的資料,可以有效的查詢問題,藉由設計客製化欄位,對於NLog掌握度更高了。