如何利用C# Unity Interception实现带关联ID的调用链路日志
这个问题我太熟了!嵌套调用的日志追踪确实容易出现这种“分不清谁调用谁”的情况,用关联ID绑定整个调用链是最直接的解决方案,刚好Unity Interceptor可以配合上下文存储来实现,我给你捋捋具体怎么做:
核心思路
要让嵌套调用的日志共享同一个关联ID,关键是在调用链的上下文中传递这个ID——也就是进入MethodA时生成ID,调用MethodB的时候直接复用这个ID,而不是每次都生成新的。这里推荐用AsyncLocal<string>来存储关联ID,它能在同步/异步调用中都保持上下文唯一性,不会出现串号的问题。
具体实现步骤
1. 写一个关联ID管理器
先整个静态类来管关联ID,用AsyncLocal存,确保每个调用链有独立的ID:
public static class CorrelationIdManager { private static readonly AsyncLocal<string> _correlationId = new AsyncLocal<string>(); public static string GetCorrelationId() { if (string.IsNullOrEmpty(_correlationId.Value)) { // 这里用简化的Guid当ID,你也可以换成自增序列、业务标识之类的 _correlationId.Value = Guid.NewGuid().ToString("N").Substring(0, 6); } return _correlationId.Value; } // 可选:手动设置ID的方法,比如外部调用时指定自定义ID public static void SetCorrelationId(string id) { _correlationId.Value = id; } // 可选:调用链结束后清空ID,避免上下文残留 public static void ClearCorrelationId() { _correlationId.Value = null; } }
2. 自定义日志拦截器
接下来写一个实现IInterceptor的日志拦截器,在方法调用前后获取/复用关联ID,输出带ID的日志:
public class LoggingInterceptor : IInterceptor { private readonly ILogger _logger; // 这里假设你用了ILogger,换成自己的日志组件也没问题 public LoggingInterceptor(ILogger logger) { _logger = logger; } public void Intercept(IInvocation invocation) { var correlationId = CorrelationIdManager.GetCorrelationId(); var methodName = invocation.Method.Name; // 输出调用开始日志 _logger.LogInformation("[Id: {CorrelationId}] Method {MethodName} is called", correlationId, methodName); try { // 执行目标方法——这里会触发嵌套方法的拦截器,此时CorrelationId已经存在,直接复用 invocation.Proceed(); } catch (Exception ex) { _logger.LogError(ex, "[Id: {CorrelationId}] Method {MethodName} threw an exception", correlationId, methodName); throw; // 别吞异常,继续往上抛 } finally { // 输出调用完成日志 _logger.LogInformation("[Id: {CorrelationId}] Method {MethodName} completed", correlationId, methodName); // 小贴士:如果是顶级入口方法,可以在这里调用ClearCorrelationId()清理上下文,嵌套方法别清,留给上层处理 } } }
3. 配置Unity容器
把拦截器注册到Unity容器,确保需要拦截的类都能用上:
var container = new UnityContainer(); // 注册日志组件(示例用Microsoft.Extensions.Logging,换成Serilog/NLog都可以) container.RegisterInstance<ILogger>(LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger("App")); // 注册拦截器 container.RegisterType<LoggingInterceptor>(); // 注册需要拦截的服务类,并绑定拦截器 container.RegisterType<IMyService, MyService>( new Interceptor<InterfaceInterceptor>(), new InterceptionBehavior<LoggingInterceptor>() );
4. 测试调用链
假设你的服务类是这样的:
public interface IMyService { void MethodA(); void MethodB(); } public class MyService : IMyService { private readonly IMyService _self; // 用自身实例触发拦截器,注入其他依赖也一样 public MyService(IMyService self) { _self = self; } public void MethodA() { _self.MethodB(); // 嵌套调用MethodB } public void MethodB() { // 你的业务逻辑 } }
当调用MethodA时,日志会输出:
[Id: 0a1b2c] Method MethodA is called
[Id: 0a1b2c] Method MethodB is called
[Id: 0a1b2c] Method MethodB completed
[Id: 0a1b2c] Method MethodA completed
如果独立调用MethodB,会生成新的关联ID:
[Id: 3d4e5f] Method MethodB is called
[Id: 3d4e5f] Method MethodB completed
额外注意点
- 异步方法场景下,
AsyncLocal是最优选择,它会自动在异步流中传递上下文;如果用CallContext,异步场景下容易出问题,需要手动处理。 - 关联ID的生成规则可以按需调整,比如结合请求ID(Web应用)、业务流水号等,方便后续排查问题。
内容的提问来源于stack exchange,提问作者Ray Tang

