使用 OpenTelemetry 构建 .NET 应用可观测性(3):.NET SDK 概览
前言
本文将介绍 OpenTelemetry .NET SDK 核心组件的设计和使用,主要是为后续给大家介绍如何在 ASP.NET Core 应用程序中使用 OpenTelemetry 做铺垫。
为方便演示,本文使用的 Exporter 都是 Console Exporter,将数据输出到控制台。
概览
我们在 OpenTelemetry 的 GitHub 仓库中搜索 dotnet,可以看到有三个仓库:

https://github.com/open-telemetry?q=dotnet&type=all&language=&sort=
opentelemetry-dotnet
OTel SDK 的核心库,主要包括以下几个部分:
- Logging, Metrics, Tracing 等核心组件
- ASP.NET Core 相关的常用 Instrumentation,如 AspNetCore、HttpClient、GrpcNetClient、SqlClient 等。
- Console、Zipkin、Prometheus 等常用 Exporter
- 依赖注入的扩展,用于在应用中快速集成 OTel
opentelemetry-dotnet-contrib
第三方贡献的 Instrumentation 和 Exporter,比如 InfluxDB、Elasticsearch、AWS 等
opentelemetry-dotnet-instrumentation
无侵入的 Instrumentation,用于在不修改代码的情况下,自动收集数据。
SDK 的基本使用
本文只介绍 OTel SDK 的基本使用,下面将创建一个 Console 应用程序,演示如何使用 OTel SDK。
安装依赖
创建一个 .NET Core Console 应用程序,然后安装下列依赖:
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.Console
本文测试使用的是 1.6.0 版本,后期 OTel SDK 的版本可能会有所变化。
Resources
Resource 是 OTel 中的一个重要概念,用于标识应用程序的一些元数据,比如应用程序的名称、版本、运行环境等。
Resource 的信息会被添加到 Log、Span、Metric 等数据中,用于后续的查询和分析。
Resource 由 ResourceBuilder 构建,ResourceBuilder 有两个方法:
ResourceBuilder.CreateDefault()
ResourceBuilder.CreateDefault():创建一个默认的 Resource,包含以下Attribute:
- ServiceName:应用程序的名称,可以通过 OTEL_SERVICE_NAME 环境变量设置。
- 自定义的Attribute:可以通过 OTEL_RESOURCE_ATTRIBUTES 环境变量设置,格式为 key1=value1,key2=value2
- OTel SDK 的信息:包括 OTel SDK 的名称、版本、语言等。
Environment.SetEnvironmentVariable("OTEL_SERVICE_NAME", "FooService");
// 可以直接在 OTEL_RESOURCE_ATTRIBUTES 中指定 service.name, 这样就不需要再指定 OTEL_SERVICE_NAME 了
Environment.SetEnvironmentVariable("OTEL_RESOURCE_ATTRIBUTES", "service.version=1.0.0,service.namespace=TestNamespace");
Resource resource = ResourceBuilder
.CreateDefault()
.Build();
foreach (var attribute in resource.Attributes)
{
Console.WriteLine($"{attribute.Key}={attribute.Value}");
}
输出:
service.name=FooService
service.version=1.0.0
service.namespace=TestNamespace
telemetry.sdk.name=opentelemetry
telemetry.sdk.language=dotnet
telemetry.sdk.version=1.6.0
ResourceBuilder.CreateEmpty()
ResourceBuilder.CreateEmpty():创建一个空的 Resource,可以按需求添加Attribute。
Environment.SetEnvironmentVariable("OTEL_RESOURCE_ATTRIBUTES", "test.attribute=foo");
Resource resource = ResourceBuilder
.CreateDefault()
.AddService("FooService", "TestNamespace", "1.0.0")
.AddTelemetrySdk()
.AddEnvironmentVariableDetector() // 可以识别 OTEL_RESOURCE_ATTRIBUTES 环境变量
.Build();
foreach (var attribute in resource.Attributes)
{
Console.WriteLine($"{attribute.Key} = {attribute.Value}");
}
输出:
test.attribute = foo
telemetry.sdk.name = opentelemetry
telemetry.sdk.language = dotnet
telemetry.sdk.version = 1.6.0
service.name = FooService
service.namespace = TestNamespace
service.version = 1.0.0
service.instance.id = 15ff37f1-5791-4afe-b130-cb947b895af3
Tracing
ActivitySource & Activity
有别于其他语言的 SDK,.NET SDK 的 Tracing 模块是通过 ActivitySource 实现的。
ActivitySource 的 API 和 OpenTelemetry 的 API 基本是一一对应的。
通过 ActivitySource.StartActivity() 创建的 Activity 对应 OTel 中的 Span,可以被 OTel SDK 的 Tracing 模块收集。
Activity 是 NET 以前就有的类,OTel 标准出来后,.NET 对 Activity 做了一些扩展,使其可以和 OTel 中的 Span 一一对应。
System.Diagnostics.ActivitySource 是 .NET Runtime 的一部分,如果编写的代码仅仅是一个收集数据的组件,可以直接使用 System.Diagnostics.ActivitySource,不需要引入 OpenTelemetry 的依赖。
ActivitySource 本质是 System.Diagnostics 命名空间里一个发布/订阅模式的工具。
ActivitySource.AddActivityListener(new ActivityListener
{
// 只监听 TestSource1
ShouldListenTo = source => source.Name == "TestSource1",
// 采样率为 100%
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllDataAndRecorded,
// 监听 Activity 的开始和结束
ActivityStarted = activity =>
{
Console.WriteLine($"Activity started: {activity.OperationName}");
},
ActivityStopped = activity =>
{
Console.WriteLine($"Activity stopped: {activity.OperationName}");
}
});
using var activitySource1 = new ActivitySource("TestSource1");
using var activitySource2 = new ActivitySource("TestSource2");
using var activity1 = activitySource1.StartActivity("Activity1");
Console.WriteLine($"Activity1 created: {activity1 != null}");
// 如果设置 Listener,ActivitySource 将不会创建 Activity,StartActivity 返回 null
activity1?.SetTag("foo", 1);
using var activity2 = activitySource2.StartActivity("Activity2");
Console.WriteLine($"Activity2 created: {activity2 != null}");
activity2?.SetTag("bar", "Hello, World!");
输出:
Activity started: Activity1
Activity1 created: True
Activity2 created: False
Activity stopped: Activity1
ActivitySource 可以通过 Name 来关联 ActivityListener,只有 ActivityListener 的 ShouldListenTo 返回 true 的 ActivitySource 才会被监听。
在上面的例子中,我们通过 ActivitySource.StartActivity() 创建了两个 Activity,但是只有一个 Activity 被监听到,这是因为我们设置了 ShouldListenTo,只监听 TestSource1。
如果没有设置 ActivityListener,ActivitySource.StartActivity() 将返回 null。
所以推荐使用 ActivitySource.StartActivity() 创建的 Activity 时,使用?.操作符来避免空指针异常。
Tracing 模块的使用
而 OpenTelemetry SDK 的 Tracing 模块,其实就是一个 ActivityListener 的实现。
在使用 OTel 的 Tracing 模块时,我们需要通过 TracerProvider.AddSource() 告诉 OTel SDK 实现的 ActivityListener 需要监听哪些 ActivitySource。
var serviceName = "MyCompany.MyProduct.MyService";
var serviceVersion = "1.0.0";
var resourceBuilder = ResourceBuilder.CreateDefault()
.AddService(serviceName: serviceName, serviceVersion: serviceVersion);
// 创建 Span 是通过 ActivitySource.StartActivity() 实现的,
// 所以这边的 tracerProvider 不会被使用
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(resourceBuilder)
.AddSource("TestSource1")
.AddSource("TestSource2")
.AddConsoleExporter()
.Build();
using var activitySource1 = new ActivitySource("TestSource1");
using var activitySource2 = new ActivitySource("TestSource2");
using (var activity1 = activitySource1.StartActivity("Activity1"))
{
activity1?.SetTag("foo", 1);
activity1?.SetTag("bar", "Hello, World!");
using (var activity2 = activitySource2.StartActivity("Activity2"))
{
activity2?.SetTag("foo", 2);
activity2?.SetTag("bar", "Hello, OpenTelemetry!");
Debug.Assert(activity2?.ParentId == activity1?.Id);
}
}
输出:
Activity.TraceId: 7497970c0c05341cadbbbd2b87b4246b
Activity.SpanId: ce96499cd0c115fd
Activity.TraceFlags: Recorded
Activity.ParentSpanId: 1cfead09b114a264
Activity.ActivitySourceName: TestSource2
Activity.DisplayName: Activity2
Activity.Kind: Internal
Activity.StartTime: 2023-09-25T13:05:36.0415480Z
Activity.Duration: 00:00:00.0000240
Activity.Tags:
foo: 2
bar: Hello, OpenTelemetry!
Resource associated with Activity:
service.name: MyCompany.MyProduct.MyService
service.version: 1.0.0
service.instance.id: 012ed685-54a3-4ec0-879b-aff9afcbd59c
telemetry.sdk.name: opentelemetry
telemetry.sdk.language: dotnet
telemetry.sdk.version: 1.6.0
Activity.TraceId: 7497970c0c05341cadbbbd2b87b4246b
Activity.SpanId: 1cfead09b114a264
Activity.TraceFlags: Recorded
Activity.ActivitySourceName: TestSource1
Activity.DisplayName: Activity1
Activity.Kind: Internal
Activity.StartTime: 2023-09-25T13:05:36.0413000Z
Activity.Duration: 00:00:00.0110830
Activity.Tags:
foo: 1
bar: Hello, World!
Resource associated with Activity:
service.name: MyCompany.MyProduct.MyService
service.version: 1.0.0
service.instance.id: 012ed685-54a3-4ec0-879b-aff9afcbd59c
telemetry.sdk.name: opentelemetry
telemetry.sdk.language: dotnet
telemetry.sdk.version: 1.6.0
两个 Activity 都有相同的 TraceId,表示它们属于同一个 Trace。
Activity1 在 Activity2 的外层作用域中创建,所以 Activity1 是 Activity2 的 Parent,Activity2 的 ParentId 等于 Activity1 的 Id。
Metrics
MeterProvider & Meter
Metrics 模块的使用和 Tracing 模块类似,通过 MeterProvider 来创建 Meter,然后通过 Meter 创建 Counter、Gauge、Measure 等。
var serviceName = "MyCompany.MyProduct.MyService";
var serviceVersion = "1.0.0";
var resourceBuilder = ResourceBuilder.CreateDefault()
.AddService(serviceName: serviceName, serviceVersion: serviceVersion);
using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("Meter1")
.SetResourceBuilder(resourceBuilder)
.AddConsoleExporter()
.Build();
var meter = new Meter(name: "Meter1", version: "1.0.0");
var counter = meter.CreateCounter<long>("counter");
counter.Add(100);
输出:
Resource associated with Metric:
service.name: MyCompany.MyProduct.MyService
service.version: 1.0.0
service.instance.id: 8b4fd315-6a8f-4198-ab1a-a4d11a14a431
telemetry.sdk.name: opentelemetry
telemetry.sdk.language: dotnet
telemetry.sdk.version: 1.6.0
Export counter, Meter: Meter1/1.0.0
(2023-09-24T13:18:45.2247000Z, 2023-09-24T13:18:45.2277870Z] LongSum
Value: 100
Metrics 的类型
OTel 定义了以下几种 Metric 类型:
- Counter:计数器,用于记录某个事件发生的次数,比如 HTTP 请求的次数、异常的次数等。
- Asynchronous Counter: Counter 的异步版本。
- UpDownCounter:和 Counter 一样用于记录某个事件发生的次数,但和 Counter 不同的是,UpDownCounter 可以增加和减少。
- Asynchronous UpDownCounter:UpDownCounter 的异步版本。
- Histogram :直方图,用于记录某个事件的分布情况,比如 HTTP 请求的耗时分布。
- Asynchronous Gauge:异步计量器,用于记录某个事件的瞬时值,比如 CPU 使用率、内存使用率等。
下面是各个类型在 Meter 中对应的创建方法:
- Counter:
CreateCounter - Asynchronous Counter:
CreateObservableCounter - UpDownCounter:
CreateUpDownCounter - Asynchronous UpDownCounter:
CreateObservableUpDownCounter - Histogram :
CreateHistogram - Asynchronous Gauge:
CreateObservableGauge
详细的介绍可以参考这几篇文章:
- https://www.timescale.com/blog/a-deep-dive-into-open-telemetry-metrics/
- https://opentelemetry.io/docs/specs/otel/metrics/api/
Logging
我们知道,.NET Core 有自己的 Logging 模块,可以通过 LoggerFactory 创建 ILogger,然后通过 ILogger 记录日志。
OTel SDK 的 Logging 模块,是 ILoggerProvider 的一个实现,将其注册到 LoggerFactory 中,就可以通过 ILogger 收集日志。
var serviceName = "MyCompany.MyProduct.MyService";
var serviceVersion = "1.0.0";
var resourceBuilder = ResourceBuilder.CreateDefault()
.AddService(serviceName: serviceName, serviceVersion: serviceVersion);
using var loggerFactory = LoggerFactory.Create(
builder => builder.AddOpenTelemetry(
options =>
{
options.AddConsoleExporter();
options.SetResourceBuilder(resourceBuilder);
}));
var logger = loggerFactory.CreateLogger("MyLogger");
logger.LogInformation("Hello World!");
输出:
LogRecord.Timestamp: 2023-09-25T13:09:19.2702090Z
LogRecord.CategoryName: MyLogger
LogRecord.Severity: Info
LogRecord.SeverityText: Information
LogRecord.Body: Hello World!
LogRecord.Attributes (Key:Value):
OriginalFormat (a.k.a Body): Hello World!
Resource associated with LogRecord:
service.name: MyCompany.MyProduct.MyService
service.version: 1.0.0
service.instance.id: 7f14c6d0-7d8b-490a-b4dc-bfb2275da108
telemetry.sdk.name: opentelemetry
telemetry.sdk.language: dotnet
telemetry.sdk.version: 1.6.0
Tracing、Metrics、Logging 三者的数据关联
在上面的例子中,我们单独使用了 Tracing、Metrics、Logging 模块,这三者的数据是相互独立的,没有关联。
我们把上面的例子放在一起看下
var serviceName = "MyCompany.MyProduct.MyService";
var serviceVersion = "1.0.0";
var resourceBuilder = ResourceBuilder.CreateDefault()
.AddService(serviceName: serviceName, serviceVersion: serviceVersion);
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(resourceBuilder)
.AddSource("TestSource1")
.AddSource("TestSource2")
.AddConsoleExporter()
.Build();
using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(resourceBuilder)
.AddMeter("Meter1")
.AddConsoleExporter()
.Build();
using var loggerFactory = LoggerFactory.Create(
builder => builder.AddOpenTelemetry(
options =>
{
options.AddConsoleExporter();
options.SetResourceBuilder(resourceBuilder);
}));
using var activitySource1 = new ActivitySource("TestSource1");
using var activitySource2 = new ActivitySource("TestSource2");
var logger = loggerFactory.CreateLogger("MyLogger");
var meter = new Meter("Meter1", "1.0.0");
var counter = meter.CreateCounter<long>("MyCounter");
using (var activity1 = activitySource1.StartActivity("Activity1"))
{
logger.LogInformation("Hello, Activity1!");
using (var activity2 = activitySource2.StartActivity("Activity2"))
{
logger.LogInformation("Hello, Activity2!");
counter.Add(100);
}
}
下面是输出内容的整理:
- 两个 Activity 的 TraceId 相同,表示它们属于同一个 Trace,Activity1 是 Activity2 的 Parent。

