只有累積,沒有奇蹟

2026年3月8日 星期日

[.NET] 可觀測性從 0 到 1:用 eShop 踩坑實錄

前言

可觀測性不是一次性的架構決策,而是跟著痛點長出來的。

在 .NET Conf 2024 有機會分享了一段關於可觀測性實踐的歷程,收到不少朋友的回饋,說希望能看到更完整的文字版。這篇文章就是那次分享的延伸,我會用 dotnet/eShop 這個官方微服務範例來說明,從一片空白的 console log 開始,到最後可以在 Grafana 裡 3 分鐘內完成跨服務的根因分析,中間每一步都有一個具體的痛點觸發它。

如果你還在思考「為什麼要做可觀測性」這個更前端的問題,可以先看我 2025 那場 為什麼我們需要 Observability?——那場講的是 Why。這篇文章假設你已經被說服,接下來要回答的是 How。

eShop 是 .NET 官方的電商微服務範例,包含 Catalog、Basket、Ordering、Identity 等服務,透過 HTTP 和 RabbitMQ 互相溝通,架構夠貼近真實場景。如果你還沒跑過這個專案,推薦先 clone 下來感受一下多服務同時運作的複雜度。

這篇文章不是「可觀測性完全指南」,而是一份演進記錄:每個工具在什麼情境下被引入、它解決了什麼問題。若對以上內容有問題或不清楚的地方,歡迎提出來一起討論。

這篇是「.NET 可觀測性四部曲」的第一篇,講個人工程師怎麼從 0 到 1 把工具裝起來。如果你讀完之後在團隊層級、方法論層級、3.0 反思層還有進一步的問題,後三篇是對應的延伸:

  • 第一篇(本文):從 0 到 1 — 用 eShop 踩坑實錄(工具層)
  • 第二篇:落地之後 — 成本、規模化與 SLO 的三個真相(組織治理層)
  • 第三篇:從可觀測性到 ODD — 把觀測性左移到開發流程的五個步驟(方法論層)
  • 第四篇:AI x Observability — 當 AI 答對了,但沒人知道為什麼(反思層)

四篇對應 Observability 1.0 → AI x o11y 的演進路徑,可以從任一篇進入。


起點:一片空白的 Console

場景:開發初期。 你剛把 eShop 微服務專案 clone 下來、dotnet run 把各服務跑起來,準備接著開發新功能。各服務啟動後的 console 輸出大概是這樣:

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.

實際截圖
看起來沒什麼問題。直到第一次出問題。


第一階段:系統掛了,但不知道是誰的問題

情境

場景:開發階段,前後端串接除錯。 前端工程師在 channel 回報結帳功能串接後端 API 失敗——畫面卡在 spinning、沒有具體錯誤訊息。你接到請求要找出問題。

打開各服務的 terminal,每個服務看起來都在跑,沒有明顯的 exception。問題是:eShop 的結帳流程依序呼叫 Basket API → Ordering API → Payment Service,三個服務各跑在不同 port,只能人工比對時間戳記來找到請求斷在哪裡——這樣的偵錯方式在微服務架構下非常耗時。

解法:OpenTelemetry Traces + TraceId

引入 OpenTelemetry Tracing,讓每一個跨服務請求都帶上同一個 TraceId,讓呼叫鏈可以被串起來,不再需要人工比對。

安裝套件:

dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Exporter.Console

Program.cs 加入:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService("ordering-api"))
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddConsoleExporter()
    );

加入 OTel 後,console 輸出變成:

Activity.TraceId:          3e2a1b4c8d9f0e1a2b3c4d5e6f7a8b9c
Activity.SpanId:           1a2b3c4d5e6f7a8b
Activity.ParentSpanId:     9f8e7d6c5b4a3f2e
Activity.DisplayName:      POST /api/orders
Activity.Kind:             Server
Activity.Duration:         00:00:02.3421
Activity.StatusCode:       Error
Activity.Tags:
    http.method: POST
    http.status_code: 500



