Private Docs

OTel + Tempo Setup by Service — Actionable Guide

คู่มือต่อ OpenTelemetry (OTel) tracing + metrics เข้า Tempo/Prometheus ต่อ service — UserService (reference), gap-closing สำหรับ TaskService/WorkflowService, และสิ่งที่ต้องเช็คฝั่ง Grafana dashboard

อัปเดต: 2026-07-01

Scope: UserService (reference — เสร็จแล้ว), WorkflowService (ยังไม่ทำ), TaskService (ทำครึ่งทาง) ตรวจโค้ด + IaC จริงจากทั้ง 3 repo แล้ว ไม่ใช่ค่าเดา


1. ภาพรวม / Architecture

flowchart LR
    subgraph Pod["Service Pod"]
        App["ASP.NET Core App\n(SupApp_util_lib.AddSupAppObservability)"]
    end

    App -- "OTLP gRPC :4317\n(Tracing.OtlpEndpoint)" --> Collector["otel-collector\n(observability-{env} ns)"]
    Collector -- "otlp/tempo exporter" --> Tempo["Tempo\n(traces storage)"]

    App -- "/metrics\n(prometheus.io/scrape annotation)" --> Prom["AMA Prometheus\n(kube-system scrape config)"]

    Tempo --> GrafanaTrace["Grafana:\nsuperapp-trace-explorer"]
    Prom --> GrafanaMetrics["Grafana:\nsuperapp-svc-health"]

2 pipeline แยกกันอิสระ ต้องต่อครบทั้งคู่ถึงจะ “ติด” ครบ:

