• 使用 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 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 创建 CounterGaugeMeasure 等。

    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 类型:

    1. Counter:计数器,用于记录某个事件发生的次数,比如 HTTP 请求的次数、异常的次数等。
    2. Asynchronous Counter: Counter 的异步版本。
    3. UpDownCounter:和 Counter 一样用于记录某个事件发生的次数,但和 Counter 不同的是,UpDownCounter 可以增加和减少。
    4. Asynchronous UpDownCounter:UpDownCounter 的异步版本。
    5. Histogram :直方图,用于记录某个事件的分布情况,比如 HTTP 请求的耗时分布。
    6. Asynchronous Gauge:异步计量器,用于记录某个事件的瞬时值,比如 CPU 使用率、内存使用率等。

    下面是各个类型在 Meter 中对应的创建方法:

    1. Counter:CreateCounter
    2. Asynchronous Counter: CreateObservableCounter
    3. UpDownCounter:CreateUpDownCounter
    4. Asynchronous UpDownCounter:CreateObservableUpDownCounter
    5. Histogram :CreateHistogram
    6. Asynchronous Gauge:CreateObservableGauge

    详细的介绍可以参考这几篇文章:

    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);
        }
    }
    

    下面是输出内容的整理:

    1. 两个 Activity 的 TraceId 相同,表示它们属于同一个 Trace,Activity1 是 Activity2 的 Parent。
    2. 两次日志输出的 TraceId 是一样的,表示这两条日志属于同一个 Trace,但是它们的 SpanId 不同,表示这两条日志属于不同的 Span。
    3. 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 来收集数据。

    欢迎关注个人技术公众号

  • 相关阅读:
    在iPhone上构建自定义数据采集完整指南
    图像处理入门:从平滑到面积测量的C++实践指南
    【基础语法】C语言、python、Java的输入(仅限控制台)
    5.24 基础题目
    StarRocks国产数据湖仓技术学习记录
    【Redis】链表和字典
    WSL安装异常:WslRegisterDistribution failed with error: 0xc03a001a
    【未完待续】计算机组成与体系结构第三次试验:微程序控制器实验
    前端监控系列3 | 如何衡量一个站点的性能好坏
    flask配置SSL证书,实现https服务
  • 原文地址:https://www.cnblogs.com/eventhorizon/p/17729014.html