只有累積,沒有奇蹟

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年12月25日 星期四

[meetup] DevOps Taiwan Meetup #74 《建構多租戶 SaaS 架構》新書導讀

議程介紹
主題 : 《建構多租戶 SaaS 架構》新書導讀
《建構多租戶 SaaS 架構》新書導讀

課程大綱
▋ 從單體到多租戶:是升級還是修羅場?
  「從單體轉向多租戶」——你以為這是一場升級,結果它往往是一場墜落。
  一個讓系統變複雜、讓工程師變沉默的坑。
  剛開始以為只要多加一個 TenantId 就能搞定,
  結果查詢變慢、資料變亂、部署變痛,成本更是像記憶體洩漏一樣,一旦開始就難以回收。
  每解一個問題,就會掉進另一個坑——
  效能解了,卻出現租戶干擾;
  資料分開了,Migration 又成災;
  部署自動化了,成本卻無法對齊。
  本來以為自己能從從容容、游刃有餘地打造架構,
  結果變成匆匆忙忙、連滾帶爬地追著 bug、救著火。
  那一刻你會明白,這條路不是升級路線,而是一次又一次的試煉。
  你寫的不只是程式,而是一連串的補丁與懺悔。
  查 log、修腳本、追異常成為日常;功能開發成了奢侈。
  你以為自己在救火,但其實你正在為系統的複雜度繳學費。
  當團隊的焦點從「創造價值」轉為「維持穩定」,
  工程師也從「開發者」變成「問題管理員」。
  你才真正理解——
  → 多租戶不只是架構上的挑戰,它更是團隊心理韌性與工程紀律的考驗。
▋ 從坑到結構的覺醒
  當你從坑裡爬出來回頭看,你會發現這些問題早已不是偶發事件,
  而是每個 SaaS 團隊都得面對的共同考題。
  這也是《建構多租戶 SaaS 架構》真正想帶你看見的現實:
  → 多租戶不是一種部署方式,而是一場關於效能、資料、維運與成本的修行。
  理解這四件事,才是真正邁向多租戶成熟的起點。
一、效能:不只是跑得快,而是信得過
  如果你曾被客戶問過:「為什麼今天比昨天慢?」
  那你就知道效能不只是技術問題,而是信任問題。
  在單體架構時,你只在意系統跑得快不快;
  進入多租戶後,問題變成——誰讓誰變慢了。
  A 客戶開了一個報表,B 客戶就開始卡頓。
  這不是 bug,而是「Noisy Neighbor」的現實。
  → 效能不只是數字,而是一種信任契約。
二、資料架構:風險與效率的對話
  每一個架構師都會面臨這個靈魂拷問:
  要不要共用 Schema?要不要分庫?
  這些看似是技術問題,其實每一個決策背後都是風險選擇。
  共用架構代表成本低,但錯誤可能牽連所有租戶;
  獨立架構代表安全,但維運與版本控制的成本將倍增。
  → 架構沒有完美的答案,但成熟的團隊懂得在風險與效率之間,
  找到能讓產品長久運行的平衡點。
三、維運:從部署變成節奏
  當部署頻率越高、租戶越多,
  每一次上線都像踩在一個尚未確認的地雷區。
  單體時,部署只是動作;
  多租戶後,部署變成節奏。
  一次上線,影響所有租戶;
  你開始需要灰度釋出、分群升級、即時回滾。
  這不再只是 CI/CD 的自動化,而是整個團隊如何定義「穩定」的制度。
  → DevOps 在這裡不只是流程,而是一種信任治理的文化。
  維運,不只是穩定系統,而是穩定人心。
四、成本切分:從效率到價值的覺醒
  如果你曾在帳單前皺眉,
  你就知道多租戶最難的,不是效能,而是「看不見的成本」。
  A 客戶每天跑十次報表、佔滿資源;
  B 客戶只上傳一份 Excel,卻付相同的錢。
  久而久之,你會發現 SaaS 的瓶頸不在 CPU,
  而在於成本缺乏對應與衡量機制。
  沒有清楚的成本映射,團隊就只能憑直覺調整。
  真正成熟的架構,不是壓低成本,而是讓成本與價值產生對應。
  → 那不只是技術能力,而是經營能力。
