只有累積,沒有奇蹟

顯示具有 Devops 標籤的文章。 顯示所有文章
顯示具有 Devops 標籤的文章。 顯示所有文章

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 火焰圖瀏覽器

2025年6月5日 星期四

[conference] DevOpsDays Taipei 2025 Bootcamp 從 Day 0 開始的可觀測性:用 ODD 與 SLO 的實作工作坊

分享心得
很高興再次有機會可以在台灣最熱門的技術研討會 DevOpsDays 分享,會有這次分享是因為艦長的邀請一起籌辦 Devops bootcamp x observability,自己也在這準備的過程中進行了第一場的工作坊,過去很多時間都是用講的方式來進行,今天希望把自己過去的經驗在工作坊設計中帶給大家體會,希望大家會喜歡

議程介紹
主題 : 從 Day 0 開始的可觀測性:用 ODD 與 SLO 的實作工作坊?
DevOpsDays Taipei 2025 Bootcamp 從 Day 0 開始的可觀測性:用 ODD 與 SLO 的實作工作坊

課程大綱
在不斷發展的雲端原生技術領域中,觀察應用程式的性能和健康狀態已不再是一種奢侈,而是必要的關鍵。隨著微服務架構成為常態,分散式系統擴展,以及資料量的爆炸性增長,傳統的監控工具難難以捕捉服務之間錯綜複雜的互動和依賴關系。這種缺乏連貫的可見性,導致了可見性缺口,使得難以精確定位性能瓶頸、診斷問題以及確保應用程式的健康。可觀測性工程的出現將補足這樣的缺口,該次工作坊將深入淺出地介紹將可觀測性理念轉化為實際行動的方法。

課程目標
體驗 Observability 帶來的可見性價值
學會制定 SLO,確保服務穩定性與可用性
設計符合自己團隊的 Observability 推動策略

主辦單位 : DevOpsDays Taipei iThome 主辦
議程表 : 連結



[conference] DevopsDays Taipei 2025 - 為什麼我們需要 Observability?

分享心得
很高興再次有機會可以在台灣最熱門的技術研討會 DevOpsDays 分享,這次受到 Devops Taipei 社群夥伴艦長的邀請,在 Devops Taipei bootcamp 規劃與籌辦 observability 可觀測性 bootcamp,可觀測系這議題在這幾年已非常火熱,但對於剛入門的新手來說可能有點挑戰,怎麼讓大家知道他的基本觀念與可以解決什麼問題是非常重要的,因此在研討會過程中設計了技術分享 + 工作坊多種議題,希望從基本觀念與實作的角度上來幫助大家,這分享是可觀測性 bootcamp 第一場,歡迎大家提出來進行討論,Happy learning 🙂

議程介紹
主題 : 為什麼我們需要 Observability?
為什麼我們需要 Observability?

課程大綱
隨著 Observability(可觀測性)的觀念逐漸普及,它早已不只是工具或儀表板的選擇,更成為團隊理解系統行為、優化效能,以及即時應對異常狀況的關鍵能力。
本場演講將從「台灣企業在導入 Observability 的真實現況與挑戰」出發,揭示企業在實作過程中最常遇到的技術困境與落地瓶頸。你將認識 Observability 如何從傳統的監控(Monitoring)演進而來,並透過 OpenTelemetry 等開放標準實現跨系統的資料串接與分析,協助團隊更有效地觀察、診斷與優化系統。
除了技術視角,我們也將初探導入 Observability 的組織挑戰,包括開發、SRE、產品與營運團隊間的溝通落差、SLO(服務水準目標)的制定困難,以及遙測數據雖豐但洞察不足的常見困境。
演講最後,我們將介紹 Observability Bootcamp 的設計核心,帶你一探團隊如何從「讓系統說話」,到「聽見並理解訊號」,最終「以數據驅動優化」,幫助與會者理解如何將 Observability 落實於日常工作流程中,以正確的觀念與態度面對 Observability。

主辦單位 : DevOpsDays Taipei iThome 主辦
議程表 : 連結



2024年7月11日 星期四

[conference] DevopsDays 2024 - From Observability to Observability Driven Development

分享心得
很高興可以再次於 DevOpsDays 分享可觀測性 Observability 相關議題,第一次分享時是在2022年分享可觀測性(Observability)的實踐,當時台灣在討論可觀測性還沒有太多人,但在國外及各大工具廠商大力宣傳下,Observability 變得相對重要,但重要之外對於開發者的日常作業或是開發流程會有甚麼樣的影響呢 ? 因此就思考與分享了 From Observability to Observability Driven Development 這主題,希望大家可以思考除了從 SRE 的角度之外,從開發者的角度可以幫助什麼 ? 以及在軟體開發前期可以多思考哪些事情,這樣系統上線後才可以提產品及維運帶來更大的幫助,以上是想要分享議程的主軸,歡迎大家提出來進行討論,Happy learning 🙂

議程介紹
主題 : From Observability to Observability Driven Development
近幾年來,隨著軟體架構進化為微服務和雲原生技術的轉變,系統的複雜性急劇增加。 這種變化使得傳統的監控工具難以全面理解並快速適應變化。 在國外研討會越來越多人探討如何透過可觀測性來提高 DevOps 的效率。面對這樣的挑戰,可觀測性變得越來越重要,它不僅讓開發和維運團隊能夠監控系統,更能透過收集系統的遙測數據來深入理解系統的行為和性能。

