https://mp.weixin.qq.com/s/4Se1FEMqZFiwtjzWjo_Dhw

01

解决的问题

为了降低日志事件发布者和订阅者之间的耦合度,日志事件的内容负载很多情况下都会采用匿名类型对象来表示,所以对于《.NET Core的诊断日志[2]:各种诊断日志体验[下篇]》和《上篇]提供的实例中,我们只有采用dynamic关键字将负载对象转换成动态类型后才能提取出所需的成员。

由于匿名类型并非公共类型,所以上述这种方式仅限于发布程序和订阅程序都在同一程序集中才使用,但是在绝大部分情况下这个条件是不满足的。在不能使用动态类型提取数据成员的情况下,我们不得不采用反射或者表达式树的方式来解决这个问题,虽然可行但会变得很繁琐。

强类型的事件订阅以一种很“优雅”的方式解决了这个问题。简单来说,所谓的强类型的日志事件订阅就是将日志订阅处理逻辑定义在某个类型对应的方法中,后者可以按照日志内容负载对象的成员结构来定义对应的参数。实现强类型的日志事件订阅需要实现两个绑定,即日志事件与方法之间的绑定,以及负载的数据成员与订阅方法参数之间的绑定。

02

DiagnosticNameAttribute 

参数绑定利用负载成员的属性名与参数名之间的映射来实现,所以订阅方法只需要根据负载对象的属性成员来决定对应的参数的类型和名称。至于日志事件与方法之间的映射则可以利用如下这个DiagnosticNameAttribute特性来实现,我们只需要在订阅方法上标注这个方法并指定映射的日志事件的名称即可。

public class DiagnosticNameAttribute : Attribute
{
    public string Name { get; }
    public DiagnosticNameAttribute(string name);    
}

03

SubscribeWithAdapter

强类型的诊断日志事件的订阅对象可以通过DiagnosticListener的如下这个几个扩展方法来完成。顾名思义,这些SubscribeWithAdapter方法重载在指定对象和标准订阅对象之间作了一个适配,它将前者转换成一个IObserver<KeyValuePair<string, object>>对象。

public static class DiagnosticListenerExtensions
{
    public static IDisposable SubscribeWithAdapter(this DiagnosticListener diagnostic, object target);
    public static IDisposable SubscribeWithAdapter(this DiagnosticListener diagnostic, object target, Func<stringbool> isEnabled);
    public static IDisposable SubscribeWithAdapter(this DiagnosticListener diagnostic, object target, Func<stringobjectobjectbool> isEnabled);
}

04

实例演示

我们现在将上面演示的实例改造成强类型日志事件订阅的方式。我们首先定义如下这个DiagnosticCollector来作为日志事件订阅类型,可以看出这仅仅是一个没有实现任何接口或者继承任何基类的普通POCO类型。我们定义了OnReceiveRequest和OnSendReply两个日志事件方法,应用在它们上面的DiagnosticNameAttribute特性设置了对应的事件名称。为了自动获取日志内容负载,我们根据负载对象的数据结构为这两个方法定义了参数。

public sealed class DiagnosticCollector
{
    [DiagnosticName("ReceiveRequest")]
    public void OnReceiveRequest(HttpRequestMessage request, long timestamp
    
=> Console.WriteLine(
    $"Receive request. Url: {request.RequestUri}; Timstamp:{timestamp}");

    [DiagnosticName("SendReply")]
    public void OnSendReply(HttpResponseMessage response, TimeSpan elaped
    
=> Console.WriteLine(
    $"Send reply. Status code: {response.StatusCode}; Elaped: {elaped}");
}

接下来我们只需改变之前的日志事件订阅方式就可以了。如下面的代码片段所示,我们在根据名称找到作为订阅目标的DiagnosticListener对象之后,直接创建上面这个DiagnosticCollector对象并将其作为参数调用其扩展方法SubscribeWithAdapter进行注册即可。

public class Program
{
    public static void Main()
    
{
        DiagnosticListener.AllListeners.Subscribe(listener =>
        {
            if (listener.Name == "Web")
            {
                listener.SubscribeWithAdapter(new DiagnosticCollector());
            }
        });
        var source = new DiagnosticListener("Web");
        var stopwatch = Stopwatch.StartNew();
        if (source.IsEnabled("ReceiveRequest"))
        {
            var request = new HttpRequestMessage(HttpMethod.Get, "https://www.artech.com");
            source.Write("ReceiveRequest"new { Request = request, Timestamp = Stopwatch.GetTimestamp() });
        }
        Task.Delay(100).Wait();
        if (source.IsEnabled("SendReply"))
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK);
            source.Write("SendReply"new { Response = response, Elaped = stopwatch.Elapsed});
        }
    }
}

改动后的程序运行之后,同样会在控制台上输出如下图所示的结果。 

.NET Core的诊断日志[11]:针对DiagnosticSource的诊断日志[中篇]_java