▋ 從效能治理到價值治理
  在雲端時代,SaaS 不只是技術選擇,
  更是一種產品與營運思維。
  《建構多租戶 SaaS 架構》就是你的解藥。
  本書由 AWS 資深架構師 Tod Golding 撰寫,
  他以多年協助 SaaS 團隊轉型的實戰經驗,
  總結出多租戶設計、部署與營運的完整方法論。
  那些讓你掉坑的問題,書中都有出口 
  ・第 09 章〈租戶隔離〉:如何避免 Noisy Neighbor
  ・第 08 章〈資料分區〉:在共用與分庫間找到平衡
  ・第 12 章〈租戶感知營運〉:建立租戶級的監控與自動化
  ・第 14、17 章〈分級策略/指導原則〉:讓架構、營運與商業模式對齊
  Golding 在書中不斷提醒我們:
  SaaS 的本質不是「上雲」,而是「統一思維與多樣彈性之間的平衡」。
  從第 01 章〈SaaS理念〉到第 17 章〈指導原則〉,
  他帶領讀者看見 SaaS 架構不只是技術結構,
  更是一種願景、策略、組織與營運文化的總和。
  如果你還不懂多租戶,這本書會帶你從 0 到 1。
  如果你正陷在坑裡,這本書能讓你少走幾年冤枉路。
  如果你已撐過那些坑,這本書會幫你整理經驗,帶領下一個團隊走得更穩。

主辦單位 : DevOps Taiwan 技術社群
議程表 : 連結
投影片 : 連結



2025年11月28日 星期五

[conference] .NET Conf Taiwan 2025 - Spec-kit x O.D.D:AI 時代的 .NET 開發新選擇

分享心得
AI 可以說是不可逆的趨勢,但礙於 LLM 先天的限制有幻覺性,大家在開發上雖然使用的很開心,但實際開發還是會有些不如預期的地方。這次就跟大家分享自己研究一款熱門的 open source 框架 spec-kit,有了 AI 後怎麼降低系統上線營運的問題也可以說是一大挑戰,這裡就帶入了之前研究可觀測性的一些重要觀念,可以透過 AI 的規範性將可觀測性重要的 singals 讓大家在開發時可以更輕鬆,當然也有帶到我個人覺得當 AI 更便利後,他也是一個放大器,放大我們不會的項目跟資訊,因此有哪些基本公式重要的也一併帶入,希望可以為大家帶來些不一樣的思考,謝謝 :D

議程介紹
主題 : Spec-kit x O.D.D:AI 時代的 .NET 開發新選擇?
Spec-kit x O.D.D:AI 時代的 .NET 開發新選擇?

課程大綱
AI 加速了開發流程,卻也讓「品質」與「透明度」成為新的挑戰。 本場分享將介紹如何結合 Spec-Kit 的規格驅動開發(Spec-Driven Development, SDD) 與 可觀測驅動開發(Observability-Driven Development, ODD),讓 .NET 開發者能從需求定義到系統行為驗證,打造一條「AI 寫得快、系統看得清」的開發流程,讓每一次上線都能被觀測、被驗證、被信任。

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



2025年10月14日 星期二

[conference] Hello World Dev Conference 2025 - 從錯誤到成長:新手主管必經的修煉之路