本次分享的內容將包括:

可觀測性的介紹:定義可觀測性的基本概念及其在現代軟體開發中的關鍵角色。探討為何可觀測性對於成功實施 DevOps 至關重要,以及它如何協助開發人員高效的運維和快速的問題解決。
可觀測性的演進 : 分析可觀測性在過去幾年是如何不斷演進與重新定義的歷程;並探討可觀測性的重要信號(Signals),如日誌(Log)、指標(Metrics)、追踪(Trace)的演化如何協助團隊更好地理解和管理系統,以及這些關鍵信號在提供系統洞察問題的侷限性,並探索如何克服這些挑戰以實現更全面的可觀測性。
可觀測性驅動開發(O.D.D):如何將可觀測性轉變為一種推動開發的策略。將可觀測性原則整合到軟體開發生命週期的各階段中,從基礎的可觀測性措施到開發過程中的全面整合,開發團隊可以更早地發現和解決潛在的問題。

主辦單位 : DevOpsDays Taipei
議程表 : 連結
共筆 : https://hackmd.io/@DevOpsDay/2024/%2F%40DevOpsDay%2FSy6azMu80
投影片 : 連結



2023年3月5日 星期日

[AzureDevops] 如何使用 Azure DevOps 和 Teams 提高代碼審查的效率

前言
現在團隊是使用 Azure Devops repo 作為版控,在既有開發規範是開發完功能後會發 PR(Pull request) 給資深同仁 Review,開發同仁會將 PR 的網址貼在 Teams 群組上,再請負責 code review 同仁看內容是否有需要調整的地方,過去大家很習慣這樣方式進行也因為太忙也沒太多時間研究是否有更好的方法,今天強者同事分享其實在 Azure Devops 內建機制可以整合到 Teams 頻道發送 PR 通知,花點時間了解後覺得十分方便,這篇文章就來針對這好用強大的功能作筆記,若有問題歡迎提出來一起討論。

設定通知
這步驟主要目的是在專案設定通知到指定的 Teams 頻道中,首先要建立 subscription 與其規則,有以下幾個步驟要進行設定

  • Step 1 : 到要設定 PR 通知專案的 Repo 進行設定,點選 Project Settings
  • Step 2 : 進入到 project Settings 頁面後裡點選 Notifications Tab
  • Step 3 : 按下上方 New subscription
接著要設定 subscription 內容,可以依據實務上團隊的項目來進行調整,目前公司是使用 AzureDevops 的 git repo 解決方案,因此在此步驟要選擇 Code(Git),Template 部分是當作甚麼動作的時候要被通知,這裡 Demo 的是希望當 PR 建立成功和更新時要發送通知到 Teams,因此選擇 pull request is created or updated.,確認無誤後按下下一步按鈕

設定新通知
在 Azure Devops 有下列多種通知方式
  • Member of project by role
  • Team preference
  • Custom email address
  • Member of project team
  • SOAP
差異可以參考 MSDN 官方說明文件 Pull request update notifications,這裡就不在多說明。今天要介紹的方式是通知到 Teams,因此在這邊選擇 Custom email address 選項
Address 部分資料可以從 Teams 頻道取得,到要通知的 Teams 頻道按下右鍵點選取得郵件地址,會跳出視窗複製其 email 即可,再貼在 address 欄位上。

Filter criteria 則是可以根據什麼動作要進行訊息的通知,可以根據團隊的需求在下方的 Add new clause 新增 or 修改調整其設定值,這裡選擇當團隊在 repo 建立 pull request 時發送其通知,確認內容無誤後接著按下 finish 按鈕設定即完成。 建立無誤後,則可以在 notifications 設定區塊看到稍早新增完成的設定項目,這裡右方有開關可以依據情境來設定是否要開啟關閉的動作。

測試
接著我們到 Azure Devops 專案發送 PR 看設定是否成功,在 repo 頁籤新增 PR (new pull request),輸入 test 後按下送出按鈕
可以在稍早設定的 Teams 頻道中看到測試發送的 PR 請求內容,大功告成 !


感想
這次在強者同事分享下又多知道 Azure Devops 的強大功能之一,這些方便的功能都可以替團隊帶來更好的效益,日後如果有在實務上學到更好玩的功能,也會再與大家分享,以上如果有不清楚的地方歡迎一起討論 :) !

參考
pull request

2022年12月18日 星期日

[conference] .NET Conf 2022 - 再不使用 APM 就芭比Q 了

分享心得
今天很開心可以到 .NET Conf 2022 分享 APM 的主題,雖然主題是介紹 Elastic APM 但其實沒有太多工具的介紹,反而希望多跟大家分享像是工具背後是想要解決什麼問題(Business Continuity or High Availability ),為了達到 HA 有哪些重要指標 (Golden Signals、USE、RED) 可以參考及觀念,最後用《為什麼比怎麼做還重要》結束。 如果對於今天分享主題有想要了解更多或是不清楚的地方,歡迎一起討論與交流自己的想法, happy Coding


