用途
该技能用于在 .NET 代码库中实现 OpenTelemetry 可观测性(instrumentation),适用于以下场景:为 .NET 代码添加追踪、指标、日志;创建或修改 ActivitySource、Meter、ILogger 用法;配置 OpenTelemetry SDK、资源、导出器或采样;审查遥测实现是否符合规范;优化遥测性能;设计会成为公共 API 一部分的遥测接口;实现跨服务边界的上下文传播。
架构前提:.NET 与其他平台不同
资料强调,.NET 的 OpenTelemetry 实现与其他平台有本质区别:.NET 框架自身就提供了追踪、指标和日志 API。因此 OTel 不提供独立的 instrumentation API,而是使用 .NET 内置 API,自身充当收集与导出层。
三个内置 .NET API(主要、零依赖):
- 追踪:ActivitySource / Activity,命名空间 System.Diagnostics
- 指标:Meter / Counter<T> / Histogram<T> 等,命名空间 System.Diagnostics.Metrics
- 日志:ILogger<T>,命名空间 Microsoft.Extensions.Logging
这些是库作者应使用的唯一 API,随 .NET 运行时提供,无需 NuGet 包。
OTel 收集/导出层(次要,仅应用根):OTel NuGet 包只在应用组合根添加,不在库中添加。资料列出的包包括 OpenTelemetry.Extensions.Hosting(DI 集成)、OpenTelemetry.Exporter.Console(控制台导出,开发/测试)、OpenTelemetry.Exporter.OpenTelemetryProtocol(OTLP 导出,生产)、OpenTelemetry.Exporter.Prometheus*(Prometheus 指标端点)、OpenTelemetry.Instrumentation.AspNetCore(自动埋点 ASP.NET Core 请求)、OpenTelemetry.Instrumentation.Http(自动埋点 HttpClient 调用)、OpenTelemetry.Instrumentation.SqlClient(自动埋点 SQL 调用)。
包决策指南:添加任何 OTel NuGet 包前应与用户讨论权衡。库作者不应添加任何包,只使用 System.Diagnostics.* 和 ILogger,由消费应用配置 SDK 与导出器;应用作者添加 OpenTelemetry.Extensions.Hosting 及所需的导出器和 instrumentation 库。绝不要向库添加 OpenTelemetry.Api——System.Diagnostics.* 本身就是 API。
核心原则
韧性优先:诊断/追踪/指标逻辑中的异常绝不能影响应用处理。假设 Activity 实例可能为 null,除 Activity 扩展方法外始终做 null 保护(使用 activity?.ExtensionMethod()),所有 instrumentation 代码都要有适当的 null 检查。
API 表面意识:任何发出的遥测都会成为公共 API 表面的一部分,变更受破坏性变更指南约束。遥测应默认发出(用户通过 OpenTelemetry 扩展选择是否收集),例外是高基数指标维度可能需要显式选择加入。
标准合规:遵循 Microsoft 分布式追踪 instrumentation 最佳实践和 OpenTelemetry 语义约定。属性值支持 string、boolean、double(IEEE 754)、int64、字节数组及这些原始类型的同质数组;null/空值按 OTel AnyValue 规范是有效且有意义的,必须存储并传给导出器。属性键必须是非 null、非空字符串。
追踪 / Span(Activity)
ActivitySource 设置:每个组件定义一个主 ActivitySource 用于主流活动,名称通常匹配组件或 NuGet 包名(如 "MyCompany.MyLibrary"),使用 SemVer 版本。为专门或选择加入场景创建单独的 ActivitySource,使用层级名称(如 MyCompany.MyLibrary 和 MyCompany.MyLibrary.Detailed),以便消费应用通过 AddSource(...) 只订阅所需源,后端也可按 instrumentation scope 过滤。
创建 Activity:创建前检查 ActivitySource.HasListeners()(零分配快速路径);创建后始终对 Activity 做 null 检查(监听器可能过滤或采样掉);不要在异步辅助方法中启动 activity(Activity.Current 使用 AsyncLocal);将昂贵的 tag 计算放在 activity.IsAllDataRequested 之后。使用 W3C TraceContext,.NET Core 3.0+ / .NET 5+ 默认使用;较旧 TFM 或 .NET Framework 应用可在启动时设置 Activity.DefaultIdFormat = ActivityIdFormat.W3C 并用 Activity.ForceDefaultIdFormat = true 覆盖层级父级。
Activity 命名:每种 span 类型有唯一的 OperationName(标识统计上有意义的 span 类别),操作名不应包含运行时数据(只含编译/配置期信息),使用人类可读的 DisplayName 表示具体信息,遵循 OpenTelemetry span 命名约定。
SpanKind 选择:Internal(默认,不跨远程边界的进程内操作)、Server(处理传入请求/响应调用,如 HTTP/gRPC/RPC 服务器)、Client(发起传出请求/响应调用,如 HTTP 客户端、数据库客户端、RPC 调用)、Producer(入队/发布延迟工作,如消息队列发布、事件发出、任务入队)、Consumer(出队/处理延迟工作,如消息队列接收、事件处理、任务出队)。单个 span 不应服务多个目的。应在将 SpanContext 注入请求之前创建传出 span;若先注入,父上下文会传播,传出 span 会悬空。
Span 属性(Tag):应用代码使用自己的命名空间(如 myapp.order_id);手动基础设施 instrumentation 使用语义约定(如 db.system.name、http.request.method)。命名规则:命名空间前缀匹配组件(myapp.、myapp.db.),全小写、下划线分隔、单数形式。业务/域属性用自有命名空间;手动埋点的 HTTP、数据库、消息、RPC 概念用语义约定;不要重复自动埋点已发出的属性;不要用 OTel 命名空间作为自定义属性前缀。
Activity 状态与错误:成功时保持状态 Unset,不要调用 SetStatus(ActivityStatusCode.Ok)——Ok 是给应用代码而非 instrumentation 库的,应用开发者和运维可设置 Ok 以覆盖库报告的 Error(如抑制嘈杂的 404),一旦设置 Ok 即为最终,后续调用被忽略。失败时应设置 ActivityStatusCode.Error、error.type tag,并将状态描述设为异常消息。使用 SetStatus,不再需要旧的 otel.status_code/otel.status_description tag。不要记录已重试或已处理并优雅完成的操作错误。
记录异常:exceptions-spans 约定(Activity.AddEvent(new ActivityEvent("exception", ...)))虽被标记为 Deprecated 以让位于 exceptions-logs,但 .NET 生态尚未跟进:没有 OpenTelemetry.* .NET 包实现规范的 OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN 选择加入(已对照 OpenTelemetry.Api 1.17.0 验证),且 OpenTelemetry.Instrumentation.AspNetCore 1.17.0 自身仍通过 Activity.AddException 即 span 事件记录异常。因此应继续默认实现 span 事件,将较新的基于日志的路径作为选择加入叠加。资料给出了通过环境变量 OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN 控制(unset/其他 → 仅 span,"logs/dup" → 两者,"logs" → 仅日志)的代码示例。规则:不要在 span 事件上设置 exception.escaped(已弃用);支持该环境变量但默认仅 span;发出日志时按 exceptions-logs 选择严重级别(未处理异常用 ERROR,尤其 SERVER/CONSUMER span;预期由调用方处理的异常用 WARN,尤其 CLIENT/PRODUCER span;不表示实际问题的异常如客户端取消用 DEBUG;仅当异常导致应用关闭时用 FATAL);日志调用必须在关联 span 仍为当前时进行;只有在稳定主版本上保持双发出至少六个月并确认实际消费者读取基于日志的异常后,才迁移到仅日志。
访问 Activity:不要依赖 Activity.Current 获取自己拥有的 span(用户代码可通过 AsyncLocal 替换它);使用捕获的 Activity 引用,仅在存活期间传递/存储;传播身份存储 ActivityContext。
Span Links:链接将 span 连接到因果相关但非直接父子关系的其他 span——批处理、分散/聚集、追踪边界跨越。资料给出了使用 ActivityLink 列表并传给 StartActivity 的示例。
适用场景与限制
该技能面向 .NET 库作者与应用作者,指导如何正确埋点、命名、记录错误与异常,并区分库与应用应添加的依赖。资料未提供具体的收费、价格、平台支持范围或效果数据。