在.NET中使用DiagnosticSource的方法

前言

DiagnosticSource是一个非常有意思的且非常有用的API,对于这些API它们允许不同的库发送命名事件,并且它们也允许应用程序订阅这些事件并处理它们,它使我们的消费者可以在运行时动态发现数据源并且订阅与其相关的数据源。

DiagnosticSource在AspNetCore、EntityFrameworkCore、HttpClient、SqlClient中被使用,在我们实际的开发过程中他使我们能够进行拦截请求与响应的http请求、数据库查询、对HttpContext、DbConnection、DbCommand、HttpRequestMessageand等对象的访问,甚至说在需要的时候我们可以进行修改这些对象来处理我们的业务。

下面我们将通过如下的简单示例来了解它.

DiagnosticSource和EventSource区别

DiagnosticSource和EventSource在架构设计上很相似,他们的主要区别是EventSource它记录的数据是可序列化的数据,会被进程外消费,所以要求记录的对象必须是可以被序列化的。而DiagnosticSource被设计为在进程内处理数据,所以我们通过它拿到的数据信息会比较丰富一些,它支持非序列化的对象,比如HttpContext、HttpResponseMessage等。另外如果想在EventSource中获取DiagnosticSource中的事件数据,可以通过DiagnosticSourceEventSource这个对象来进行数据桥接。

需求来了

为了更好的理解DiagnosticSource的工作方式,如下这个示例将拦截数据库请求,假设我们有一个简单的控制台应用程序,它向数据库发出请求并将结果输出到控制台。

class Program
{
  public const string ConnectionString =
    @"Server=localhost;Database=master;Trusted_Connection=True;";
  static async Task Main(string[] args)
  {
    var result = await Get();
    Console.WriteLine(result);

  }
  public static async Task<int> Get() {
    using (var connection=new SqlConnection(ConnectionString))
    {
      return await connection.QuerySingleAsync<int>("SELECT 42;");
    }
  }
}

我们再来思考一下,假设来了一个需求:我们需要获取到所有数据库查询的执行时间,或者说我们要进行获取执行的一些sql语句或者数据进行存储作为记录我们该如何处理?
好了下面我们将尝试使用DiagnosticSource来实现该需求。

使用System.Diagnostics.DiagnosticSource

来吧,我们先来创建一个类作为该事件的处理程序或者说作为该事件的消费者。

public sealed class ExampleDiagnosticObserver
{}

下面我们将处理该事件,我们需要将这个类进行实例化,并且将它注册到静态对象中的观察器中DiagnosticListener.AllListeners,代码如下所示:

static async Task Main(string[] args)
{
  var observer = new ExampleDiagnosticObserver();
  IDisposable subscription = DiagnosticListener.AllListeners.Subscribe(observer);
  var result = await Get();
  Console.WriteLine(result);
}

下面我们再来修改我们的ExampleDiagnosticObserver类,其实如上代码片段中编译器已经提醒我们要实现接口IObserver<diagnosticsListener>,下面我们实现它

public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener>
{
  public void OnCompleted()
  {
  }

  public void OnError(Exception error)
  {
  }

  public void OnNext(DiagnosticListener value)
  {
    Console.WriteLine(value.Name);
  }
}

接下来我们运行该程序,结果将在控制台进行打印如下所示:

SqlClientDiagnosticListener
SqlClientDiagnosticListener
42

看如上结果,这意味着在我们当前这个应用程序中的某个地方注册了两个类型为DiagnosticListener的对象,名字为SqlClientDiagnosticListener。

对于应用程序中创建的每个实例diagnosticsListener,在第一次使用时将调用IObserver<DiagnosticListener>.OnNext方法一次,现在我们只是将实例的名称输出到了控制台中,但实际情况中我们想一下,我们应该对这个实例名称做什么?对,没错,我们要对这些实例名称做检查,那么我们如果要对这个实例中某些事件,我们只需要使用subscribe方法去订阅它。