議程介紹
主題 : 再不使用 APM 就芭比Q 了
隨著科技與技術不斷的進步與創新,軟體架構從單體式(Monolithic)到 SOA(Service Oriented Architecture) 再到微服務(Micro Service),在應用程式服務顆粒度切分得更細的情況下,當系統發生問題時也隨著架構複雜度變高更難定位問題,有沒有更好的方案可以解決呢 ?
這個議程將會用淺顯易懂的方式,帶你一起探究下列議題
1. .NET 如何與 Elastic APM 整合
2. Elastic APM 內建的監控數據可以為團隊帶來哪些幫助
3. 如何與現代化遙測標準 OpenTelemetry 整合
透過 Elastic APM 快速了解問題的脈絡(Tracing),找到可能的系統效能瓶頸,讓發生線上問題時團隊可以更快速定位問題並止血,分享在這過程學習到的經驗以及小小心得。

主辦單位 : study4
議程表 : 連結
投影片 : 連結



2022年9月17日 星期六

[conference] DevopsDays 2022 - 可觀測性(Observability)的實踐

分享心得
這兩年因緣際會接觸可觀測性(Observability)這議題,研究後覺得挺有趣於是不要臉的報名 DevopsDays Taipei 研討會分享,也很幸運地獲得評審青睞有機會在今年 DevopsDays 2022 分享,這次分享有兩個大的挑戰分別是議程內容與時間
1. 議程內容 : 指的是在過去雖然有在 .NET Conf 2021 分享過一小段,但要在這麼大的場合要把 Observability 說明清楚是個不容易的事情,尤其是這議題在國內比較少看到有人提到,因此很多時間在看國外研討會影片或是文章來釐清自己的觀念與想法。
2. 時間 : 由於議題只有 25 分鐘,可觀測性到底是甚麼 ? 想要解決甚麼問題 ? 它與監控差異到底在哪裡呢 ? 觀念說明完再提到實踐工具有哪些,要在短時間把資訊整理清楚並說讓第一次的會眾了解是個很大的挑戰,也有試著把整理的內容給幾位強者同事分享看是否哪裡不清楚的地方,強者同事也聽完給很多實用的建議,整個投影片內容修改不下 20 次才完成初版 (頭髮都白了 XDDD
由於之前都太緊張所以都沒注意到場地,當天實際閒晃到場地才傻眼過去分享都是比較小場地這次被安排到 ABC 會議室,心裡只能默默祈禱今天準備內容不要太掉漆就好,幸好過程中還算順利準備的議題都有分享到,會後也有很多朋友詢問相關議題與實作細節,護國神山也有 HR 與主管來找我聊聊(但太緊張完全忘記說啥 XDD),結束緊張又刺激的一天。
一個月後傍晚收到 DevOpsDays Taipei 活動的議程問卷調查,過去參加 DevOpsDays 都是站在台下當聽眾,沒想到會有一天站在 #DevOpsDays 台上分享感到很開心,感謝當天議程與會者的建議跟留言,也期許自己未來還有機會分享自己研究的心得 :)


議程介紹
主題 : 可觀測性(Observability)的實踐
隨著科技不斷的進步與雲端的普及,企業軟體在架構的演進與應用程式開發的速度與越來越快,隨之而來的挑戰是開發人員如何針對這些服務進行更好的監控(Monitor),以提供高可用性的服務。
可觀測性(Observability)在這幾年越來越多人在討論,甚至在 CNCF 還有專區介紹 Observability 相關的工具,可觀測性與監控到底有何不同?為何國外大型研討會與雲端廠商都開始提到可觀測性?
這主題將會分享以下內容
➊ 什麼是 Observability ?
➋ 可觀測性 vs 監控 
➌ 實踐工具

主辦單位 : DevOpsDays Taipei
議程表 : 連結
共筆 : https://hackmd.io/@DevOpsDay/2022/%2F%40DevOpsDay%2FBkLestagj
投影片 : 連結



2022年6月4日 星期六

[Azure] App Service Diagnostics - Collect Memory Dump

前言
前兩篇分別介紹了 App Service Diagnostics 中的 Collect .Net Profiler Trace 與 Auto heal,分別都可以透過工具來蒐集雲端伺服器的緩慢問題分析與蒐集記憶體資訊,這一篇則是介紹如何 dump 目前伺服器 memory 的資料,以及有多個伺服器的時候該如何抓取特定的 Server memory data。若對於上述內容有問題或是不清楚的地方,歡迎提出來一起討論。

Collect Memory Dump
在 Azure 上提供非常多的分析工具可以協助開發人員找到應用程式的問題,應用程式出問題不管在地端的機房或是雲端上都是有可能會發生的,在過去服務還沒上到雲端時是透過一些工具,像之前黑暗大就寫過 ASP.NET CPU 飆高問題之傻瓜分析工具-DebugDiag Tools 找到應用程式 Crash 或是緩慢原因,今天要分享的是 Memory Dump 把應用程式當下的記憶體 clone 一份下來,再進行問題的盤查或是分析,另外在 Memory Dump 之前提醒事項如下
  • While collecting the memory dump, a clone of your app's process is created so the impact on the site availability is negligible.
  • Dumps are collected for the worker process (w3wp.exe) and child processes of the worker process.
  • Size of the memory dump is directly proportional to the process size, so processes consuming more memory will take longer to be dumped.
  • Your App will not be restarted as a result of collecting the memory dump.
白話來說,Memory Dump 會 Clone 當下應用程式 (w3wp.exe) Process 的資訊,花費時間多寡取決於 Process 用量多寡決定,應用程式不會被重啟。了解以上資訊後,就來看看如何在 Azure 設定 Memory Dump
Step 1 : 開啟 App Service
Step 2 : 點選左邊清單的 Diagnose and solve problems 功能
Step 3 : 在右邊框輸入 "點選 Memory Dump"
Step 4 : 選擇 Dump 下來的檔案放置的位置,如果之前沒執行過需要設定 Dump file 要存放的位置
Step 5 : 接著下一步,Mode 部分是選擇要蒐集 Dump Data 或者是除了 Dump 之外還希望進行 Memory 的分析,如果 Server 是多台機器的話,下面會列出目前的機器有哪些提供使用者選擇所要蒐集的 Instance
另外,如果之前有執行過蒐集過 Dump 的動作,下方也會列出之前手動或是自動蒐集的 Dump 檔案清單
Step 6 : 按下 Collect MemoryDump 按鈕之後,會開始蒐集應用程式的 Memory 資訊 (需等待一段時間,時間長短跟你應用程式 Memory 成正比)
Step 7 : 完成後可以看到 dump file 存放到指定的 storate account 位置,如果有勾選 Analyze Data 則會產生分機報告。
Step 10 : 點選 Report 可以看到分析 memory 的結果,打完收工 !
另外,如果是在線上環境發生緊急問題時,蒐集應用程式的 Memory dump 可以有助於我們找到問題,但其 dump 處理時間是很漫長會花費比較長的時間,勢必也會影響到處理線上問題的時間,與微軟技術支援討論建議如果遇到這狀況,可以透過 Metric Apply Splitting 找到特定的 instance,接著在 dump 時選擇該 instance 加速其 collect dump file 的時間 (又多學到一招,Azure 初學者覺得開心)。

結論
以上是簡單介紹 Azure Memory Dump 的方式,另外在微軟官方 youtube 也有影片說明如何在 App service 進行 debugging memory 的說明,有需要的朋友可以自己觀看,Hope it helps :D

2022年4月4日 星期一

[Azure] App Service Diagnostics - Auto-Heal

前言
上一篇提到了如何在 Azure 取得當下的 memory dump 資訊 App Service Diagnostics - 應用服務診斷,這一篇則是透過另外一種方式使用 Auto-heal 的方式設定 memory 達到一定的水位時,觸發自動收集 memory 的使用狀況,並在自訂的條件下抓取 memory 資料,並指定 dump file 放置在某個 storage account 帳號中。若對於上述內容有問題或是不清楚的地方,歡迎提出來一起討論

Auto-Heal
今天要分享的是透過 Auto-Heal 方式取得應用程式發生例外或是異常時,透過設定特定的行動、內存限制定義各種條件,採取什麼特定的行動方案,例如重啟應用程式、紀錄事件或是啟動另外一個可執行方案,以下就來介紹如何使用 Azure 來設定 Auto-Heal 選項
Step 1 : 開啟 App Service
Step 2 : 點選左邊清單的 Diagnose and solve problems 功能
Step 3 : 在右邊框輸入 "點選 Auto-heal"
Step 4 : Custom Auto-Heal Rules Enabled 選擇開啟 (on)
Step 5 : 在 Define Conditions 中選擇 Memory limit,並設置水位值為 (5872025),此設定值可以依據實體狀況做調整,是指以 KB 為單位的 App 占用 momory 的值
Step 6 : 在 Configure Actions 選 Custom Action,下方則選擇 Memory dump
Step 7 : Tool options 選擇 collectLogs
Step 8 : 選擇按下 Select,選擇要儲存的 storage 位置
Step 9 : 點擊儲存 Auto-heal 設定配置,此功能將自動監控應用程式的 Memory 使用情況,並在剛剛自定義的條件下取得應用程式的 Dump files

透過以上詳細的資訊,就可以發生異常問題的 dump file 上傳給微軟或者是有經驗的 SRE 團隊分析與使用;另外,如果一陣子發現 storage account 都沒有異常的 memory dump file 的話,可以試著調降 memory 水位值,這樣更有機會可以取得所需要的資訊內容,才有機會找到蛛絲馬跡再進行下一步的推斷。

結論
如果想要了解更多細節可以參考 Announcing the New Auto Healing Experience in App Service Diagnostics 以及 Azure App Service tips: Increase reliability with the Auto-Healing features 這兩篇文章內容都有詳細說明 Auto-heal 的用法與步驟,Hope it helps :D

2022年3月9日 星期三

[Azure] App Service Diagnostics - 應用服務診斷

前言
目前工作主要服務都是放在 Azure 服務上,因此接觸到 Azure 時間也越來越多,當應用程式發生問題時要如何進行診斷找到 Root cause 呢 ? 過去在地端機房上可能可以先取得執行當下的 Memory Dump 檔案,再把抓下來的 dump file 透過一些記憶體分析工具像是 DebugDiag Tools 或是 WinDBG 來分析原因,那如果今天我們使用的是雲端 Azure 的 Paas 服務該如何處理呢 ? 今天就透過這篇文章來簡單跟大家分享如何在 Azure 上取得執行當下的 Memory dump file,若對於上述內容有問題或是不清楚的地方,歡迎提出來一起討論

App Service Diagnostics
在 Azure App Service 中提供多種工具來協助開發者來針對問題做分析與排除,目前 Diagnostic Tools 分為以下幾類
  • Collect .Net Profiler Trace
  • Collect Memory Dump
  • Check Connection Strings
  • Collect Network Trace
  • Network/Connectivity Troubleshooter
今天要分享的是透過 Profile Trace 可以幫助識別 .NET 異常的 Exception Type、異常訊息資訊與 Callstack,不需要再額外安裝其他的工具,以下就來介紹如何使用 Azure 來設定 .NET Profile Trace 選項
Step 1 : 開啟 App Service
Step 2 : 點選左邊清單的 Diagnose and solve problems 功能
Step 3 : 點選 Diagnostic tools
Step 4 : 在 Diagnostic tools 中選擇 Collect .NET Profiler Traces
Step 5 : Mode 是這次目的是單純蒐集或是蒐集後進行分析,Instance 部分則可以選擇要分析的機器名稱,選擇後即可按下 Collect Profiler Trace 按鈕
Step 6 : 按下執行後會開始進行蒐集的動作,會在一分鐘後結束(有選產生報表會多一點點時間),完畢後會在下方產生分析後的報表連結
Step 7 : 開啟報表後則可以看到本次蒐集後的內容與分析結果,中間區塊則有分為五個 section,分別會是在蒐集站台的資訊期間
Slow Request,例如前100名緩慢有哪些,打到那些 endpoint,所花費的時間多久
Fail Requests : 失敗的請求清單,失敗定義 HTTP Status >= 400
Thread Callstacks
.NET Exception : 蒐集到關於 .NET 的錯誤分別有哪些

CPU Stacks : Instance CPU 使用的狀況是甚麼
透過以上詳細的資訊,就可以針對異常的問題找到可能潛在的原因是什麼,舉例來說 : 17:00 開始發現系統緩慢,平台可以使用程度下降,這時請求回應的時間是 503,後來發現是呼叫 Auth API 雍塞受到影響造成,都可以猜測的方向看找到蛛絲馬跡再進行下一步的推斷。

結論
如果想要了解關於報告的更多細節可以參考 App Service Diagnostics – Profiling an ASP.NET Web App on Azure App Service,另外 Diagnostics tools 還具備其他更多分析的功能,如果在後續有機會使用到會在分享相關心得的,Hope it helps :D

2022年1月26日 星期三

[OpenTelemetry] 現代化監控使用 OpenTelemetry 實現 : 在 .NET 如何使用 OpenTelemetry

前言
最近在 suvery 監控相關議題時接觸到 OpenTelemetry 蒐集遙測數據的開源框架,覺得這議題挺有趣的因此整理變成系列文,這篇是研究 OpenTelemetry 的系列文第三篇, 這系列主要會分為四篇分別是 若對於上述內容有問題或是不清楚的地方,歡迎提出來一起討論

OpenTelemetry in .NET
在前面分別提到了甚麼是可觀測性 (Observability) 以及 OpenTelemetry 蒐集遙測數據的規範標準,在 OpenTelemetry 在多種主流的程式語言都有實作,這邊就來分享在 .NET 中要如何使用 OpenTelemetry。在 OTel (OpenTelemetry簡稱)中 .NET Core 及 .NET Framework 都有支援(後者版本需高於 4.6.1),在 Github 位置是 opentelemetry-dotnet,與開發者最直接相關的為下列部分
  • API
  • SDK
  • Instrumentation libraries
  • Exporter libraries
以下分別針對個別項目做簡單介紹

API
Github : OpenTelemetry .NET API
可觀測性 (Observability) 中的三個重要元素 Tracing、Metrics、Logging 在 OTel dotnet 分別皆有實作,在支援的程度上除了 Tracing 是 1.0 之外,其他兩項分別還在 Alpha 與 Beta 階段。在 OTel dotnet 上提供 Tracing API, Logging API, Metrics API, Context and Propagation API 等 API 讓開發者依據各自需求作使用。

SDK
Github : OpenTelemetry .NET SDK
SDK 是 OTel API 的實現,目前 SDK 中實現了 Tracing API、Metrics API 和 Context API 等功能。當開發者使用與安裝配置的 SDK 後,所使用到的 API 方法將會開始生成、蒐集、發送遙測數據內容,並提供開發者自訂義 process pipleline 與導出到特定後端功能。目前 SDK 有與 ILogging 進行整合。

Instrumentation libraries
Github : OpenTelemetry .NET Instrumentation Library
提供常用的 Library 讓開發者在使用 OpenTelemetry 上更方便,像是 ASP.NET、ASP.NET Core、HttpClient、SQL Client 或是 SQL Client 等。你的應用程式代碼中如果有用到 httpclient 就可以引用其 OTel httpclient library,使用後就會自動蒐集生成遙測資料,開發者不用寫很多代碼即可達到其目的。目前在 dotnet 支援的 library 可以參考官方網站 OpenTelemetry registry 說明頁。

Exporter libraries
Github : OpenTelemetry .NET Exporter Library
蒐集遙測數據之後這些數據內容呈現的工具,像是在 Demo code 常使用的 console 或是 memory 中,亦或可以選擇 jaeger、zipkin、prometheus 等常見的工具,如果是在 Cloud 上在三大雲端服務廠商 AWS、Azure、GCP 也開始支援 OpenTelemetry 的輸出資料呈現。舉例來說在 Azure 上就可以透過 Azure Application Insights 以及 Azure Monitor 等服務查看蒐集到的資料呈現。

Package
Metrics
這裡列出在 .NET 中與 OpenTelemetry 會有相關的 namespace,其中 Instrumentation 與 Exporter 前面章節已經有提到過不在重複。在 .NET 6 之後的版本開始添加 OpenTelemetry 的支援。System.Diagnostics.Metrics 是 OpenTelemetry Metrics API 規範在 .NET 的實現,Meter 類別是在使用時須要先建立的物件與對象,在依據場景來使用所需要 Metrics 的指標與方法,在目前 Metrics API 支援以下幾個儀器類型
  • Counter : 計數器,可以使用 Add 方法來計算變化率或是總和
  • ObservableCounter : 與 Counter 類似,用於可觀察的儀器值 例如 CPU 時間(針對不同 Process、Task or 用戶模式)
  • ObservableGauge : 非相加值得可觀察儀器,例如當前室溫
  • Histogram : 繪製直方圖或計算百分位數的工具
或許對於上述描述的說明還有些模糊或是不清楚,在微軟 MSDN 文件有提到使用 Metrics 儀器的使用時機
  • 如果是計算內容是隨著時間增加的值 : 建議使用 Counter 與 ObservableCounter
  • 如果是計算時事物 : 建議使用 Histogram
  • 其他類像是業務指標、Cache 命中率、緩存大小 : 建議使用 ObservableGauge
詳細可以參考 MSDN Best practices when selecting an instrument type 文件說明

Tracing
在 Trace 部分可以透過 .NET 內建類別 Activity 或是使用 OpenTelemetry Tracing API 都可以達到其目的,兩者使用後都會定義到 Span 概念。.NET 5 開始提供 Activity 的類別作為 tracing 追蹤的目的,建立實例後可以透過 StartActivity 啟動 Activity 並定義其追蹤名稱;在 OpenTelemetry API 也可以透過 Tracer 達到一樣的目的,透過 TracerProvider 建立實體後使用 StartActiveSpan 來定義追蹤物件。因此你可以發現在微軟官方的部落格或是 sample code 上都是使用內建的 Activity API 做完範例, 兩者的 API 比較差異可以參考上圖

今晚來點 Demo
以上講很多概念與 OpenTelemetry API 的介紹,接著我們就來針對些好玩的部分做 Demo 讓大家對於 OpenTelemetry 在 .NET 的應用更了解,每個 Demo Code 範例代碼皆已上傳到 Github 上,有興趣的朋友可以將 clone 下來自己執行試玩看看

Demo#1 : Console
source code : OpenTelemetrySample
第一個 Demo 開始透過簡單的 Console 作為起手式,在建立專案後請先引用 OpenTelemerey.NET SDK 及 OpenTelemerey.NET Exporter Console 相關 Package,相關指令如下
dotnet add package OpenTelemetry --version 1.1.0
dotnet add package OpenTelemetry.Exporter.Console --version 1.1.0
Program.cs 中進入點 main 程式碼如下
class Program
{
	private static readonly ActivitySource MyActivitySource = new ActivitySource("Company.Product.Library");

	public static void Main()
	{
		using var tracerProvider = Sdk.CreateTracerProviderBuilder()
			.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("SampleService"))
			.SetSampler(new AlwaysOnSampler())
			.AddSource("Company.Product.Library")
			.AddConsoleExporter()
			.Build();

		using (var activity = MyActivitySource.StartActivity("SayHello"))
		{
			activity?.SetTag("foo", 1);
			activity?.SetTag("bar", "NET Conf 2021, Hej !");
			activity?.SetTag("baz", new int[] { 1, 2, 3 });
		}
	}
}
程式碼說明
  • Line 3 : 建立 ActivitySource 物件,並定義名稱為 Company.Product.Library
  • Line 7 : 透過 SDK 建立 TracerProvider 物件並設定 Trace Service 的名稱為 SampleService
  • Line 9 : 設定 Sampler 採樣器配置設定為 alwaysOn
  • Line 10 : 定義 TraceProvider source 名稱為 Company.Product.Library (需要與 line 3 相同)
  • Line 11 : 設定 Exporter,這裡使用的是在 Console 輸出其結果
  • Line 14~19 : 設定 Actitity 物件並定義其 tag 的 key 與 value, ex : foo 值為 1
