客户端的作用就是捕获未处理异常, 发送异常到服务端。 关于捕获未处理异常的方法参考 http://www.cnblogs.com/youring2/archive/2012/04/25/2469974.html http://www.cnblogs.com/eaglet/archive/2009/02/17/1392191.html 这两篇博客。
总结下来对于web程序来说, 捕获未处理异常有两种方式:
1. 定义一个HttpModule, 在这个module的context_Error方法中来捕获未处理的异常。
2. 在Global.asax中有一个Application_Error的方法, 当程序发生未处理异常的时候, 会调用这个方法。
对于以上两种方式, 以上两篇博客也做了对比, 对异常的捕获, HttpModule会优于Global,同时从编程的角度来说, 如果使用Global, 那就意味着代码侵入。 所以最后决定采用了HttpModule的方式。
public class ErrorLogModule : IHttpModule { #region IHttpModule Members public void Dispose() { } public void Init(HttpApplication context) { context.Error += context_Error; } #endregion private void context_Error(object sender, EventArgs e) { HttpApplication application = (HttpApplication) sender; ClientErrorEntity errorMessage = new ClientErrorEntity(application.Server.GetLastError().GetBaseException(), application.Context); SendErrorEntity.SendError(errorMessage); } }
异常信息组装。
public ClientErrorEntity(Exception exception, HttpContext httpContext) { Id = Guid.NewGuid().ToString("N"); MachineName = Environment.MachineName; Ip = Common.GetLocalIp(); ExceptionType = exception.GetType().FullName; ExceptionMessage = exception.Message; ExceptionSource = exception.Source; ExceptionDetail = exception.ToString(); DateTime = DateTime.Now; HttpException httpException = exception as HttpException; if (httpException != null) { HttpStatusCode = httpException.GetHttpCode(); } if (httpContext != null) { HttpRequest request = httpContext.Request; RequestUrl = request.Url.AbsoluteUri; ServerVariables = Common.ConvertCollectionToDictionary(request.ServerVariables); QueryString = Common.ConvertCollectionToDictionary(request.QueryString); Form = Common.ConvertCollectionToDictionary(request.Form); Cookies = Common.ConvertCollectionToDictionary(request.Cookies); } Type = 0; }
这里实现了异常信息的发送,为了避免影响主逻辑, 采用了异步的方式。 注意一个问题, 对于API的地址, 我们采用的硬编码的方式, 当然正式环境为了避免单点, 应该采用域名的方式, 然后通过负载均衡在在多台服务器中选择。 如果仅仅有一台服务器, 那我们要将这个地址放在配置文件中, 一旦地址发生了变化,可以方便我们更改。
public static void SendError(ClientErrorEntity errorMessage) { try { string jsonstr = JsonConvert.SerializeObject(errorMessage); //异步, 确保不会影响主逻辑 sendPostdelegate.BeginInvoke("http://192.168.16.39:90/api/Error", jsonstr, ContentType.Json, 3000, Encoding.UTF8, Encoding.UTF8, Response, sendPostdelegate); //这里请求地址 建议改为域名的方式, 如果没有域名,建议请求地址和webtoken类似为可配置 } catch (Exception exception) { Common.Log("错误信息上传失败:" + exception.Message); } }
为了标示异常是从哪个网站发出的, 我们会对每个网站分配一个token,token会随异常信息一起发送到服务端。
<configSections> <section name="WebWarningSetting" type="System.Configuration.SingleTagSectionHandler"/> </configSections> <WebWarningSetting WebToken="1E8C46FC74DC41569EFF63C92A7A4087"/>
服务端API主要功能是接收异常信息,考虑到性能问题, 并没有将异常信息直接入库,而是将异常信息放入到Redis的List中, 因为提供的驱动中Redis的List的Value并不支持对象, 所以这里采用分开存放的方法, 将ErrorEntity.Id放入到队列中, ErrorEntity则以普通的方式set到Redis中, 在获取数据的时候,从队列中获取到ErrorEntity.Id , 这样取出来的Id确保顺序不会变, 再根据这个Id, 通过get取出实体。这种方法其实还有待考虑, 虽然List不支持复杂Value, 但是通过json序列化,我们还是可以直接将ErrorEntity放入到List中。
public HttpResponseMessage Post([FromBody]ErrorEntityDto errorEntity) { if (errorEntity != null && !string.IsNullOrWhiteSpace(errorEntity.Id)) { if (RedisHelper.EnqueueItemOnList("ErrorEntityQueue", errorEntity.Id)) //先将ErrorEntity.Id放入到队列中,确保顺序不会变 { if (RedisHelper.Set(errorEntity.Id, errorEntity)) //将实体添加到Redis中 { return ReturnPlainText("ok"); } return ReturnPlainText("set error"); } return ReturnPlainText("add item to list error"); } return ReturnPlainText("modle error"); }