分享心得
這是我今年在 Hello World Dev 的分享,起心動念源於去年聽了好友 Kyle 的議程後,決定動手整理這幾年的技術管理經驗;這份簡報聚焦於我在新手主管時期踩過的無數個坑,是一份關於「從入門到差點放棄」的真實紀錄。 老實說我自己以前聽管理學講座時也很容易睡著,所以當我要分享這段從資深工程師轉職為主管的「血淚史」時,我決定用 RPG 遊戲攻略來講故事! 還記得剛拿到 Supervisor 頭銜時 ,以為自己轉職成了「英雄」,結果進了副本才發現,自己只是換了個地方當「高級打雜工」。 這份簡報試圖幫大家拆解那個最可怕的 Dead Loop(死循環): 💀 任務變多 ➔ 解釋太慢 ➔ 自己動手做 ➔ 授權變少 ➔ 團隊依賴 ➔ 主管更忙 其實忙到年底,原本已經忘記這場演講了,直到最近收到大會寄來的 Feedback Report,看到許多會眾的回饋讓我很有收穫,而且評分竟然有 8.89 / 10(全場最高分是 9.33);回想起自己在跌倒爬起來的過程中,也曾受到很多 mentor 與前輩的幫忙;抱著或許也能拉後來的人一把的心情,我決定「厚著臉皮」把這份簡報貼出來 最後,就算今天 Game Over 了也別怕,這份攻略就是你的 復活點 (Save Point)。 真的再過不去... 先去茶水間 CD 補個血,我們隨時可以按下重啟 restart (重生) 再戰

議程介紹
主題 : 從錯誤到成長:新手主管必經的修煉之路
從錯誤到成長:新手主管必經的修煉之路

課程大綱
從專業的開發者,轉變為帶領團隊協同作戰的主管,這是一場思維模式與職責重心的革命。許多新手主管在初期往往誤以為只要延續過去的成功經驗,或僅憑一腔熱血就能勝任。但現實上管理職不僅要求你驅動團隊達成目標,更要你在上層的戰略期望與成員的職涯發展之間,尋求精妙的平衡點,複雜度遠超想像。第一次被賦予管理責任,總對這個新角色抱持著美好憧憬,但這些想像真的能幫助我們應對真實世界中那些隱藏的挑戰嗎?
在這場議程中,我將以自身失敗經驗出發,帶你走過新手主管最容易因為認知偏差與實踐誤區所導致的五大盲點。我會分享這些問題是如何發生的,以及其背後的核心癥結(例如:溝通壁壘、授權困境、衝突迴避等),並提供實用的應對策略與思維框架。這條路上我學到了許多關於領導、協作與自我反思的寶貴經驗。希望透過這次分享能幫助與會者快速識破這些潛在的陷阱,少走冤枉路,從容應對管理上的複雜挑戰,共同成長,實現從專業技術開發者到新手主管的轉型。

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



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 主辦
議程表 : 連結



2025年3月8日 星期六

[NET] 移除未使用的 ASP.NET SDK 版本

前言
最近筆電空間告急,在 Google 解決方案時發現過去自己有撰寫過一篇文章是 移除未使用的 ASP.NET Core SDK,但仔細看完內容加上已經過了五年的時間,深深覺得五年過去會有更好的解決方法,於是乎終於讓我找到更有效的方法可以移除未使用的 dotnet framework 版本,這篇就跟大家簡單分享,與若對於上述內容有問題或是不清楚的地方,歡迎提出來一起討論。

查看 SDK 版本
首先,我們來透過指令來看目前電腦有安裝哪些版本 SDK (chatGPT 用多了,在用字描述上都不自覺用首先...其次 XDDDD)
dotnet --list-sdks 
透過此指令可以查詢到目前電腦中安裝的 SDK 版本與安裝路徑位置
如果想要查看 runtime 則可以輸入
dotnet --list-runtimes
透過上述方式看到目前電腦所安裝的 Runtime 與 SDK 版本,如果長期使用 dotnet 開發的朋友們可能也知道安裝時會在固定位置,例如
執行檔 : C:\program files\dotnet\dotnet.exe
.NET SDK : C:\program files\dotnet\sdk\版號
.NET Runtime : C:\program files\dotnet\shared\{runtime-type}\版號\