執行結果如下
除了上述程式碼的說明之外可以看到的是其中 activity 會自動蒐集其起始的時間 (startTime) 與花費時間 (Duration) 等在 trace 中重要的資訊,tagObject 也可以依據需求定義客製化的 attribute 或是屬性資訊,以上是簡單的範例一部分

Demo#2 : API
source code : OpenTelemetrySample.API
接著的範例是使用 API 的部分,在將範例的內容結果顯示在 Zipkin 工具上,因此在建立專案後請先引用 OpenTelemerey.NET SDK 及 OpenTelemerey.NET Exporter Console 及 OpenTelemetry.Exporter.Zipkin Package,相關指令如下
dotnet add package OpenTelemetry --version 1.1.0
dotnet add package OpenTelemetry.Exporter.Console --version 1.1.0
dotnet add package OpenTelemetry.Exporter.Zipkin --version 1.1.0
在這範例中會將蒐集到的遙測數據資料輸出到 Zipkin 中,Zipkin 是分散式追蹤的工具,可以幫助開發者在工具中以視覺化的方式了解到請求中哪一段緩慢的原因與百分比資訊,相關介紹可以透過 官網說明,快速使用方式可以透過 docker 在本機執行下列語法
docker run -d -p 9411:9411 openzipkin/zipkin
Program.cs 中進入點 main 程式碼如下
class Program
{
	private static readonly ActivitySource MyActivitySource = new ActivitySource("Company.Product.Library", "1.0");
	static void Main(string[] args)
	{
		using var tracerProvider = Sdk.CreateTracerProviderBuilder()
		   .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MyService"))
		   .AddSource("Company.Product.Library")
		   .AddZipkinExporter(zipkinOptions =>
		   {
			   zipkinOptions.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
		   })               
		   .AddConsoleExporter()
		   .Build();

		DoSomeWork();

		Console.WriteLine("Example work done"); 
	}