Pipelineกลไกจุดที่ service ต้อง config
Traces → TempoAddSupAppObservabilityoptions.Tracing.OtlpEndpoint → ส่งผ่าน otel-collector (generic, ไม่ต้องแก้ต่อ service) → TempoCode: ObservabilityExtensions.cs • IaC: secret OpenTelemetry__OtlpEndpoint
Metrics → PrometheusAddSupAppObservabilityoptions.Metrics.ExportToPrometheus = trueapp.UseOpenTelemetryPrometheusScrapingEndpoint() expose /metrics → AMA Prometheus scrape ผ่าน pod annotation (auto-discovery, ไม่ต้องแก้ config กลาง)Code: ObservabilityExtensions.cs + Program.cs • IaC: pod annotations prometheus.io/*

otel-collector (Backend_Iac/src/yamls/{env}/observability/otel-collector.yaml) และ Prometheus scrape config (ama-metrics-prometheus-config.yaml) เป็น generic ใช้ร่วมกันทุก service อยู่แล้ว — ไม่ต้องแก้เมื่อเพิ่ม service ใหม่ ตราบใดที่ service ส่ง OTLP เข้า otel-collector:4317 และ pod มี annotation ที่ถูกต้อง


2. สถานะปัจจุบัน (ตรวจโค้ด+IaC จริงแล้ว)

ServiceTraces (OTLP→Tempo)Metrics (/metrics→Prometheus)IaC secret OpenTelemetry__OtlpEndpointIaC prometheus.io annotation
UserService✅ ครบ✅ ครบ✅ ทุก env✅ (dev port 5111)
TaskService❌ โค้ดผูกกับ App Insights เท่านั้น ไม่อ่าน OtlpEndpoint เลย❌ ไม่ได้ตั้ง ExportToPrometheus=true และไม่มี middleware call✅ ทุก env (dev/sit/uat) — พร้อมแล้วแต่โค้ดไม่ได้ใช้✅ dev/sit (port 5600) — รอ scrape แต่ยังไม่มี metrics ให้ scrape จริง
WorkflowServiceObservabilityExtensions.cs เป็น stub ว่าง ไม่เรียก AddSupAppObservability เลย❌ เช่นเดียวกัน❌ ไม่มีเลยในทุก env⚠️ dev ไม่มี, sit มี (port 9009) แต่ไม่มีผลเพราะ code ไม่ expose /metrics — inconsistent ระหว่าง env

3. Reference Walkthrough — UserService (ใช้อ้างอิงได้จริง)

Code: Backend_UserService/src/UserService01.API/Extensions/ObservabilityExtensions.cs

public static class ObservabilityExtensions
{
    public static IServiceCollection AddAppObservability(
        this IServiceCollection services,
        IConfiguration configuration,
        IHostEnvironment environment)
    {
        services.AddSupAppObservability(options =>
        {
            options.ServiceName = "UserService-API";
            options.ServiceVersion = "1.UserService";
            options.ServiceNamespace = "UserService";
            options.EnableTracing = true;
            options.EnableMetrics = true;
            options.Tracing.InstrumentAspNetCore = true;
            options.Tracing.InstrumentHttpClient = true;
            options.Tracing.InstrumentEfCore = true;
            options.Tracing.InstrumentRedis = true;
            options.Tracing.ExportToConsole = false;
            options.Metrics.InstrumentAspNetCore = true;
            options.Metrics.InstrumentHttpClient = true;
            options.Metrics.InstrumentRuntime = true;

            // Trace Pipeline: OTLP → OTel Collector → Tempo
            var otlpEndpoint = configuration["OpenTelemetry:OtlpEndpoint"];
            if (!string.IsNullOrEmpty(otlpEndpoint))
                options.Tracing.OtlpEndpoint = otlpEndpoint;

            // Metrics Pipeline: expose /metrics ← Prometheus scrapes
            options.Metrics.ExportToPrometheus = true;
        });

        return services;
    }
}

Program.cs — ต้องมี middleware นี้เพิ่มจาก AddAppObservability:

app.UseAppPipeline();
app.UseOpenTelemetryPrometheusScrapingEndpoint();   // ← ขาดไม่ได้ ไม่งั้น /metrics ไม่ทำงาน

IaC — secret (Backend_Iac/src/yamls/{env}/superapp/secret-providers/user-secret-provider.yaml):

- |
  objectName: dev--OpenTelemetry--OtlpEndpoint     # env-level shared secret (ไม่ใช่ service-specific)
  objectType: secret
  objectAlias: OpenTelemetry__OtlpEndpoint

และใน secretObjects[].data ต้องมี mapping คู่กัน:

- objectName: OpenTelemetry__OtlpEndpoint
  key: OpenTelemetry__OtlpEndpoint

IaC — pod annotation (user-deployment.yaml):

annotations:
  prometheus.io/scrape: "true"
  prometheus.io/port: "5111"     # port ที่ Kestrel ฟัง (ต้องตรงกับ container port จริง)
  prometheus.io/path: "/metrics"

หมายเหตุสำคัญ: ทุก deployment ในระบบใช้ envFrom: secretRef (ทั้งก้อน secret) ไม่ใช่ env: รายตัว ⇒ แค่เพิ่ม key OpenTelemetry__OtlpEndpoint ใน secretObjects ของ secret-provider.yaml ก็จะกลายเป็น env var ให้แอปอ่านได้เองทันที ไม่ต้องแก้ deployment.yaml เพิ่มสำหรับ secret


4. Checklist ทั่วไป (ใช้ได้กับ service ไหนก็ได้ในอนาคต)

  1. Code — ObservabilityExtensions.cs
    • เรียก services.AddSupAppObservability(options => { ... })
    • ตั้ง ServiceName ให้ตรงกับที่จะใช้ค้นหาใน Grafana (ดู §6 — ต้องตรงเป๊ะกับ dropdown ที่มีอยู่)
    • EnableTracing = true, EnableMetrics = true
    • อ่าน configuration["OpenTelemetry:OtlpEndpoint"]options.Tracing.OtlpEndpoint
    • ตั้ง options.Metrics.ExportToPrometheus = true ⚠️ (จุดที่มักลืม — ดู §5.2)
  2. Code — Program.cs
    • เรียก app.UseOpenTelemetryPrometheusScrapingEndpoint() หลัง app.Build() ⚠️ (จุดที่มักลืมอีกจุด)
  3. IaC — secret-provider.yaml (ทุก env ที่เกี่ยวข้อง: dev/sit/uat)
    • เพิ่ม dev--OpenTelemetry--OtlpEndpoint (หรือ sit--, uat-- ตาม env) เข้า objects + secretObjects.data
  4. IaC — deployment.yaml (ทุก env)
    • เพิ่ม pod annotation prometheus.io/scrape, prometheus.io/port, prometheus.io/path
    • port ต้องตรงกับ container port จริงที่ Kestrel ฟัง
  5. Grafana — ตรวจ (ไม่ใช่แก้เสมอไป — ดู §6) ว่า ServiceName ที่ตั้งใน step 1 ตรงกับ value ใน dashboard dropdown หรือยัง

5. TaskService — ปิด Gap เฉพาะจุด (ไม่ต้องแก้ IaC)

IaC ของ TaskService พร้อมหมดแล้วทุก env (secret + prometheus annotation) เหลือแค่แก้โค้ด 2 จุด

5.1 Backend_TaskService/src/Task01.API/Extensions/ObservabilityExtensions.cs

โค้ดปัจจุบัน (สรุป): เรียก AddSupAppObservability แล้วแต่ผูกแค่ Azure Monitor (ApplicationInsights:ConnectionString) เพียงอย่างเดียว ไม่เคยอ่าน OpenTelemetry:OtlpEndpoint และไม่ได้ตั้ง Metrics.ExportToPrometheus

ต้องเพิ่ม (ไม่ต้องลบ Azure Monitor เดิม — ส่งได้พร้อมกันทั้ง 2 ทาง):

services.AddSupAppObservability(options =>
{
    options.ServiceName = "Task-API";   // ← เดิมมีอยู่แล้ว ตรงกับ Grafana dropdown แล้ว ไม่ต้องแก้
    // ... instrumentation flags เดิมคงไว้ ...

    // ➕ เพิ่ม: Trace Pipeline OTLP → Tempo
    var otlpEndpoint = configuration["OpenTelemetry:OtlpEndpoint"];
    if (!string.IsNullOrEmpty(otlpEndpoint))
        options.Tracing.OtlpEndpoint = otlpEndpoint;

    // ➕ เพิ่ม: เปิด Prometheus exporter (ไม่งั้น /metrics จะไม่มีข้อมูลแม้เรียก middleware แล้ว)
    options.Metrics.ExportToPrometheus = true;

    // ... AzureMonitorConnectionString เดิมคงไว้ตามปกติ ...
});

สำคัญ: ใน shared package (Backend_Package/src/Extension/ObservabilityExtensions.cs) Prometheus exporter ถูก gate ด้วย if (options.Metrics.ExportToPrometheus) builder.AddPrometheusExporter(); — ถ้าไม่ตั้ง flag นี้ ต่อให้เรียก UseOpenTelemetryPrometheusScrapingEndpoint() ใน Program.cs ก็จะไม่มี metrics ให้ scrape จริง (endpoint จะไม่ทำงานตามที่คาด)

5.2 Backend_TaskService/src/Task01.API/Program.cs

เพิ่มหลัง var app = builder.Build();:

var app = builder.Build();

app.UseAppPipeline();
app.UseOpenTelemetryPrometheusScrapingEndpoint();   // ➕ เพิ่มบรรทัดนี้
app.MapControllers().RequireRateLimiting("GlobalPolicy");
app.MapStaticAssets();

await app.RunAsync().ConfigureAwait(false);

5.3 IaC — ไม่ต้องแก้

task-secret-provider.yaml (dev/sit/uat) มี OpenTelemetry__OtlpEndpoint อยู่แล้ว และ task-deployment.yaml (dev/sit) มี prometheus.io/port: "5600" รออยู่แล้ว — deploy โค้ดใหม่แล้วจบ (แต่ควร verify uat ว่ามี prometheus annotation ด้วยหรือยัง — ดู §7 verification)


6. WorkflowService — ปิด Gap เต็มรูปแบบ (โค้ด + IaC)

6.1 Code — Backend_WorkflowService/src/WorkFlow01.API/Extensions/ObservabilityExtensions.cs

ปัจจุบันเป็น stub เปล่า ต้องเขียนใหม่ทั้งไฟล์ (อ้างอิง pattern จาก UserService §3):

using SupApp_util_lib.Extension;
using SupApp_util_lib.Extensions;

namespace WorkFlow01.API.Extensions;

/// <summary>
/// Observability — OpenTelemetry (Tracing + Metrics)
/// </summary>
public static class ObservabilityExtensions
{
    public static IServiceCollection AddAppObservability(
        this IServiceCollection services,
        IConfiguration configuration,
        IWebHostEnvironment environment)
    {
        services.AddSupAppObservability(options =>
        {
            options.ServiceName = "Workflow-API";   // ⚠️ ต้องตรงตัวพิมพ์เล็ก-ใหญ่นี้เป๊ะ ดู §6.3
            options.ServiceVersion = "1.0.0";
            options.ServiceNamespace = "Workflow";
            options.EnableTracing = true;
            options.EnableMetrics = true;
            options.Tracing.InstrumentAspNetCore = true;
            options.Tracing.InstrumentHttpClient = true;
            options.Tracing.InstrumentEfCore = true;
            options.Tracing.ExportToConsole = environment.IsDevelopment();
            options.Metrics.InstrumentAspNetCore = true;
            options.Metrics.InstrumentHttpClient = true;
            options.Metrics.InstrumentRuntime = true;

            var otlpEndpoint = configuration["OpenTelemetry:OtlpEndpoint"];
            if (!string.IsNullOrEmpty(otlpEndpoint))
                options.Tracing.OtlpEndpoint = otlpEndpoint;

            options.Metrics.ExportToPrometheus = true;
        });

        return services;
    }
}

WorkflowService ไม่มี Redis instrumentation ในโค้ดปัจจุบัน (ไม่เห็นการใช้ IConnectionMultiplexer ใน service นี้ตอนตรวจ) — ถ้าไม่ได้ใช้ Redis จริงให้ข้าม Tracing.InstrumentRedis ไปได้เลย ไม่ต้องเปิดเผื่อ

Program.cs เรียก AddAppObservability(...) อยู่แล้ว (WorkFlow01.API/Program.cs บรรทัดที่เรียก .AddAppObservability(...) มีอยู่แล้วในลำดับ builder.Services chain) — ไม่ต้องแก้ Program.cs ส่วน DI

ต้องเพิ่มเพิ่มเติมใน Program.cs หลัง var app = builder.Build(); (ปัจจุบันยังไม่มี):

app.UseMiddlewares();
app.UseOpenTelemetryPrometheusScrapingEndpoint();   // ➕ เพิ่มบรรทัดนี้ก่อน app.Run()

app.Run();

6.2 IaC — เพิ่มใหม่ทุก env (dev/sit/uat)

workflow-secret-provider.yaml — เพิ่มเข้า objects array:

- |
  objectName: dev--OpenTelemetry--OtlpEndpoint    # sit/uat ให้เปลี่ยน prefix ตาม env
  objectType: secret
  objectAlias: OpenTelemetry__OtlpEndpoint

และเพิ่มเข้า secretObjects[].data:

- objectName: OpenTelemetry__OtlpEndpoint
  key: OpenTelemetry__OtlpEndpoint

workflow-deployment.yaml — เพิ่ม pod annotations (dev ไม่มีเลยตอนนี้ ต้องเพิ่ม; sit มีอยู่แล้วที่ port 9009 แต่ก่อนหน้านี้ไม่มีผลเพราะ code ไม่ expose — หลังแก้โค้ดแล้วจะเริ่มใช้งานได้จริง):

annotations:
  prometheus.io/scrape: "true"
  prometheus.io/port: "<container port จริงของ Workflow>"   # ตรวจ container port ปัจจุบันก่อนใส่ (ดู §7)
  prometheus.io/path: "/metrics"

dev ไม่มี annotation เลย ต้องเพิ่มใหม่ทั้งหมด — sit มี port: "9009" อยู่แล้ว ให้ตรวจว่า Kestrel ฟัง port นี้จริงหรือไม่ก่อนเชื่อค่าเดิม (อาจเป็นค่าที่ตั้งไว้ล่วงหน้าแบบเดา)

6.3 ⚠️ ServiceName ต้องตรงกับ Grafana dropdown ที่มีอยู่แล้ว

พบว่า dashboard superapp-trace-explorer (ไฟล์จริงใน Backend_Iac/src/observability/grafana-dashboards/master/superapp-trace-explorer.json) มี custom-list dropdown ที่ใส่ค่า "Workflow-API" และ "Task-API" ไว้รอแล้ว (พร้อมกับ Centralized-API, ThirdPartyFx-API, Orchestrator-API) — ไม่ตรงกับที่ README ของโฟลเดอร์นั้นเขียนไว้ว่ามีแค่ 9 ตัว (readme เก่ากว่าไฟล์ json จริง)

สรุป: ถ้าใช้ ServiceName = "Workflow-API" (W ใหญ่, f เล็ก) ตรงตัวนี้ → ไม่ต้องแก้ dashboard JSON เลย เพราะ dropdown มีรออยู่แล้ว — ถ้าตั้งชื่ออื่น เช่น "WorkFlow-API" (F ใหญ่) trace จะเข้า Tempo แต่จะไม่โผล่ในตัวเลือก dropdown (ต้องพิมพ์ custom value เอาเอง หรือแก้ dashboard เพิ่ม)

TaskService ปัจจุบันตั้ง ServiceName = "Task-API" อยู่แล้ว ตรงกับ dropdown เป๊ะ ไม่ต้องแก้อะไรฝั่ง dashboard


7. Grafana Dashboard — สรุปว่าต้องอัปเดตอะไรบ้าง

Dashboardตัวแปรกลไก dropdownต้องแก้ไฟล์ไหม
superapp-svc-health (metrics)$servicedynamiclabel_values() จาก Prometheus, โผล่เองเมื่อ pod ส่ง metric ที่มี label service_name ถูกต้อง❌ ไม่ต้องแก้ — แค่ deploy ให้ถูก
superapp-trace-explorer (traces)$servicecustom list มือ — แต่ตรวจแล้วมี Task-API และ Workflow-API อยู่ใน list แล้ว (ในไฟล์ master/superapp-trace-explorer.json)❌ ไม่ต้องแก้ไฟล์ — แต่ต้อง verify กับ Grafana ที่ deploy จริงว่าตรงกับไฟล์ master/ หรือยัง (ดู §8) เพราะ README เดิมของโฟลเดอร์นี้เขียนข้อมูลไม่ตรงกับไฟล์ json จริง — มีความเป็นไปได้ที่ Grafana จริงยังไม่ sync ล่าสุด

คำแนะนำ: ก่อนเชื่อว่า dropdown พร้อมแล้ว ให้เปิด Grafana จริงเช็คตาม deep-link ด้านล่าง (§8) ครั้งเดียวก่อน ถ้าไม่เจอ Workflow-API/Task-API ใน dropdown จริง ให้ re-import ไฟล์ master/superapp-trace-explorer.json ด้วย scripts/Import-GrafanaDashboard.ps1 ตามขั้นตอนใน Backend_Iac/src/observability/grafana-dashboards/README.md


8. Verification Checklist

หลัง deploy โค้ด + IaC ใหม่ ให้ตรวจตามลำดับนี้:

  1. /metrics ขึ้นจริง:
    kubectl exec -n superappdev deploy/task-service -- curl -s localhost:5600/metrics | Select-String "http_server"
    kubectl exec -n superappdev deploy/workflow-service -- curl -s localhost:<port>/metrics | Select-String "http_server"
    
  2. Prometheus scrape ติด: เช็คใน Grafana → Explore → Prometheus datasource → query up{service_name="task-service"} / up{service_name="workflow-service"}
  3. Trace เข้า Tempo: ยิง request จริงผ่าน service แล้วเช็ค Grafana:
    <GRAFANA_URL>/d/superapp-trace-explorer?var-service=Task-API
    <GRAFANA_URL>/d/superapp-trace-explorer?var-service=Workflow-API
    
  4. Dropdown มีชื่อจริงไหม: เปิด dashboard เอง เช็ค dropdown “Service” ว่ามี Task-API / Workflow-API หรือไม่ (ถ้าไม่มีให้ re-import ตาม §7)
  5. IaC parity ครบทุก env: ตรวจว่า secret + annotation ที่เพิ่มถูก apply ครบทั้ง dev/sit/uat ไม่ใช่แค่ dev (ตามกฎ IaC parity ของทีม — แก้ appsettings/secret ที่ env เดียวแล้วลืม env อื่น = พังตอน deploy จริง)
  6. ตรวจ container port ของ Workflow ก่อนใส่ annotation — อย่าใช้ค่า 9009 จาก sit ตรงๆ โดยไม่ verify ว่า Kestrel ฟัง port นี้จริง (docker-compose*.yml หรือ Dockerfile ของ WorkflowService)

9. หมายเหตุ — ขอบเขตเอกสารนี้

เอกสารนี้เป็น guide สำหรับทำเอง ยังไม่ได้แก้ไฟล์โค้ด/IaC จริงในทั้ง 2 repo (Backend_WorkflowService, Backend_TaskService, Backend_Iac) — ถ้าต้องการให้ implement ตามนี้จริง ให้แจ้งแยกเป็นงานถัดไป