一、為什么我們需要服務調用日志
在三個月前,我一個朋友他們公司的內部軟件系統更新換代。在新系統中,用戶有時會說慢,但是具體怎么慢,慢到什么程度也講不清楚。問題難定位,從而更難解決。他們的新系統是CS模式,客戶端使用的是WPF,服務端使用的是WCF。后來在他們的WCF服務中加了一個消息攔截器,在不影響現有的代碼情況下,記錄了WCF服務調用日志。可以清晰的記錄哪個用戶調用了服務,傳的參數是什么,服務調用是什么時候調用的,服務耗時是多少,調用服務的過程中執行了哪些SQL語句,每個SQL耗時是多少等等。
有了這些日志幫助,可以非常容易的定位問題在哪里,從而大大的提高了用戶體驗度。
在很多的項目中,特別是.Net項目中,日志一般都是記錄在本地文件中,但是這種日志非常難以閱讀。如果將日志存儲到數據庫中,又影響系統性能,所以又不得不選擇記錄在本地,因為有日志總比沒有日志強!
為了解決這一問題,我們可以搭建一個日志系統,該系統有獨立的日志數據庫,日志系統可以是Java、C#也可以是Node.js,因為日志系統只需要對接消息隊列既可,消息隊列可以是ActiveMQ、RabbitMQ、Kafaka等。而其他系統只需要將日志信息發送到消息隊列即可,性能是非常的高。這就大大的解耦了系統之間的耦合度,運維人員也可以很方便的查看日志消息。
二、為什么我們需要使用WCF服務
在制造業中,特別是工廠內部使用的軟件,小編認為還是CS的模式比較合適。小編曾給過幾家制造業公司開發過BS和CS模式的項目,其中有一個項目的客戶是一家汽車制造業公司,他們指定要BS模式的系統,小編開發了兩三個月的時間,簡直就是個噩夢。作為過來人,強烈建議使用CS模式開發制造業的生產系統。
CS相比較BS,有以下特點:
1、因為CS開發速度快,代碼簡單,客戶端的代碼也不會像HTML、CSS、JS那么難操作,對於那些擅長寫服務端代碼的人來說,難度會降低很多。
2、CS模式充分的利用了客戶端的電腦性能,減輕服務端壓力。
3、CS響應速度快,CS模式中客戶端與服務端IO交互少,只需要交互數據,而BS中需要下載很多的HTML、CSS、JS文件。
在CS模式中,服務端建議使用WCF,雖然MVC API更出名,但是WCF更適合公司內部的項目開發。VS幫助我們開發者一鍵生成客戶端調用代碼,十分的便捷。
對於企業內部項目,如MES、倉庫管理、品質監控、生產排程等系統,小編認為C#比Java更合適,CS比BS更合適,WCF比Mvc API 更合適。而權限管理、單點登錄、報表、HR等系統,使用BS模式要更適合一些。在CS模式中,客戶端是(Winform、WPF)可以內嵌網頁。所以說,小編認為沒有技術誰比誰好,只是在不同的業務場景下那個技術更合適而已。
三、WCF服務日志設計圖
1、用戶發送一個wcf的服務請求時,wcf的消息攔截器會攔截客戶端發出的請求。
2、在消息攔截器中,首先觸發的是AfterReceiveRequest方法,官方給出的說明:在已接收入站消息后將消息調度到應發送到的操作之前調用。
3、在AfterReceiveRequest方法執行結束之后,將會執行具體的服務接口,也就是我們所開發的業務接口。
4、在接口執行完畢之后,會觸發攔截器中BeforeSendReply方法,該官方給出的說明:在操作已返回后發送回復消息之前調用。
5、在BeforeSendReply方法中,可以將所要記錄的日志信息記錄在本地並將日志信息發送到消息隊列中。等待日志系統監聽到日志消息。
6、日志系統監聽到隊列中有消息時,將消息從隊列中拿出來並存儲到對應的日志數據庫中,方便查詢分析。
四、運行效果圖
為了測試方便,小編只在用戶服務上加了消息攔截器,先看效果圖,后面再上代碼。
為了方便方便查看日志消息,小編做了一個可供查詢日志的界面,每次服務調用信息都能詳細的查看到,特別是接受時間、響應時間、以及耗時時間:
由於我的電腦屏幕只有13寸,不能完全顯示出日志列表的數據,以下是列表的代碼:
<dxlc:GroupBox Header="日志列表" Grid.Row="2"> <dxg:GridControl CustomColumnDisplayText="grid_CustomColumnDisplayText" ItemsSource="{Binding ServiceAuditInfoList, Mode=TwoWay}" SelectedItem="{Binding SelectedServiceAuditInfo, Mode=TwoWay}"> <dxg:GridControl.View> <dxg:TableView ShowGroupPanel="False" ShowFilterPanelMode="Never" AutoWidth="False" /> </dxg:GridControl.View> <dxg:GridColumn FieldName="Host" Header="服務器" Width="120" /> <dxg:GridColumn FieldName="Port" Header="端口號" Width="80" /> <dxg:GridColumn FieldName="ServiceUri" Header="服務全稱" Width="150" /> <dxg:GridColumn FieldName="ServiceName" Header="服務名稱" Width="110" /> <dxg:GridColumn FieldName="Action" Header="接口名" Width="80" /> <dxg:GridColumn FieldName="Realm" Header="客戶端域名" Width="120" /> <dxg:GridColumn FieldName="UserIP" Header="客戶端IP" Width="120" /> <dxg:GridColumn FieldName="UserName" Header="用戶人姓名" Width="100" /> <dxg:GridColumn FieldName="ReceiveTime" Header="接收時間" Width="120"/> <dxg:GridColumn FieldName="SendTime" Header="響應時間" Width="120" /> <dxg:GridColumn FieldName="ThreadId" Header="線程編號" Width="120" /> <dxg:GridColumn FieldName="VisitId" Header="訪問編號" Width="120" /> <dxg:GridColumn FieldName="ElapsedMilliseconds" Header="耗時(毫秒)" Width="100" /> </dxg:GridControl> </dxlc:GroupBox>
當我們有了這些服務調用的日志系統,我們可以做各種圖表來分析我們的系統哪里有問題,那些服務耗時,對系統優化很有好處。
五、WCF服務消息攔截器
WCF服務中,.Net在System.ServiceModel.dll中給開發者提供了一個WCF服務自定義擴展的接口,具體如下:
1 using System.Collections.ObjectModel; 2 using System.ServiceModel.Channels; 3 4 namespace System.ServiceModel.Description 5 { 6 // 7 // 摘要: 8 // 提供一種在整個服務內修改或插入自定義擴展的機制,包括 System.ServiceModel.ServiceHostBase。 9 public interface IServiceBehavior 10 { 11 // 12 // 摘要: 13 // 用於向綁定元素傳遞自定義數據,以支持協定實現。 14 // 15 // 參數: 16 // serviceDescription: 17 // 服務的服務說明。 18 // 19 // serviceHostBase: 20 // 服務的宿主。 21 // 22 // endpoints: 23 // 服務終結點。 24 // 25 // bindingParameters: 26 // 綁定元素可訪問的自定義對象。 27 void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters); 28 // 29 // 摘要: 30 // 用於更改運行時屬性值或插入自定義擴展對象(例如錯誤處理程序、消息或參數攔截器、安全擴展以及其他自定義擴展對象)。 31 // 32 // 參數: 33 // serviceDescription: 34 // 服務說明。 35 // 36 // serviceHostBase: 37 // 當前正在生成的宿主。 38 void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase); 39 // 40 // 摘要: 41 // 用於檢查服務主機和服務說明,從而確定服務是否可成功運行。 42 // 43 // 參數: 44 // serviceDescription: 45 // 服務說明。 46 // 47 // serviceHostBase: 48 // 當前正在構建的服務主機。 49 void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase); 50 } 51 }
我們只需要將 IServiceBehavior 接口實現,並將WCF消息攔截器加入到服務的終結點中:
1 public class ServiceAuditAttribute : Attribute, IServiceBehavior 2 { 3 public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) 4 { 5 return; 6 } 7 public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) 8 { 9 foreach (ChannelDispatcher dispatcherBase in serviceHostBase.ChannelDispatchers) 10 { 11 var channelDispatcher = dispatcherBase as ChannelDispatcher; 12 13 foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints) 14 { 15 endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new MessageInspector()); 16 } 17 } 18 } 19 public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) 20 { 21 22 } 23 }
WCF消息攔截器的具體實現如下:
/// <summary> /// WCF消息攔截器 /// </summary> public class MessageInspector : IDispatchMessageInspector { /// <summary> /// 在已接收入站消息后將消息調度到應發送到的操作之前調用。 /// </summary> /// <param name="request">請求消息</param> /// <param name="channel">傳入通道</param> /// <param name="instanceContext">當前服務實例</param> /// <returns>Stopwatch</returns> public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) { Stopwatch stw = Stopwatch.StartNew(); //TODO 日志代碼 return stw; } /// <summary> /// 在操作已返回后發送回復消息之前調用 /// </summary> /// <param name="reply">回復消息。 如果操作是單向的,則此值為 null。</param> /// <param name="correlationState">計時器</param> public void BeforeSendReply(ref Message reply, object correlationState) { var watch = (Stopwatch)correlationState; watch.Stop(); //TODO 日志代碼 } }
在這兩個接口中,我們就可以自由發揮了。
1 ServiceAuditInfo serviceAuditInfo = new ServiceAuditInfo(); 2 serviceAuditInfo.Id = Guid.NewGuid().ToString(); 3 serviceAuditInfo.Host = context.IncomingMessageHeaders.To.Host; 4 serviceAuditInfo.Port = context.IncomingMessageHeaders.To.Port; 5 serviceAuditInfo.ServiceUri = context.IncomingMessageHeaders.To.LocalPath; 6 serviceAuditInfo.ServiceName = ParseUriLastPart(context.IncomingMessageHeaders.To.LocalPath); 7 serviceAuditInfo.Action = ParseUriLastPart(context.IncomingMessageHeaders.Action); 8 serviceAuditInfo.Realm = ApplicationContext.Current.UserVisitInfo.Realm; 9 serviceAuditInfo.UserIP = ApplicationContext.Current.UserVisitInfo.UserIP; 10 serviceAuditInfo.UserId = ApplicationContext.Current.UserVisitInfo.UserId; 11 serviceAuditInfo.UserName = ApplicationContext.Current.UserVisitInfo.UserName; 12 serviceAuditInfo.LoginToken = ApplicationContext.Current.UserVisitInfo.LoginToken; 13 serviceAuditInfo.VisitId = ApplicationContext.Current.UserVisitInfo.Id; 14 serviceAuditInfo.ThreadId = Thread.CurrentThread.ManagedThreadId.ToString(); 15 serviceAuditInfo.ReceiveTime = ApplicationContext.Current.UserVisitInfo.ReceiveTime; 16 serviceAuditInfo.SendTime = DateTime.Now; 17 serviceAuditInfo.ElapsedMilliseconds = (int)watch.ElapsedMilliseconds; 18 string logJson = JsonConvert.SerializeObject(serviceAuditInfo); 19 20 //發送消息 21 var publisher = new MQProducer("LogExchange", "localhost", 22 "guest", "guest", "/", new Uri("amqp://localhost/")); 23 24 publisher.PublishDirectMessage("ServiceAudit", logJson);
為了方便演示,我簡化了消息隊列的代碼,生產環境中肯定是不能用這樣的代碼。至此我們就能將日志信息發送到RabbitMQ消息隊列中。
五、應用
六、應用攔截器
我們只需要在服務實現類上加上【ServiceAudit】特性即可
啟動程序,並登錄,消息隊列中就有了三條日志信息,等待被消費。由於消息隊列中消息很快就會被消費掉,我停止了日志服務系統的服務。
如果我們啟動日志服務,數據將會很快的被消費掉。畢竟,RabbitMQ是非常快的消息隊列。
總結:
有了服務日志,我們可以再一步進行擴展,記錄系統中執行的SQL語句,這樣我們又能知道每次調用服務具體執行了那些SQL語句。日志系統可以給公司IT建設帶來很大的便利。補充一句:網上的代碼只是示例,具體應用到生產環境中還需要進一步的封裝。