在.NET中使用DiagnosticSource

前言

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

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

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

DiagnosticSource和EventSource区别

DiagnosticSourceEventSource在架构设计上很相似,他们的主要区别是EventSource它记录的数据是可序列化的数据,会被进程外消费,所以要求记录的对象必须是可以被序列化的。而DiagnosticSource被设计为在进程内处理数据,所以我们通过它拿到的数据信息会比较丰富一些,它支持非序列化的对象,比如HttpContextHttpResponseMessage等。另外如果想在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.150908642

现在我们已经解决了我们的需求,但是目前还存在一个小的问题,当我们订阅事件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.WriteCommandBeforeSystem.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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307450.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode142. 环形链表 II(详解)

一:题目 二:思路分析 三:上码 class Solution { public:ListNode *detectCycle(ListNode *head) {ListNode* slow head;ListNode* fast head;while (fast ! NULL && fast->next ! NULL && fast->next->next ! NULL) {//这里选用快指针fast fast-&g…

国产化之路-统信UOS /Nginx /Asp.Net Core+ EF Core 3.1/达梦DM8实现简单增删改查操作

引言经过前期的准备工作&#xff0c;.net core 3.1的运行环境和WEB服务器已经搭建完毕&#xff0c;这里需要注意一下&#xff0c;达梦DM8数据库对于Entity Framework Core 3.1 的驱动在NuGet官方源上并没有正式发布&#xff0c;需要从Win64安装版本中的drivers/dotNet提取&…

网络知识 | 《图解TCP/IP》读书笔记(下)

【网络知识】| 作者 / Edison Zhou这是EdisonTalk的第291篇原创内容作为一个专业的IT技术人&#xff0c;一个Web应用开发者&#xff0c;不了解网络基础和协议&#xff0c;怎么能行&#xff1f;本文是我2016年阅读《图解TCP/IP》一书的读书笔记下半部分。上半部分&#xff1a;点…

leetcode202. 快乐数(详解)

一:题目 二:上码 class Solution { public:/**思路&#xff1a;1.破解这道题的关键是 我们得破解这个无限循环2.根据这个题目给出的定义2 我们可以知道 无限循环的条件是我们 在计算的过程中出现了自己的数*/bool isHappy(int n) {unordered_set<int>s;s.insert(n);…

自由软件基金会庆祝成立35周年

美国东部时间 10 月 4 日&#xff0c;自由软件基金会&#xff08;Free Software Foundation&#xff0c;FSF&#xff09;在官网发文庆祝”其为争取软件自由而奋斗的第 35 年“。1985 年 10 月&#xff0c;自由软件运动的主要发起人 RMS&#xff08;Richard Matthew Stallman&am…

使用 C# sdk 连接 高可用的 rabbitmq 镜像集群

我们知道rabbitmq是一个专业的MQ产品&#xff0c;而且它也是一个严格遵守AMQP协议的玩意&#xff0c;但是要想高端大气上档次&#xff0c;一定需要拿出高可用的东西出来&#xff0c;这不本篇就跟大家说一下cluster的概念&#xff0c;rabbitmq是erlang写的一个成品&#xff0c;所…

java并发之CyclicBarrier(通俗易懂)

文章目录一:简介(1):官方解释(2):通俗解释a:CountDownLatch()b:CyclicBarrier()二:小demo一:简介 (1):官方解释 说到简介那就非得拿他和countdownlatch()进行比较讨论了CountDownLatch是一个同步的辅助类&#xff0c;允许一个或多个线程&#xff0c;等待其他一组线程完成操作…

BeetleX之简单HTTP/HTTPS实现

在通讯应用中很多时候需要和已有标准的应用协议进行通讯&#xff0c;针对这情况就要针对相应协议的实现&#xff1b;标准协议上考虑的情况比较多&#xff0c;所以协议的复杂度也相对高些&#xff0c;对比之前的Protobuf通讯的简单协议来说则会复杂。接下来用组件去实现一个简单…

体验.NET5 RC1极致性能,你也要“卧槽”!

“ 9月14日&#xff0c;.NET5发布了(Release Candidate)RC1版本&#xff0c;是11月正式版本之前两个RC版本中第一个&#xff0c;包含语言新版本C#9和F#5&#xff0c;需要用Visual Studio 2019 (v16.8, Preview 3)才能使用&#xff0c;注意不是Visual Studio 2019&#xff0c;二…

leetcod383. 赎金信

一:题目 二:上码 class Solution { public:bool canConstruct(string ransomNote, string magazine) {unordered_map<char,int>m;for (auto ch:ransomNote) {m[ch];}for (auto ch: magazine) {if(m.find(ch) ! m.end() && m[ch] > 0) m[ch]--;//当magazine中的…

马斯克推崇的第一性原理,究竟有多重要?

职场&认知洞察 丨 作者 / findyi这是findyi公众号的第79篇原创文章最近&#xff0c;第一性原理这个原本离大众很远的物理概念&#xff0c;被媒体炒的火热。第一性原理最开始是由亚里士多德提出来的。他认为任何一个系统都有一个「第一性原理」。他说&#xff1a;“在每个系…

Gartner 组织世界BI大会,PowerBI 与 Tableau 同台竞技

日前&#xff0c;Gartner 组织了全球顶级 BI 产品大会&#xff0c;由于疫情影响&#xff0c;只能远程举办。官方网址如下&#xff1a;https://blogs.gartner.com/rita-sallam/2020/09/11/bi-bake-off-goes-virtual/该会议的原文为&#xff1a;BI Bake-Off&#xff0c;含义为&am…

leetcode15. 三数之和(详解)

一:题目 二:上码 class Solution { public:/**思路:1.这个题出其不意之处在于,其先对这个序列进行了排序排序的好处是1>:如果首元素是大于0的,那么如果无论无何也凑不出02>:同时我们也把相同的元素凑成一块了 可以方便去重了2.接下来,我们就是利用双指针 来处理 数据是否…

BeetleX之TCP消息通讯Protobuf/TLS

在网络通讯应用中直接操作数据流是比较繁琐的事情&#xff0c;毕竟在业务层面处理的都是对象化消息&#xff1b;为了让网络数据操作变得更友好直观&#xff0c;一般都会引用序列化组件来处理网络流和对象之前的转换工作&#xff1b;在这里介绍组件如何使能Protobuf进行数据交互…

leetcode18. 四数之和

一:题目 二:上码 class Solution { public:/**思路:1.这个跟三数之和差别在于本题加了一层循环,就是我们将第二层循环加在了我们遍历的第一个元素的后面2.其他跟三数之和解题思路一样*/vector<vector<int>> fourSum(vector<int>& nums, int target) {vec…

面试 .NET 开发​,为什么也要考算法?​

最近有不少人面试&#xff0c;我发现很多候选人&#xff0c;聊起架构、框架侃侃而谈&#xff0c;但一写代码&#xff0c;就暴露真实水平。说白了&#xff0c;还是基本功不够扎实。在我看来&#xff0c;所有基本功中&#xff0c;最核心的一定是数据结构与算法。也因此&#xff0…

leetcode344. 反转字符串

一:题目 二:上码 class Solution { public:/**思路:利用双指针来进行处理*/void reverseString(vector<char>& s) {for (int i 0,j s.size()-1; i < j; i,j--) {swap(s[i],s[j]);}} };

跟我一起学.NetCore之WebApi接口裸奔有风险(Jwt)

前言撸码需谨慎&#xff0c;裸奔有风险。经常在一些技术交流群中了解到&#xff0c;还有很多小伙伴的项目中Api接口没有做任何安全机制验证&#xff0c;直接就裸奔了&#xff0c;对于一些临时项目或是个人小项目还好&#xff0c;其余的话&#xff0c;建议小伙伴们酌情考虑都加上…

leetcode541. 反转字符串 II

一:题目 二:上码 class Solution { public:/**思路:1.这个我们需要利用到一个库函数 reverse(nums.begin(),nums.end()); // 将字符串反转那么将局部字符串的反转reverse(nums.begin(),nums.begin()2);//表示的是下标左闭右开的 [0,2)2.还有就是在处理每隔2*k这个问题上 我们选…

你是个失败者,有什么资格说话?

这是头哥侃码的第219篇原创这次十一长假&#xff0c;想必有不少朋友和我一样&#xff0c;跟家人聚在一起&#xff0c;吃个饭&#xff0c;喝点小酒&#xff0c;或者吃吃甜点&#xff0c;喝喝下午茶&#xff0c;“享受” 着在疫情严峻形式下难得的宁静时光。两天前&#xff0c;为…