惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threatpost
P
Privacy International News Feed
T
Tenable Blog
Know Your Adversary
Know Your Adversary
C
Cisco Blogs
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
G
GRAHAM CLULEY
爱范儿
爱范儿
U
Unit 42
K
Kaspersky official blog
C
Cybersecurity and Infrastructure Security Agency CISA
Jina AI
Jina AI
O
OpenAI News
L
LangChain Blog
PCI Perspectives
PCI Perspectives
P
Privacy & Cybersecurity Law Blog
Latest news
Latest news
Cisco Talos Blog
Cisco Talos Blog
F
Full Disclosure
L
Lohrmann on Cybersecurity
V
V2EX
L
LINUX DO - 热门话题
S
Security Affairs
量子位
Martin Fowler
Martin Fowler
云风的 BLOG
云风的 BLOG
Schneier on Security
Schneier on Security
月光博客
月光博客
MyScale Blog
MyScale Blog
C
CERT Recently Published Vulnerability Notes
AWS News Blog
AWS News Blog
博客园 - 叶小钗
Forbes - Security
Forbes - Security
W
WeLiveSecurity
T
Troy Hunt's Blog
J
Java Code Geeks
Hacker News - Newest:
Hacker News - Newest: "LLM"
Google DeepMind News
Google DeepMind News
Attack and Defense Labs
Attack and Defense Labs
Apple Machine Learning Research
Apple Machine Learning Research
雷峰网
雷峰网
Google Online Security Blog
Google Online Security Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
TaoSecurity Blog
TaoSecurity Blog
H
Help Net Security
The Cloudflare Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

博客园 - Hey,Coder!

jaeger界面显示异常 .net opentelemetry collector jaeger c# opentelemetry 自定义export c# 生成对象的初始化代码 Elasticsearch ILM 与 Data Stream 概念及实例说明文档 portainer httphelper封装 Ocelot + Consul + SignalR 粘性会话 正交软件架构 docker postgresql17 主从复制 rabbitmq总结与c#示例 docker rabbitmq quartz dashboard docker consul registrator 服务自动注册consul 微服务动态扩容 ubuntu24.04 安装docker RAG 检索增强生成 软考 - 架构设计师 知识点总结 c# MailKit3.4.3 发送邮件、附件 nginx 反向代理postgresql c# 信号量 elsa 3.5 中间件记录最后执行的节点数据 xp密钥 c# System.Text.Json 反序列化Dictionary<string,object>时未转换基础类型的处理方法 docker配置代理 openclaw 接入 LMStudio的模型服务 wpf canvas 移动 缩放 windows平台openclaw搭建 wpf scrollerview触摸滚动 c# 动态切换sqlsugar连接字符串 c# Elastic.Clients.Elasticsearch 动态查询 c# es 封装Elastic.Clients.Elasticsearch c# scrollerview滚动到指定元素位置 WPF 重写Expander 基于elsa工作流封装一套变量、组件的体系 常用活动重写 DynamicExpresso 轻量级动态表达式库 c# elsa 3.5.2 程序化工作流常用功能及自定义中间件 leaflet docker 连接老版本的sqlserver ssl错误 c# quartz 动态创建任务 控制任务运行和停止 依赖注入 cefsharp 模拟点击 批量重置数据库的数据 docker 复制远程镜像本地并创建容器 c# newtonsoft dynamic
c# opentelemetry 自定义export
Hey,Coder! · 2026-07-22 · via 博客园 - Hey,Coder!

OpenTelemetry 自定义 Exporter + MQ + 请求体记录

整体架构

.NET App (OTel SDK) → 自定义 Exporter → MQ (RabbitMQ/Kafka) → 消费者 → Elasticsearch
  • 目的:解耦应用与 ES,利用 MQ 削峰填谷,提高稳定性。
  • 优势:应用不直接依赖 ES 可用性,消费者可按需批量写入。

nuget

dotnet add package OpenTelemetry --version 1.17.0
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol --version 1.17.0
dotnet add package OpenTelemetry.Extensions.Hosting --version 1.17.0
dotnet add package OpenTelemetry.Instrumentation.AspNetCore --version 1.17.0
dotnet add package OpenTelemetry.Instrumentation.Http --version 1.17.0

自定义 Exporter 实现

1. 继承 BaseExporter<Activity>

