保存呼叫WCF/Web Service的完整通聯記錄
2 | 14,848 |
在系統整合時,難免會涉及廠商或其他系統提供的WCF/Web Service。
要呼叫別人寫的WCF/WS,技術層面一點都不難! 在專案中Add Service Reference / Add Web Reference,Visual Studio便會打理剩下的瑣碎細節,自動幫你建立好Proxy物件,使用對方的服務就跟呼叫自己專案的元件一樣方便。
不過,老江湖都知道,呼叫別人服務的凶險所在並不在技術! 跨系統整合最怕的就是 -- 發生問題時各系統互踢皮球,堅稱問題不在自己身上,複雜的案情讓李組長不只要皺眉,就算把臉糾成包子也無解;網頁取存過程有Log可查,但呼叫其他系統的Web Service,相關Log可能遠在天邊廠商的主機上,呼叫端預設並沒有記錄。若相關系統或廠商不配合主動提供,要追查Web Service/WCF的呼叫是否出錯就變得格外困難。
要解決這樣的難題,最簡單的做法就是每次呼叫WCF/WS時,將傳入參數以及對方的回應寫入Log檔。不過,找出所有呼叫WCF/WS的程式片段,再依不同方法不同參數硬刻(Hard-Coding)出寫Log的程式碼顯然有點愚公移山。於是我有個點子,何不針對該WCF/WS建立一個LogWrapper類別,提供與各WebMethod完全相同參數與傳回型別的介面。當呼叫端呼叫時,LogWrapper先偷偷將所有傳入參數寫入Log檔,再呼叫遠端真正的WCF/WS;取得結果後,先將結果寫入Log檔,再把結果傳回呼叫端。如此,呼叫端只要將原本直接呼叫WCF/WS的程式稍做修改,改呼叫LogWrapper,我們就能保留呼叫歷程的詳細記錄,要追查問題就方便多了!
我用一個很簡單(且無聊)的WCF來示範:
public class SpiderManService : ISpiderManService
{
public string SuperCalc(int x, int y)
{
return string.Format("{0} + {1} = {2}",
x, y, x + y);
}
}
在ASP.NET專案中Add Service Reference後,我們可使用SpiderManServiceClient Class來呼叫它:
protected void Page_Load(object sender, EventArgs e)
{
SMS.SpiderManServiceClient smsc = new SMS.SpiderManServiceClient();
Response.Write(smsc.SuperCalc(1, 1));
Response.End();
}
下一步,我們就來產生一個SpideManServiceClientLogWrapper,把SuperCalc方法包起來! 借助.NET神奇的Reflection功能,我寫了一個LogWrapper程式碼產生器:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Reflection;
using System.Text;
public class LogWrapperCodeGen
{
public static string GenWrapperClass(Type t)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(@"using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Xml;
using System.Text;
using System.IO;
");
sb.AppendFormat("public class {0}LogWrapper", t.ToString().Split('.').Last());
sb.AppendLine(@"
{
class Logger
{
public string MethodName;
public string LogPath;
private StringBuilder sb = new StringBuilder();
private void log(string msg)
{
sb.AppendFormat(""{0:yyyy/MM/dd HH:mm:ss.fff} {1}\r\n"", DateTime.Now, msg);
}
private void log(string fmt, params object[] args)
{
log(string.Format(fmt, args));
}
public Logger(string methodName)
{
MethodName = methodName;
LogPath = HttpContext.Current.Server.MapPath(
string.Format(""~/App_Data/WSLog/{0:yyyyMMdd}.txt"", DateTime.Today));
log(new String('=', 80));
log(""Url = "" + HttpContext.Current.Request.Url.ToString());
log(""IP = "" + HttpContext.Current.Request.UserHostAddress);
log(""Call Method[{0}]"", methodName);
}
public void LogParam(string paramName, object paramValue)
{
log("" Parameter[{0}] => {1}"", paramName, paramValue);
}
public void LogResult(string result)
{
log("" Result => {0}"", result);
}
public void Flush()
{
using (StreamWriter sw = new StreamWriter(LogPath, true))
{
sw.Write(sb.ToString());
}
}
}
");
foreach (MethodInfo mi in t.GetMethods(
//BindingFlags.DeclaredOnly 可以排除繼承來的Method
BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
{
sb.AppendFormat(" public static {1} {0} (", mi.Name, mi.ReturnType.ToString());
sb.Append(
string.Join(", ", mi.GetParameters()
.Select(o => string.Format("{0} {1}", o.ParameterType.ToString(), o.Name))
.ToArray()));
sb.AppendLine(")");
sb.AppendLine(" {");
sb.AppendFormat(" Logger log = new Logger(\"{0}\");\r\n", mi.Name);
foreach (ParameterInfo pi in mi.GetParameters())
sb.AppendFormat(" log.LogParam(\"{0}\", {0});\r\n", pi.Name);
sb.AppendFormat(" {0} objWS = new {0}();\r\n", t.ToString());
sb.AppendFormat(" {1} result = objWS.{0}(",
mi.Name, mi.ReturnType.ToString());
sb.Append(
string.Join(", ",
mi.GetParameters().Select(o => string.Format("{0}", o.Name)).ToArray()));
sb.AppendLine(");");
sb.AppendLine(" log.LogResult(result.ToString());");
sb.AppendLine(" log.Flush();");
sb.AppendLine(" return result;");
sb.AppendLine(" }");
}
sb.AppendLine("}");
return sb.ToString();
}
}
呼叫LogWrapperCodeGen.GenWrapperClass(typeof(SMS.SpiderManServiceClient)),就可以得到SpiderManServiceClientLogWrapper的程式碼如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Xml;
using System.Text;
using System.IO;
public class SpiderManServiceClientLogWrapper
{
class Logger
{
public string MethodName;
public string LogPath;
private StringBuilder sb = new StringBuilder();
private void log(string msg)
{
sb.AppendFormat("{0:yyyy/MM/dd HH:mm:ss.fff} {1}\r\n", DateTime.Now, msg);
}
private void log(string fmt, params object[] args)
{
log(string.Format(fmt, args));
}
public Logger(string methodName)
{
MethodName = methodName;
LogPath = HttpContext.Current.Server.MapPath(
string.Format("~/App_Data/WSLog/{0:yyyyMMdd}.txt", DateTime.Today));
log(new String('=', 80));
log("Url = " + HttpContext.Current.Request.Url.ToString());
log("IP = " + HttpContext.Current.Request.UserHostAddress);
log("Call Method[{0}]", methodName);
}
public void LogParam(string paramName, object paramValue)
{
log(" Parameter[{0}] => {1}", paramName, paramValue);
}
public void LogResult(string result)
{
log(" Result => {0}", result);
}
public void Flush()
{
using (StreamWriter sw = new StreamWriter(LogPath, true))
{
sw.Write(sb.ToString());
}
}
}
public static System.String SuperCalc (System.Int32 x, System.Int32 y)
{
Logger log = new Logger("SuperCalc");
log.LogParam("x", x);
log.LogParam("y", y);
SMS.SpiderManServiceClient objWS = new SMS.SpiderManServiceClient();
System.String result = objWS.SuperCalc(x, y);
log.LogResult(result.ToString());
log.Flush();
return result;
}
}
將以上Class放進App_Code,再修改呼叫程式,改成SpiderManServiceClientLogWrapper.SuperCalc(1, 1):
protected void Page_Load(object sender, EventArgs e)
{
//SMS.SpiderManServiceClient smsc = new SMS.SpiderManServiceClient();
//Response.Write(smsc.SuperCalc(1, 1));
Response.Write(SpiderManServiceClientLogWrapper.SuperCalc(1, 1));
Response.End();
}
就可以在App_Data/WSLog/yyyyMMdd.txt中找到詳細的呼叫記錄囉!
2010/10/21 05:38:16.852 ================================================================================
2010/10/21 05:38:16.852 Url = httq://localhost/ASPNET35/Default.aspx
2010/10/21 05:38:16.852 IP = ::1
2010/10/21 05:38:16.853 Call Method[SuperCalc]
2010/10/21 05:38:16.853 Parameter[x] => 1
2010/10/21 05:38:16.853 Parameter[y] => 1
2010/10/21 05:38:16.875 Result => 1 + 1 = 2
有了詳細呼叫Log,不管是要偵錯抓蟲射茶包,還是準備起訴抗辯上公堂,都能無往不利! (提到這點,相信老鳥們應該會發出會心一笑吧~)
Comments
# by Lan
我遇過某鳥廠幹的好事是這樣 : 寫進Log的是A,傳出去的變成A'
# by chicken
這種苦差事可以用 Policy Injection 方式,解的比較漂亮 :D 尤其像 WCF 這種已經是 remoting 型態的機制了,應該有更簡單的地方可以插入 Logging code.. http://columns.chicken-house.net/post/2008/11/18/Policy-Injection-Application-Block-e5b08fe799bce78fbe.aspx