解法 : Windows 內建移除
如果是要移除單一的應用程式,一般可以到 應用程式 > 已安裝的應用程式 頁面來移除 .NET SDK 與 Runtime,
解法 : dotnet-core-uninstall
但上述做法只適用於 SDK 量比較少的狀況,如果你電腦使用一段時間比較多的時候要逐一移除就會比較辛苦跟麻煩,這次主要是分享的方法是微軟官方提供在 2020 年所提供的新方法,提供一個好用的工具,將系統中的 SDK 進行控制與清理的動作,只保留自己在開發時所需版本的 runtime 和 SDK。

工具下載連結 : https://github.com/dotnet/cli-lab/releases

接著往下滑到 asset 區域,我所使用的 OS 是 windows 因此選擇下載 dotnet-core-unistall.msi 檔案
執行安裝下載的檔案,以目前最新版為例是 1.7.550802,安裝完畢後,開啟命令提示字元輸入 dotnet-core-uninstall
可以看到所支援的 command 如下
list : List .NET Core SDKs or Runtimes that can be removed with this tool.
 dry-run, whatif  : Display .NET Core SDKs and Runtimes that will be removed.
 remove  : Remove the specified .NET Core SDKs or Runtimes.
該程式在移除上還是有提醒工具的限制
This tool cannot uninstall versions of the runtime or SDK that are?
    - SDKs installed using Visual Studio 2019 Update 3 or later.
    - SDKs and runtimes installed via zip/scripts.
    - Runtimes installed with SDKs (these should be removed by removing that SDK).
小提醒 : 在使用上還是記得開啟執行程式 cmd or powershell 前,需要使用系統管理員權限才能夠移除,權限不夠會出現 The current user does not have adequate privileges.提示

找出電腦所安裝的 SDK
輸入 dotnet-core-uninstall list 指令,可以透過程式找到目前電腦所安裝的 SDK 清單
移除 SDK
移除時請輸入指令 dotnet-core-uninstall remove 加上特定版本,例如上面看到目前電腦中有 7.0 & 8.0 版本,要移除時請輸入完整版號與指令,例如 dotnet-core-uninstall remove 7.0.410 --sdk 才會生效,確定後在下 Yes 即可

小結
以上快速介紹了使用微軟所提供的工具來刪除所安裝的 SDK,希望可以有機會幫到跟我一樣困擾的夥伴們,happy Coding !

參考
cli-lab

2025年2月8日 星期六

[NET] 探索 OpenTelemetry Auto-Instrumentation 的核心技術

前言
Observability 是手段,不是目的。透過 Auto-Instrumentation,我們更接近「讓系統自己說話」的願景。

在 COSCUP 2024 我有幸(不要臉) 再次參與並分享這一年來我在可觀測性實踐上最有趣的內容:「OpenTelemetry 的 Auto-Instrumentation 技術核心與應用實戰」。這篇文章,我會完整拆解 OpenTelemetry SDK 的 Auto-Instrumentation 的原理、架構、實務流程,並從開發者的視角出發,帶你理解什麼是「讓系統自己說話」,希望可以讓想要了解 OTel SDK 底層細節實作的朋友有幫助。


OpenTelemetry 是什麼

OpenTelemetry(簡稱 OTel)是 CNCF(Cloud Native Computing Foundation)旗下的觀測性開源計畫。它整合了 OpenTracing 和 OpenCensus,成為雲原生時代的統一觀測標準。

它主要處理三件事:

  • Instrument:幫助你從應用中收集觀測訊號(Signals)
  • OTLP:無論是 Trace、Metric 還是 Log,都有標準格式
  • Export:支援多種後端(如 Jaeger、Zipkin、Grafana Tempo、Prometheus...)

OpenTelemetry 是目前 CNCF 第二活躍的專案(僅次於 Kubernetes),也是現代 Observability 的標準。


Code-based v.s zero-code

讓程式「可以被觀察」,首先就要「儀表化」程式碼(Instrumentation)。這裡透過一張表整理 Code-based 與 zero-code 兩者的差異,更重要的是實際應用場景