/// <summary>
/// 
/// </summary>
public class RabbitMqTraceExporter : BaseExporter<Activity>
{
    //private readonly IMqPublisher _publisher;  // 你的 MQ 发布接口

    public RabbitMqTraceExporter()
    {
    }

    public override ExportResult Export(in Batch<Activity> batch)
    {
        //关键:抑制自产自销的遥测,避免无限循环[6](@ref)
        using var scope = SuppressInstrumentationScope.Begin();

        try
        {
            foreach (var activity in batch)
            {
                // 将 Activity 转换为可序列化对象
                var spanDoc = new Dictionary<string, object?>
                {
                    ["traceId"] = activity.TraceId.ToHexString(),
                    ["spanId"] = activity.SpanId.ToHexString(),
                    ["parentSpanId"] = activity.ParentSpanId.ToHexString(),
                    ["name"] = activity.DisplayName,
                    ["kind"] = (int)activity.Kind,
                    ["startTimeUnixNano"] = activity.StartTimeUtc.Ticks * 100,
                    ["endTimeUnixNano"] = (activity.StartTimeUtc + activity.Duration).Ticks * 100,
                    ["attributes"] = activity.TagObjects.Select(t => new
                    {
                        key = t.Key,
                        value = new { stringValue = t.Value?.ToString() }
                    }).ToList(),
                    ["resource"] = ParentProvider?.GetResource()?.Attributes
                        .Select(a => new { key = a.Key, value = a.Value }).ToList()
                };

                // 发送到 MQ(注意:SDK 不做重试,需要自己实现)[6](@ref)
                //_publisher.Publish("trace", spanDoc);
            }
            return ExportResult.Success;
        }
        catch (Exception ex)
        {
            // 绝不能抛出异常[6](@ref)
            Console.Error.WriteLine($"MQ export failed: {ex.Message}");
            return ExportResult.Failure;
        }
        return ExportResult.Success;
    }
}

