LoggingBehavior - 如何使用简单的行为将日志打印与 WCF 操作的详细信息连接起来





5.00/5 (14投票s)
在本文中,我将逐步解释如何使用 WCF 行为来记录操作的调用和结果、错误、警告和包含操作详细信息的信息日志。
引言
在开发 WCF 服务时,我们有时希望记录有关操作的一些条目。我们想知道我们的操作已被调用,以及发送了哪些参数。我们想知道我们的操作是否已成功结束(如果已成功),以及结果是什么。我们想知道在我们的操作过程中发生了哪些错误,以及发送给导致错误的那个操作的参数是什么。
通常,为了实现这个目标,我过去会编写类似这样的代码片段
WriteToLog(string.Format("MyOperation called. param1={0}, param2={1}", param1, param2));
或者,在记录错误时,尽管主要信息是错误原因,但为了将其与操作及其参数关联起来,我需要编写类似这样的内容
WriteToLog(string.Format("Error occurred in MyOperation.\n Parameters: param1={0},
param2={1}\n Error reason: some reason.", param1, param2));
好的,当只有 2 或 3 个基本参数时,这并不是什么大事。但是,如果您有一些复杂的参数,其中可能包含其他复杂类的集合,您可能会浪费大量的代码屏幕,仅仅为了打印日志。由于每次需要日志时都这样做让我感到厌倦,所以我决定编写一个行为来简化这项任务。
背景
在此解决方案中,我使用了一个 WCF 行为(实现了:IServiceBehavior
、IEndpointBehavior
、IContractBehavior
或 IOperationBehavior
的类)。在此行为中,我使用参数检查器(实现了 IParameterInspector
的类),用于获取操作调用和操作结果的详细信息。
有关更多信息,您可以阅读 MSDN 关于扩展调度程序的主题。
它是如何工作的?
日志记录策略
在创建用于记录操作调用和结果的参数检查器之前,我们需要一个日志记录机制来使用。我的解决方案的想法是将“记录什么”和“何时记录”的实现,与“如何处理日志条目”的实现分离开来。这样,我们的行为负责在需要时创建日志条目,而此行为的用户可以选择如何处理这些日志条目。它可以打印到控制台、写入文件或数据库表、写入事件日志,甚至发送到另一个服务进行处理。为了实现这一点,我们可以创建一个接口,其中包含一个 Log
方法,该方法可以为处理日志条目而实现。
public interface ILoggingStrategy
{
bool Log(LoggingArgument arg);
}
日志记录参数
Log
方法接收一个类型为 LoggingArgument
的参数。
public class LoggingArgument
{
public LoggingArgument()
{
LogTime = DateTime.Now;
LogType = LoggingType.Information;
}
public DateTime LogTime { get; set; }
public string OperationName { get; set; }
public LoggingType LogType { get; set; }
public LoggingInputsData InputsData { get; set; }
public LoggingOutputsData OutputsData { get; set; }
public LoggingReturnValueData ReturnValueData { get; set; }
public LoggingExceptionData ExceptionData { get; set; }
public LoggingInformationData InformationData { get; set; }
public override string ToString()
{
...
}
}
此参数包含一些属性:日志类型、日志时间、操作名称以及每个日志类型的某些特殊部分(属性)。每个特殊部分都有一个特殊的类型。该类型根据其目的包含一些属性,并实现了 ToString
方法。其思想是让参数的接收者选择打印默认的 ToString
结果,或者根据每个特殊部分的给定属性创建自己的字符串。例如,这是 LoggingReturnValueData
的实现。
public class LoggingReturnValueData
{
public object Value { get; set; }
public override string ToString()
{
return ObjectToStringConverter.ConvertToString(Value);
}
}
将数据转换为字符串
每个特殊部分的 ToString
方法使用一个名为 ObjectToStringConverter
的类。该类使用反射来遍历对象的类型并根据它创建一个 string
。
为了通常将 object
转换为 string
,我们有 ConvertToString
方法。
public static string ConvertToString(object value)
{
if (value == null)
{
return "null";
}
Type valueType = value.GetType();
if (valueType == typeof(string) || valueType.IsEnum || IsParsable(valueType))
{
return value.ToString();
}
if (value is Exception)
{
return ConvertExceptionToString(value as Exception);
}
if (value is IEnumerable)
{
return ConvertCollectionToString(value as IEnumerable);
}
if (value is Type)
{
return ConvertTypeToString(value as Type);
}
return ConvertClassToString(value);
}
在此方法中,我们检查对象的类型,并根据它调用相应的方法。
在 ConvertExceptionToString
方法中,我们创建一个 string
,其中包含异常的消息以及内部异常的消息(递归地)。
在 ConvertCollectionToString
方法中,我们遍历集合中的每个元素,并使用它调用 ConvertToString
。
在 ConvertTypeToString
方法中,我们构建一个包含类型名称及其程序集描述的 string
。
在 ConvertClassToString
方法中,我们遍历类的属性和数据字段,并使用它们调用 ConvertToString
。
日志记录行为
现在,有了这个日志记录机制,我们可以实现我们的日志记录行为。首先,我们创建一个参数检查器。
class LoggingParameterInspector : IParameterInspector
{
#region IParameterInspector Members
public void AfterCall(string operationName, object[] outputs,
object returnValue, object correlationState)
{
throw new NotImplementedException();
}
public object BeforeCall(string operationName, object[] inputs)
{
throw new NotImplementedException();
}
#endregion
}
向此参数检查器添加以下属性:
public bool LogBeforeCall { get; set; }
public bool LogAfterCall { get; set; }
#region LoggingStrategy
private ILoggingStrategy _loggingStrategy;
public ILoggingStrategy LoggingStrategy
{
get { return _loggingStrategy ?? (_loggingStrategy = new ConsoleLoggingStrategy()); }
set { _loggingStrategy = value; }
}
#endregion
public Type ServiceType { get; set; }
LogBeforeCall
属性决定参数检查器是否记录操作调用。
LogAfterCall
属性决定参数检查器是否记录操作结果。
LoggingStrategy
属性保存参数检查器使用的日志记录策略。默认策略是 ConsoleLoggingStrategy
。该类实现了 ILoggingStrategy
以将日志条目写入控制台。
ServiceType
属性保存服务的类型。它用于获取有关服务方法的信息。
在 BeforeCall
方法中,我们创建一个包含方法调用数据的 LoggingArgument
,并像下面这样使用它调用日志记录策略:
public object BeforeCall(string operationName, object[] inputs)
{
if (ServiceType == null)
{
return null;
}
MethodInfo mi = ServiceType.GetMethod(operationName);
if (mi == null)
{
return null;
}
if (LogBeforeCall)
{
LoggingArgument arg = CreateArgumentForInvokeLog(mi, inputs);
LoggingStrategy.Log(arg);
}
return null;
}
private LoggingArgument CreateArgumentForInvokeLog(MethodInfo mi, object[] inputs)
{
if (mi == null)
{
return null;
}
LoggingArgument res =
new LoggingArgument
{
LogType = LoggingType.Invoke,
OperationName = mi.Name
};
if (inputs != null && inputs.Length > 0)
{
res.InputsData = new LoggingInputsData
{
InputParameters = mi.GetParameters().Where(p => !p.IsOut).ToArray(),
InputValues = inputs
};
}
return res;
}
在 AfterCall
方法中,我们创建一个包含方法结果数据的 LoggingArgument
,并像下面这样使用它调用日志记录策略:
public void AfterCall(string operationName, object[] outputs,
object returnValue, object correlationState)
{
if (!LogAfterCall)
{
return;
}
if (ServiceType == null)
{
return;
}
MethodInfo mi = ServiceType.GetMethod(operationName);
if (mi == null)
{
return;
}
LoggingArgument arg = CreateArgumentForResultLog(mi, outputs, returnValue);
LoggingStrategy.Log(arg);
}
private LoggingArgument CreateArgumentForResultLog
(MethodInfo mi, object[] outputs, object returnValue)
{
if (mi == null)
{
return null;
}
LoggingArgument res =
new LoggingArgument
{
LogType = LoggingType.Result,
OperationName = mi.Name
};
if (outputs != null && outputs.Length > 0)
{
res.OutputsData = new LoggingOutputsData
{
OutputParameters =
mi.GetParameters().Where(p => p.IsOut ||
p.ParameterType.IsByRef).ToArray(),
OutputValues = outputs
};
}
if (mi.ReturnType != typeof(void))
{
res.ReturnValueData = new LoggingReturnValueData
{
Value = returnValue
};
}
return res;
}
使用 LoggingParameterInspector
,我们创建了一个服务行为,可以将其用作我们服务的属性。
public class LoggingBehaviorAttribute : Attribute, IServiceBehavior
{
#region IServiceBehavior Members
public void AddBindingParameters(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase,
Collection endpoints,
BindingParameterCollection bindingParameters)
{
throw new NotImplementedException();
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
throw new NotImplementedException();
}
public void Validate(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
throw new NotImplementedException();
}
#endregion
}
在 ApplyDispatchBehavior
方法中,我们将 LoggingParameterInspector
添加到每个 DispatchOperation
,如下所示:public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
LoggingParameterInspector paramInspector = new LoggingParameterInspector
{
ServiceType = serviceDescription.ServiceType,
LoggingStrategy = GetLoggingStrategy(),
LogAfterCall = LogAfterCall,
LogBeforeCall = LogBeforeCall
};
foreach (ChannelDispatcher chDisp in serviceHostBase.ChannelDispatchers)
{
foreach (EndpointDispatcher epDisp in chDisp.Endpoints)
{
foreach (DispatchOperation op in epDisp.DispatchRuntime.Operations)
{
op.ParameterInspectors.Add(paramInspector);
}
}
}
}
为了也将此行为用作操作的属性,我们还必须实现 IOperationBehavior
。在 ApplyDispatchBehavior
方法中,我们删除现有的 LoggingParameterInspector
(如果行为也用于服务,则会有一个 LoggingParameterInspector
),并根据行为的属性添加一个新的 LoggingParameterInspector
,如下所示:
public void ApplyDispatchBehavior(OperationDescription operationDescription,
DispatchOperation dispatchOperation)
{
LoggingParameterInspector paramInspector =
dispatchOperation.ParameterInspectors.FirstOrDefault(
pi => pi.GetType() == typeof(LoggingParameterInspector))
as LoggingParameterInspector;
if (paramInspector != null)
{
// The logging inspector already exist...
dispatchOperation.ParameterInspectors.Remove(paramInspector);
}
paramInspector = new LoggingParameterInspector
{
ServiceType = operationDescription.DeclaringContract.ContractType,
LoggingStrategy = GetLoggingStrategy(),
LogAfterCall = LogAfterCall,
LogBeforeCall = LogBeforeCall
};
dispatchOperation.ParameterInspectors.Add(paramInspector);
}
日志记录上下文
为了将操作的详细信息与附加的日志打印(在操作范围内发生的错误、警告和信息日志)连接起来,我们需要在每个想要写入日志打印的地方知道操作的详细信息。为了实现这一点,我们可以创建一个类来保存每个操作所需的详细信息。
首先,我们需要一个当前会话的标识符。我们可以为此目的使用当前 OperationContext
的 SessionId
,如下所示:
public class LoggingContext
{
protected static string GetCurrentContextId()
{
OperationContext currContext = OperationContext.Current;
if (currContext == null)
{
return null;
}
return currContext.SessionId;
}
}
使用该标识符,我们可以注册当前会话的操作详细信息。为了保存每个会话的操作详细信息,我们可以使用当前会话标识符和会话操作详细信息的 Dictionary
,如下所示:
#region Contexts
private static Dictionary<string, LoggingContextDetails> _contexts =
new Dictionary<string, LoggingContextDetails>();
protected static Dictionary<string, LoggingContextDetails> Contexts
{
get { return _contexts; }
}
#endregion
#region Contexts methods
public static bool SetCurrentContextDetails(LoggingContextDetails contextDetails)
{
string currContextId = GetCurrentContextId();
if (currContextId == null)
{
return false;
}
AddContext(currContextId, contextDetails, true);
return true;
}
protected static void AddContext(string id,
LoggingContextDetails contextDetails,
bool replaceIfExist)
{
if (id == null)
{
return;
}
lock (Contexts)
{
if (replaceIfExist && Contexts.ContainsKey(id))
{
Contexts.Remove(id);
}
if (!Contexts.ContainsKey(id) && contextDetails != null)
{
Contexts.Add(id, contextDetails);
}
}
}
public static bool ClearCurrentContextDetails()
{
string currContextId = GetCurrentContextId();
if (currContextId == null)
{
return false;
}
RemoveContext(currContextId);
return true;
}
protected static void RemoveContext(string id)
{
if (id == null)
{
return;
}
lock (Contexts)
{
if (Contexts.ContainsKey(id))
{
Contexts.Remove(id);
}
}
}
#endregion
操作的详细信息由 LoggingContextDetails
类型表示。该类型声明如下:
public class LoggingContextDetails
{
public MethodInfo MethodDetails { get; set; }
public object[] Inputs { get; set; }
public bool LogErrors { get; set; }
public bool LogWarnings { get; set; }
public bool LogInformation { get; set; }
#region LoggingStrategy
private ILoggingStrategy _loggingStrategy;
public ILoggingStrategy LoggingStrategy
{
get { return _loggingStrategy ??
(_loggingStrategy = new ConsoleLoggingStrategy()); }
set { _loggingStrategy = value; }
}
#endregion
}
我们还添加了一个 static
属性来获取当前的 LoggingContext
,如下所示:
public static LoggingContext Current
{
get
{
LoggingContext res = new LoggingContext();
string currContextId = GetCurrentContextId();
lock (Contexts)
{
if (Contexts.ContainsKey(currContextId))
{
res.Details = Contexts[currContextId];
}
}
return res;
}
}
#region Details
private LoggingContextDetails _details;
public LoggingContextDetails Details
{
get { return _details ?? (_details = new LoggingContextDetails()); }
protected set { _details = value; }
}
#endregion
为了执行日志记录,我们添加了一个方法来根据 LoggingContext
的详细信息创建基本的 LoggingArgument
。
private LoggingArgument CreateArgumentForCommonLog()
{
LoggingArgument arg = new LoggingArgument();
MethodInfo mi = Details.MethodDetails;
if (mi != null)
{
arg.OperationName = mi.Name;
if (Details.Inputs != null && Details.Inputs.Length > 0)
{
arg.InputsData = new LoggingInputsData
{
InputParameters = mi.GetParameters().Where(p => !p.IsOut).ToArray(),
InputValues = Details.Inputs
};
}
}
return arg;
}
我们创建了一个方法来记录想要的日志打印。
public bool Log(Exception ex, string text, LoggingType logType)
{
LoggingArgument arg = CreateArgumentForCommonLog();
arg.LogType = logType;
if (ex != null)
{
arg.ExceptionData = new LoggingExceptionData
{
Exception = ex
};
}
if (text != null)
{
arg.InformationData = new LoggingInformationData
{
Text = text
};
}
return Details.LoggingStrategy.Log(arg);
}
并创建了每个特定日志打印的方法。
public bool LogError(Exception ex, string text)
{
if (Details.LogErrors)
{
return Log(ex, text, LoggingType.Error);
}
return false;
}
public bool LogWarning(Exception ex, string text)
{
if (Details.LogWarnings)
{
return Log(ex, text, LoggingType.Warning);
}
return false;
}
public bool LogInformation(string text)
{
if (Details.LogInformation)
{
return Log(null, text, LoggingType.Information);
}
return false;
}
为了设置每个操作的操作详细信息,我们可以在 LoggingParameterInspector
的 BeforeCall
方法中调用 SetCurrentContextDetails
,如下所示:
public object BeforeCall(string operationName, object[] inputs)
{
if (ServiceType == null)
{
return null;
}
MethodInfo mi = ServiceType.GetMethod(operationName);
if (mi == null)
{
return null;
}
SetLoggingContext(inputs, mi);
if (LogBeforeCall)
{
LoggingArgument arg = CreateArgumentForInvokeLog(mi, inputs);
LoggingStrategy.Log(arg);
}
return null;
}
private void SetLoggingContext(object[] inputs, MethodInfo mi)
{
LoggingContextDetails lcd = new LoggingContextDetails
{
MethodDetails = mi,
Inputs = inputs,
LoggingStrategy = LoggingStrategy,
LogErrors = LogErrors,
LogWarnings = LogWarnings,
LogInformation = LogInformation
};
LoggingContext.SetCurrentContextDetails(lcd);
}
并在 LoggingParameterInspector
的 AfterCall
方法中调用 ClearCurrentContextDetails
,如下所示:
public void AfterCall(string operationName, object[] outputs,
object returnValue, object correlationState)
{
LoggingContext.ClearCurrentContextDetails();
if (!LogAfterCall)
{
return;
}
if (ServiceType == null)
{
return;
}
MethodInfo mi = ServiceType.GetMethod(operationName);
if (mi == null)
{
return;
}
LoggingArgument arg = CreateArgumentForResultLog(mi, outputs, returnValue);
LoggingStrategy.Log(arg);
}
行为配置
为了在配置文件中使用日志记录行为,我们必须创建一个配置元素。
public class LoggingBehaviorExtensionElement : BehaviorExtensionElement
{
public override Type BehaviorType
{
get { throw new NotImplementedException(); }
}
protected override object CreateBehavior()
{
throw new NotImplementedException();
}
}
添加与行为属性对应的属性。
[ConfigurationProperty("logBeforeCall", DefaultValue = true)]
public bool LogBeforeCall
{
get { return (bool)this["logBeforeCall"]; }
set { this["logBeforeCall"] = value; }
}
[ConfigurationProperty("logAfterCall", DefaultValue = true)]
public bool LogAfterCall
{
get { return (bool)this["logAfterCall"]; }
set { this["logAfterCall"] = value; }
}
[ConfigurationProperty("logErrors", DefaultValue = true)]
public bool LogErrors
{
get { return (bool)this["logErrors"]; }
set { this["logErrors"] = value; }
}
[ConfigurationProperty("logWarnings", DefaultValue = true)]
public bool LogWarnings
{
get { return (bool)this["logWarnings"]; }
set { this["logWarnings"] = value; }
}
[ConfigurationProperty("logInformation", DefaultValue = true)]
public bool LogInformation
{
get { return (bool)this["logInformation"]; }
set { this["logInformation"] = value; }
}
[ConfigurationProperty("loggingStrategyType")]
public string LoggingStrategyType
{
get { return (string)this["loggingStrategyType"]; }
set { this["loggingStrategyType"] = value; }
}
实现 BehaviorType
属性以返回行为的类型。
public override Type BehaviorType
{
get { return typeof(LoggingBehaviorAttribute); }
}
并实现 CreateBehavior
方法以返回行为的实例。
protected override object CreateBehavior()
{
return new LoggingBehaviorAttribute
{
LogBeforeCall = LogBeforeCall,
LogAfterCall = LogAfterCall,
LogErrors = LogErrors,
LogWarnings = LogWarnings,
LogInformation = LogInformation,
LoggingStrategyType = ConvertStringToType(LoggingStrategyType)
};
}
private Type ConvertStringToType(string strType)
{
if (string.IsNullOrEmpty(strType))
{
return null;
}
Type res = null;
try
{
int firstCommaIndex = strType.IndexOf(",");
if (firstCommaIndex > 0)
{
string typeFullName = strType.Substring(0, firstCommaIndex);
string assemblyFullName = strType.Substring(firstCommaIndex + 1);
Assembly typeAssembly = Assembly.Load(assemblyFullName);
if (typeAssembly != null)
{
res = typeAssembly.GetType(typeFullName);
}
}
}
catch
{
}
return res;
}
如何使用?
在代码中使用日志记录行为
为了在代码中使用日志记录行为,我们可以将其作为我们服务的属性。
[LoggingBehavior]
public class MyService : IMyService
{
public int MyOperation(int myArg, List families, out string myResult)
{
myResult = "There are " + families.Count + " families.";
return 5;
}
}
当客户端调用 MyOperation
时,服务控制台上的日志打印可以显示如下:

为了对特定操作应用不同的日志记录设置,我们可以将日志记录行为作为特定操作的属性。
[LoggingBehavior(LogBeforeCall = false)]
public int MySecondOperation(int myArg)
{
return 10;
}
要编写错误、警告和信息日志的日志打印,我们可以使用 LoggingContext
类。
[LoggingBehavior]
public class MyService : IMyService
{
public void MyThirdOperation(int i)
{
LoggingContext.Current.LogInformation("In MyThirdOperation");
MyClass c = new MyClass();
c.MyMethod();
}
}
public class MyClass
{
public void MyMethod()
{
LoggingContext.Current.LogInformation("This is a log-print" +
" from a different method in a different class." +
" The operation's details that connected to this log are" +
" the details of the service's operation.");
}
}
在配置文件中使用日志记录行为
为了在配置文件中使用日志记录行为,我们可以添加一个类型为 LoggingBehaviorExtensionElement
的行为扩展。
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="loggingBehavior"
type="WcfLogPrints.LoggingBehaviorExtensionElement,
WcfLogPrints, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
...
</system.serviceModel>
并在服务的行为配置中使用它。
<system.serviceModel>
...
<services>
...
<service name="Example.Services.MySecondService"
behaviorConfiguration="myServiceBehavior">
<endpoint address="net.tcp://:8731/MySecondService"
binding="netTcpBinding"
contract="Example.Contracts.IMySecondService" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="myServiceBehavior">
<loggingBehavior logInformation="false" />
</behavior>
</serviceBehaviors>
</behaviors>
...
</system.serviceModel>
使用不同的日志记录策略
要使用与默认策略不同的日志记录策略,我们可以实现 ILoggingStrategy
。
public class FileLoggingStrategy : ILoggingStrategy
{
public bool Log(LoggingArgument arg)
{
if (arg == null)
{
return false;
}
try
{
string logFilePath = FilePath ?? "C:\\Example.txt";
using (FileStream fs = File.Open
(logFilePath, FileMode.Append, FileAccess.Write))
{
using (TextWriter tw = new StreamWriter(fs))
{
tw.Write(arg.ToString());
}
}
}
catch
{
return false;
}
return true;
}
public string FilePath { get; set; }
}
并在 LoggingBehaviorAttribute
属性上设置 LoggingStrategyType
属性。
[LoggingBehavior(LoggingStrategyType = typeof(FileLoggingStrategy))]
public class MyThirdService : IMyThirdService
{
public int MyOperation(int myArg)
{
return 5;
}
}
或者,在配置元素中设置 loggingStrategyType
属性。<behavior name="myServiceBehavior">
<loggingBehavior logInformation="false"
loggingStrategyType="Example.Services.FileLoggingStrategy, Example.Services" />
</behavior>