技術重點

W3C TraceContext 規範定義了 traceparent header。AddHttpClientInstrumentation() 在發出 outgoing request 時會自動帶上這個 header;下游服務的 AddAspNetCoreInstrumentation() 則自動解析它並建立子 Span。整個傳遞機制不需要額外的程式碼。

小小建議:SetResourceBuilder 加上 service name,在多服務場景下讓 span 的來源一目了然,這個習慣養起來之後在查 trace 時省很多時間。

改變了什麼:找到問題服務的時間從 1 小時降到 15 分鐘。


第二階段:知道哪個服務壞了,但不知道為什麼

情境

場景:仍在開發階段,承接前一段。 Trace 把問題定位到 Ordering.API 之後,下一步是看 log 找根本原因——但這時候會發現一個更深的問題:log 雖然有,但內容太貧乏,看了等於沒看。

TraceId 告訴我問題出在 Ordering.API。打開它的 log:

info: Ordering.API[0]
      An error occurred.
fail: Ordering.API[0]
      Exception thrown.

orderId 是什麼?哪個 buyer?Exception 的 message 呢?這兩行看了等於沒看。

解法:結構化日誌(Structured Logging)

改用結構化 log,讓每筆 log 帶有可查詢的具名欄位,而不只是人類可讀的字串。

安裝套件:

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Enrichers.Span   # 自動注入當前 Activity 的 TraceId / SpanId

Program.cs 設定:

builder.Host.UseSerilog((ctx, cfg) => cfg
    .ReadFrom.Configuration(ctx.Configuration)
    .Enrich.WithSpan()                                    // TraceId / SpanId 自動附上
    .Enrich.WithMachineName()
    .WriteTo.Console(new RenderedCompactJsonFormatter())
);

在 Controller 加上有意義的 log:

[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
{
    _logger.LogInformation(
        "Creating order for buyer {BuyerId}, item count: {ItemCount}",
        command.BuyerId,
        command.Items.Count
    );

    try
    {
        var result = await _mediator.Send(command);
        _logger.LogInformation(
            "Order {OrderId} created in {ElapsedMs}ms",
            result.OrderId,
            sw.ElapsedMilliseconds
        );
        return Ok(result);
    }
    catch (InsufficientStockException ex)
    {
        _logger.LogWarning(
            "Order rejected for buyer {BuyerId}: insufficient stock for SKU {Sku}",
            command.BuyerId,
            ex.Sku
        );
        return BadRequest(new { error = ex.Message });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex,
            "Unexpected error creating order for buyer {BuyerId}",
            command.BuyerId
        );
        throw;
    }
}

加入結構化 log 後,console 輸出變成:

{
  "Timestamp": "2024-01-15T14:32:01.123Z",
  "Level": "Warning",
  "MessageTemplate": "Order rejected for buyer {BuyerId}: insufficient stock for SKU {Sku}",
  "BuyerId": "usr_9981",
  "Sku": "SKU-4421",
  "TraceId": "3e2a1b4c8d9f0e1a2b3c4d5e6f7a8b9c",
  "SpanId": "1a2b3c4d5e6f7a8b",
  "MachineName": "ordering-api-pod-7f9b"
}

Before : 「An error occurred.」

After : 帶有 BuyerId、Sku、TraceId 的 JSON log


技術重點

有一個常見錯誤值得特別注意:不要用字串插值 $"buyer {id}" 寫 log message,這會讓 id 的值被拼進字串,無法作為獨立欄位查詢。一定要用 named template {BuyerId},Serilog 才能把它存成結構化欄位。

WithSpan() 會自動把當前 Activity 的 TraceId / SpanId 注入每筆 log,不需要手動傳遞,這個 enricher 幾乎是必裝的。

改變了什麼:找到根本原因的時間從 15 分鐘降到 5 分鐘。


第三階段:問題解了,但不知道系統快撐不住了

情境