注册函数封装

    public static void AddOT(this WebApplicationBuilder builder, IConfiguration configuration)
    {
        //builder.Services.AddOpenTelemetry()
        //.ConfigureResource(resource => resource.AddService("my-service"))
        //.WithTracing(tracing => tracing
        //    .AddAspNetCoreInstrumentation()
        //    .AddHttpClientInstrumentation()
        //    .AddOtlpExporter(opt =>  // ← 现在能找到了
        //    {
        //        opt.Endpoint = new Uri("https://your-es:9200/_otlp");
        //        opt.Protocol = OtlpExportProtocol.HttpProtobuf;
        //        opt.Headers = "Authorization=ApiKey your_api_key";
        //    }));



        builder.Services.AddOpenTelemetry()
            .ConfigureResource(resource => resource.AddService("my-service"))
            .WithTracing(tracing => tracing
                .AddAspNetCoreInstrumentation(options =>
                {
                    // 1) 过滤:排除健康检查
                    options.Filter = ctx =>
                        !ctx.Request.Path.StartsWithSegments("/health");

                    // 2) 记录异常详情
                    options.RecordException = true;

                    // 3) 入参:EnrichWithHttpRequest
                    options.EnrichWithHttpRequest = async (activity, request) =>
                    {
                        var ctx = request.HttpContext;

                        // —— 基础信息 ——
                        activity.SetTag("request.protocol", request.Protocol);
                        activity.SetTag("request.scheme", request.Scheme);
                        activity.SetTag("request.host", request.Host.ToString());
                        activity.SetTag("request.path", request.Path.ToString());
                        activity.SetTag("request.query_string", request.QueryString.ToString());

                        // —— 客户端 IP ——
                        activity.SetTag("request.client_ip",
                            ctx.Connection.RemoteIpAddress?.ToString());

                        // —— Query 参数(扁平化)——
                        foreach (var q in request.Query)
                        {
                            activity.SetTag($"request.query.{q.Key}", q.Value.ToString());
                        }

                        // —— 指定 Header(注意脱敏)——
                        if (request.Headers.TryGetValue("X-Correlation-Id", out var corr))
                            activity.SetTag("request.correlation_id", corr.ToString());
                        if (request.Headers.TryGetValue("User-Agent", out var ua))
                            activity.SetTag("request.user_agent", ua.ToString());

                        // —— 请求体(仅当存在且体积可控)——
                        if (request.ContentLength > 0 && request.ContentLength <= 1024 * 1024)
                        {
                            try
                            {
                                // 确保已启用缓冲(以防外部未设置)
                                request.EnableBuffering();

                                // 1. 将 Body 内容复制到 MemoryStream
                                 var memoryStream = new MemoryStream();
                                await request.Body.CopyToAsync(memoryStream);
                                memoryStream.Position = 0;

                                // 2. 读取 Body 内容
                                using var reader = new StreamReader(memoryStream, Encoding.UTF8, leaveOpen: true);
                                var body = reader.ReadToEnd();
                                activity.SetTag("request.body", body);

                                // 3. 将 request.Body 替换为 MemoryStream(可 Seek)
                                memoryStream.Position = 0;
                                request.Body = memoryStream; // 替换后,MVC 模型绑定就能正常读取了
                            }
                            catch (Exception ex)
                            {
                                // 兜底,防止读取 Body 导致主程序崩溃
                                activity.SetTag("request.body.error", ex.Message);
                            }
                        }
                    };

                    // 4) 出参:EnrichWithHttpResponse
                    options.EnrichWithHttpResponse = async (activity, response) =>
                    {
                        activity.SetTag("response.status_code", response.StatusCode);
                        activity.SetTag("response.content_type", response.ContentType);

                        if (response.HttpContext.Items.TryGetValue("response_body", out var bodyObj))
                        {
                            var body = bodyObj as string;
                            if (!string.IsNullOrEmpty(body))
                            {
                                // 脱敏
                                //body = MaskSensitiveFields(body);
                                activity.SetTag("response.body", body);
                            }
                        }
                    };

                    // 5) 异常:EnrichWithException
                    options.EnrichWithException = (activity, exception) =>
                    {
                        activity.SetTag("exception.type", exception.GetType().FullName);
                        activity.SetTag("exception.message", exception.Message);
                        // 堆栈通过 RecordException=true 自动作为 ActivityEvent 附加
                    };
                })
                .AddHttpClientInstrumentation()
                .AddHttpClientInstrumentation()
                // 使用工厂函数注入自定义 Exporter,并用 BatchActivityExportProcessor 包装
                .AddProcessor(sp =>
                {
                    //var mqPublisher = sp.GetRequiredService<IMqPublisher>();
                    var exporter = new RabbitMqTraceExporter();

                    // 生产环境必须用 Batch,不要用 Simple[6](@ref)
                    return new BatchActivityExportProcessor(
                        exporter,
                        maxQueueSize: 2048,
                        scheduledDelayMilliseconds: 5000,
                        exporterTimeoutMilliseconds: 30000,
                        maxExportBatchSize: 512);
                }));
    }

注册

builder.AddOT(builder.Configuration);

必须的管道配置(Program.cs)

app.Use((context, next) =>
{
    context.Request.EnableBuffering();
    return next();
});
// 然后才是 UseRouting、UseMiddleware 等

//在routing后面增加

app.UseMiddleware<ResponseCaptureMiddleware>();  // 放在路由之后、端点之前

ResponseCaptureMiddleware

 public class ResponseCaptureMiddleware
 {
     private readonly RequestDelegate _next;

     public ResponseCaptureMiddleware(RequestDelegate next) => _next = next;

     public async Task InvokeAsync(HttpContext context)
     {
         var originalBodyStream = context.Response.Body;

         using var memoryStream = new MemoryStream();
         context.Response.Body = memoryStream;

         await _next(context);

         // 读取响应体
         memoryStream.Position = 0;
         var responseBody = await new StreamReader(memoryStream).ReadToEndAsync();

         // 把响应体挂到 HttpContext.Items,供 Enrich 回调读取
         if (responseBody.Length <= 1024 * 1024)
             context.Items["response_body"] = responseBody;

         // 把流写回原始 Response
         memoryStream.Position = 0;
         await memoryStream.CopyToAsync(originalBodyStream);
         context.Response.Body = originalBodyStream;
     }
 }

性能与安全提醒

  1. 限制 Body 大小ContentLength <= 1MB,防止大文件撑爆 MQ/ES。
  2. 脱敏:在记录前替换敏感字段(密码、Token 等)。
  3. 采样:高流量接口启用采样(如 TraceIdRatioBasedSampler)。
  4. 抑制自监控SuppressInstrumentationScope.Begin() 防止 Exporter 自身产生的遥测被再次采集。
  5. 包版本对齐:所有 OpenTelemetry.* 包版本一致(推荐 1.9+)。