65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2011年8月20日

CPOL

6分钟阅读

viewsIcon

54763

downloadIcon

1566

在本文中,我将逐步解释如何使用 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 行为(实现了:IServiceBehaviorIEndpointBehaviorIContractBehaviorIOperationBehavior 的类)。在此行为中,我使用参数检查器(实现了 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);
}

日志记录上下文

为了将操作的详细信息与附加的日志打印(在操作范围内发生的错误、警告和信息日志)连接起来,我们需要在每个想要写入日志打印的地方知道操作的详细信息。为了实现这一点,我们可以创建一个类来保存每个操作所需的详细信息。

首先,我们需要一个当前会话的标识符。我们可以为此目的使用当前 OperationContextSessionId,如下所示:

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;
}

为了设置每个操作的操作详细信息,我们可以在 LoggingParameterInspectorBeforeCall 方法中调用 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);
}

并在 LoggingParameterInspectorAfterCall 方法中调用 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 时,服务控制台上的日志打印可以显示如下:

Example.JPG

为了对特定操作应用不同的日志记录设置,我们可以将日志记录行为作为特定操作的属性。

[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>
© . All rights reserved.