ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ASP.NET Core 事件源接入指南:EventSource、EventCounter 埋点规范与自动化测试实践

ASP.NET Core 事件源接入指南:EventSource、EventCounter 埋点规范与自动化测试实践 ASP.NET Core 事件源接入指南EventSource、EventCounter 埋点规范与自动化测试实践【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcoreEventSource 与 EventCounter 是 .NET 生态中面向生产可观测性的底层基础设施前者以 ETW/manifest 机制发布结构化事件后者以自描述计数器的形式输出随时间聚合的指标。本文以 ASP.NET Core 仓库的官方开发规范文档 docs/EventSourceAndCounters.md 为核心骨架系统讲解在 ASP.NET Core 类库中新增 EventSource/EventCounter 追踪时应遵循的事件命名模式、代码风格、标准实现范式并延伸到仓库内置的Microsoft.AspNetCore.InternalTesting.Tracing测试设施覆盖 Event ID 一致性校验与事件功能测试两套自动化验证方案。读完本文你将掌握一套可照抄的埋点模板以及保证它与运行时行为一致、不会随合并冲突悄悄腐化的测试套路。适用背景为什么 ASP.NET Core 类库需要自带 EventSourceASP.NET Core 的大量核心组件如 Kestrel、SignalR、认证中间件等并不是在框架内部为每个功能预埋一套统一的监控 API而是遵循每一个库自己定义自己的 EventSource的约定。这样做的原因在于EventSource 由 .NET 运行时原生支持可通过 PerfView、dotnet-trace、Windows ETW 等工具按名称订阅无需应用代码改动事件以二进制 manifest 自描述方式发布payload 只允许基元类型便于跨进程/跨平台收集EventCounter 是计数器式指标通道仅在监听器以EventCounterIntervalSec参数启用时才真正触发天然适合低频采样聚合。因此给库加可观测性的正确姿势是像写日志一样在每个关键路径上写事件——文档开篇就明确给出总原则所有加了 EventSource 追踪的地方同时也要加ILogger追踪除非有充分理由不这么做最低可以用Trace级别兜底。EventSource 面向工具化诊断ILogger 面向人类可读的应用日志二者互为补充而非替代。本文后续的规范与示例来自 ASP.NET Core 仓库的工程实践文档即规范来源并有 src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs 这样的生产级实现可对照。预备知识按照文档约定在动手前应对以下两个概念有基本了解EventSource 的事件发布机制[Event]特性标注的方法如何被生成 ETW manifestWriteEvent(id, ...)如何与特性中的 eventId 对应。EventCounter 的工作原理计数器何时被启用取决于监听器传入的EventCounterIntervalSec、WriteMetric写入的原始值如何被聚合成多个统计量。历史踩坑提醒源自仓库文档查看 EventCounter 需要.NET Core 2.0.3 及以上版本——2.0.0 RTM 中计数器在运行时是损坏的。用 2.0 RTM 编译没有问题只是计数器实际上不会被触发。事件模式Event Patterns文档汇总了一套团队沉淀的事件设计规范逐条拆解如下这些规则直接决定了事件在 PerfView 等工具里的可读性与可关联性结构层面Start/Stop 事件必须共享至少一个 payload 值用于关联配对例如 Request ID、Request Path、Action Name 等。Error 事件一律使用EventLevel.Error级别。Stop 事件必须携带一个double durationInMillisecondspayload表示 Start 到 End 之间的毫秒耗时。计时使用ValueStopwatch来自仓库 src/Shared/ValueStopwatch/ValueStopwatch.cs。它是一个简单的 struct本质是对一个long时间戳的封装避免计时产生堆分配。payload 只能是基元类型富对象必须在写入前展开或者通过一个[NonEvent]包装方法来展开。异常要展开为三个 string payloadexceptionType取.GetType().AssemblyQualifiedName、exceptionMessage取.Message、exceptionDetails取.ToString()。payload 命名要描述性强因为会原样展示在 PerfView 等事件查看工具中——例如用durationInMilliseconds而不是duration。命名模式操作开始事件后缀用Start不要用Begin或Started。操作结束事件后缀用Stop不要用Stopped、End或Ended。即使同时触发了Failure事件也必须触发Stop事件——把 Stop 当作finally块对待保证配对事件完整。错误事件后缀用Failure。动词用现在时Timeout而不是TimedOut。采用NounVerb语序ConnectionStart而不是StartConnection。代码组织在 EventSource 类型内所有带[Event]的方法放在一起并按eventId排序。计时事件的标准套路对需要计时的操作文档给出了非常具体的启停协作约定Start事件方法返回ValueStopwatch当事件被禁用时返回default(ValueStopwatch)启用时才返回ValueStopwatch.StartNew()End事件接收该ValueStopwatch先通过.IsActive判断它是否真的被启动了若是再用GetElapsedTime计算耗时。这套逻辑保证了事件被禁用时不会付出调用ValueStopwatch.StartNew()的性能开销。仓库里的 ValueStopwatch 实现 印证了这一设计IsActive _startTimestamp ! 0当未初始化default时GetElapsedTime()会抛出InvalidOperationException在 .NET 7 上它直接委托给Stopwatch.GetElapsedTime早期 TFM 则通过TimestampToTicks手工换算StartNew()用Stopwatch.GetTimestamp()只取一个时间戳全程零分配。代码风格Code Style为了让 EventSource 在诊断工具中可被稳定识别仓库对 EventSource 类的形态有一致性要求规则要求依据/示例事件源名称与所在程序集名一致但.换成-Microsoft.AspNetCore.Authentication→Microsoft-AspNetCore-Authentication可见性一律internal如 KestrelEventSource.cs 中internal sealed class构造函数声明private无参构造函数private KestrelEventSource() { }单例声明public static readonly实例命名为Logpublic static readonly KestrelEventSource Log new KestrelEventSource();类型名后缀类型名以EventSource结尾DependencyInjectionEventSourceEventSource 名称之所以必须是internal是因为它们是进程内全局概念对外暴露引用只会诱导使用者把库内部的可观测性细节当成公共 API。事件实现模式Event Pattern完整形态带计数器 复杂 payload下面是文档给出的覆盖几乎所有场景的标准形态。核心思想是两层方法分工[NonEvent]包装方法负责类型安全地接收富对象、做复杂计算与分级开关判断[Event]私有方法只负责向 ETW 写出可序列化的基元 payload。// 必须标注 NonEventAttribute否则 EventSource 会尝试为它自动生成 manifest [NonEvent] public void SomethingHappened(ObjectNeededToCalculateThePayload p, AnotherObjectNeededToCalculateThePayload p2) { // 检查 source 是否启用不区分 level if (IsEnabled()) { // 若该事件关联了计数器无论 level 如何都写入指标 _somethingsHappenedCounter.WriteMetric(1.0f); // 再检查这个具体事件是否按 level可选 keywords启用 if (IsEnabled(EventLevel.Informational, EventKeywords.None)) { // 做任何计算 payload 所需的复杂操作 var payloadValue CalculateThePayload(p); // 触发真正的事件方法 SomethingHappened(payloadValue, p2.MorePayload, p2.SomeValue - p2.SomeOtherValue); } } } // 必须是独立方法EventSource 才能为它生成 ETW manifest。 // eventId 字段必填且必须与传给 WriteEvent 的 id 一致。 [Event(eventId: 42, Level EventLevel.Informational)] private void SomethingHappened(string payloadValue, int anotherPayloadValue, double morePayload) WriteEvent(42, payloadValue, anotherPayloadValue, morePayload);要点解读计数器写入放在外层IsEnabled()检查之后、具体事件 level 检查之前因为文档约定只要 EventSource 被启用就写计数器而不受 level/keyword 控制详见下文事件计数器一节。WriteEvent(42, ...)第一个参数是 eventId必须与[Event(eventId: 42)]严格一致——这正是后面EventSourceValidator测试要守护的约束。方法体写成 expression-bodied只是风格选择重点是 eventId 同步。简化形态简单 payload、无计数器当没有复杂 payload 计算、也不关联计数器时可以把全部逻辑收敛到一个带[Event]的方法里直接用IsEnabled(EventLevel, EventKeyword)重载判断[Event(eventId: 42, Level EventLevel.Informational)] public void SomethingHappened(string payloadValue) { if (IsEnabled(EventLevel.Informational, EventKeywords.None)) { WriteEvent(42, payloadValue); } }关键字Keywords当某些事件只希望用户显式要求时才启用可以用 Keywords 控制。Keywords 是一个简单的标志位枚举值在监听器启用某个 EventSource 时提供通过IsEnabled检查生效——属于按需细分的能力开关适用于默认不开、诊断时打开的详细事件。事件计数器Event Counters文档指出EventCounter只有在监听器为 EventSource 提供EventCounterIntervalSec参数时才会真正启用因此不需要用 level 或 keyword 去控制它们。仓库的约定是EventSource 本身一旦被启用就始终向计数器写入对应上面完整形态里外层IsEnabled() 无条件WriteMetric的结构。计数器会提供多种聚合Count、Mean、StdDev、Min、Max而不同种类的计数器适合不同的聚合维度。仓库把计数器分为三类种类语义写入方式消费者如何解读命名要求例子Counter计数某事件发生的次数.WriteMetric(1.0f)读取某时间区间内的 Count 聚合得到事件发生次数复数名词 形容词RequestsStartedMetric指标随时间或按单位变化的值如每次请求、每条连接.WriteMetric(当前值)用各聚合值了解指标随时间的分布描述该指标的单数名词RequestBodySizeDuration时长一种以毫秒记录时长的 Metric.WriteMetric(毫秒数)同上名字以Duration结尾RequestDuration值得注意Kestrel 的生产实现 KestrelEventSource.cs 在此之上还使用了PollingCounter与IncrementingPollingCounter配合Interlocked维护的连接数/队列长度等字段用于采样当前值类指标——这说明实际库往往组合 EventCounter 与 PollingCounter 来覆盖计数、时长、瞬时水位三类可观测性需求。完整示例一个认证中间件的 EventSource以下是文档给出的认证场景 EventSource 完整示例straw-man把上述所有规则落成可编译代码。注意类名、事件源名、Log单例、私有构造、[NonEvent]与[Event]分层、Start/Stop 共享traceIdentifier与path、Stop 携带durationMilliseconds、Failure 展开异常三元组等要点全部齐备using System; using System.Diagnostics.Tracing; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Authentication.Internal { [EventSource(Name Microsoft-AspNetCore-Authentication)] public class AuthenticationEventSource : EventSource { public static readonly AuthenticationEventSource Log new AuthenticationEventSource(); private readonly EventCounter _authenticationMiddlewareDuration; private AuthenticationEventSource() { _authenticationMiddlewareDuration new EventCounter(AuthenticationMiddlewareDuration, this); } [NonEvent] internal void AuthenticationMiddlewareStart(HttpContext context) { if (IsEnabled(EventLevel.Informational, EventKeywords.None)) { AuthenticationMiddlewareStart(context.TraceIdentifier, context.Request.Path.Value); } } [NonEvent] internal void AuthenticationMiddlewareEnd(HttpContext context, TimeSpan duration) { if (IsEnabled()) { _authenticationMiddlewareDuration.WriteMetric((float)duration.TotalMilliseconds); if (IsEnabled(EventLevel.Informational, EventKeywords.None)) { AuthenticationMiddlewareEnd(context.TraceIdentifier, context.Request.Path.Value, duration.TotalMilliseconds); } } } [NonEvent] internal void AuthenticationMiddlewareFailure(HttpContext context, Exception ex) { if(IsEnabled(EventLevel.Error, EventKeywords.None)) { AuthenticationMiddlewareFailure(context.TraceIdentifier, context.Request.Path.Value, ex.GetType().FullName, ex.Message, ex.ToString()); } } [Event(eventId: 1, Level EventLevel.Informational)] private void AuthenticationMiddlewareStart(string traceIdentifier, string path) WriteEvent(1, traceIdentifier, path); [Event(eventId: 2, Level EventLevel.Informational)] private void AuthenticationMiddlewareEnd(string traceIdentifier, string path, double durationMilliseconds) WriteEvent(2, traceIdentifier, path, durationMilliseconds); [Event(eventId: 3, Level EventLevel.Error)] private void AuthenticationMiddlewareFailure(string traceIdentifier, string value, string exceptionTypeName, string message, string fullException) WriteEvent(3, traceIdentifier, value, exceptionTypeName, message, fullException); } }仓库中的真实对照实现KestrelEventSource上述示例并非纸上谈兵。以 KestrelEventSource.cs 为例它是 ASP.NET Core 仓库中遵循该套模式的真实生产实现类声明处第 16-19 行[EventSource(Name Microsoft-AspNetCore-Server-Kestrel)] internal sealed class ... : EventSource并带public static readonly ... Log单例与私有构造。源码注释特别强调Start/Stop后缀在 EventSource 中具有特殊含义会激活 activity 关联correlation能力且 Stop 事件的 eventId 必须是其 Start 的下一个值同时避免重命名带[Event]的方法或参数因为 EventSource 靠它们构成事件对象。ConnectionStart第 55-76 行先无条件用Interlocked.Increment维护计数器的底层字段再在IsEnabled(EventLevel.Informational, EventKeywords.None)内展开连接三要素体现了低开销 延迟分配的双层写法ConnectionStop第 78-94 行递减_currentConnections后触发 event 2与 event 1 通过connectionId关联。RequestStart第 96-119 行使用[NonEvent]局部函数做二次判断避免在日志未启用时分配 trace identifier 字符串——与文档避免在禁用时付出不必要开销的原则一脉相承。EventSource 的自动化测试文档强调一个残酷现实EventSource 的许多错误如[Event]的 eventId 与WriteEvent实参不一致只有到运行时才会暴露。为此仓库在 src/Testing/src/Tracing/ 下提供了一套专门的测试设施包含两个互补的测试维度。维度一校验 Event ID 一致性EventSourceValidator所有EventSource子类都应有一个测试用于校验[Event(N)]特性中的 ID 与WriteEvent(N, ...)调用实参是否匹配。这能捕获因错误合并或漏更新导致的漂移——这类问题平时无感只会在运行时以错误形式爆出。工具是Microsoft.AspNetCore.InternalTesting.Tracing命名空间下的EventSourceValidatorusing Microsoft.AspNetCore.InternalTesting.Tracing; public class MyEventSourceTests { [Fact] public void EventIdsAreConsistent() { EventSourceValidator.ValidateEventSourceIdsMyEventSource(); } }从 EventSourceValidator.cs 的实现看它做了两件事重复 ID 检查遍历类型上所有[Event]标注方法含非 public、仅 DeclaredOnly用字典登记每个EventId发现重复即报错。IL 级校验调用EventSource.GenerateManifest(type, assemblyPathToIncludeInManifest, EventManifestOptions.Strict)让运行时内部用GetHelperCallFirstArg反汇编检查每个方法体中传给WriteEvent的整数常量是否与[Event(id)]一致——这正是 .NET 运行时构造 EventSource 时执行的同一套校验。任何不匹配都会以ArgumentException形式抛出并被收集为测试失败。仓库中已有真实用例例如 Kestrel 的测试 KestrelEventSourceTests.cs 通过反射拿到内部类型后调用EventSourceValidator.ValidateEventSourceIds(esType)同文件还展示了用EventSource.GetName/GetGuid/GenerateManifest校验事件源名称、GUID 与 manifest 有效性的配套做法。重要约定每一个新增的 EventSource 类都应包含这一行校验测试。维度二功能测试EventSourceTestBase除了静态校验还可以对事件源做真实触发的功能测试。基类位于 src/Testing/src/Tracing/EventSourceTestBase.cs// 测试 EventSource 必须使用该基类EventSource 是进程全局的并行测试会引发问题。 // 基类加入了相应机制来规避。 public class SomeTest : EventSourceTestBase { [Fact] public void TestName() { // Arrange: 显式注册要监听的事件源 CollectFrom(Microsoft-AspNetCore-SomeEventSourceName); // Act: 做一些会触发事件的操作 DoStuff(); // Assert: 取出收集到的事件并断言符合预期 var events GetEvents(); // EventAssert 是测试事件的辅助类。它的特殊之处在于 // EventAssert.Event 返回一个builder用于构造 ActionEventWrittenEventArgs // 在被调用时会断言你配置的各项内容。这种模式让测试代码更清晰。 EventAssert.Collection(events, EventAssert.Event(1, Test, EventLevel.Informational), EventAssert.Event(2, TestWithPayload, EventLevel.Verbose) .Payload(payload1, 42) .Payload(payload2, 4.2)); } }这套设施为何能处理全局并行问题从源码可以看清三块机制串行化集合EventSourceTestBase.cs 类上标注了[Collection(CollectionName)]常量值为Microsoft.AspNetCore.InternalTesting.Tracing.EventSourceTestCollection这个 xUnit collection 特性会强制所有继承它的测试顺序执行从根上避免进程级 EventSource/EventListener 相互踩踏。收集监听器底层是 CollectingEventListener.cs —— 一个EventListener子类用ConcurrentQueueEventWrittenEventArgs缓存事件。CollectFrom(string)支持按名预约若目标 EventSource 尚未创建就记入待启用集合待OnEventSourceCreated回调到来时立即补启用规避了监听器与源创建的先后竞争实际启用时调用EnableEvents(source, EventLevel.Verbose, EventKeywords.All)即以 Verbose 级别全量接收。链式断言EventAssert.cs 的Event(id, name, level)返回 builder.Payload(name, expectedValue)或.Payload(name, Actionobject)自定义断言逐步追加校验EventAssert.Collection把每个 builder 转成ActionEventWrittenEventArgs后交给 xUnit 的Assert.Collection同时校验事件的EventId、EventName、Level以及PayloadNames与Payload的逐项对应。这样 Arrange → Act → Assert 三段式中前两段与普通测试几乎无异区别只在通过基类提供的CollectFrom/GetEvents完成捕获从而在不依赖外部 ETW 会话的情况下验证某个操作真的发出了期望的事件序列。已知限制文档明确标注当前测试监听器暂不支持收集 EventCounters。如果你的测试需要验证计数器数据需要在仓库测试基础设施中另行提交 issue 跟进目前可验证的范围是事件本身含 payload而非计数器的区间聚合输出。小结把本指南浓缩成三条落地清单写任何新 EventSource 按统一代码风格落盘——internal 私有构造 static readonly Log、Assembly.Name转-作为源名、类型以EventSource结尾事件命名遵循NounVerb、Start/Stop/Failure后缀与现在时约定。排Start 事件方法返回ValueStopwatchStop 事件固定带double durationInMilliseconds且与 Start 共享关联键复杂 payload 与异常展开为exceptionType/exceptionMessage/exceptionDetails通过[NonEvent]包装层处理[Event]方法只做基元写出。测为每个 EventSource 添加一行EventSourceValidator.ValidateEventSourceIdsT()防止 ID 漂移凡是需要验证触发行为的测试继承EventSourceTestBase获得 xUnit 串行化 CollectFrom/GetEvents/EventAssert能力。遵循这套源自 ASP.NET Core 仓库自身的规范你的类库事件在 PerfView、dotnet-trace 等工具中会呈现一致、可关联、可聚合的结构同时获得自动化测试对运行时才暴露类问题的兜底。【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcore创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表