	static void DoSomeWork()
	{
		using (var a = MyActivitySource.StartActivity("SomeWork"))
		{
			StepOne();
			StepTwo();
		}

		using (var activity = MyActivitySource.StartActivity("StepThree", ActivityKind.Server))
		{
			activity?.SetTag("http.method", "GET");
			if (activity != null && activity.IsAllDataRequested == true)
			{
				activity.SetTag("http.url", "http://www.google.com");
				activity.SetTag("body", "This is a book");
			}
		}
	}

	static void StepOne()
	{
		using (var a = MyActivitySource.StartActivity("StepOne"))
		{
			Task.Delay(500);
		}
	}

	static void StepTwo()
	{
		using (var a = MyActivitySource.StartActivity("StepTwo"))
		{
			Task.Delay(1000);
		}
	}
}  
程式碼說明,其中有些與 Demo#1 相同的就不在重複說明唷
  • Line 7 : 設定 Service 的名稱為 MyService
  • Line 9 : 將遙測數據在 zipkin 上顯示,並定義輸出的 endpoint 為 http://localhost:9411/api/v2/spans
  • Line 23 : 定義 activity 物件名稱叫 Somework,其中執行 StepOne 與 StepTwo 兩個方法
  • Line 29 : 定義新的 activity 物件名稱為 StepThree,類型為 Server,內容模擬向第三方發出請求,因此會有定義 httpMethod、呼叫 endpoint 方法與送出 body 內容。
  • Line 40 : 定義 StepOne activity 物件,內容為 delay 500 毫秒
  • Line 48 : 定義 StepTwo activity 物件,內容為 delay 1 秒
