C#如何保证 Activity 不为 null

Intro

之前写过一篇 “基于 Activity 来实现后台服务的日志追踪”,我们的服务里基本也是这样做的,前段时间发现有时间日志里会没有 traceId, 即使有 parentId 也可能没有 traceId, 后来发现这些没有 traceId 的数据基本都是 parentId 没有被采样的,而在测试环境之前我们设置的是始终采样所以在测试环境之前没有这个问题,起初的想法是解决当有 parentId 的时候,保证创建的 activity 不为 null 至于没有 parentId 也没有被采样的就不关心了,但是后面又想了一下觉得即使没有被采样,activity 也不应该为 null 我们不仅仅依赖它做 tracing 还依赖它做 logging,tracing 可以没有,但是 logging 要有,不然一些日志就没有办法串联起来了.

Sample

我们来看一个简单的示例,来证实一下前面说的问题,首先使用原生的 activity 的 API,示例如下:

var activitySource = new ActivitySource("test");
using var activityListener = new ActivityListener();
activityListener.ShouldListenTo = _ => true;
activityListener.ActivityStarted = activity =>
{
    Console.WriteLine($"activity {activity.DisplayName} started, activityId: {activity.Id}, traceId: {activity.TraceId}");
};
activityListener.ActivityStopped = activity =>
{
    Console.WriteLine($"activity {activity.DisplayName} stopped, activityId: {activity.Id}, traceId: {activity.TraceId}");
};
activityListener.Sample = (ref ActivityCreationOptions<ActivityContext> _) => 
     Random.Shared.Next(100) < 90 ? ActivitySamplingResult.None : ActivitySamplingResult.AllData;

ActivitySource.AddActivityListener(activityListener);


for (var i = 0; i < 10; i++)
{
    using var activity = activitySource.StartActivity();
    if (activity is null)
        Console.WriteLine("activity is null");
}

我们这里使用 Random 来模拟一个概率采样,这里模拟的有 90% 的概率是完全不采样,这种情况创建出来的 activity 会是 null,我们通过一个简单的 for 循环创建 10 个 activity 来看一下输出结果,输出结果如下:

C#如何保证 Activity 不为 null

输出结果有 9 个 activity 是 null,有一个不是 null 是有 activityId/traceId 的

我们改一下采样的逻辑如下:

activityListener.Sample = (ref ActivityCreationOptions<ActivityContext> options) => 
     options.Source.Name == activitySource.Name ? ActivitySamplingResult.AllData : ActivitySamplingResult.None;

我们可以通过 ActivitySource 的过滤实现我们自己业务的数据完全采样其他的数据不采样或者百分比采样等,这样的话我们只关注我们自己业务的ActivitySource 创建的数据,再来试一下

C#如何保证 Activity 不为 null

可以看到这次我们所有的 activity 都不是 null 了

这里说明一下 AllData 和 AllDataAndRecorded,可以看到前面示例的 activityId 最后一位都是 0,这意味着它是不会被采样到 tracing 系统中的,只有最后一位是 1 才会认为是采样并记录成功,并且下游的 tracing 记录也会被记录下来,实际我们用的话会是希望一部分数据会采样并记录的,我们稍作修改一下我们的采样逻辑

activityListener.Sample = (ref ActivityCreationOptions<ActivityContext> options) => options.Source.Name == activitySource.Name
    ? Random.Shared.Next(100) < 90 ? ActivitySamplingResult.AllData : ActivitySamplingResult.AllDataAndRecorded
    : ActivitySamplingResult.None;

我们在前面两个示例的基本上做了一下整合,有 10% 的概率会被采样并记录,再跑一下我们的示例:

C#如何保证 Activity 不为 null

这里采样并记录的概率有点高,因为我们的数据量比较少,难免会有点误差

有时候我们如果配置有问题导致 ActivitySource 创建的 activity 是 null 但仍然想生成一个 activity,我们应该怎么做呢?从 asp.net core 的源码里找到了一种方式,我们可以直接 new 一个 Activity 作为 fallback,修改 for 循环的逻辑如下,并使用第一个示例的采样逻辑

using var activity = activitySource.CreateActivity(nameof(MainTest), ActivityKind.Internal) 
                         ?? new Activity(nameof(MainTest));
activity.Start();
Console.WriteLine($"new activity created {activity.Source.Name}, {activity.Id}");

输出结果如下:

C#如何保证 Activity 不为 null

可以看到 activity 会有两种形式,一种 ActivitySource name 是 test 这种是由我们 activitySource.CreateActivity() 创建的,而另外一种则是通过 new Activity() 创建的,这样我们就能始终拥有一个 activity 了

More

我们实际去用的话可能大概率不会直接使用 activity 的 API 去收集数据,大多会使用 OpenTelemetry 封装好的 API 去做

在 OpenTelemetry 里我们可以自定义一个 Sampler 来控制采样的逻辑,但是目前 OpenTelemtry 的采样参数里没有 ActivitySource 参数,所以目前是没有办法直接通过 OpenTelemetry Sampler 来针对 ActivitySource 来做定制的,提了一个 issue 希望能增加 ActivitySource, 但是一段时间内应该是不会出现的

https://github.com/open-telemetry/opentelemetry-dotnet/issues/4752