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

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
月光博客
月光博客
Martin Fowler
Martin Fowler
人人都是产品经理
人人都是产品经理
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog
T
Tailwind CSS Blog
爱范儿
爱范儿
博客园_首页
Last Week in AI
Last Week in AI
N
Netflix TechBlog - Medium
NISL@THU
NISL@THU
H
Help Net Security
A
Arctic Wolf
K
Kaspersky official blog
Y
Y Combinator Blog
G
GRAHAM CLULEY
T
Threatpost
I
Intezer
Attack and Defense Labs
Attack and Defense Labs
Jina AI
Jina AI
Recent Announcements
Recent Announcements
D
Docker
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
GbyAI
GbyAI
The Hacker News
The Hacker News
C
Cyber Attacks, Cyber Crime and Cyber Security
Help Net Security
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
G
Google Developers Blog
F
Fortinet All Blogs
S
Security Affairs
Security Archives - TechRepublic
Security Archives - TechRepublic
量子位
AWS News Blog
AWS News Blog
Google DeepMind News
Google DeepMind News
罗磊的独立博客
V
Vulnerabilities – Threatpost
阮一峰的网络日志
阮一峰的网络日志
T
Tor Project blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
H
Hacker News: Front Page
Security Latest
Security Latest
The Cloudflare Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
M
MIT News - Artificial intelligence
B
Blog
云风的 BLOG
云风的 BLOG

博客园 - 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+)。