定制网站为什么贵,网站建设和网络优化,销售产品网站有哪些,wordpress 08影院模板1. 引言最近为了解决ABP集成CAP时无法通过拦截器启用工作单元的问题#xff0c;从小伙伴那里学了一招。借助DiagnossticSource#xff0c;可以最小改动完成需求。关于DiagnosticSource晓东大佬18年在文章 在 .NET Core 中使用 Diagnostics (Diagnostic Source) 记录跟踪信息就… 1. 引言最近为了解决ABP集成CAP时无法通过拦截器启用工作单元的问题从小伙伴那里学了一招。借助DiagnossticSource可以最小改动完成需求。关于DiagnosticSource晓东大佬18年在文章 在 .NET Core 中使用 Diagnostics (Diagnostic Source) 记录跟踪信息就有介绍文章开头就说明了Diagnostics 一直是一个被大多数开发者忽视的东西。是的我也忽略了这个好东西有必要学习一下下面就和大家简单聊一聊System.Diagnostics.DiagnosticSource在.NET上的应用。2. System.Diagnostics.DiagnosticSourceDiagnostics位于System命名空间下由此可见Diagnostics在.NET 运行时中的地位不可小觑。其中System.Diagnostics命名空间下又包含不同类库提供了允许与系统进程事件日志和性能计数器进行交互的类。如下图所示Diagnostic Namespace其中System.Diagnostics.DiagnosticSource模块它允许对代码进行检测以在生产时记录丰富的数据负载可以传递不可序列化的数据类型以便在进程内进行消耗。消费者可以在运行时动态发现数据源并订阅感兴趣的数据源。在展开之前有必要先梳理下涉及的以下核心概念IObservable可观测对象IObserver观察者DiagnosticSource 诊断来源DiagnosticListener诊断监听器Activity活动3. 观察者模式IObservable IObserver)IObservable 和IObserver位于System命名空间下是.NET中对观察者模式的抽象。观察者设计模式使观察者能够从可观察对象订阅并接收通知。 它适用于需要基于推送通知的任何方案。 此模式定义可观察对象以及零个、一个或多个观察者。 观察者订阅可观察对象并且每当预定义的条件、事件或状态发生更改时该可观察对象会通过调用其方法之一来自动通知所有观察者。 在此方法调用中该可观察对象还可向观察者提供当前状态信息。 在 .NET Framework 中通过实现泛型 System.IObservable 和 System.IObserver 接口来应用观察者设计模式。泛型类型参数表示提供通知信息的类型。 泛型类型参数表示提供通知信息的类型。第一次学习观察者模式应该是大学课本中基于事件烧水的例子咱们就基于此实现个简单的Demo吧。首先执行dotnet new web -n Dotnet.Diagnostic.Demo创建示例项目。3.1. 定义可观察对象实现IObservable接口对于烧水的示例主要关注水温的变化因此先定义Temperature来表示温度变化public class Temperature
{public Temperature(decimal temperature, DateTime date){Degree temperature;Date date;}public decimal Degree { get; }public DateTime Date { get; }
}
接下来通过实现IObservableT接口来定义可观察对象。public interface IObservableout T
{IDisposable Subscribe(IObserverT observer);
}
从接口申明来看只定义了一个Subscribe方法从观察者模式讲观察者应该既能订阅又能取消订阅消息。为什么没有定义一个UnSubscribe方法呢其实这里方法申明已经说明期望通过返回IDisposable对象的Dispose方法来达到这个目的。/// summary
/// 热水壶
/// /summary
public class Kettle : IObservableTemperature
{private ListIObserverTemperature observers;private decimal temperature 0;public Kettle(){observers new ListIObserverTemperature();}public decimal Temperature{get temperature;private set{temperature value;observers.ForEach(observer observer.OnNext(new Temperature(temperature, DateTime.Now)));if (temperature 100)observers.ForEach(observer observer.OnCompleted());}}public IDisposable Subscribe(IObserverTemperature observer){if (!observers.Contains(observer)){Console.WriteLine(Subscribed!);observers.Add(observer);}//使用UnSubscriber包装返回IDisposable对象用于观察者取消订阅return new UnSubscriberTemperature(observers, observer);}/// summary/// 烧水方法/// /summarypublic async Task StartBoilWaterAsync(){var random new Random(DateTime.Now.Millisecond);while (Temperature 100){Temperature 10;await Task.Delay(random.Next(5000));}}
}//定义泛型取消订阅对象用于取消订阅
internal class UnSubscriberT : IDisposable
{private ListIObserverT _observers;private IObserverT _observer;internal UnSubscriber(ListIObserverT observers, IObserverT observer){this._observers observers;this._observer observer;}public void Dispose(){if (_observers.Contains(_observer)){Console.WriteLine(Unsubscribed!);_observers.Remove(_observer);}}
}
以上代码中ListIObserver存在线程安全问题因为简单Demo就不予优化了。3.2. 定义观察者实现IObserver接口)比如定义一个报警器实时播报温度。public class Alter : IObserverTemperature
{public void OnCompleted(){Console.WriteLine(du du du !!!);}public void OnError(Exception error){//Nothing to do}public void OnNext(Temperature value){Console.WriteLine(${value.Date.ToString()}: Current temperature is {value.Degree}.);}
}
添加测试代码访问localhost:5000/subscriber控制台输出结果如下endpoints.MapGet(/subscriber, async context
{var kettle new Kettle();//初始化热水壶var subscribeRef kettle.Subscribe(new Alter());//订阅var boilTask kettle.StartBoilWaterAsync();//启动开始烧水任务var timoutTask Task.Delay(TimeSpan.FromSeconds(15));//定义15s超时任务//等待如果超时任务先返回则取消订阅var firstReturnTask await Task.WhenAny(boilTask, timoutTask);if (firstReturnTask timoutTask)subscribeRef.Dispose();await context.Response.WriteAsync(Hello subscriber!);
});------------------------------------------------------------------Subscribed!
10/2/2020 4:53:20 PM: Current temperature is 10.
10/2/2020 4:53:20 PM: Current temperature is 20.
10/2/2020 4:53:21 PM: Current temperature is 30.
10/2/2020 4:53:21 PM: Current temperature is 40.
10/2/2020 4:53:24 PM: Current temperature is 50.
10/2/2020 4:53:25 PM: Current temperature is 60.
10/2/2020 4:53:26 PM: Current temperature is 70.
10/2/2020 4:53:30 PM: Current temperature is 80.
Unsubscribed!
4. DiagnosticSource DiagnosticListener4.1. 概念讲解DiagnosticSource直译就是诊断源也就是它是诊断日志的来源入口。DiagnosticSource是一个抽象类主要定义了以下方法//Provides a generic way of logging complex payloads
public abstract void Write(string name, object value);
//Verifies if the notification event is enabled.
public abstract bool IsEnabled(string name);
DiagnosticListener直译就是诊断监听器继承自DiagnosticSource同时实现了IObservableKeyValuePairstring, object接口因此其本质是一个可观察对象。小结以下DiagnosticSource 作为诊断日志来源提供接口用于写入诊断日志。诊断日志的可观察数据类型为KeyValuePairstring, object。DiagnosticListener 继承自DiagnosticSource作为可观察对象可由其他观察者订阅以获取诊断日志。DiagnosticListener 其构造函数接收一个name参数。private static DiagnosticSource httpLogger new DiagnosticListener(System.Net.Http);
可以通过下面这种方式记录诊断日志if (httpLogger.IsEnabled(RequestStart))httpLogger.Write(RequestStart, new { Urlhttp://clr, RequestaRequest });
然后需要实现IObserverKeyValuePairstring, object接口以便消费诊断数据。定义DiagnosticObserver进行诊断日志消费public class DiagnosticObserver : IObserverKeyValuePairstring, object
{public void OnCompleted(){//Noting to do}public void OnError(Exception error){Console.WriteLine(${error.Message});}public void OnNext(KeyValuePairstring, object pair){ // 这里消费诊断数据Console.WriteLine(${pair.Key}-{pair.Value});}
}
ASP.NET Core 项目中默认就依赖了System.Diagnostics.DiagnosticSourceNuget包同时在构建通用Web主机时就注入了名为Microsoft.AspNetCore的DiagnosticListener。//GenericWebHostBuilder.cs
DiagnosticListener instance new DiagnosticListener(Microsoft.AspNetCore);
services.TryAddSingletonDiagnosticListener(instance);
services.TryAddSingletonDiagnosticSource((DiagnosticSource) instance);
因此我们可以直接通过注入DiagnosticListener进行诊断日志的订阅public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DiagnosticListener diagnosticListener)
{diagnosticListener.Subscribe(new DiagnosticObserver());//订阅诊断日志
}
当然也可以直接使用DiagnosticListener.AllListeners.Subscribe(IObserverDiagnosticListener observer);进行订阅不过区别是接收的参数类型为IObserverDiagnosticListener。运行项目输出Microsoft.AspNetCore.Hosting.HttpRequestIn.Start-Microsoft.AspNetCore.Http.DefaultHttpContext
Microsoft.AspNetCore.Hosting.BeginRequest-{ httpContext Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp 7526300014352 }
Microsoft.AspNetCore.Routing.EndpointMatched-Microsoft.AspNetCore.Http.DefaultHttpContext
Microsoft.AspNetCore.Hosting.EndRequest-{ httpContext Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp 7526300319214 }
Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop-Microsoft.AspNetCore.Http.DefaultHttpContext
从中可以看出ASP.NET Core Empty Web Project在一次正常的Http请求过程中分别在请求进入、请求处理、路由匹配都埋了点除此之外还有请求异常、Action处理都有埋点。因此根据需要可以实现比如请求拦截、耗时统计等系列操作。4.2. 耗时统计基于以上知识下面尝试完成一个简单的耗时统计。从上面的内容可知ASP.NET Core在BeginRequest和EndRequest返回的诊断数据类型如下所示Microsoft.AspNetCore.Hosting.BeginRequest-{ httpContext Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp 7526300014352 }
Microsoft.AspNetCore.Hosting.EndRequest-{ httpContext Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp 7526300319214 }
因此只要拿到两个timestamp就可以直接计算耗时修改DiagnosticObserver的OnNext方法如下private ConcurrentDictionarystring, long startTimes new ConcurrentDictionarystring, long();
public void OnNext(KeyValuePairstring, object pair)
{//Console.WriteLine(${pair.Key}-{pair.Value});//获取httpContextvar context pair.Value.GetType().GetTypeInfo().GetDeclaredProperty(httpContext)?.GetValue(pair.Value) as DefaultHttpContext;//获取timestampvar timestamp pair.Value.GetType().GetTypeInfo().GetDeclaredProperty(timestamp)?.GetValue(pair.Value) as long?;switch (pair.Key){case Microsoft.AspNetCore.Hosting.BeginRequest:Console.WriteLine($Request {context.TraceIdentifier} Begin:{context.Request.GetUri()});startTimes.TryAdd(context.TraceIdentifier, timestamp.Value);//记录请求开始时间break;case Microsoft.AspNetCore.Hosting.EndRequest:startTimes.TryGetValue(context.TraceIdentifier, out long startTime);var elapsedMs (timestamp - startTime) / TimeSpan.TicksPerMillisecond;//计算耗时Console.WriteLine($Request {context.TraceIdentifier} End: Status Code is {context.Response.StatusCode},Elapsed {elapsedMs}ms);startTimes.TryRemove(context.TraceIdentifier, out _);break;}
}
输出如下大功告成Request 0HM37UNERKGF0:00000001 Begin:https://localhost:44330
Request 0HM37UNERKGF0:00000001 End: Status Code is 200,Elapsed 38ms
上面有通过反射去获取诊断数据属性的代码var timestamp pair.Value.GetType().GetTypeInfo().GetDeclaredProperty(timestamp) ?.GetValue(pair.Value) as long?;非常不优雅。但我们可以安装**Microsoft.Extensions.DiagnosticAdapter**包来简化诊断数据的消费。安装后添加HttpContextDiagnosticObserver通过添加DiagnosticName指定监听的诊断名称即可进行诊断数据消费。public sealed class HttpContextDiagnosticObserver
{private ConcurrentDictionarystring, long startTimes new ConcurrentDictionarystring, long();[DiagnosticName(Microsoft.AspNetCore.Hosting.BeginRequest)]public void BeginRequest(HttpContext httpContext,long timestamp){Console.WriteLine($Request {httpContext.TraceIdentifier} Begin:{httpContext.Request.GetUri()});startTimes.TryAdd(httpContext.TraceIdentifier, timestamp);//记录请求开始时间}[DiagnosticName(Microsoft.AspNetCore.Hosting.EndRequest)]public void EndRequest(HttpContext httpContext,long timestamp){startTimes.TryGetValue(httpContext.TraceIdentifier, out long startTime);var elapsedMs (timestamp - startTime) / TimeSpan.TicksPerMillisecond;//计算耗时Console.WriteLine($Request {httpContext.TraceIdentifier} End: Status Code is {httpContext.Response.StatusCode},Elapsed {elapsedMs}ms);startTimes.TryRemove(httpContext.TraceIdentifier, out _);}
}然后使用SubscribeWithAdapter进行订阅即可。public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DiagnosticListener diagnosticListener)
{// diagnosticListener.Subscribe(new DiagnosticObserver());diagnosticListener.SubscribeWithAdapter(new HttpContextDiagnosticObserver());
}
到这里可能也有小伙伴说我用ActionFilter也可以实现没错但这两种方式是完全不同的DiagnosticSource是完全异步的。4.3. 应用场景思考根据DiagnosticSource的特性可以运用于以下场景 AOP因为Diagnostics命名事件一般是成对出现的因此可以做些拦截操作。比如在Abp集成Cap时若想默认启用Uow就可以消费DotNetCore.CAP.WriteSubscriberInvokeBefore命名事件创建Uow再在命名事件DotNetCore.CAP.WriteSubscriberInvokeAfter中提交事务并Dispose。APMSkyAPM-dotnet的实现就是通过消费诊断日志进行链路跟踪。 EventBus充分利用其发布订阅模式可将其用于进程内事件的发布与消费。5. Activity活动5.1. Activity 概述那Activity又是何方神圣用于解决什么问题呢关于Activity官方只有一句简要介绍Represents an operation with context to be used for logging。表示包含上下文的操作用于日志记录。Activity用来存储和访问诊断上下文并由日志系统进行消费。当应用程序开始处理操作时例如HTTP请求或队列中的任务它会在处理请求时创建Activity以在系统中跟踪该Activity。Activity中存储的上下文可以是HTTP请求路径方法用户代理或关联ID所有重要信息都应与每个跟踪一起记录。当应用程序调用外部依赖关系以完成操作时它可能需要传递一些上下文例如关联ID以及依赖关系调用以便能够关联来自多个服务的日志。先来看下Activity主要以下核心属性Tags标签IEnumerableKeyValuePairstring, string Tags { get; } - 表示与活动一起记录的信息。标签的好例子是实例/机器名称传入请求HTTP方法路径用户/用户代理等。标签不传递给子活动。典型的标签用法包括添加一些自定义标签并通过它们进行枚举以填充日志事件的有效负载。可通过Activity AddTag(string key, string value)添加Tag但不支持通过Key检索标签。Baggage行李IEnumerableKeyValuePairstring, string Baggage { get; } - 表示要与活动一起记录并传递给其子项的信息。行李的例子包括相关ID采样和特征标记。Baggage被序列化并与外部依赖项请求一起传递。典型的Baggage用法包括添加一些Baggage属性并通过它们进行枚举以填充日志事件的有效负载。可通过Activity AddBaggage(string key, string value)添加Baggage。并通过string GetBaggageItem(string key)获取指定Key的Baggage。OperationName操作名称string OperationName { get; } - 活动名称必须在构造函数中指定。StartTimeUtcDateTime StartTimeUtc { get; private set; } - UTC格式的启动时间如果不指定则在启动时默认指定为DateTime.UtcNow。可通过Activity SetStartTime(DateTime startTimeUtc)指定。DurationTimeSpan Duration { get; private set; } - 如果活动已停止则代表活动持续时间否则为0。Idstring Id { get; private set; } - 表示特定的活动标识符。过滤特定ID可确保您仅获得与操作中特定请求相关的日志记录。该Id在活动开始时生成。Id传递给外部依赖项并被视为新的外部活动的[ParentId]。ParentIdstring ParentId { get; private set; } - 如果活动是根据请求反序列化的则该活动可能具有进程中的[Parent]或外部Parent。ParentId和Id代表日志中的父子关系并允许您关联传出和传入请求。RootIdstring RootId { get; private set; } - 代表根IdCurrentstatic Activity Current { get; } - 返回在异步调用之间流动的当前Activity。ParentActivity Parent { get; private set; } - 如果活动是在同一过程中从另一个活动创建的则可以使用Partent获得该活动。但是如果“活动”是根活动或父项来自流程外部则此字段可能为null。Start()Activity Start() - 启动活动设置活动的Activity.Current和Parent生成唯一的ID并设置StartTimeUtc如果尚未设置。Stop()void Stop() - 停止活动设置活动的Activity.Current并使用Activity SetEndTime(DateTime endTimeUtc)或DateTime.UtcNow中提供的时间戳计算Duration。另外DiagnosticSource中也定义了两个相关方法StartActivityActivity StartActivity(Activity activity, object args) - 启动给定的Activity并将DiagnosticSource事件消息写入OperationName.Start格式的命名事件中。StopActivityvoid StopActivity(Activity activity, object args) - 停止给定的Activity并将DiagnosticSource事件消息写入{OperationName}.Stop格式的命名事件中。5.2. Activity在ASP.NET Core中的应用要想弄懂Activity我们还是得向源码学习看一下HostingApplicationDiagnostics的实现。首先来看下BeginRequst中的StartActivity方法。private Activity StartActivity(HttpContext httpContext, out bool hasDiagnosticListener)
{Activity activity new Activity(Microsoft.AspNetCore.Hosting.HttpRequestIn);hasDiagnosticListener false;IHeaderDictionary headers httpContext.Request.Headers;StringValues stringValues1;if (!headers.TryGetValue(HeaderNames.TraceParent, out stringValues1))headers.TryGetValue(HeaderNames.RequestId, out stringValues1);if (!StringValues.IsNullOrEmpty(stringValues1)){activity.SetParentId((string) stringValues1);StringValues stringValues2;if (headers.TryGetValue(HeaderNames.TraceState, out stringValues2))activity.TraceStateString (string) stringValues2;string[] commaSeparatedValues headers.GetCommaSeparatedValues(HeaderNames.CorrelationContext);if (commaSeparatedValues.Length ! 0){foreach (string str in commaSeparatedValues){NameValueHeaderValue parsedValue;if (NameValueHeaderValue.TryParse((StringSegment) str, out parsedValue))activity.AddBaggage(parsedValue.Name.ToString(), parsedValue.Value.ToString());}}}this._diagnosticListener.OnActivityImport(activity, (object) httpContext);if (this._diagnosticListener.IsEnabled(Microsoft.AspNetCore.Hosting.HttpRequestIn.Start)){hasDiagnosticListener true;this.StartActivity(activity, httpContext);}elseactivity.Start();return activity;
}
从中可以看出在ASP.NET Core 开始处理请求之前首先创建了名为Microsoft.AspNetCore.Hosting.HttpRequestIn的Activity该Activity首先尝试从HTTP请求头中获取TraceParent/euqstId作为当前Activity的ParentId这个很显然是用来链路跟踪的。其次尝试从CorrelationContext中获取关联上下文信息然后将其添加到创建的Activity的Baggage中进行关联上下文的继续传递。然后启动Activity然后向Name为Microsoft.AspNetCore.Hosting.HttpRequestIn.Start中写入诊断日志。这里大家可能有个疑问这个关联上下文信息CorrelationContext又是何时添加到Http请求头中的呢在System.Net.Http中的DiagnosticsHandler中添加的。因此我们应该明白了整个关联上下文的传递机制。紧接着再来看一看RequestEnd中的StopActivity方法。private void StopActivity(Activity activity, HttpContext httpContext)
{if (activity.Duration TimeSpan.Zero)activity.SetEndTime(DateTime.UtcNow);this._diagnosticListener.Write(Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop, (object) httpContext);activity.Stop();
}
从中可以看出主要是先SetEndTime再写入Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop命名事件最后调用Stop方法停止当前Activity。简单总结一下借助Activity中附加的Baggage信息可以实现请求链路上上下文数据的共享。5.3. 应用场景思考从上面的命名事件中可以看出其封送的数据类型是特定的因此可以借助Activity的Tags或Baggage添加自定义的数据进行共享。按照上面我们的耗时统计只能统计到整个http请求的耗时但对于我们定位问题来说还是有困难比如某个api即有调用redis又操作了消息队列同时又访问了数据库那到底是那一段超时了呢显然不好直接定位借助activity我们就可以很好的实现细粒度的链路跟踪。通过activity携带的信息可以将一系列的操作关联起来记录日志再借助AMP进行可视化快速定位跟踪。6. 参考资料在 .NET Core 中使用 Diagnostics (Diagnostic Source) 记录跟踪信息Logging using DiagnosticSource in ASP.NET Core.Net Core中的诊断日志DiagnosticSource讲解Observer Design PatternDiagnosticSource User GuideActivity User GuideDiagnosticSourcery 101 - Mark RendleImprovements in .NET Core 3.0 for troubleshooting and monitoring distributed apps