場景:整合完畢,準備上 Production。 系統 dev 階段該修的都修了,QA 也跑過了。在某次 sprint review,PM 提了一個情境讓你冷汗:「下個月雙 11 大促預估流量是平常的 5 倍,你怎麼確定當天不會出事?我們需要在事情發生之前就知道。

這個問題你答不出來——因為現在的觀測能力是「事情發生了再追查」,不是「事情發生前先預警」。

事實上更早一次大促活動就已經給過教訓:Ordering 服務的回應時間悄悄從 50ms 爬到 800ms,這個過程花了將近 40 分鐘,一直到出現大量 timeout、用戶開始投訴,工程師才發現。事後回頭看,這 40 分鐘完全可以被提早預警到。

事後救火的成本遠高於事前預警,這個體驗讓我意識到缺少了 Metrics 這一塊。

解法:Metrics 監控水位

引入 OpenTelemetry Metrics,持續輸出服務健康指標,並在 Grafana 設定告警閾值。

安裝套件:

dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Exporter.Prometheus.AspNetCore

Program.cs 加入:

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService("ordering-api"))
        .AddAspNetCoreInstrumentation()   // http.server.request.duration、active_requests
        .AddRuntimeInstrumentation()       // dotnet.gc.*、thread pool、memory
        .AddPrometheusExporter()
    );

app.MapPrometheusScrapingEndpoint();      // 暴露 /metrics 給 Prometheus scrape

自訂業務 Metrics:

public class OrderingMetrics
{
    private readonly Counter<long> _ordersTotal;
    private readonly Histogram<double> _processingDuration;

    public OrderingMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("eShop.Ordering");

        _ordersTotal = meter.CreateCounter<long>(
            "orders.total",
            description: "Total orders by outcome"
        );

        _processingDuration = meter.CreateHistogram<double>(
            "orders.processing.duration",
            unit: "ms"
        );

        // Gauge:從外部 pull 當前值,不需要主動 push
        meter.CreateObservableGauge(
            "orders.pending.count",
            () => _repo.GetPendingCount(),
            description: "Orders waiting to be processed"
        );
    }

    public void RecordOrder(string status, double durationMs)
    {
        _ordersTotal.Add(1, new("status", status));
        _processingDuration.Record(durationMs, new("status", status));
    }
}

關鍵監控指標與告警建議:

  • http.server.request.duration (p99):> 500ms 持續 5 分鐘 → API 整體回應變慢
  • http.server.active_requests:> 150 → 請求積壓
  • orders.total{status="failed"} 比率:> 5% → 訂單失敗率異常
  • dotnet.gc.heap.total_allocated:異常上升趨勢 → 疑似 memory leak
  • DB connection pool 使用率:> 80% → 連線池壓力大



改變了什麼:從事後救火變成事前預警,讓工程師有機會在用戶感受到問題之前主動介入。


第四階段:Metrics 說 latency 高,但不知道是哪段 code 慢

情境

場景:上線後,SRE 通知異常。 系統已經上 Production 一段時間。某次 sprint 上版後第三天,SRE 在 Slack 把告警截圖丟出來:「你們的 Ordering.API 從早上 10 點開始 p99 latency 越來越高,剛剛破 1 秒,幫我看一下。」這是事前預警機制起作用——你還來得及在 SLO 違反之前介入,但接下來要找出「到底哪一段 code 變慢了」。

打開 Tempo 看了 trace,時間集中在某個 Handler 的 span。但那個 Handler 裡呼叫了三個 repository method,不確定是哪一個慢,也不確定是 SQL 的問題還是記憶體分配造成 GC pause。

需要比 trace span 更細的執行細節。

解法:EF Core Instrumentation + Profiling

Step 1:先加 EF Core SQL Trace

大多數效能問題都出在 SQL,建議先從這裡開始。

dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddEntityFrameworkCoreInstrumentation(options =>
        {
            options.SetDbStatementForText = true;   // 把實際 SQL 記進 span attribute
        })
    );