小小建議:「Auto-Instrumentation 優先、Manual 優化補強」

Auto-Instrumentation 設計願景

根據 OpenTelemetry .NET Auto-Instrumentation 官方設計文件,這個 SDK 是以五大設計願景(Vision)為核心來開發的,每一個願景都對應到實務中我們真正關心的「導入門檻」、「維運穩定」、「擴充能力」。

高效能(High Performance)
  • 使用 IL 重寫,不改原始碼
  • Lazy Loading 降低資源開銷
  • 非同步匯出 Trace 資料
開箱即用(Useful by Default)
  • 內建常見欄位與框架支援
  • 啟動即生效
穩定可靠(Reliable)
  • 錯誤不影響應用
  • 具容錯能力與 fallback
可見(Visible)
  • 啟動時列出已啟用套件
  • 支援 metrics 與 debug mode
可擴展(Extensible)
  • 插件化結構,支援自定義與社群套件

Auto-Instrumentation 架構與元件
Application & Runtime(應用與執行環境)
  • Application IL(中間語言)
    .NET 應用程式編譯後的中間碼,是 Auto-Instrumentation 插入追蹤邏輯的目標對象。
  • CLR(Common Language Runtime)
    .NET 的執行核心,提供 ICorProfilerCallback / ICorProfilerInfo 介面供原生 Profiler 攔截執行事件與注入邏輯。
OTel CLR Profiler(Native)
  • 職責: 掛載在 CLR 上,透過 ICorProfiler API 攔截應用啟動流程。
  • 功能: 將 Bytecode Instrumentation 插入 Application IL,實現 Zero-code Observability。
Bytecode Instrumentations
  • 職責: 為特定框架提供具體的儀表邏輯,例如對 HttpClient、SQLClient、gRPC 注入觀測碼。
  • 功能: 在 method 內部插入 OTel API(如 StartSpan、SetAttribute)。
OTel Managed Profiler
  • 職責: 在應用執行階段進行進一步的邏輯注入與監控。
  • 用途: 管理細部的 Trace 拓展,例如更細緻的 DB query 或 gRPC 呼叫資訊。
Source Instrumentation / OTel SDK
  • Source Instrumentation: 指開發者手動使用 OTel API(例如 tracer.StartActiveSpan())插入觀測點。
  • OTel SDK: 提供觀測資料處理能力,負責收集、封裝並導出 Trace、Metrics、Logs 至後端。
架構運作流程
  1. 啟動階段: OTel CLR Profiler 掛載至 CLR,監控模組與 method 載入事件。
  2. 注入階段: Bytecode Instrumentations 對 IL 方法注入 StartSpan 等邏輯。
  3. 執行階段: Managed Profiler 進一步補強觀測點(如內部呼叫、Async 方法)。
  4. 資料導出: 所有訊號被 OTel SDK 收集後導出至 Tempo、Jaeger 等觀測平台。

設計重點 : 「模組化、低侵入、高彈性」


三大流程解析
註冊流程
重點:
  1. 設定環境變數:
    • CORECLR_ENABLE_PROFILING=1
    • CORECLR_PROFILER=OTel CLSID
    • CORECLR_PROFILER_PATH=Profiler DLL 路徑
  2. CoreCLR 啟動階段:
    • 檢查是否啟用 Profiling
    • 從 Registry 查找 CLSID 對應 DLL
    • 解析出 Profiler 的實際路徑
  3. 載入與初始化:
    • 載入 Profiler DLL
    • 呼叫 Initialize 方法註冊
    • 透過 ICorProfilerInfo 註冊事件攔截(Set Event Mask)