執行結果如下
整理一下幾個重點
  • 順序為 StepOne > StepTwo > SomeWork > StepThree
  • StepOne 的 Id 為 StepTwo、SomeWork、StepThree 的 ParentId,上一篇提到的 Context Propagation 上下文關聯是透過此來定義
  • 每個 activity 會自動取得執行的 instance Id,這在雲端環境有這資訊很方便
  • 每個 Activity 皆有紀錄花費時間與起始時間
接著我們可以透過 zipkin 來看一下上面數據的視覺化呈現為和,開啟瀏覽器後輸入 http://localhost:9411/zipkin/ ,在按下 run query 按鈕

可以透過視覺化的方式來看到整個的關聯圖,已這個例子來說,Total Spans 一共三個,總共花費的時間為 60.288 ms,其中 stepOne 與 stepTwo 各自花費的時間,以及這兩個 span 中所各自定義的屬性是甚麼(右邊區塊),可以發現的是透過 zipkin 分散式追蹤工具在理解上更為清晰,如果在執行過程中有某個 span 是緩慢的可以更清楚的知道緩慢的原因與時間,那麼回到真實的世界會是甚麼樣子呢 ? 我們可以透過範例三知道。

Demo#3 : Automatic instrumentation
source code :
OpenTelemetrySample.WeatherForecast.Client
OpenTelemetrySample.WeatherForecast.API
在這範例中會透過較真實案例來說明,前端 Client 專案會透過 httpClient 呼叫後端 API 來取得 WeatherForecast 資訊,後端 API 會將資訊紀錄在 Redis 資料庫中,在亂數回 weather 資訊。我們會在這專案中引用自動蒐集遙測數據的 Library 像是 AddHttpClientInstrumentation,在發出 Httpclient 請求時自動蒐集相關遙測數據,並將其關聯結果呈現在 zipkin 工具上。
在 Client 專案的 Startup.cs 程式碼如下
public class Startup
{	
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddControllersWithViews();