加上後,trace 裡每個 DB span 都會直接顯示執行的 SQL 和時間,N+1 query 問題馬上現形:

Span: SELECT o.* FROM Orders WHERE BuyerId = @p0          3ms
Span: SELECT i.* FROM OrderItems WHERE OrderId = @p1      2ms
Span: SELECT i.* FROM OrderItems WHERE OrderId = @p2      2ms
Span: SELECT i.* FROM OrderItems WHERE OrderId = @p3      2ms
  ... × 47 次

只要加 .Include(o => o.Items) 就能解決,但沒有這個 span 的話幾乎找不到問題在哪。



Step 2:用 dotnet-trace 找 CPU / Memory hot path

# 安裝工具(只需一次)
dotnet tool install -g dotnet-trace

# 對線上 process 收集 30 秒的 CPU sample
dotnet-trace collect --process-id $(pgrep -f Ordering.API) \
    --profile cpu-sampling \
    --duration 00:00:30 \
    -o ordering-trace.nettrace

# 轉換成 Speedscope 格式
dotnet-trace convert ordering-trace.nettrace --format Speedscope

開啟 speedscope.app 上傳檔案,火焰圖會直接指出哪個 method 佔用最多 CPU 時間。dotnet-trace 是 .NET 內建工具,不需要安裝 agent,臨時診斷非常好用。

Step 3:持續監控考慮 Pyroscope

dotnet add package Pyroscope.OpenTelemetry

Pyroscope 的優勢是 profile data 可以對應到具體的 TraceId,讓你從「這個 trace 很慢」直接跳到「這個 trace 期間的火焰圖」,適合長期監控。

改變了什麼:效能問題的診斷從「加 log 猜測、部署、等重現」變成「直接從 trace 和火焰圖找到根源」。


第五階段:訊號豐富了,但只有本機 console 才看得到

情境

場景:服務數量上升、多個 instance 規模化。 服務從一個變多個、每個又被 scale out 成多個 instance、開發 / staging / production 環境各有一份,到處 ssh 進機器拉 console log 已經不可行——值班的 SRE 也沒辦法在自己機器看到 production 的訊號。console 的時代已經結束

歷經四個演進階段,console 現在長這樣:

{
  "Timestamp": "2024-01-15T14:32:01.123Z",
  "Level": "Warning",
  "BuyerId": "usr_9981",
  "Sku": "SKU-4421",
  "TraceId": "3e2a1b4c8d9f0e1a2b3c4d5e6f7a8b9c",
  "SpanId": "1a2b3c4d5e6f7a8b",
  "MachineName": "ordering-api-7f9b"
}

訊號確實豐富很多。但 console 有幾個根本限制:沒辦法跨服務做時間軸查詢、值班的 SRE 沒辦法在自己機器上看到生產環境的 log、也沒有人在「值班查 log」以外的時間主動監控系統。

Console 是開發時的觀察窗,不是生產環境的解決方案。

解法:OTLP Exporter → Grafana Stack

把 console exporter 換成 OTLP,讓訊號送到可以被集中查詢的後端:

服務(OTel SDK)
    ↓  gRPC / OTLP Protocol (port 4317)
OpenTelemetry Collector
    ↓
┌──────────────────────────────────────┐
│  Traces  → Grafana Tempo (TraceQL)   │
│  Logs    → Grafana Loki (LogQL)      │
│  Metrics → Prometheus (PromQL)       │
└──────────────────────────────────────┘
    ↓
Grafana(統一查詢介面 + Dashboard + 告警)

切換到 OTLP Exporter:

dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol

var otlpEndpoint = new Uri(builder.Configuration["OTLP_ENDPOINT"] ?? "http://otel-collector:4317");

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("ordering-api", serviceVersion: "1.0.0"))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(opt => opt.RecordException = true)
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation(opt => opt.SetDbStatementForText = true)
        .AddOtlpExporter(opt => opt.Endpoint = otlpEndpoint)
    )
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()
        .AddOtlpExporter(opt => opt.Endpoint = otlpEndpoint)
    );