請求流程
重點 : 系統如何自動蒐集數據 Data (自己說話)
  1. 應用啟動: CoreCLR 發出多種事件通知,包括類別載入、JIT 編譯、方法進入與離開等。
  2. OTel CLR Profiler 攔截: 根據事件動態判斷並透過 IL 注入觀測邏輯(Bytecode Instrumentation)。
  3. Managed Profiler 建立與關閉 Span: 當方法被呼叫(FunctionEnter)時建立 Span;完成(FunctionLeave)後關閉該 Span。
  4. 外部服務呼叫: 如果方法內呼叫外部服務(API、DB),會建立額外的子 Span 表示呼叫鏈。
  5. OTel SDK 負責收集與導出: 所有已關閉的 Span 會統一被 SDK 收集並匯出至後端系統,如 Jaeger 或 Grafana Tempo。
數據流
重點
  • Application: 應用程式啟動,觸發 Auto-Instrumentation。
  • OTel CLR Profiler: 原生層級注入元件,處理 IL 攔截與上下文傳遞。
  • Bytecode Instrumentation: 插入具體的追蹤邏輯至目標方法(如 HTTP 請求)。
  • OTel Managed Profiler: 管理應用邏輯層級的觀測行為,呼叫 Instrumentation Handler。
  • OTel SDK: 中央資料處理站,封裝資料並準備匯出。
  • Collector: 集中資料後導出至 Jaeger(Trace)或 Prometheus(Metrics)。

ASP.NET Core 實作範例
services.AddOpenTelemetry()
    .WithTracing(builder => builder
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddGrpcClientInstrumentation()
        .AddOtlpExporter());

這段程式會自動收集:

  • http.method, http.status_code, http.url
  • trace_id, span_id, user_agent.original
.AddAspNetCoreInstrumentation()
  • Application : 在應用程式層級攔截整體 ASP.NET Core 的 Middleware Pipeline。自動追蹤整個 HTTP 請求從進入到離開的過程。
  • Controller : 對個別 Controller 進行觀測,例如:哪個 endpoint 被呼叫、哪些路由參數被解析
.AddHttpClientInstrumentation()、.AddGrpcClientInstrumentation()
  • 程式對外發出的 HTTP 請求(如 API 呼叫)
  • gRPC 呼叫事件,包含 method 名、service 名、status code
  • 只要你用標準的 HttpClient 或 .NET gRPC,這些都會被攔截、追蹤並補上 TraceContext。
.AddOtlpExporter()
  • 把觀測資料送到你的可視化平台
  • 例如 : OTLP → Grafana Tempo / Honeycomb / Lightstep / Jaeger / New Relic
用 C4 概念則可以劃出 Container 與 Component 內容
Container
Component

小結
Auto-Instrumentation 讓開發者可以不用動原始碼,就輕鬆讓系統「說出程式碼執行的狀態」,對於初學者在導入可觀測性文化中重要的環節,希望這篇介紹 Auto-Instrumentation 對各位有幫助,若以上內容有不清處的地方,歡迎留言討論,Hayyp Coding :)

參考
opentelemetry-dotnet
opentelemetry-dotnet-instrumentation
opentelemetry-dotnet-instrumentation/docs/design.md
dotnet/runtime/blob/main/docs/design/coreclr/botr/profiling

2025年1月16日 星期四

[NET] Task.WhenEach 等待多個任務的更靈活方式

前言
在過去如果要使用多個 Task 在 Dotnet 中可以使用 Task.WaitAll 和 Task.WhenAll 兩個內建的 API,其使用的差異之前也有撰寫文章 [NET] Task 等待多個任務 - Task.WaitAll 與 Task.WhenAll,這兩個方法都會等到所有任務完成後再繼續執行。如果當需求是完成一個就先進行處理,而不是全部完成後該怎麼辦呢 ? 這時就可以使用 Task.WhenEach 方法,來達到你所要的目的,這篇文章就簡單來介紹其概念與如何使用。

介紹
Task.WhenEach 是一種基於 Task.WhenAny 的模式,它的核心理念是當每個 Task 完成時立即執行對應的內容,而不用等待所有 Task 完成後再處理。這在某些場景下能提升應用的回應速度,尤其是當每個任務的執行時間不相同時,避免了某些較早完成的任務閒置等待其他長時間運行的任務,拖累到整體任務的完成時間。讓我們用一個範例來展示 Task.WhenEach 的概念。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