- 两次日志输出的 TraceId 是一样的,表示这两条日志属于同一个 Trace,但是它们的 SpanId 不同,表示这两条日志属于不同的 Span。

- Metrics 并没有记录 TraceId 和 SpanId,但和 Tracing、Logging 的 Resource 是一样的,表示它们属于同一个应用程序。通过 Resource 和 记录 Metrics 的时间,可以和 Tracing、Logging 的数据关联起来。
Resource associated with Metric:
service.name: MyCompany.MyProduct.MyService
service.version: 1.0.0
service.instance.id: 9f8306cb-c4a6-42f9-8d5b-897ba7f5df72
telemetry.sdk.name: opentelemetry
telemetry.sdk.language: dotnet
telemetry.sdk.version: 1.6.0
Export MyCounter, Meter: Meter1/1.0.0
(2023-09-25T13:38:33.6109280Z, 2023-09-25T13:38:33.6342240Z] LongSum
Value: 100
下期预告
下期将介绍如何在 ASP.NET Core 应用程序中使用 OpenTelemetry,并使用 Elastic APM 来收集数据。
使用 OpenTelemetry 构建 .NET 应用可观测性(3):.NET SDK 概览的更多相关文章
- OpenTelemetry - 云原生下可观测性的新标准
CNCF 简介 CNCF(Cloud Native Computing Foundation),中文为"云原生计算基金会",CNCF是Linux基金会旗下的基金会,可以理解为一个非 ...
- 当 .NET 5 遇上OpenTelemetry,会碰撞出怎样的火花?
OpenTelemetry 介绍 我在之前的几篇文章都介绍了 OpenTelemetry, 你可以在这里找到 OpenTelemetry - 云原生下可观测性的新标准 深入研究 .NET 5 的开放式 ...
- 基于 OpenTelemetry 的链路追踪
链路追踪的前世今生 分布式跟踪(也称为分布式请求跟踪)是一种用于分析和监控应用程序的方法,尤其是使用微服务架构构建的应用程序.分布式跟踪有助于精确定位故障发生的位置以及导致性能差的原因. 起源 链路追 ...
- gradle构建android项目
工具: Android Studio2.0 gradle-2.10 一.Android常识 在做Android开发的时候我们首先必须要有一个SDK.一般SDK的主要作用就是将硬件和软件进行分离,做软件 ...
- 快速构建Windows 8风格应用9-竖直视图
原文:快速构建Windows 8风格应用9-竖直视图 本篇博文主要介绍竖直视图概览.关于竖直视图设计.如何构建竖直视图 竖直视图概览 Windows 8为了支持旋转的设备提供了竖屏视图,我们开发的应用 ...
- Android官方技术文档翻译——开发工具的构建概述
本文译自Android官方技术文档<Build Overview>,原文地址:http://tools.android.com/build. 因为<Android Lint Chec ...
- [Android] 基于 Linux 命令行构建 Android 应用(七):自动化构建
本章将演示如何基于 Linux 命令行构建 Android 应用,在开始本章之前,希望你已经阅读之前几章内容. 本文环境为 RHEL Sandiego 32-bits,要基于 Linux CLI 构建 ...
- Expo大作战(四)--快速用expo构建一个app,expo中的关键术语
简要:本系列文章讲会对expo进行全面的介绍,本人从2017年6月份接触expo以来,对expo的研究断断续续,一路走来将近10个月,废话不多说,接下来你看到内容,讲全部来与官网 我猜去全部机翻+个人 ...
- Docker多阶段构建实战(multi-stage builds)
在编写Dockerfile构建docker镜像时,常遇到以下问题: RUN命令会让镜像新增layer,导致镜像变大,虽然通过&&连接多个命令能缓解此问题,但如果命令之间用到docker ...
- 使用maven-pom进行依赖管理与自动构建
使用maven-pom进行依赖管理与自动构建 span.kw { color: #007020; font-weight: bold; } /* Keyword */ code > span.d ...
随机推荐
- 插件化工程R文件瘦身技术方案 | 京东云技术团队
随着业务的发展及版本迭代,客户端工程中不断增加新的业务逻辑.引入新的资源,随之而来的问题就是安装包体积变大,前期各个业务模块通过无用资源删减.大图压缩或转上云.AB实验业务逻辑下线或其他手段在降低包体 ...
- 春秋杯春季联赛&&ciscn2023华北赛区部分题解
前言 复现几个比赛时没做出来的题 1.[CISCN 2023 华北赛区]ez_ruby 查文档可知 ruby内置的open函数,如果第一个字符是管道符|,后面就可以接命令.这可能是考察涉猎的知识范围广 ...
- Kubernetes(k8s)访问控制:权限管理之RBAC鉴权
目录 一.系统环境 二.前言 三.Kubernetes访问控制 四.鉴权简介 五.配置客户端机器 六.设置k8s集群允许所有请求访问 七.设置k8s集群拒绝所有请求访问 八.RBAC授权 8.1 ro ...
- 基于JavaFX的扫雷游戏实现(三)——交互逻辑
相信阅读过上期文章,动手能力强的朋友们已经自己跑出来界面了.所以这期我要讲的是交互部分,也就是对于鼠标点击事件的响应,包括计时计数对点击事件以及一些状态量的影响. 回忆下第一期介绍的扫雷规则和 ...
- 一文了解io包中的discard类型
1. 引言 io.discard是Go语言标准库提供一个结构体类型,其在丢弃不需要的数据场景下非常好用.本文我们将从io.discard 类型的基本定义出发,讲述其基本使用和实现原理,接着简单描述 i ...
- ListView选中获取数据并弹出菜单项
前言 作为一名Android小白,我在编写过程中,使用ListView列表,想要使用他来完成长按弹出菜单选项,并且还要进行事件操作,经过百度编程的经历后,终于成功完成.在此附上这块比较完整的代码,理论 ...
- 安装centos系统,硬盘检测报错:修改BIOS为 Legacy
进bios,将模式修改为legacy. 硬盘使用 MBR 分区,需要用 Legacy BIOS 启动,而不是 UEFI BIOS . 至于为什么安装的时候会报错? 可能是系统有这方面限制.也可能是别的 ...
- Powe AutoMate:列表操作
大纲 记录对列表的操作 创建列表 向列表中添加元素 添加多个 合并列表 运行结果 反转列表 反转前 反转后 删除列表中的重复项 结果: 减去列表 结果:
- QPushButton中常用的方法
常用方法如下所示: setCheckable():设置按钮是否已经被选中,如果设置为True,则表示按钮将保持已点击和释放状态. toggl():在按钮之间进行切换 setIcon():设置按钮上的图 ...
- 这些年写过的花式sql - 第3句 今日流失用户
第3句 今日流失用户 需求: 当日流失用户的定义:昨天登录的,今天没登录的用户数 有一张用户登录日志表,有字段 date_stamp(日期时间戳),用户id(uid).如果用户在某天登录了,该表会有一 ...