下面我们来实现IObserver<DiagnosticListener>:

public class ExampleDiagnosticObserver1 : IObserver<DiagnosticListener>,
     IObserver<KeyValuePair<string, object>>
{
  private readonly List<IDisposable> _subscriptions = new List<IDisposable>();

  public void OnCompleted()
  {
  }

  public void OnError(Exception error)
  {
  }

  public void OnNext(KeyValuePair<string, object> value)
  {
    Write(value.Key, value.Value);
  }

  public void OnNext(DiagnosticListener value)
  {
    if (value.Name == "SqlClientDiagnosticListener")
    {
      var subscription = value.Subscribe(this);
      _subscriptions.Add(subscription);
    }
  }

  private void Write(string name, object value)
  {
    Console.WriteLine(name);
    Console.WriteLine(value);
    Console.WriteLine();
  }
}

在如上代码片段中我们实现了接口IObserver<KeyValuePair<string, object>>的IObserver<KeyValuePair<string,object>>.OnNext的方法,参数为KeyValuePair<string,object>,其中Key是事件的名称,而Value是一个匿名对象.

运行程序输出结果如下所示:

System.Data.SqlClient.WriteConnectionOpenBefore
{ OperationId = f5f4d4f0-7aa1-46e6-bd48-78acca3dac0a, Operation = OpenAsync, Connection = System.Data.SqlClient.SqlConnection, Timestamp = 1755845041766 }

System.Data.SqlClient.WriteCommandBefore
{ OperationId = 3d8617d1-0317-4f75-bffd-5b0fddf5cc12, Operation = ExecuteReaderAsync, ConnectionId = 554f4ee4-47c3-44ff-a967-cc343d1d5019, Command = System.Data.SqlClient.SqlCommand }

System.Data.SqlClient.WriteConnectionOpenAfter
{ OperationId = f5f4d4f0-7aa1-46e6-bd48-78acca3dac0a, Operation = OpenAsync, ConnectionId = 554f4ee4-47c3-44ff-a967-cc343d1d5019, Connection = System.Data.SqlClient.SqlConnection, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 1755851869508 }

System.Data.SqlClient.WriteCommandAfter
{ OperationId = 3d8617d1-0317-4f75-bffd-5b0fddf5cc12, Operation = ExecuteReaderAsync, ConnectionId = 554f4ee4-47c3-44ff-a967-cc343d1d5019, Command = System.Data.SqlClient.SqlCommand, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 1755853467664 }

System.Data.SqlClient.WriteConnectionCloseBefore
{ OperationId = ed240163-c43a-4394-aa2d-3fede4b27488, Operation = Close, ConnectionId = 554f4ee4-47c3-44ff-a967-cc343d1d5019, Connection = System.Data.SqlClient.SqlConnection, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 1755854169373 }

System.Data.SqlClient.WriteConnectionCloseAfter
{ OperationId = ed240163-c43a-4394-aa2d-3fede4b27488, Operation = Close, ConnectionId = 554f4ee4-47c3-44ff-a967-cc343d1d5019, Connection = System.Data.SqlClient.SqlConnection, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 1755854291040 }

42

如上结果可以清楚的看到里面存在6个事件,我们可以看到两个是在打开数据库之前和之后执行的,两个是在执行命令之前和之后执行的,还有两个是在关闭数据库连接之前和之后执行的。

另外可以看到每个事件中都包含一组参数,如OperationId、Operation、ConnectionId等,这些参数通常作为匿名对象属性传输,我们可以通过反射来获取这些属性的类型化的值。

现在我们解决了我们最初的需求,获取数据库中所有查询的执行时间,并将其输出到控制台中,我们需要进行修改,代码如下所示:

private readonly AsyncLocal<Stopwatch> _stopwatch = new AsyncLocal<Stopwatch>();

