前言
当一个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日志中心去,是不是非常简单。当然,如果作者有更好的建议,欢迎分享和交流。