前言
當一個APM或一個日志中心實際部署在生產環境中時,是有點力不從心的。
比如如下場景分析的問題:
-
從APM上說,知道某個節點出現異常,或延遲過過高,卻不能及時知道日志反饋情況,總不可能去相應的節點上一個一個的翻日志文件吧。
-
從日志中心上說(特別是Exceptionless,能及時反饋出異常信息),知道某個節點出現異常日志,可不知道引起異常的源頭在哪;或者出現延遲過高日志,卻不能及時知道節點問題,還是鏈路問題;就算諸上問題都能應付,那么一行行的、一個個的日志文件和使用圖形化的表述形式,誰會更加直觀,當然,你說你可以一目十行,甚至百行來分析日志,那我挺佩服你的。


本節內容較多,所以筆者特列舉了如下目錄。
一:准備
1.SkyWalking和Exceptionless簡單回顧
2.新建多個站點(物理節點)
3.附加SkyApm-dotnet程序集到宿主
二:將SkyApm-dotnet的日志輸出到Exceptionless
4.SkyApm-dotnet的日志入口
5.繼承ILoggerFactory獲取全局ILogger對象
6.將Logger寫入到Exceptionless
三:運行
7.SkyWalking和Exceptionless的結合分析
SkyWalking和Exceptionless簡單回顧



前兩篇就《NET Core微服務之路:SkyWalking+SkyApm-dotnet分布式鏈路追蹤系統的分享》和《NET Core微服務之路:簡單談談對ELK,Splunk,Exceptionless統一日志收集中心的心得體會》簡單的介紹了SkyApm-dotnet和三個日志收集中心。為何最終會選擇SkyWalking和Exceptionless來作為生產實戰,很簡單:
1.SkyWalking和Exceptionless的存儲和檢索都是使用的ElasticSearch,ES的強大之處不用介紹:“you know, for search”
2.SkyWalking作為國人(吳晟)開發的一套開源追蹤系統,雖然比不上Pinpoint功能強大,但社區活躍且免費,相信開源的力量,會越來越完善,甚至更好。
3.Exceptionless作為.Net開源社區的新起之秀,目前也十分活躍,原生.Net語言支持,能做到日后無縫擴展。
新建多個站點(物理節點)
傳統單體應用(或站點)沒必須要做到APM追蹤,因為她毫無意義。只有在分布式架構模式下,例如SOA、微服務等架構才有意義,比如說,你在兩個地方分別部署了多個應用,當某個地方的應用出現了故障,你總不可能專門跑去一個一個文件的查閱日志吧,假如這個應用部署在火星呢(哈哈,開個玩笑)。
我們就SkyApm-dotnet中的
Sample做一些二次修改和擴展,來模擬一個實際的分布式系統。
先看看這個系統的網絡拓撲圖:

asp-net-core-*為系統主要節點,而localhost:50000為Exceptionless的日志中心,114.215是數據庫,具體每個線條的顏色請查閱SkyWalking手冊。
asp-net-core-aspnetcore:我們可以把她理解為請求端,筆者在里面做了一個單請求,和一個並行請求,嚴格意義上來說,代碼中不應該有try catch來進行重試,而是應該使用polly的Retry進行重試和異常處理,可以參考《NET Core微服務之路:彈性和瞬態故障處理庫Polly的介紹》,代碼參考如下:
[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { [HttpGet] public async Task<string> Get() { var httpClient = new HttpClient(); var values = await httpClient.GetStringAsync("http://localhost:5001/api/values"); ExceptionlessClient.Default.SubmitLog(JsonConvert.SerializeObject(values), LogLevel.Debug); return values; } [HttpGet("getall")] public string GetAll() { var list = new List<int>(); var listValue = new List<string>(); for (var i = 1; i <= 50; i++) { list.Add(i); } Parallel.ForEach(list, (i, state) => { try { using (var httpClient = new HttpClient()) { listValue.Add(httpClient.GetStringAsync($"http://localhost:5001/api/values/{i}/other").Result); } } catch (Exception) { // ignored } }); ExceptionlessClient.Default.SubmitLog(JsonConvert.SerializeObject(listValue), LogLevel.Debug); return JsonConvert.SerializeObject(listValue); } }
asp-net-core-frontend:我們可以把她理解為一個網關,一個中繼,或者一個權限驗證等等,筆者沒做太多處理,就單純做了一個switch的參數選擇橋接,參考代碼如下:
[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { [HttpGet] public async Task<string> Get() { var httpClient = new HttpClient(); var values = await httpClient.GetStringAsync("http://localhost:5002/api/values"); return values; } [HttpGet("{id:int}/other")] public async Task<string> Get(int id) { var httpClient = new HttpClient(); var values = ""; switch (id) { case 1: values = await httpClient.GetStringAsync("http://localhost:5002/api/delay/100"); break; case 2: values = await httpClient.GetStringAsync("http://localhost:5002/api/Error"); break; case 3: values = await httpClient.GetStringAsync("http://localhost:5002/api/Values"); break; case 4: values = await httpClient.GetStringAsync("http://localhost:5002/api/Apps"); break; case 5: { var userClient = new User.UserClient(new Channel("127.0.0.1:5050", ChannelCredentials.Insecure)); var response = await userClient.GetListAsync(new GetListRequest()); if (response.Code == 1000) { return JsonConvert.SerializeObject(response.Data); } break; } } return values; } }
asp-net-core-backend:我們可以把她理解為一個節點,筆者還創建了一個Grpc的服務節點,不知是因為目前SkyApm-dotnet探針沒做Grpc的適配,還是筆者這邊配置錯誤,目前並未實現Grpc的追蹤,代碼較多,就不一一的貼上來了,做個截圖即可,源碼在文章最后

附加SkyApm-dotnet程序集到宿主
我們在啟動Aspnetcore應用的時候,可以通過Appsettings.*.json來配置應用的環境參數,比如ASPNETCORE_ENVIRONMENT,可以設置當前應用的環境是開發(Development)還是生產(Production),關於多環境的介紹,可以參考這篇文章
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-2.0。而SkyApm-dotnet的最大優勢,就是達到了開箱即用,我們只需要通過ASPNETCORE_HOSTINGSTARTUPASSEMBLIES參數來指定SkyApm即可,當然,你可以使用set ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=SkyApm.Agent.AspNetCore,也可以通過配置文件來啟用SkyApm.Agent.AspNetCore。ASPNETCORE_HOSTINGSTARTUPASSEMBLIES是個什么鬼,我們來查一查微軟官方的解釋(地址
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/platform-specific-configuration?view=aspnetcore-2.0):
An IHostingStartup (hosting startup) implementation adds enhancements to an app at startup from an external assembly. For example, an external library can use a hosting startup implementation to provide additional configuration providers or services to an app. IHostingStartup is available in ASP.NET Core 2.0 or later.
通過追加外部程序集來增強宿主功能,例如,可以在外部程序集中提供額外的服務或配置,此項功能支持NET Core 2.0+。
當然,能加載也就能禁用 ,使用ASPNETCORE_PREVENTHOSTINGSTARTUP便可實現。除以上通過set的方式配置環境參數以外,還可以通過代碼的方式來指定ASPNETCORE_HOSTINGSTARTUPASSEMBLIES啟動擴展程序集。
Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", "SkyAPM.Agent.AspNetCore");
對了,在WebHosting的環境變量定義中,默認提供了如下環境變量,有興趣的朋友可深入研究。

SkyApm-dotnet的日志入口
在SkyApm-dotnet的配置文件中,默認是開啟了本地日志的,像這樣
"Logging": { "Level": "Information", "FilePath": "logs/skyapm-{Date}.log" },
如果部署了多個SkyApm-dotnet探針到節點,那是不是要在多個節點上來查閱日志呢?答案肯定是拒絕的,如果這樣下來,那么我們的日志收集中心就沒有任何存在的意義了。所以,為了實現這個功能,找到了SkyApm.Logging.ILoggerFactory的接口,使用再次注入的方式,替換了原來默認的DefaultLoggerFactory(當然,如果有更好的方式,或者已經提供了接口,麻煩大家告知一下),這是默認日志注入的源碼:

可以看到,SkyApm-dotnet的日志默認通過ServiceCollection進行注入,我們只需要實現ILoggerFactory便可實現自定義的日志處理方式。
繼承ILoggerFactory獲取全局ILogger對象
通過F12我們可以定位接口的具體源碼定義,可以看到SkyApm.Logging中,定義了一個ILoggerFactory的接口定義,內部需實現一個Ilogger的創建,代碼源碼截圖如下:


我們可以實現這個接口,定義為我們自己實現的處理方式。但是,其實我們可以將源碼拷貝過來,因為我們仍然需要將日志保存在本地作為副本,而不是單純將日志發送到日志中心,所以需要另起一個實現的名字,我這里取名叫SkyApmExtensionsLoggerFactory,源碼如下:
namespace SkyApmExceptionless { public class SkyApmExtensionsLoggerFactory : SkyApm.Logging.ILoggerFactory { private const string OutputTemplate = @"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{ServiceName}] [{Level}] {SourceContext} : {Message}{NewLine}{Exception}"; private readonly LoggerFactory _loggerFactory; public SkyApm.Logging.ILogger CreateLogger(Type type) { return new SkyApmExtensionsLogger(_loggerFactory.CreateLogger(type)); } public SkyApmExtensionsLoggerFactory(IConfigAccessor configAccessor) { _loggerFactory = new LoggerFactory(); var loggingConfig = configAccessor.Get<LoggingConfig>(); var instrumentationConfig = configAccessor.Get<InstrumentConfig>(); var level = EventLevel(loggingConfig.Level); _loggerFactory.AddSerilog(new LoggerConfiguration().MinimumLevel.Verbose().Enrich .WithProperty("SourceContext", null).Enrich .WithProperty(nameof(instrumentationConfig.ServiceName), instrumentationConfig.ServiceName ?? instrumentationConfig.ApplicationCode).Enrich .FromLogContext().WriteTo.RollingFile(loggingConfig.FilePath, level, OutputTemplate, null, 1073741824, 31, null, false, false, TimeSpan.FromMilliseconds(500)).CreateLogger()); } private static LogEventLevel EventLevel(string level) { return Enum.TryParse<LogEventLevel>(level, out var logEventLevel) ? logEventLevel : LogEventLevel.Error; } } }
從上面的代碼加粗的代碼中可以看到,通過ILoggerFactory創建了一個SkyApm.Logging.ILogger的實現SkyApmExtensionsLogger,這樣,我們便拿到的SkyApm.Logging.ILoggerFactory的ILogger接口,接下來便是將ILogger的具體實現功能寫到Exceptionless。
將Logger寫入到Exceptionless
先看看SkyApm.Logging.ILogger的接口定義,源碼截圖如下:

超級簡單,跟NLog,Log4net等等日志組件的接口定義大同小異,幾乎可以說是一樣的,包含Debug, Information, Warning, Error, Trace,接下來該怎么做,就變得十分簡單了,不過,在寫入這個日志前,先簡單了解一下Exceptionless的用法。
1.創建一個日志。源碼定義為Source,我覺得叫組比較容易理解,她就像一個分類器,指定她的名稱是SkyApmExtensionsLogger,其次,可以提交不同的日志類型,Exceptionless定義了如下幾種日志等級,其實有部分我們用不着。

ExceptionlessClient.Default.CreateLog(nameof(SkyApmExtensionsLogger), "Create logging started.", Exceptionless.Logging.LogLevel.Info).Submit();
2.創建一個會話Session。ession會話的作用在Exceptionless算是一個特殊功能的存在了,她可以自動發送會話開始,會話心跳和會話結束事件,使用非常簡單,后面會截圖介紹這個功能的作用。
ExceptionlessClient.Default.Configuration.UseSessions();
OK,Exceptionless就介紹這么點用法(詳細更多用法可參考官網),已經可以滿足日志的寫入(或收集)了,接下來看看完整的源碼:
using System; using Exceptionless; using Microsoft.Extensions.Logging; namespace SkyApmExceptionless { internal class SkyApmExtensionsLogger : SkyApm.Logging.ILogger { private readonly ILogger _readLogger; public SkyApmExtensionsLogger(ILogger readLogger) { _readLogger = readLogger; ExceptionlessClient.Default.CreateLog(nameof(SkyApmExtensionsLogger), "Create logging started.", Exceptionless.Logging.LogLevel.Info).Submit(); ExceptionlessClient.Default.Configuration.UseSessions(); ExceptionlessClient.Default.Configuration.SetUserIdentity("SetUserIdentity", $"{nameof(SkyApmExtensionsLogger)} Groups"); } public void Debug(string message) { _readLogger.LogDebug(message); ExceptionlessClient.Default .CreateLog(nameof(SkyApmExtensionsLogger), message, Exceptionless.Logging.LogLevel.Debug).Submit(); } public void Information(string message) { _readLogger.LogInformation(message); ExceptionlessClient.Default .CreateLog(nameof(SkyApmExtensionsLogger), message, Exceptionless.Logging.LogLevel.Info).Submit(); } public void Warning(string message) { _readLogger.LogWarning(message); ExceptionlessClient.Default .CreateLog(nameof(SkyApmExtensionsLogger), message, Exceptionless.Logging.LogLevel.Warn).Submit(); } public void Error(string message, Exception exception) { _readLogger.LogError(message + Environment.NewLine + exception); ExceptionlessClient.Default .CreateLog(nameof(SkyApmExtensionsLogger), message + Environment.NewLine + exception, Exceptionless.Logging.LogLevel.Error) .Submit(); } public void Trace(string message) { _readLogger.LogTrace(message); ExceptionlessClient.Default .CreateLog(nameof(SkyApmExtensionsLogger), message, Exceptionless.Logging.LogLevel.Trace).Submit(); } } }
這樣,通過SkyApm-dotnet生成的日志,將自動發送到Exceptionless日志中心去,是不是非常簡單。當然,如果作者有更好的建議,歡迎分享和交流。

SkyWalking和Exceptionless的結合分析
通過上面的擴展和部署,我們已經可以開始跑起來玩一玩了,如果有小伙伴跑不通,或者懶得敲代碼(哎...),源碼在文章結尾,但如何配置環境還請自行搜索,以免浪費篇幅。
萬惡的再來兩張截圖,哈哈,其實是先看看默認狀態下SkyWalking和Exceptionless的初始界面。


讓我們啟動這個項目。嗯,很好,發現日志正在蹭蹭的上漲,再來一張萬惡的全屏截圖。

我們並沒運行任何一個接口,也並沒調用任何一個接口,這日志是哪來的呢,對,就是SkyApm-dotnet的日志,我們可以通過Session里面查看到SkyApmExtensionsLogger正在不斷的追加日志,這是因為SkyApm-Agent正在運行追蹤,這里也清晰的解釋了Session事件在這個SkyApmExtensionsLogger中的作用(目前還在不斷的追加中)。

再看看SkyWalking,很好,出現了三個服務(節點)


運行一下,代碼在上面,萬惡的全屏截圖再來一張:

我們發現,在ListMode中有報錯的情況,這樣:

趕緊定位到日志,搜索Api/Error

嗯,這正是剛才刷新兩次所產生的錯誤結果,也是筆者故意拋出的,查看一下詳情

確實由於5001上面接受到了遠程返回404錯誤,因為這個接口實際就不存在。
反之,你也可以通過Exceptionless的exception模塊或其他日志來反查SkyWalking詳情,但是這樣的效率不高。
萬惡的全屏截圖已結束,感謝!
總結
通過APM和日志中心(例如SkyWalking和Exceptionless)進行整合分析的場景越來越被重視和使用,如果還是停留在單個日志分析,或者單個APM分析,那么隨着節點數的增加,服務的規模增加,那將無法及時確定問題所在的。還有更多的結合用法,歡迎小伙伴們共同交流。

參考
Exceptionless :
https://github.com/exceptionless/Exceptionless/wiki
SkyApm-dotnet Sample :
https://github.com/SkyAPM/SkyAPM-dotnet/tree/master/sample
感謝閱讀!