public class Program
{
    static async Task Main(string[] args)
    {
        List> tasks = new List>
        {
            SimulateWork(1, 1000),
            SimulateWork(2, 2000),
            SimulateWork(3, 3000),
            SimulateWork(4, 4000),
            SimulateWork(5, 5000),
        };

        await TaskWhenEach(tasks);
    }

    static async Task SimulateWork(int id, int delay)
    {
        await Task.Delay(delay);
        Console.WriteLine($"Task {id} completed after {delay / 1000.0:F2} seconds");
        return id;
    }

    static async Task TaskWhenEach(IEnumerable> tasks)
    {
        var taskList = tasks.ToList();
        
        while (taskList.Any())
        {
            var completedTask = await Task.WhenAny(taskList);
            taskList.Remove(completedTask);
            Console.WriteLine($"Processed result: {await completedTask}");
        }
    }
}

  
  
簡單說明 Task.WhenEach 的運作方式
1. 建立多個非同步任務,這些任務模擬不同時間長度的工作。
2. 使用 Task.WhenAny 找到第一個完成的任務,立即處理結果。
3. 從列表中移除已完成的任務,然後繼續等待剩餘的任務。
4. 重複上述步驟直到所有任務完成。
這樣的設計確保了不會有執行緒閒置等待較長的任務結束,而是每當一個任務完成就立即執行處理邏輯,提升了 application 或功能的回應速度。

執行結果
Task 1 completed after 1.00 seconds
Processed result: 1
Task 2 completed after 2.00 seconds
Processed result: 2
Task 3 completed after 3.00 seconds
Processed result: 3
Task 4 completed after 4.00 seconds
Processed result: 4
Task 5 completed after 5.00 seconds
Processed result: 5

 

應用場景
即時資料處理:
- 當一組異步任務代表不同的 API 請求時,可以使用 Task.WhenEach 來立即處理已完成的請求,提升使用者體驗。
非同步串流處理:
- 當我們要處理大量非同步工作時,Task.WhenEach 可以確保已完成的工作不會閒置,而是立即傳遞到下一個處理步驟。
UI 更新:
- 在 UI 應用中,當我們需要執行多個背景任務,並希望它們完成後立即更新 UI,可以使用 Task.WhenEach 來確保 UI 變更即時發生。

小結
希望這篇文章能幫助讀者了解 Task.WhenEach 的基本概念與應用,如果有任何問題或建議,歡迎交流討論,happy Coding !

參考
Task.WhenEach 方法

2024年12月14日 星期六

[conference] .NET Conf 2024 - .NET x Keycloak:從 0 到 1 建構身份驗證與授權解決方案

分享心得
很高興再次有機會可以在 .NET Conf 分享,在開發的過程中相信授權與認證是非常重要的環節,但在 AuthN & AuthZ 都有不同的規範與協議,這在開發上可能需要遵守同時也需要開發者在傳接上多留意,在 open source 有套好用的工具可以協助開發者降低這方面的 loading,名字叫做 keycloak,在這議程中就要跟大家分享在 dotnet 上要如何使用此套件,以及在開發過程中需要注意的地方,歡迎大家提出來進行討論,Happy learning 🙂

議程介紹
主題 : .NET x Keycloak:從 0 到 1 建構身份驗證與授權解決方案
.NET x Keycloak:從 0 到 1 建構身份驗證與授權解決方案
課程大綱
隨著企業應用邁向微服務化與雲原生架構,身份驗證與授權管理的重要性日益提升。透過 Keycloak 與 .NET 的整合,我們可以構建一個高效且符合企業級需求的身份驗證平台。本次演講將從設計到實現,探討開發者在身份驗證中面臨的挑戰,並分享 Keycloak 與 .NET 的整合實務及系統維運中的設計與優化策略。

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



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

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com