在.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://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 Core 中使用 DiagnosticSource 记录跟踪信息
前言 最新一直在忙着项目上的事情,很久没有写博客了,在这里对关注我的粉丝们说声抱歉,后面我可能更多的分享我们在微服务落地的过程中的一些经验.那么今天给大家讲一下在 .NET Core 2 中引入的全新 ...
- 在 .NET Core 中使用 Diagnostics (Diagnostic Source) 记录跟踪信息
前言 最新一直在忙着项目上的事情,很久没有写博客了,在这里对关注我的粉丝们说声抱歉,后面我可能更多的分享我们在微服务落地的过程中的一些经验.那么今天给大家讲一下在 .NET Core 2 中引入的全新 ...
- Dotnet全平台下APM-Trace探索
背景 随着支撑的内部业务系统越来越多,向着服务化架构进化,在整个迭代过程中,会逐渐暴露出以下问题. 传统依赖于应用服务器日志等手段的排除故障原因的复杂度越来越高,传统的监控服务已经无法满足需求. 终端 ...
- `prometheus-net.DotNetRuntime` 获取 CLR 运行指标原理解析
prometheus-net.DotNetRuntime 介绍 Intro 前面集成 Prometheus 的文章中简单提到过,prometheus-net.DotNetRuntime 可以获取到一些 ...
- Python开源框架
info:更多Django信息url:https://www.oschina.net/p/djangodetail: Django 是 Python 编程语言驱动的一个开源模型-视图-控制器(MVC) ...
- .Net Core中的诊断日志DiagnosticSource讲解
前言 近期由于需要进行分布式链路跟踪系统的技术选型,所以一直在研究链路跟踪相关的框架.作为能在.Net Core中使用的APM,SkyWalking自然成为了首选.SkyAPM-dotnet是 ...
- Asp.net core中的依赖注入
使用服务 在Asp.net core的Controller中,可以通过如下两种方式获取系统注入的服务: 构造函数 可以直接在构造函数中传入所依赖的服务,这是非常常见的DI注入方式. public Va ...
- ASP.NET Core中的依赖注入【上】
此为系列文章,对MSDN ASP.NET Core 的官方文档进行系统学习与翻译.其中或许会添加本人对 ASP.NET Core 的浅显理解 ASP.NET Core支持DI软件设计模式,其是一种为了 ...
- 诊断日志知多少 | DiagnosticSource 在.NET上的应用
1. 引言 最近为了解决ABP集成CAP时无法通过拦截器启用工作单元的问题,从小伙伴那里学了一招.借助DiagnossticSource,可以最小改动完成需求.关于DiagnosticSource晓东 ...
随机推荐
- [源码解析] Flink的Slot究竟是什么?(2)
[源码解析] Flink 的slot究竟是什么?(2) 目录 [源码解析] Flink 的slot究竟是什么?(2) 0x00 摘要 0x01 前文回顾 0x02 注册/更新Slot 2.1 Task ...
- 答应我,用了这个jupyter插件,别再重复造轮子了
1 简介 在使用Python.R等完成日常任务的过程中,可能会经常书写同样或模式相近的同一段代码,譬如每次使用matplotlib绘制图像的时候可以在开头添加下面两行代码来解决中文乱码等显示问题: p ...
- Python学习—Anaconda详细 下载、安装与使用,以及如何创建虚拟环境,不仅仅只有安装步骤哦
上一期我们介绍了Python.Pycharm.Anaconda三者之间的关系以及使用,这一期主要详细介绍如何在Windows上下载和安装工具Anaconda,然后使用其自带的conda管理不同项目的虚 ...
- 实验 2:Mininet 实验——拓扑的命令脚本生成
实验 2:Mininet 实验--拓扑的命令脚本生成 一.实验目的 掌握 Mininet 的自定义拓扑生成方法:命令行创建.Python 脚本编写 二.实验任务 通过使用命令行创建.Python 脚本 ...
- 深入理解Java中的装箱与拆箱
一.Java数据类型 1.在说装箱与拆箱之前,先说一下Java的基本数据类型,Java从数据类型上可以划分为值类型与引用类型,值类型是四类八种,分别是: 整数型:byte̵,short̵,int̵,l ...
- HTML模仿实现京东登录页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Android 重构方案
前言 最近面试了很多候选人,发现很多同学在简历上都写得非常厉害,负责架构设计,项目重构之类的.但是问起来,很多人都说不出个所以然来.今天我们不谈架构设计,我们聊一下重构.我面试时候经常会问,你是怎么重 ...
- [LeetCode]671. 二叉树中第二小的节点(递归)
题目 给定一个非空特殊的二叉树,每个节点都是正数,并且每个节点的子节点数量只能为 2 或 0.如果一个节点有两个子节点的话,那么这个节点的值不大于它的子节点的值. 给出这样的一个二叉树,你需要输出所有 ...
- 如何创建本地git分支到远程
创建本地分支到远程: 1.$git init 之后创建的本地仓库默认master分支 如果现在就要$ git branch 查看当前分支,是不显示任何分支的,只有在add,commit文件之后才显示, ...
- 【系统之音】Android进程的创建及启动简述
Android系统中的进程(这里不包括init等底层的进程)都是通过Zygote fork而来的,那这些进程的启动流程都是怎样的呢? 这里将Android进程分为两个部分: (1)系统框架进程Syst ...