应用程序信息中心 Application Infomcation Center 简称AIC其实是一套已经实现的程序,集应用程序数据收集、数据存储以及后台查询、报警为一体。主要的作用是实现网站特殊信息(比如未处理异常)的监控和报警。在Adhesive中进行了重写和升华,把功能分为日志、异常、性能和状态几个部分:
public class AppInfoCenterService { public static ILoggingService LoggingService { get { return LocalServiceLocator.GetService(); } } public static IExceptionService ExceptionService { get { return LocalServiceLocator.GetService (); } } public static IPerformanceService PerformanceService { get { return LocalServiceLocator.GetService (); } } public static IStateService StateService { get { return LocalServiceLocator.GetService (); } } }
我们可以直接通过AppInfoCenterService类访问到这些功能。
分别来看一下每一个接口,首先是日志:
public interface ILoggingService : IDisposable { void LogDebug(string message, ExtraInfo extraInfo = null, bool localOnly = false); void LogDebug(string message, string categoryName, string subcategoryName, ExtraInfo extraInfo = null, bool localOnly = false); void LogInfo(string message, ExtraInfo extraInfo = null, bool localOnly = false); void LogInfo(string message, string categoryName, string subcategoryName, ExtraInfo extraInfo = null, bool localOnly = false); void LogError(string message, ExtraInfo extraInfo = null, bool localOnly = false); void LogError(string message, string categoryName, string subcategoryName, ExtraInfo extraInfo = null, bool localOnly = false); void LogWarning(string message, ExtraInfo extraInfo = null, bool localOnly = false); void LogWarning(string message, string categoryName, string subcategoryName, ExtraInfo extraInfo = null, bool localOnly = false); }
这里要说明几点:
1、之所以结合使用了重载和命名参数,是因为命名参数虽然灵活但是对于调用者来说代码还是不够简洁,因此把不常用的开关作为命名参数。在这里localOnly指的是是否只是记录本地日志,其实把这个参数设置为true的话相当于直接调用LocalLoggingService,只不过这样调用更统一一点。
2、对于日志,除了根据自己的需要限定不同的等级之外,还可以提供大类和小类,这是很常用的,比如我们可以在一个业务网站内部把大类写为类名(页面名),把小类写为方法名,比如:
AppInfoCenterService.LoggingService.LogDebug(string.Format("内存队列服务 '{0}' 调节最大项!", configuration.MemoryQueueName), categoryName: ServiceName, subcategoryName: configuration.MemoryQueueName);
这是一条内存队列服务中的日志,我们把大类设置为服务名也就是内存队列服务,把小类设置为队列的名字,这样我们在后台可以很方便筛选内存队列服务模块中的日志。当然,如果不设置大类和小类的话,系统会自动使用General作为大类名,而小类名对于网站就自动设置为页面名,对于普通应用程序就设置为调用方法所在类名。
3、对于ExtraInfo,这里允许补充一些信息,包括两个单选过滤的栏位、两个多选过滤的栏位、两个文本搜索的栏位、以及几个字典用于保存任意多只是用于呈现的数据,比如:
var extraInfo = new ExtraInfo { DisplayItems = new Dictionary<string, string>() { { "DisplayItem1", "DisplayItem1" }, { "DisplayItem2", "DisplayItem2" } }, DropDownListFilterItem1 = stringfilterpool[rnd.Next(4)], DropDownListFilterItem2 = stringfilterpool[rnd.Next(4)], CheckBoxListFilterItem1 = stringfilterpool[rnd.Next(4)], CheckBoxListFilterItem2 = stringfilterpool[rnd.Next(4)], }; AppInfoCenterService.LoggingService.LogError("测试日志", extraInfo: extraInfo);
在有的时候这是很有用的,比如我们仅仅依靠大类和小类不能很方便筛选到需要的数据,还可以使用单选或多选过滤进一步定位日志,并且额外的的DisplayItems可以以KeyValue的形式存放任意多需要查看的数据,这比手动拼接需要的信息放入Message中方便很多了。
此外,还针对string做了日志服务的各个重载的扩展方法:
public static void LogDebug(this string message, ExtraInfo extraInfo = null, bool localOnly = false) { AppInfoCenterService.LoggingService.LogDebug(message, string.Empty, string.Empty, extraInfo, localOnly); }
那么就可以这样调用了:
"清除历史数据-删除表".LogInfo(databaseName, collectionName);
再来看看异常服务:
public interface IExceptionService : IDisposable { void Handle(Exception exception, ExtraInfo extraInfo = null, bool localOnly = false); void Handle(Exception exception, string categoryName, string subcategoryName, ExtraInfo extraInfo = null, bool localOnly = false); void Handle(Exception exception, string description, ExtraInfo extraInfo = null, bool localOnly = false); void Handle(Exception exception, string categoryName, string subcategoryName, string description, ExtraInfo extraInfo = null, bool localOnly = false); }
这里要说明几点:
1、在这里只有一个Handle方法,具体是怎么Handle的取决于配置,在稍候我们会统一来看看AIC的配置。同样,可以为异常提供大类和小类。
2、在这里提供了一个描述字符串,这么做的目的是为了方便补充异常的一些信息,比如位置信息:
AppInfoCenterService.ExceptionService.Handle(ex, categoryName: ServiceName, subcategoryName: typeFullName, description: "更新元数据到数据库出现错误", extraInfo: new ExtraInfo { DisplayItems = new Dictionary<string, string>() { {"DatabaseName" , MongodbServerConfiguration.MetaDataDbName}, {"TableName", databaseDescription.DatabasePrefix} } });
这样会比重新包装一个异常然后设置它的Message并把原来的异常设置为内部异常来的好。
3、原先在设计的时候考虑增加一个bool类型的rethrow参数,表明是否要重新抛出异常,后来思考后觉得不需要也没必要。因为一旦我们在框架内部再抛出这个异常的话就会丢失原来的堆栈,如果真的要重新抛出还是推荐在原来的catch{}处直接使用throw关键字。
当然,异常接口也做了扩展方法,这样调用是不是方便很多了:
catch (Exception ex) { ex.Handle(WcfLogProvider.LogCategoryName, "ClientMessageInspector.BeforeSendRequest"); }
说到这里想谈谈日志和异常,为什么我们需要自己开发一套而不是使用企业库或类似Elmah开源框架呢?因为很多地方不能满足我们的需求,如果使用的话需要改或扩展相当多的东西:
1、我们会为所有数据附加大量的信息,并且这些配置是可以根据每一个应用程序需要自己配置的(依赖于配置服务)。
2、我们需要把数据存储的到Mongodb中去,并且使用统一的后台查看数据(依赖于Mongodb数据服务)。
3、我们需要整合各种应用程序的监控和错误页跳转等。
其实说白了,把日志和异常数据集中保存,这会比使用本地日志文件方式保存在各个Web服务器上好很多:
1、首先是查看方便,可以搜索可以过滤,对于文件日志的话要搜索和查看是很不方便的,并且根本不可能他保持大量结构化数据
2、其次是可以及时发现问题,比如可以根据数据量进行报警,一般保持在Web服务器上的话,只有发现问题才会去看而不是主动去看来发现问题
3、运维方便,可以及时删除过期的数据,可以对数据进行统计
第三个接口是性能服务:
public interface IPerformanceService : IDisposable { void StartPerformanceMeasure(string name, ExtraInfo extraInfo = null); void StartPerformanceMeasure(string name, string categoryName, string subcategoryName, ExtraInfo extraInfo = null); void SetPerformanceMeasurePoint(string name, string pointName); }
性能服务的API(性能服务除了API部分还有框架内置的部分,这里的AppInfoCenterService中的接口只是AIC组件对于代码方式提供的功能,还有很多功能是AIC自己实现的,不需要任何代码)主要是通过代码加点方式对代码进行性能的衡量。比如:
AppInfoCenterService.PerformanceService.StartPerformanceMeasure("性能测试1"); aa(); AppInfoCenterService.PerformanceService.SetPerformanceMeasurePoint("性能测试1", "性能测试1aa"); cc(); AppInfoCenterService.PerformanceService.SetPerformanceMeasurePoint("性能测试1", "性能测试1cc"); dd(); AppInfoCenterService.PerformanceService.SetPerformanceMeasurePoint("性能测试1", "性能测试1dd");
我们这样就建立了一个名为性能测试1的测试项目,并且加了3个测试点,在这里每一个测试点统计的是SetPerformanceMeasurePoint方法到之前那个SetPerformanceMeasurePoint或StartPerformanceMeasure之间的代码消耗的CPU时间、逝去的时间等信息,比如这些方法定义如下:
public void aa() { for (int i = 0; i < 2; i++) { bb(); AppInfoCenterService.PerformanceService.SetPerformanceMeasurePoint("性能测试1", "性能测试1bb"); } } public void bb() { Thread.Sleep(100); } public void cc() { Thread.Sleep(200); } public void dd() { for (long i = 0; i < 100000; i++) { Random r = new Random(); double j = r.NextDouble(); } }
那么我们就可以看到性能测试1这个测试项目中有一个性能测试1aa,总共消耗200毫秒的时间,其中有两个性能测试1bb每一个分别消耗100毫秒的时间,但是它们都没有消耗CPU时间等等。。在这里CPU时间指占用CPU的时间,消耗的时间指的是程序执行需要的时间,是两个不同的概念,往往消耗时间比CPU时间高很多。在这里,我们不给出后台呈现的样子,那是因为所有的这些数据记录和呈现都是使用通用的Mongodb数据服务,在之后我们介绍通用Mongodb数据服务的时候会介绍。需要说明的是,我们往往是在遇到瓶颈的时候会采用代码加点方式到线上排查性能问题,但在问题找到之后会去除相关的代码(或者通过配置来限定一个收集数据的时间段,之后会介绍),毕竟进行相关数据的收集是本身也是消耗性能的。
第四个接口是状态服务:
public interface IStateService : IDisposable { void Init(StateServiceConfiguration configuration); }
状态服务用于定期汇报一些程序内部的状态,在这里只有一个初始化的接口。因为,状态服务会根据配置定期调用回调方法去拉数据,然后提交,而不是手动定时调用状态服务去记录数据(那样的话就和日志服务没区别了)。其实,状态服务也就是在内部使用了后台线程定时调用回调方法收集数据,然后使用Mongodb数据服务把数据进行入库罢了。使用方式如下:
applicationStateService = AppInfoCenterService.StateService; applicationStateService.Init(new StateServiceConfiguration(typeof(ApplicationStateInfo).FullName, ApplicationStateService.GetState));
这是一段AIC内部的代码,之前也说过了,AIC内部本身就使用了一些状态服务来定期汇报程序的状态,这里我们的applicationStateService字段是静态字段,那是因为状态服务作为一个长期驻留后台的服务内部需要有一个根来确保不被回收。现在来看一下配置类StateServiceConfiguration:
public class StateServiceConfiguration { public Func> ReportStateFunc { get; private set; } public string TypeFullName { get; private set; } public StateServiceConfiguration(string typeFullName, Func > reportStateFunc) { this.ReportStateFunc = reportStateFunc; this.TypeFullName = typeFullName; } }
非常简单,只有状态服务所汇报数据对象的类型完整名以及回调委托,委托返回的是BaseInfo的集合,也就是所有需要通过状态服务保存的状态至少都是继承BaseInfo类的。
看到这里,大家可能觉得这些API的使用都很简单,但不知如何根据自己的需要对异常、日志、状态以及性能进行配置。其实框架使用了统一的配置服务保存所有配置,并且设置为每一个应用程序有自己独立的配置(而不是全局配置),现在就来看一下有哪些信息可以配置(在这里会给出配置后台的一些截图,需要注意配置后台是通用的,并不是根据每一个配置单独制作的,Mongodb数据的后台也是这样):
1、首先打开配置服务的后台找到需要配置的应用程序:
2、可以看到里面有一个非全局配置:
3、里面针对每一个模块都有配置:
4、首先要介绍的是包含信息策略。我们知道很多时候我们希望记录的日志和异常信息能自动附加诸如时间、调用堆栈、页面地址等信息,那么我们可以通过不同的包含信息策略来灵活配置每一种日志或异常信息需要自动附加哪些详细的信息。在这里,我们默认提供了四种(也可以根据需要增加任意多种配置是非常灵活的):
这里看一个Simple的配置:
这里可以看到对于Simple这种策略,我们只会自动包含Get信息以及请求和响应的Cookie信息。
5、那么每一种日志或异常记录对应那种IncludeInfoStrategy呢,如下图:
在这里我们配置了每一种类型对应的包含信息详细程度的策略,我们进入LogLevel=Error的LogInfo进去看看:
这里表明了对于LogInfo这个完整类型,我们使用Full这种包含信息的策略(错误日志当然要详细点),但是我们怎么限定条件呢?在Conditions里面:
在这里增加了一个过滤条件,并且这个值是枚举类型的(配置服务支持枚举):
之所以不能是字符串的是因为代码里面我们通过反射来根据当前数据对象中的值和配置的条件来判断应该应用哪种包含信息策略,一旦数据类型不一致的话无法匹配。看到这里,是不是惊叹配置服务的灵活了,也就是说如果我们以后在代码里增加了什么XXXInfo的话,其包含信息的策略配置只需要在配置后台直接完成,不需要添加一行代码,当然如果没有配置的话,就会使用默认值(全部为false)了。
6、介绍完了包含信息策略,再来往下看:
公共配置里面暂时只有一个开关,配置了是否嵌入基本信息到页面(这些信息会以HTML注释呈现,用户不能看到,因此这个功能也只是针对网站有用)。所谓基本信息包括当前页面、机器名、页面执行时间。别小看了这些数据,如果网站采用负载均衡的话,那么内嵌一个机器名会很有用,有助于排查问题;如果页面采用缓存的话,那么输出一个时间则会很有用;而如果希望关注页面性能的话,输出一个页面执行时间也是不错的。这个功能不需要进行任何的编码,通过HttpModule进行,因此,如果要使用应用程序信息中心模块的话,第一就是引用Adhesive.AppInfoCenter.Imp程序集,第二如果是网站的话需要配置HttpModule:
"AppInfoCenterHttpModule" type="Adhesive.AppInfoCenter.Imp.AppInfoCenterHttpModule, Adhesive.AppInfoCenter.Imp"/>
第三就是之前提到了需要在Global中添加框架的启动和结束代码。
7、然后是日志服务的配置:
如果关闭开关的话,所有日志将不会记录。具体日志的配置在策略列表配置中:
在这里的几种级别的日志配置是固定的(如果新增也不会用到,如果删除那么就只会使用默认的了),可以点击进去看看每一个的配置:
也就是说,对于Info这个级别的日志,我们需要同时记录本地日志(基于本地日志服务)和远程日志(基于Mongodb数据服务)。
8、然后是性能服务:
在这里有两种框架内置的服务,一个是对于网站适用的,记录执行慢的页面的执行情况,一个是对于网站和普通程序都适用的用代码加点方式记录代码性能(也就是之前提到的性能服务API)。首先看看前者:
这里的配置很简单,只是是否开启的开关和阀值,也就是当页面执行时间超过1秒时,这个页面的执行情况就会记录下来。那么通过后台统计我们就可以看到哪些页面执行比较慢,网站出现慢页面的情况是增加了还是减少了等等。。再来看看性能测量的配置:
这里除了开关之外还可以指定一个阀值,当页面慢到一定程序的时候才去汇报性能测试的结果。另外一个很有用的是我们可以指定测试的起始时间,比如一天,这样可以又可以抓取到足够的样本又可以及时减少服务器压力。
9、然后是状态服务的配置:
很明显,这是一个配置表,继续往下看:
在这里可以看到有应用程序的状态、网站请求状态、wcf服务端和客户端状态以及Mongodb服务状态。很明显,系统内部自动会收集这些状态数据,在这里属于AIC范畴的是前两种状态:
我们10秒一次汇报应用程序的状态和网站的请求状态(只对网站适用)。应用程序状态的数据结构如下:
[MongodbPersistenceEntity("State", DisplayName = "应用程序状态", Name = "Application")] public class ApplicationStateInfo : BaseInfo { [MongodbPresentationItem(DisplayName = "进程名")] public string ProcessName { get; set; } [MongodbPresentationItem(DisplayName = "工作集内存")] public long WorkingSet64 { get; set; } [MongodbPresentationItem(DisplayName = "非分页系统内存")] public long NonpagedSystemMemorySize64 { get; set; } [MongodbPresentationItem(DisplayName = "分页内存")] public long PagedMemorySize64 { get; set; } [MongodbPresentationItem(DisplayName = "分页的系统内存")] public long PagedSystemMemorySize64 { get; set; } [MongodbPresentationItem(DisplayName = "私有内存")] public long PrivateMemorySize64 { get; set; } [MongodbPresentationItem(DisplayName = "虚拟内存")] public long VirtualMemorySize64 { get; set; } [MongodbPresentationItem(DisplayName = "工作线程")] public int CurrentWorkThreadCount { get; set; } [MongodbPresentationItem(DisplayName = "完成端口线程")] public int CurrentCompletionPortThreadCount { get; set; } }
而网站请求状态的数据结构如下:
[MongodbPersistenceEntity("State", DisplayName = "网站请求状态", Name = "WebsiteRequest")] public class WebsiteRequestStateInfo : BaseInfo { public Dictionary<string, WebsiteRequestStateItem> WebsiteRequestStateItems { get; set; } }
public class WebsiteRequestStateItem { [MongodbPresentationItem(DisplayName = "当前请求数量")] public long CurrentRequestCount { get; set; } [MongodbPresentationItem(DisplayName = "最大请求数量")] public long MaxRequestCount { get; set; } [MongodbPresentationItem(DisplayName = "最大请求数量发生在")] public DateTime MaxRequestCountOccur { get; set; } [MongodbPresentationItem(DisplayName = "总共请求数量")] public long TotalRequestCount { get; set; } [MongodbPresentationItem(DisplayName = "总共请求执行时间")] public long TotalRequestExecutionTime { get; set; } [MongodbPresentationItem(DisplayName = "平均请求执行时间")] public long AverageRequestExecutionTime { get; set; } [MongodbPresentationItem(DisplayName = "最大请求执行时间")] public long MaxRequestExecutionTime { get; set; } [MongodbPresentationItem(DisplayName = "最大请求执行发生在")] public DateTime MaxRequestExecutionTimeOccur { get; set; } [MongodbPresentationItem(DisplayName = "上一次请求执行时间")] public long LastRequestExecutionTime { get; set; } [MongodbPresentationItem(DisplayName = "上一次请求执行发生在")] public DateTime LastRequestExecutionTimeOccur { get; set; } [MongodbPresentationItem(DisplayName = "页面地址")] public string Url { get; set; } }
每10秒都可以看到网站中所有页面执行情况的最新数据。对于这两种状态数据的收集不需要编写任何代码,是由框架内部实现的。在这里,我们也看到了其实所有要记录到Mongodb中的数据都是通过特性的形式进行标注元数据的,我们要做的只是定义数据结构和加上特性,之后的事情都交给Mongodb数据服务了。在之后的文章中我们会详细介绍这些特性。
10、最后来看一下异常服务的配置:
在这里需要说明的是未处理异常的处理方式:
1)如果是本地请求则不会捕获异常,还是可以看到黄页。
2)如果不是本地请求则会看是否配置了跳转(比如转到统一的出错站点),如果没配置的话直接显示配置的UnhandledExceptionMessage。
跳转地址配置在处理策略中:
这里我们根据的是异常类的类名来匹配策略,可以看到配置了网站未处理异常、Mvc网站处理异常、Wcf客户端未处理异常、Wcf服务端未处理异常、处理异常以及应用程序域未处理异常的策略。所谓处理异常就是手动调用异常服务Handle()方法的异常,未处理异常就是非程序捕获的,框架捕获的会导致黄页的异常。我们来看看网站未处理异常的配置:
在这里要区分一下异常类型名和异常分类名,前者是诸如NullReferenceException、ArgumentException,后者是诸如WebSiteUnhandledExceptionInfo、HandledExceptionInfo。这么做的目的是增加灵活性,比如我们可以定义一种BusinessException的异常,然后指定对于这种异常类型WebSiteUnhandledExceptionInfo的我们不需要跳转到出错页面,也不需要记录本地日志,而对于其它类型异常的话则还是使用默认的(在这里我们对虽有异常类型都采取相同的策略,因此异常类型名字段留空)。这里的策略是记录本地日志、记录远程日志,不进行跳转输出错误提示语句,Http响应代码为200。
至此配置介绍完了,本文一开始从代码使用角度介绍了应用程序信息中心模块,然后从后台配置角度介绍了模块的配置。之后的文章会从实现角度介绍其中的一些关键实现。