// Logs 也透過 OTLP 送出(不再需要 Serilog WriteTo.Console)
builder.Logging.AddOpenTelemetry(logging =>
{
    logging.IncludeFormattedMessage = true;
    logging.IncludeScopes = true;
    logging.AddOtlpExporter(opt => opt.Endpoint = otlpEndpoint);
});

Grafana 三柱串聯的調查流程

這是可觀測性真正讓工程師「有感」的一刻:

  • Grafana Dashboard 看到 p99 latency 在 14:32 出現峰值
  • 點擊峰值上的 Exemplar → 自動跳到 Grafana Tempo,顯示那個時間點的某筆 TraceId
  • Tempo 看到完整跨服務呼叫鏈,找到 Ordering.API 某個 span 耗時 900ms
  • 點擊 span 旁邊的「Logs」按鈕 → 連結到 Grafana Loki
  • Loki 顯示同一個 TraceId 在那段時間的所有結構化 log,找到 InsufficientStockException

整個調查過程不超過 3 分鐘,不需要 ssh 進任何機器、不需要 grep。



捷徑:用 .NET Aspire 跳過五個階段

寫到這裡你可能會想:「五個階段都要手動做太累。」確實——而且這條路在 .NET 8 之後其實有捷徑。

Microsoft 推的 .NET Aspire 把這篇文章所有觀測性設定壓縮成「開專案就有」:一行 builder.AddServiceDefaults() 等同於 Stage 1~4 的 OTel 全部設定、AppHost 取代 docker-compose 把 Stage 5 的 Grafana stack 也省掉。

但 Aspire 主題夠豐富,獨立寫一篇才講得清楚——ServiceDefaults 的設計原則、AppHost 編譯時拓撲、Aspire Manifest 導出 K8s / ACA、跟 Grafana Stack 的混合策略等等。我會另外寫一篇〈.NET Aspire × Observability:捷徑跟代價〉做完整介紹,本篇主軸是「從零到一手動走一遍」——這個過程帶來的理解價值,跟用 Aspire 一鍵就有的工具能力,是兩件事。


附錄:本地跑起完整 Grafana Stack

以下 docker-compose 可以在本機啟動完整的可觀測性後端,對應上面所有範例的 exporter 設定。

目錄結構:

observability/
├── docker-compose.yml
├── otel-collector-config.yaml
├── prometheus.yml
└── grafana/
    └── provisioning/
        └── datasources/
            └── datasources.yaml

docker-compose.yml

version: "3.9"

services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.96.0
    command: ["--config=/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317"   # OTLP gRPC(服務連這裡)
      - "4318:4318"   # OTLP HTTP
    depends_on:
      - tempo
      - loki
      - prometheus

  tempo:
    image: grafana/tempo:2.4.0
    command: ["-config.file=/etc/tempo.yaml"]
    volumes:
      - ./tempo.yaml:/etc/tempo.yaml
      - tempo-data:/var/tempo
    ports:
      - "3200:3200"

  loki:
    image: grafana/loki:2.9.4
    command: ["-config.file=/etc/loki/local-config.yaml"]
    volumes:
      - loki-data:/loki
    ports:
      - "3100:3100"

  prometheus:
    image: prom/prometheus:v2.50.0
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--enable-feature=exemplar-storage"   # 啟用 Exemplar,讓 metrics 連結 TraceId
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:10.3.3
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
      - GF_FEATURE_TOGGLES_ENABLE=traceqlEditor
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning
      - grafana-data:/var/lib/grafana
    ports:
      - "3000:3000"
    depends_on:
      - tempo
      - loki
      - prometheus

volumes:
  tempo-data:
  loki-data:
  prometheus-data:
  grafana-data:

otel-collector-config.yaml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
  filter/health:
    traces:
      span:
        - 'attributes["http.route"] == "/health"'

exporters:
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true
  loki:
    endpoint: http://loki:3100/loki/api/v1/push
    tls:
      insecure: true
  prometheusremotewrite:
    endpoint: http://prometheus:9090/api/v1/write

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [filter/health, batch]
      exporters: [otlp/tempo]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [loki]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheusremotewrite]

