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 → Tempo | AddSupAppObservability → options.Tracing.OtlpEndpoint → ส่งผ่าน otel-collector (generic, ไม่ต้องแก้ต่อ service) → Tempo | Code: ObservabilityExtensions.cs • IaC: secret OpenTelemetry__OtlpEndpoint |
| Metrics → Prometheus | AddSupAppObservability → options.Metrics.ExportToPrometheus = true → app.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 จริงแล้ว)
| Service | Traces (OTLP→Tempo) | Metrics (/metrics→Prometheus) | IaC secret OpenTelemetry__OtlpEndpoint | IaC 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 จริง |
| WorkflowService | ❌ ObservabilityExtensions.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:รายตัว ⇒ แค่เพิ่ม keyOpenTelemetry__OtlpEndpointในsecretObjectsของsecret-provider.yamlก็จะกลายเป็น env var ให้แอปอ่านได้เองทันที ไม่ต้องแก้ deployment.yaml เพิ่มสำหรับ secret
4. Checklist ทั่วไป (ใช้ได้กับ service ไหนก็ได้ในอนาคต)
- 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)
- เรียก
- Code —
Program.cs- เรียก
app.UseOpenTelemetryPrometheusScrapingEndpoint()หลังapp.Build()⚠️ (จุดที่มักลืมอีกจุด)
- เรียก
- IaC — secret-provider.yaml (ทุก env ที่เกี่ยวข้อง: dev/sit/uat)
- เพิ่ม
dev--OpenTelemetry--OtlpEndpoint(หรือsit--,uat--ตาม env) เข้าobjects+secretObjects.data
- เพิ่ม
- IaC — deployment.yaml (ทุก env)
- เพิ่ม pod annotation
prometheus.io/scrape,prometheus.io/port,prometheus.io/path - port ต้องตรงกับ container port จริงที่ Kestrel ฟัง
- เพิ่ม pod annotation
- 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) | $service | dynamic — label_values() จาก Prometheus, โผล่เองเมื่อ pod ส่ง metric ที่มี label service_name ถูกต้อง | ❌ ไม่ต้องแก้ — แค่ deploy ให้ถูก |
superapp-trace-explorer (traces) | $service | custom 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 ใหม่ ให้ตรวจตามลำดับนี้:
/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"- Prometheus scrape ติด: เช็คใน Grafana → Explore → Prometheus datasource → query
up{service_name="task-service"}/up{service_name="workflow-service"} - 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 - Dropdown มีชื่อจริงไหม: เปิด dashboard เอง เช็ค dropdown “Service” ว่ามี
Task-API/Workflow-APIหรือไม่ (ถ้าไม่มีให้ re-import ตาม §7) - IaC parity ครบทุก env: ตรวจว่า secret + annotation ที่เพิ่มถูก apply ครบทั้ง dev/sit/uat ไม่ใช่แค่ dev (ตามกฎ IaC parity ของทีม — แก้ appsettings/secret ที่ env เดียวแล้วลืม env อื่น = พังตอน deploy จริง)
- ตรวจ 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 ตามนี้จริง ให้แจ้งแยกเป็นงานถัดไป