private void Write(string name, object value)
{
  switch (name)
  {
    case "System.Data.SqlClient.WriteCommandBefore":
    {
      _stopwatch.Value = Stopwatch.StartNew();
      break;
    }
    case "System.Data.SqlClient.WriteCommandAfter":
    {
    var stopwatch = _stopwatch.Value;
    stopwatch.Stop();
    var command = GetProperty<SqlCommand>(value, "Command");
    Console.WriteLine($"CommandText: {command.CommandText}");
    Console.WriteLine($"Elapsed: {stopwatch.Elapsed}");
    Console.WriteLine();
    break;
    }
  }
}

private static T GetProperty<T>(object value, string name)
{
  return (T)value.GetType()
        .GetProperty(name)
        .GetValue(value);
}

在这我们将拦截数据库中查询的开始和结束事件,在执行之前我们创建并且启动stopwatch,将其存储在AsyncLocal<stopwatch>中,以后面将其返回,在执行完成后,我们获取之前启动的stopwatch,停止它,通过反射从参数值中获取执行命令,并将结果输出到控制台。

执行结果如下所示:

CommandText: SELECT 42;
Elapsed: 00:00:00.1509086

42

现在我们已经解决了我们的需求,但是目前还存在一个小的问题,当我们订阅事件diagnosticListener时,我们从它里面将接收到所有的事件,包括我们不需要的事件,但是呢发送的每个事件都会创建一个带有参数的匿名对象,这会在GC上造成额外的压力。

我们需要解决如上的问题,避免我们去处理所有的事件,我们需要指定Predicate<string>这个特殊的委托类型,我们声明IsEnabled方法,在此筛选对应名称的消费者。

下面我们修改一下方法IObserver<DiagnosticListener>.OnNext

public void OnNext(DiagnosticListener value)
{
  if (value.Name == "SqlClientDiagnosticListener")
  {
    var subscription = value.Subscribe(this, IsEnabled);
    _subscriptions.Add(subscription);
  }
}

private bool IsEnabled(string name)
{
  return name == "System.Data.SqlClient.WriteCommandBefore"
    || name == "System.Data.SqlClient.WriteCommandAfter";
}

现在我们只会对事件System.Data.SqlClient.WriteCommandBefore和System.Data.SqlClient.WriteCommandAfter调用Write方法。

使用Microsoft.Extensions.DiagnosticAdapter

上面虽然我们实现了需求,但是我们也可以发现我们从DiagnosticListener接收到的事件参数通常作为匿名对象传递,因此通过反射去处理这些参数这样给我们造成了比较昂贵的消耗,不过开发团队也考虑到了该问题向我们提供了Microsoft.Extensions.DiagnosticAdapter来完成我们的操作。

下面我们需要将Subscribe改为SubscribeWithAdapter,另外在这种情况下我们不需要实现IObserver<KeyValuePair<string, object>>接口,相反的是我们需要为每个事件声明一个单独的方法,并且使用[DiagnosticNameAttribute]特性去标注

如下所示:

public class ExampleDiagnosticObserver4 : IObserver<DiagnosticListener>
{
  private readonly List<IDisposable> _subscriptions = new List<IDisposable>();
  private readonly AsyncLocal<Stopwatch> _stopwatch = new AsyncLocal<Stopwatch>();

  public void OnCompleted()
  {
  }

  public void OnError(Exception error)
  {
  }

  public void OnNext(DiagnosticListener value)
  {
    if (value.Name == "SqlClientDiagnosticListener")
    {
      var subscription = value.SubscribeWithAdapter(this);
      _subscriptions.Add(subscription);
    }
  }

  [DiagnosticName("System.Data.SqlClient.WriteCommandBefore")]
  public void OnCommandBefore()
  {
    _stopwatch.Value = Stopwatch.StartNew();
  }

  [DiagnosticName("System.Data.SqlClient.WriteCommandAfter")]
  public void OnCommandAfter(DbCommand command)
  {
    var stopwatch = _stopwatch.Value;
    stopwatch.Stop();
    Console.WriteLine($"CommandText: {command.CommandText}");
    Console.WriteLine($"Elapsed: {stopwatch.Elapsed}");
    Console.WriteLine();
  }
}