grafana/provisioning/datasources/datasources.yaml(自動設定三柱串聯):

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    url: http://prometheus:9090
    isDefault: true
    jsonData:
      exemplarTraceIdDestinations:
        - name: traceID
          datasourceUid: tempo        # Exemplar 點擊後跳到 Tempo

  - name: Tempo
    uid: tempo
    type: tempo
    url: http://tempo:3200
    jsonData:
      tracesToLogsV2:
        datasourceUid: loki           # Trace span 點擊後跳到 Loki
        filterByTraceID: true
      lokiSearch:
        datasourceUid: loki
      serviceMap:
        datasourceUid: prometheus

  - name: Loki
    uid: loki
    type: loki
    url: http://loki:3100
    jsonData:
      derivedFields:
        - matcherRegex: '"TraceId":"(\w+)"'
          name: TraceID
          url: "$${__value.raw}"
          datasourceUid: tempo        # Log 裡的 TraceId 點擊後跳到 Tempo

啟動方式:

cd observability
docker compose up -d
open http://localhost:3000

服務的 OTLP_ENDPOINT 環境變數設為 http://localhost:4317,重啟 eShop 後訊號就會開始流進來。


整體演進回顧

這裡整理一下五個階段的演進脈絡,以及每次引入的觸發原因:

  • 起點:空白 console → 數小時才能定位
  • 第一階段:不知道哪個服務出問題 → 引入 OTel Traces + TraceId → 約 60 分鐘
  • 第二階段:知道服務但不知道原因 → 結構化 Logs → 約 15 分鐘
  • 第三階段:出問題才知道,太晚了 → Metrics + 告警 → 事前預警
  • 第四階段:知道慢但不知道哪裡慢 → EF Core Trace + Profiling → 數據導向診斷
  • 第五階段:訊號只在本機 console → Grafana 三柱串聯 → 約 3 分鐘


小結

回顧這五個演進階段,每一個工具的引入都有一個具體的痛點觸發它,沒有人在第一天就建好完整的可觀測性堆疊——在那些痛點出現之前,這些工具也不值得引入。

我自己的習慣是:每次系統出問題,問自己「如果我有什麼資訊,這個問題可以更快解決?」然後把那個資訊加進去。可觀測性就是這樣一點一點長出來的,而不是某次架構設計決策的產物。

希望這篇文章對想要在 .NET 微服務專案導入可觀測性的朋友有幫助,若以上內容有不清楚的地方,歡迎留言討論,Happy Coding :)


技術速查表

Tracing
  • OpenTelemetry.Instrumentation.AspNetCore:HTTP request span
  • OpenTelemetry.Instrumentation.Http:Outgoing HTTP span
  • OpenTelemetry.Instrumentation.EntityFrameworkCore:SQL trace

Logging
  • Serilog.AspNetCore:結構化 log
  • Serilog.Enrichers.Span:自動注入 TraceId / SpanId

Metrics
  • OpenTelemetry.Instrumentation.Runtime:.NET runtime metrics
  • OpenTelemetry.Exporter.Prometheus.AspNetCore:暴露 /metrics endpoint

Export
  • OpenTelemetry.Exporter.OpenTelemetryProtocol:OTLP(對接 Collector)

Profiling
  • dotnet-trace(內建):臨時 CPU / memory 診斷
  • Pyroscope.OpenTelemetry:持續式 profiling

Dev
  • .NET Aspire Dashboard:開發環境零設定整合視圖


參考

dotnet/eShop
OpenTelemetry .NET
.NET Aspire Telemetry
Serilog Documentation
Grafana LGTM Stack
OpenTelemetry Collector Contrib
W3C TraceContext Specification
Speedscope 火焰圖瀏覽器

Copyright © m@rcus 學習筆記 | Powered by Blogger

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com