		services.AddOpenTelemetryTracing(Configuration);
	}

	public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
		//...省略
	}
	
	static class ServiceCollectionExtensions
    {

        public static IServiceCollection AddOpenTelemetryTracing(this IServiceCollection services, IConfiguration configuration)
        {
            var zipkinServiceName = configuration.GetValue("Zipkin:ServiceName");
            var zipkinEndpoint = configuration.GetValue("Zipkin:Endpoint");
            
            services.AddOpenTelemetryTracing((builder) => builder
                    .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(zipkinServiceName))
                    .AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddZipkinExporter(zipkinOptions =>
                    {
                        zipkinOptions.Endpoint = new Uri($"{zipkinEndpoint}");
                    })
                    .AddConsoleExporter());

            return services;
        }
    }
}
程式碼說明
  • Line 7 : 在 ConfigureService 方法中加上 AddOpenTelemetryTracing 擴充方法
  • Line 20~21 : 取得 Config 設定檔中定義 zipkin 的 serviceName 與 endpoint
  • Line 25 : 將 OpenTelemerey 新增到 ASP.NET Core 應用程式中
  • Line 26 : 針對 httpclient 進行遙測數據的蒐集
  • Line 27 : 輸出到 zipkin 工具

另外,在 API 專案的 Startup.cs 程式碼如下
public class Startup
{	
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddControllers();

