借助 Interceptor 实现拦截 IServiceProvider.CreateScope
Intro
之前写过几篇后台服务的日志追踪的文章如 基于 Activity 来实现后台服务的日志追踪,之前我们提到过 IServiceProvider.CreateScope
是没办法拦截的,但是借助 C# 12 中的 Interceptor 拦截代码中的 CreateScope
就变得可能了,一起来看下吧
Sample
我们的实现目标是创建 scope 的时候自动创建一个 Activity
,考虑一些自定义的需求,我们可以通过一个服务来创建,通过 config 来配置这个服务的行为,比如下面服务的 ActivitySource
的配置,以及自动附加一些 activity 的 tag 等等
public sealed class ActivityScope : IDisposable, IAsyncDisposable
{
private static readonly ActivitySource ActivitySource = new(nameof(ActivityScope));
private readonly Activity? _activity = ActivitySource.StartActivity(nameof(ActivityScope));
public Activity? Activity => _activity;
public void Dispose()
{
Console.WriteLine("Dispose ing...");
_activity?.Dispose();
Console.WriteLine("Dispose done");
}
public ValueTask DisposeAsync()
{
Console.WriteLine("DisposeAsync ing...");
_activity?.Dispose();
Console.WriteLine("DisposeAsync done");
return ValueTask.CompletedTask;
}
}
测试代码如下:
using var activityListener = new ActivityListener();
activityListener.ShouldListenTo = s => s.Name == nameof(ActivityScope);
activityListener.Sample = (ref ActivityCreationOptions<ActivityContext> _) =>
ActivitySamplingResult.PropagationData;
ActivitySource.AddActivityListener(activityListener);
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<ActivityScope>();
await using var serviceProvider = serviceCollection.BuildServiceProvider();
{
using var scope = serviceProvider.CreateScope();
Console.WriteLine("activity scope activityId:");
Console.WriteLine(scope.ServiceProvider.GetRequiredService<ActivityScope>().Activity?.Id);
Console.WriteLine();
Console.WriteLine("Current activityId:");
Console.WriteLine(Activity.Current?.Id);
}
Console.WriteLine();
{
await using var scope = serviceProvider.CreateAsyncScope();
Console.WriteLine("CreateAsyncScope Current activityId:");
Console.WriteLine(Activity.Current?.Id);
}
直接跑这个代码输出的结果如下:
在 ActivityScope
实例创建之前 activityId 是空,在实例创建之后,Activity.Current?.Id
就有值了
我们可以通过 intercept CreateScope
方法,在 CreateScope
之后马上创建 ActivityScope
实例,这样我们就实现了自动创建 Activity
Interceptor 代码如下:
namespace CSharp12Sample.Generated
{
public static partial class GeneratedActivityScope
{
[System.Runtime.CompilerServices.InterceptsLocationAttribute(@"C:\projects\sources\SamplesInPractice\CSharp12Sample\ServiceScopeActivitySample.cs", 19, 47)]
public static Microsoft.Extensions.DependencyInjection.IServiceScope ScopeActivityInterceptorMethod(this System.IServiceProvider provider)
{
System.Console.WriteLine("scope creating...");
var scope = provider.CreateScope();
_ = scope.ServiceProvider.GetRequiredService<CSharp12Sample.ActivityScope>();
System.Console.WriteLine("scope created...");
return scope;
}
[System.Runtime.CompilerServices.InterceptsLocationAttribute(@"C:\projects\sources\SamplesInPractice\CSharp12Sample\ServiceScopeActivitySample.cs", 30, 53)]
public static Microsoft.Extensions.DependencyInjection.AsyncServiceScope ScopeActivityInterceptorAsyncMethod(this System.IServiceProvider provider)
{
System.Console.WriteLine("async scope creating...");
var scope = provider.CreateAsyncScope();
_ = scope.ServiceProvider.GetRequiredService<CSharp12Sample.ActivityScope>();
System.Console.WriteLine("async scope created...");
return scope;
}
}
}
不要忘记添加 InterceptsLocationAttribute
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute;
}
使用 Interceptor 之后的输出结果如下:
可以看到现在我们的输出 Activity.Current?.Id
始终是有值的,并且,第一个示例中的 activityId 始终是一致的,因为同在一个 scope 内,activity 只创建了一次,并且在创建 scope 的时候会打印 scope creating
/scoped created
前面的 interceptor 代码可以通过 source generator 来生成,这里主要介绍一下实现思路,具体 source generator 的代码可以参考:https://github.com/WeihanLi/SamplesInPractice/blob/main/CSharp12Sample/ScopeActivityGenerator.cs
前面示例中使用了一个外部的 service,也可以直接在 interceptor 代码里直接 create 一个 activity 来代替这个 service,实现方式可以根据自己的需要选择
借助 interceptor 我们可以不再需要显式的修改 CreateScope 的代码,可以比较方便的拦截代码里的 CreatScope 方法调用来实现自动生成 activity,对于多个 CreateScope 和 内嵌的 CreateScope 也是支持的
例如这样的一个内嵌的 CreateScope 调用
生成的 interceptor 会多一个调用的地方
运行代码可以看到下面这样的输出结果
References
https://github.com/WeihanLi/SamplesInPractice/blob/main/CSharp12Sample/ScopeActivityGenerator.cs
https://github.com/WeihanLi/SamplesInPractice/blob/main/CSharp12Sample/ServiceScopeActivitySample.cs