现在我们实现了对数据执行的监控或者说拦截功能,同时也能为我们的数据库执行时间做记录,并且特别注意的是我们并没有对应用程序本身做修改,这样也减轻了很多的冗余,同时节省了大量的编码时间。这是一个很不错的编程体验。

创建DiagnosticListener实例

在大多数情况下,我们对DiagnosticSource都会去订阅已经存在的事件,基本我们都不需要去创建自己的DiagnosticListener去发送事件,当然去了解一下这一特性也是比较好的,请继续往下看

创建自己的实例

private static readonly DiagnosticSource diagnosticSource =
  new DiagnosticListener("MyLibraty");

发送事件,我们将调用Write进行写入事件

if (diagnosticSource.IsEnabled("MyEvent"))
  diagnosticSource.Write("MyEvent", new { /* parameters */ });

参考

https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/DiagnosticSourceUsersGuide.md

https://sudonull.com/post/3671-Using-the-DiagnosticSource-in-NET-Core-Theory

https://github.com/dotnet/runtime/issues/20992

https://github.com/hueifeng/BlogSample/tree/master/src/DiagnosticDemo

到此这篇关于在.NET中使用DiagnosticSource的方法的文章就介绍到这了,更多相关.NET使用DiagnosticSource内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • ASP.NET2.0数据库入门之SqlDataSource

    当使用SqlDataSource控件选择数据时,可以从两个属性:ConnectionString和SelectCommand开始,如下所示: <asp:SqlDataSource ID="MySourceControlName" Runat="server" ConnectionString="Server=MyServer ; Database=Northwind" SelectCommand=" SELECT Fieldl,

  • ASP.NET中 ObjectDataSource控件的DataObjectTypeName属性

    一.ObjectDataSource 控件说明 获取或设置某个类的名称,ObjectDataSource 控件将该类用于更新.插入或删除数据操作中的参数,而不是从数据绑定控件传递个别的值. 您不用指定传递给 Update.Insert 和 Delete 方法的多个参数,而是可以创建一个累计多个数据字段值的对象.仅给方法传递这一个对象,而不是多个参数. 绑定到数据绑定控件的 ObjectDataSource 控件的默认行为是,数据绑定控件为数据源中的每个参数创建一个 Parameter 对象.如果

  • 在ASP.NET 2.0中操作数据之四:使用ObjectDataSource展现数据

    在完成了应用程序架构和页面的设计后,我们可以开始探讨如何实现多种多样的普通数据到报表展现的任务.前面我们已经了解到如何在从数据访问层和业务逻辑层绑定数据到ASP.NET页面上的数据控件.在ASP.NET 1.x应用程序中,通过对数据控件的DataSource属性赋值然后调用控件的DataBind()方法从而实现数据展现的方式在ASP.NET 2.0应用程序中可以继续使用.然而,ASP.NET 2.0的数据源控件提供了一种新的方式,使用这些控件可以让你轻松从上文中创建的业务逻辑层中进行数据绑定,甚

  • 在ASP.NET 2.0中操作数据之五十六:使用ObjectDataSource缓存数据

    导言 就计算机科学而言,caching就是将所需要的数据或信息的备份放在某个地方,便于快速访问的这样一个过程.以数据处理(data-driven)程序为例,程序的大部分时间浪费在数据查询上.要提升这种程序的性能,通常的做法是将查询结果存放在程序的存储器里. ASP.NET 2.0提供了各种各样的缓存方式.对web页面和用户控件可以通过output caching进行缓存:同样我们可以通过ObjectDataSource 和SqlDataSource控件,在控件级(control level)对数

  • 在ASP.NET 2.0中操作数据之四十六:使用SqlDataSource控件检索数据

    导言 到目前为止,我们探讨的教程是由表现层,业务逻辑层和数据访问层构成的层次体系结构.数据访问层和业务逻辑层分别在教程第一和第二章提到.在Displaying Data With the ObjectDataSource 这篇教程里,我们探讨了怎样用ASP.NET 2.0的新控件--ObjectDataSource控件在表现层展示数据. 本教程到目前为止用这种层次结构来处理数据.然而绕过这种体系结构,通过直接把数据查询和业务逻辑放在Web页面上,也可以达到直接在ASP.NET页面上访问,插入,更

  • Asp.net中使用PageDataSource分页实现代码

    复制代码 代码如下: 注:封装数据绑定控件(如 System.Web.UI.WebControls.DataGrid.System.Web.UI.WebControls.GridView.System.Web.UI.WebControls.DetailsView    //     和 System.Web.UI.WebControls.FormView)的与分页相关的属性,以允许该控件执行分页操作.无法继承此类.DataListpublic DataTable GetDataSet(strin

  • 在ASP.NET 2.0中操作数据之六:编程设置ObjectDataSource的参数值

    导言 正如我们在上一节的教程中所看到的,有很多可供选择的方式把参数的值传递到OjbectDataSource的方法里.假如参数值是采用硬编码方式,来源于页面上的一个Web控件,又或者其他可被数据源Parameter对象读取的源,那么这个值可以绑定到输入参数而不需要写一行的代码. 然而有些时候,参数值来自某些在数据源的内置Parameter对象里还没有计算出来的源.假如我们的站点支持我们的考虑那么我们也许希望参数基于当前登录用户.又或者我们在参数传送到ObjectDataSource的隐含对象的方

  • 在ASP.NET 2.0中操作数据之四十七:用SqlDataSource控件插入、更新、删除数据

    导言: 正如在教程概述插入.更新和删除数据里讨论的那样,GridView控件内置更新和删除功能,而DetailsView和FormView控件不仅具有编辑和删除功能,还有插入功能.我们不要写一行代码就可一将这些功能直接应用于一个数据源控件.在这篇教程里,我们指出ObjectDataSource控件最好与GridView, DetailsView和FormView控件一起使用,才更好的实现插入.更新和删除功能.对SqlDataSource控件来说,同样如此! 对ObjectDataSource控件

  • .NET 资源文件resx、Resources详细说明

    资源文件简介 (1)resx文件: 基于文本的格式是特定于.NET 框架的 XML 格式,称为 ResX(.resx 文件).不考虑其 XML 基础,该格式不是专门为人工阅读而设计的(XML 格式很少是这样的).但是,Visual Studio .NET 仍然为 .resx 文件提供了一个基本编辑器. (2)Resources文件: .resources 扩展名来自于在将 .resx 文件作为资源嵌入之前 Visual Studio .NET 处理该文件时所使用的工具.工具名称是 resgen.

  • 在.NET中使用DiagnosticSource的方法

    前言 DiagnosticSource是一个非常有意思的且非常有用的API,对于这些API它们允许不同的库发送命名事件,并且它们也允许应用程序订阅这些事件并处理它们,它使我们的消费者可以在运行时动态发现数据源并且订阅与其相关的数据源. DiagnosticSource在AspNetCore.EntityFrameworkCore.HttpClient.SqlClient中被使用,在我们实际的开发过程中他使我们能够进行拦截请求与响应的http请求.数据库查询.对HttpContext.DbConn

  • AngularJs中route的使用方法和配置

    angular是Google开发的一个单页面应用框架,是现在比较主流的单页面应用框架之一.该强大的地方有很多,比如双向数据绑定,应用了后端的MVC模式到前端,自定义指令等. 既然是单页面应用,肯定离不开页面的切换.我们首先来说一下angular的路由. angular实现页面切换时用了route. <script src="js/plugins/angular/angular.min.js"></script> <script src="js/p

  • 深入学习JavaScript的AngularJS框架中指令的使用方法

    指令(Directives)是所有AngularJS应用最重要的部分.尽管AngularJS已经提供了非常丰富的指令,但还是经常需要创建应用特定的指令.这篇教程会为你讲述如何自定义指令,以及介绍如何在实际项目中使用.在这篇文章的最后(第二部分),我会指导你如何使用Angular指令来创建一个简单的记事本应用. 概述 一个指令用来引入新的HTML语法.指令是DOM元素上的标记,使元素拥有特定的行为.举例来说,静态的HTML不知道如何来创建和展现一个日期选择器控件.让HTML能识别这个语法,我们需要

  • AngularJS入门教程二:在路由中传递参数的方法分析

    本文实例讲述了AngularJS在路由中传递参数的方法.分享给大家供大家参考,具体如下: 我们不仅可以在控制器中直接定义属性的值,比如: app.controller('listController',function($scope){ $scope.name="ROSE"; }); AngularJS还提供了传递参数的功能,目前我接触到的一种方式是从视图中传参: <!--首页html--> <li><a href="#/user/18"

  • Java中synchronized关键字修饰方法同步的用法详解

    Java的最基本的同步方式,即使用synchronized关键字来控制一个方法的并发访问. 每一个用synchronized关键字声明的方法都是临界区.在Java中,同一个对象的临界区,在同一时间只有一个允许被访问. 静态方法则有不同的行为.用synchronized关键字声明的静态方法,同时只能够被一个执行线程访问,但是其他线程可以访问这个对象的非静态的synchronized方法.必须非常谨慎这一点,因为两个线程可以同时访问一个对象的两个不同的synchronized方法,即其中一个是静态s

  • python编程之requests在网络请求中添加cookies参数方法详解

    哎,好久没有学习爬虫了,现在想要重新拾起来.发现之前学习爬虫有些粗糙,竟然连requests中添加cookies都没有掌握,惭愧.废话不宜多,直接上内容. 我们平时使用requests获取网络内容很简单,几行代码搞定了,例如: import requests res=requests.get("https://cloud.flyme.cn/browser/index.jsp") print res.content 你没有看错,真的只有三行代码.但是简单归简单,问题还是不少的. 首先,这

  • JS使用正则表达式除去字符串中重复字符的方法

    本文实例讲述了JS使用正则表达式除去字符串中重复字符的方法.分享给大家供大家参考,具体如下: 这里演示一个简单的JavaScript正则表达式实例,将一串含有重复字符串中的多余字符滤除掉,请运行查看效果. 具体代码如下: <html> <head> <title>利用正则表达法除去字符串中的重复字符</title> </head> <body> <script language="javascript">

  • js正则表达式中test,exec,match方法的区别说明

    js正则表达式中test,exec,match方法的区别说明 test test 返回 Boolean,查找对应的字符串中是否存在模式.var str = "1a1b1c";var reg = new RegExp("1.", "");alert(reg.test(str)); // true exec exec 查找并返回当前的匹配结果,并以数组的形式返回.var str = "1a1b1c";var reg = new R

  • Python读取mp3中ID3信息的方法

    本文实例讲述了Python读取mp3中ID3信息的方法.分享给大家供大家参考.具体分析如下: pyid3不好用,常常有不认识的. mutagen不错,不过默认带的easyid3不会读取注释,需要手工hack一下 Python代码如下: from mutagen.mp3 import MP3 import mutagen.id3 from mutagen.easyid3 import EasyID3 EasyID3.valid_keys["comment"]="COMM::'X

  • Python实现比较两个文件夹中代码变化的方法

    本文实例讲述了Python实现比较两个文件夹中代码变化的方法.分享给大家供大家参考.具体如下: 这里将修改代码后的目录与原始目录做对比,罗列出新增的代码文件,以及修改过的代码文件 # -*- coding: utf-8 -*- import os; folderA = "F:\\Projects\\FreeImageV3_14_1\\".lower(); folderB = u"E:\\Software\\图像解码库\\FreeImage3141\\FreeImage\\&q

随机推荐