		services.AddOpenTelemetryTracing(Configuration);

		services.AddStackExchangeRedisCache(options =>
		{
			options.InstanceName = "Redis Cache";
			options.Configuration = "localhost:6379";
		});
	}

	public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
   {
		// 省略
	}	
	static class ServiceCollectionExtensions
    {

        public static IServiceCollection AddOpenTelemetryTracing(this IServiceCollection services, IConfiguration configuration)
        {
            var zipkinServiceName = configuration.GetValue("Zipkin:ServiceName");
            var zipkinEndpoint = configuration.GetValue("Zipkin:Endpoint");
            var redisEndpoint = configuration.GetValue("Redis:Endpoint");

            var connection = ConnectionMultiplexer.Connect(redisEndpoint);
            services.AddSingleton(connection);
            
            services.AddOpenTelemetryTracing((builder) => builder
                    .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(zipkinServiceName))
                    .AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddRedisInstrumentation(
                        connection,
                        option=>
                        {
                            option.FlushInterval = TimeSpan.FromSeconds(5);
                        }
                    )
                    .AddZipkinExporter(zipkinOptions =>
                    {
                        zipkinOptions.Endpoint = new Uri($"{zipkinEndpoint}");
                    })
                    .AddConsoleExporter());

            return services;
        }
    }	
}
  
API 的 Startup.cs 程式碼說明
  • Line 9 : 新增 Redis instance
  • Line 30 : 設定 redis life cycle
  • Line 36 : 設定 redis 自動蒐集 Library

撰寫完代瑪之後,開啟瀏覽器輸入 https://localhost:44321/weatherforecast 進行測試,可以發現有正常的回傳 weather 的資訊

接著在開啟 zipkin 看這次的範例會如何呈現

在 zipkin 中可以看到請求的相關連資訊,整個請求可以分為四個 span 每個 span 所花費的時間都可以透過 zipkin 顯示出來,從 Demo Website 到呼叫 Demo API 再到後面的 Redis 緩存 cache 都一目了然呈現,其中像是點選 localhost:6379 redis 的話,右邊區塊會呈現蒐集 redis 相關的遙測數據資料,舉例來說 redis 的 servicename, endpoint、db.system 都有提供,是透過哪個 library 搜集器的資訊與版本也可以看到。

結論
在過去如果要達到相同目蒐集請求資訊的話,可能在代碼上會加上很多不同的類別,像是要蒐集時間可能會使用 stopwatch 來計算執行時間、定義要蒐集的資訊加上logging,logging 會在加上 tag 或是自定義 attribute 蒐集想要的內容、上述部分如果沒有一個好的規範可能在代碼上就會相對雜亂一些,如果希望規模或一致性會定義蒐集資訊的共用規範或是 library 則需要很大的前置作業與功夫來規範。透過 OpenTelemerey 則可以大大省下這些時間,就像上面範例三的 Demo 內容透過 OTel 提供的 Library 就可蒐集請求的遙測數據資料,並 exporter 到指定的工具或是平台上,在透過自己習慣使用的平台來查詢異常問題的 root cause 或是異常的 service 是哪個,三大雲平台廠商在 OpenTelemetry 規範孵蛋成功後,也開始在自己的平台上陸續支援這套蒐集遙測數據的規範,像是在 Azure 就有在 MSDN 與技術部落格說明 Azure Monitor 與 Application insign 如何整合 OpenTelemetry。

這篇文章的範例與程式碼在 Github 上都可以看的到,位置是 OpenTelemetrySample with .NET Core,各位有興趣的朋友都可以 Clone 專案下來在自己的本機環境試試看,hope it help :D

參考
https://opentelemetry.io/

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

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com