ตั้ง stack & สัญญาณแรก
บทที่แล้ววางแบบจำลองความคิดไว้จนครบ — คราวนี้เลิกพูดทฤษฎี เป้าหมายของบทนี้แคบและจับต้องได้: รัน ClickStack บน Docker, wire app Ordering เข้า OpenTelemetry, แล้วเห็น trace แรก end-to-end โผล่ใน HyperDX รวมถึง DB span จาก EF Core — โดย ยังไม่เขียน span เองสักบรรทัด เราจะพึ่ง auto-instrumentation ล้วนๆ เพื่อพิสูจน์ว่าสายส่งสัญญาณเชื่อมถึงกันจริง แล้วปิดท้ายด้วยความจริงข้อสำคัญว่าของฟรีก้อนนี้เป็นแค่ พื้น ไม่ใช่ เพดาน
code ในบทนี้ instrument platform Order ตัวเดิมที่ #8 สร้างไว้ — repo kaen-food-ordering (กำลังจัดทำ) เราแตะเฉพาะ Program.cs ของ context Ordering (FoodOrdering.Host) เพื่อเดินสาย OpenTelemetry เข้าไป คร่อม code เดิม — ไม่แก้ business logic, ไม่เพิ่ม bounded context, ไม่แตะ handler ของ Wolverine (นั่นคือบท3) code ทุกชิ้นในบทนี้ pin version จริงไว้ให้ copy-paste ได้ — ตรวจล่าสุด 2026-07-22
ขั้นที่ 1 — รัน ClickStack บน Docker
หัวข้อที่มีชื่อว่า “ขั้นที่ 1 — รัน ClickStack บน Docker”ClickStackClickStackstack observability โอเพนซอร์สของ ClickHouse: HyperDX (UI) + ClickHouse (columnar store) + OTel Collector distro; เป็น 'ปลายทาง' ที่สลับได้Architecture แจกเป็น image all-in-one ที่รวมทั้ง OTel Collector, ClickHouse (ที่เก็บ) และ HyperDX (UI) ไว้ใน container เดียว — เหมาะสำหรับเรียนรู้บนเครื่องตัวเอง เปิด terminal แล้วรัน (pin tag ให้ชัด อย่าใช้ latest):
docker run --name clickstack \ -p 8080:8080 -p 4317:4317 -p 4318:4318 -p 8123:8123 \ clickhouse/clickstack-all-in-one:2.31.0port ทั้งสี่มีหน้าที่ต่างกันชัดเจน จำไว้เพราะเราจะอ้างถึงมันทั้งคอร์ส:
| port | ใครใช้ |
|---|---|
| 8080 | HyperDX UI — เปิดในเบราว์เซอร์ |
| 4317 | OTLPOTLPOpenTelemetry Protocol protocol มาตรฐานส่ง telemetry (gRPC :4317 / HTTP :4318)Architecture ผ่าน gRPC (ค่า default ที่ SDK ยิงมา) |
| 4318 | OTLP ผ่าน HTTP/protobuf |
| 8123 | ClickHouse HTTP (query ตรงเข้า store) |
image clickstack-all-in-one ออกแบบมาเพื่อ เรียนรู้/ทดลอง เท่านั้น — storage ไม่ persistent: ปิด container เมื่อไหร่ข้อมูลหายหมด (ต้อง mount volume ที่ /data/db, /var/lib/clickhouse, /var/log/clickhouse-server ถึงจะเก็บอยู่) และเอกสารทางการระบุตรงๆ ว่ามัน ไม่เหมาะกับ production สำหรับ production จริงต้องแยก component ออกจากกัน (หรือใช้ Managed ClickStack บน ClickHouse Cloud) เราจะ ไม่ พูดเรื่องนี้ซ้ำ — บทนี้ทุกอย่างคือ dev loop บนเครื่องคุณ
เปิดเบราว์เซอร์ไปที่ http://localhost:8080 ทำ first-run setup (ตั้งบัญชี admin ในเครื่อง) พอเข้าได้แล้ว ไปที่ Team Settings → API Keys เพื่อคว้า ingestion API key ไว้ — เราจะใช้มันตอนคุยเรื่อง production ในขั้นที่ 4
ขั้นที่ 2 — ปัก package (pin version)
หัวข้อที่มีชื่อว่า “ขั้นที่ 2 — ปัก package (pin version)”InstrumentationInstrumentationการฝัง code/library ให้ app ปล่อย telemetry; auto = ได้ฟรีจาก framework, manual = เขียนเองให้มีความหมายเชิง domain (เป็น 'พื้น' ไม่ใช่ 'เพดาน')Process คือการฝัง code/library ให้ app ปล่อย telemetry ออกมา OpenTelemetry .NET แจก instrumentation แบบ อัตโนมัติ เป็น package แยกต่อ framework — ปักเข้าไปแล้วได้ span ของ ASP.NET Core, HttpClient, EF Core และ metric ของ runtime มาฟรีทันที เพิ่มชุดนี้ใน project FoodOrdering.Host:
dotnet add package OpenTelemetry.Extensions.Hosting --version 1.17.0dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol --version 1.17.0dotnet add package OpenTelemetry.Instrumentation.AspNetCore --version 1.17.0dotnet add package OpenTelemetry.Instrumentation.Http --version 1.17.0dotnet add package OpenTelemetry.Instrumentation.Runtime --version 1.17.0dotnet add package OpenTelemetry.Exporter.Console --version 1.17.0OpenTelemetry.Extensions.Hosting ดึง core package OpenTelemetry ตามมาเอง (transitive) จึงไม่ต้องปักแยก ส่วน Exporter.Console เป็นของ dev — ไว้เอาไว้ดู telemetry ด้วยตาตอน debug
EF Core instrumentation อยู่คนละสถานะกับตัวอื่น — มันยัง เป็น prerelease ต้องใส่ --prerelease ถึงจะปักได้:
dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore --version 1.17.0-beta.1 --prereleaseOpenTelemetry.Instrumentation.EntityFrameworkCore ล่าสุดคือ 1.17.0-beta.1 (ตรวจ nuget.org 2026-07-22) — มัน ยังไม่มี stable release ต่างจากอีก 5 package ในชุดที่ stable ที่ 1.17.0 แล้ว แปลว่า API ของมันอาจเปลี่ยนก่อนถึง 1.0 ถ้านโยบายทีมคุณห้าม prerelease บน production ทางเลือกที่ซื่อสัตย์คือ เขียน span ของ repository เอง (เราจะสอนวิธีสร้าง manual span ในบท3) แล้วค่อยสลับมาใช้ตัว auto เมื่อมัน GA เราเลือกใช้ตัว beta ในคอร์สนี้เพราะมันให้ DB span ฟรี — แต่บอกไว้ตรงนี้ว่ามันคือ beta ไม่ใช่ของนิ่ง
ขั้นที่ 3 — เดินสาย OpenTelemetry เข้า Ordering
หัวข้อที่มีชื่อว่า “ขั้นที่ 3 — เดินสาย OpenTelemetry เข้า Ordering”ทั้งหมดอยู่ใน Program.cs เพิ่ม block เดียวก่อน builder.Build() โครงคือ AddOpenTelemetry() แล้วต่อ .WithTracing(...) กับ .WithMetrics(...) — ในบทนี้ใส่ เฉพาะ auto-instrumentation (AspNetCore + Http + EFCore + Runtime) ยังไม่มี custom span ปิดท้ายด้วย .UseOtlpExporter() เพียงครั้งเดียว ที่เดินสาย OTLP ให้ทั้งสามสัญญาณ:
using OpenTelemetry.Logs;using OpenTelemetry.Metrics;using OpenTelemetry.Resources;using OpenTelemetry.Trace;
builder.Services.AddOpenTelemetry() .ConfigureResource(r => r .AddService(serviceName: "ordering-service", serviceVersion: "1.0.0") .AddAttributes(new Dictionary<string, object> { ["deployment.environment"] = builder.Environment.EnvironmentName })) .WithTracing(t => t .AddAspNetCoreInstrumentation() // span ฝั่งรับ HTTP request .AddHttpClientInstrumentation() // span ฝั่งยิง HTTP ออก (เช่น ACL ไป AcmePay) .AddEntityFrameworkCoreInstrumentation()) // span ของ query DB .WithMetrics(m => m .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddRuntimeInstrumentation()) // GC, thread pool, memory .UseOtlpExporter(); // ★ เดียว → OTLP สำหรับ traces + metrics + logs (gRPC http://localhost:4317 เป็น default)
// ★ ยังจำเป็น: UseOtlpExporter() ให้แค่ตัว exporter — บรรทัดนี้ต่างหากที่จับ output ของ ILogger เข้า OTelbuilder.Logging.AddOpenTelemetry(o =>{ o.IncludeFormattedMessage = true; o.IncludeScopes = true;});มีสามจุดที่พังเงียบได้ถ้าพลาด — ปักหมุดไว้:
.UseOtlpExporter()แบบไม่มี argument จะอ่าน env var ตระกูลOTEL_EXPORTER_OTLP_*และ default ไป gRPChttp://localhost:4317ซึ่งตรงกับ port รับของ ClickStack พอดี (zero-config) มันลงทะเบียน OTLP ให้ ทั้งสามสัญญาณในคำสั่งเดียวbuilder.Logging.AddOpenTelemetry(...)ยังต้องมี แม้จะเรียกUseOtlpExporter()แล้ว — เพราะตัวหลังให้แค่ ช่องส่งออก ไม่ได้ต่อ pipeline ของILoggerเข้ามาให้ ถ้าลืมบรรทัดนี้ trace กับ metric จะมา แต่ log จะเงียบ- ห้ามผสม
.UseOtlpExporter()กับ.AddOtlpExporter()แบบราย signal (เชื่อว่าจะ throw) — เลือก อย่างใดอย่างหนึ่ง: จะใช้UseOtlpExporter()ตัวเดียวแบบนี้ หรือจะใช้.AddOtlpExporter()สามครั้งแยกราย signal (แบบที่ block ClickHouse ใช้) ก็ได้ แต่ อย่าปนกัน
สังเกตว่าบทนี้ ยังไม่มี .AddSource(...) หรือ .AddMeter(...) — เพราะเรายังไม่มี ActivitySource/Meter ของตัวเอง auto-instrumentation ลงทะเบียน source ของมันเองให้แล้ว พอถึงบท3 (custom span) และบท4 (custom metric) เราจะเพิ่ม AddSource/AddMeter เข้ามา และจะย้ำกฎเหล็ก: ชื่อต้องตรงเป๊ะ กับ ActivitySource/Meter ไม่งั้น telemetry ถูกทิ้งเงียบ (เช่น meter ของ Wolverine ชื่อ Wolverine:{AppName} ต้อง AddMeter("Wolverine*") — wildcard บังคับ)
ขั้นที่ 4 — ชี้ปลายทางด้วย env var (ไม่แตะ code)
หัวข้อที่มีชื่อว่า “ขั้นที่ 4 — ชี้ปลายทางด้วย env var (ไม่แตะ code)”จุดขายของ OpenTelemetry คือปลายทางเป็นแค่ config ภายนอก ไม่ใช่ code นี่คือ honesty spine เส้น (A) ที่จับต้องได้เป็นครั้งแรก: OTel คือสายไฟที่พกพาได้ ClickStack คือปลั๊กที่สลับได้ — code C# ในขั้นที่ 3 ไม่มีคำว่า “ClickStack” หรือ “HyperDX” อยู่เลยสักตัว ปลายทางกำหนดผ่าน env var ล้วนๆ:
OTEL_SERVICE_NAME=ordering-serviceOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317OTEL_EXPORTER_OTLP_PROTOCOL=grpc# production เท่านั้น — OSS ClickStack ต้องแนบ ingestion API key มากับทุก request:# OTEL_EXPORTER_OTLP_HEADERS='authorization=<YOUR_INGESTION_API_KEY>'วันไหนอยากสลับจาก ClickStack ไป Jaeger, Grafana Tempo, Datadog หรือ Honeycomb — เปลี่ยนแค่ OTEL_EXPORTER_OTLP_ENDPOINT (กับ header auth ของเจ้านั้น) code instrument ไม่ต้องแก้แม้แต่บรรทัดเดียว
เอกสารของ ClickStack ระบุว่า OSS ต้องแนบ header authorization=<ingestion API key> (ได้จาก HyperDX → Team Settings → API Keys) มากับทุก OTLP request แต่ สำหรับ all-in-one บน localhost เรายังไม่ฟันธงว่ามันบังคับ header จริงหรือไม่ — ตัวอย่าง compose ที่รันในเครือข่าย Docker เดียวกันดูเหมือนจะส่งมาแบบไม่มี header ก็ยังเข้า ฉะนั้นสำหรับ dev loop บนเครื่องนี้ ลองไม่ใส่ header ก่อน ถ้า telemetry ไม่เข้า HyperDX ค่อยเพิ่ม OTEL_EXPORTER_OTLP_HEADERS ด้วย key ที่คว้ามาในขั้นที่ 1 กฎที่แน่นอนคือ: บน production ต้องมี key เสมอ — อย่าปล่อย endpoint ไว้เปิดโล่ง
ขั้นที่ 5 — ยิง place-order แล้วดู trace แรก
หัวข้อที่มีชื่อว่า “ขั้นที่ 5 — ยิง place-order แล้วดู trace แรก”รัน app Ordering แล้วยิง endpoint สร้างออเดอร์หนึ่งครั้ง (สมมติ Ordering ฟังที่ :5080):
curl -X POST http://localhost:5080/orders \ -H 'Content-Type: application/json' \ -d '{"customerId":"cust-42","items":[{"sku":"pad-thai","qty":1}]}'กลับไปที่ HyperDX ที่ :8080 เปิดหน้า trace — คุณจะเห็น trace แรกเป็น waterfall สองชั้น: server span ของ POST /orders เป็น parent และข้างใต้มัน child span ของ EF Core ที่เป็น INSERT ลงตาราง orders ทั้งคู่มาจาก auto-instrumentation ล้วนๆ — เราไม่ได้เขียน code trace เองเลย
flowchart LR
subgraph APP["app Ordering (.NET)"]
EP["server span<br/>POST /orders<br/>(AspNetCore)"]
EF["child span<br/>INSERT orders<br/>(EF Core)"]
EP --> EF
end
APP -->|"OTLP gRPC :4317"| CS
subgraph CS["ClickStack (Docker, all-in-one)"]
COL["OTel Collector distro"]
CH["ClickHouse<br/>columnar store"]
HDX["HyperDX UI :8080<br/>trace waterfall"]
COL --> CH --> HDX
end
classDef app fill:#0e7490,stroke:#155e75,color:#f0fdff;
classDef sink fill:#1e293b,stroke:#0f172a,color:#94a3b8;
class EP,EF app;
class COL,CH,HDX sink;
คำบรรยายภาพ: auto-instrumentation ให้ waterfall สองชั้นมาฟรี — server span ของ POST /orders ห่อ child span ของ EF Core ที่ INSERT ลง DB code ปล่อย OTLP ผ่าน gRPC port 4317 ไปเข้า OTel Collector ของ ClickStack ซึ่งเขียนลง ClickHouse แล้ว HyperDX อ่านออกมาเป็น trace ที่ไล่ดูได้ทั้งเส้น
ขั้นที่ 6 — script ยิงโหลดให้บทหลังมีสัญญาณเล่น
หัวข้อที่มีชื่อว่า “ขั้นที่ 6 — script ยิงโหลดให้บทหลังมีสัญญาณเล่น”ออเดอร์เดียวไม่พอให้บท6 (สำรวจ) และบท7 (alert) มีอะไรดู เตรียม script เล็กๆ ที่ยิง place-order ซ้ำๆ ด้วย customer และ item หลากหลาย เก็บไว้เป็น scripts/load.sh:
#!/usr/bin/env bash# ยิง place-order 200 ครั้ง ให้มี trace/metric/log สะสมไว้ให้บทหลังเล่นskus=(pad-thai green-curry mango-sticky-rice tom-yum)for i in $(seq 1 200); do sku=${skus[$((RANDOM % ${#skus[@]}))]} curl -s -o /dev/null -X POST http://localhost:5080/orders \ -H 'Content-Type: application/json' \ -d '{"customerId":"cust-'"$((RANDOM % 50))"'","items":[{"sku":"'"$sku"'","qty":1}]}' sleep 0.2doneecho "ยิงครบ 200 ออเดอร์ — เปิด HyperDX ดูได้เลย"รัน bash scripts/load.sh ทิ้งไว้สักครู่ แล้ว HyperDX จะมี trace หลายร้อยเส้นให้ค้นหา จัดกลุ่ม และทำ dashboard ในบทถัดๆ ไป — เราจะกลับมาใช้ script ตัวนี้ตลอดคอร์ส
สิ่งที่คุณเพิ่งได้มาน่าตื่นเต้น — เปิดไฟให้ระบบก้อนหนึ่งด้วยการปักไม่กี่ package แต่ต้องซื่อสัตย์ว่ามันไปได้แค่ไหน: auto-instrumentation ให้ span ของ HTTP กับ DB ที่รู้เรื่อง กลไก (route ไหน, query กี่มิลลิวินาที) แต่มัน ไม่รู้เรื่อง domain ของคุณเลย — trace นี้ไม่รู้ว่านี่คือ “ออเดอร์ของลูกค้ารายไหน”, “ยอดเท่าไหร่”, “จ่ายเงินสำเร็จหรือไม่” เพราะข้อมูลพวกนั้นเป็นของ คุณ ไม่ใช่ของ framework
หลักการที่จะกำกับทั้งคอร์ส: คุณ debug สิ่งที่คุณไม่ได้ instrument ไม่ได้ auto-instrumentation คือ พื้น (floor) ที่ได้มาฟรี ไม่ใช่ เพดาน (ceiling) — span/attribute ที่มีความหมายเชิง domain (order.id, ผลลัพธ์การจ่ายเงิน) ต้องเขียนเอง อย่าเผลอขายว่า “เปิด auto-instrumentation แล้วคุณ observable” นั่นคือคำโม้ บท3 เริ่มเติมความหมายเชิง domain ที่หายไปก้อนนี้
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”บทนี้พิสูจน์ว่าสายส่งสัญญาณเชื่อมถึงกันจริง: รัน clickhouse/clickstack-all-in-one:2.31.0 บน Docker (port 8080/4317/4318/8123), ปักชุด package ที่ pin 1.17.0 — โดย EF Core ยังเป็น 1.17.0-beta.1 ที่ต้อง --prerelease และเราพูดตรงๆ ว่ามันคือ beta, เดินสาย AddOpenTelemetry() ด้วย auto-instrumentation ล้วน + UseOtlpExporter() เดียว (บวก builder.Logging.AddOpenTelemetry(...) ที่ยังต้องมี), ชี้ปลายทางด้วย env var — ตอกย้ำว่า OTel คือสายไฟที่สลับปลั๊กได้, แล้วเห็น trace แรก (server span → EF Core child) ใน HyperDX สุดท้ายเตรียม script ยิงโหลดไว้ให้บทหลัง และปักหมุด honesty spine (D): auto-instrumentation เป็นแค่พื้น
บทหน้าเราจะเริ่มเขียน span เอง — ตาม หนึ่งออเดอร์เป็น trace เดียว ข้าม outbox + RabbitMQ ไปถึง Kitchen และ Delivery ที่ถูกแยกออกเป็น service เดี่ยว นี่คือบทที่ความ “กระจาย” ของระบบกลายเป็นเส้นเดียวที่ไล่ดูได้จริง
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- opentelemetry-dotnet — AddOpenTelemetry builder doc (เข้าถึง 2026-07-22) — โครง
AddOpenTelemetry().WithTracing().WithMetrics(), กฎAddSource/AddMeterต้องตรงชื่อ, และbuilder.Logging.AddOpenTelemetry(...)ยังจำเป็น (S16) - Microsoft Learn — .NET observability with OpenTelemetry (2026-07-01) — .NET map เข้า framework API:
ILogger→logs,Meter→metrics,ActivitySource/Activity→traces (S17) - NuGet — OpenTelemetry core package index (stable 1.17.0) (2026-07-22) — ยืนยัน stable = 1.17.0 (S18)
- NuGet — OpenTelemetry.Instrumentation.EntityFrameworkCore index (2026-07-22) —
1.17.0-beta.1เป็น prerelease-only ต้อง--prerelease(S19) - ClickHouse blog — Logging, Metrics, and Distributed Tracing in .NET with OpenTelemetry and ClickStack (2026-06-03) — grounding หลักฝั่ง .NET: การเดินสาย OTLP เข้า ClickStack (S20)
- ClickStack — Getting started (OSS) (เข้าถึง 2026-07-22) — image all-in-one, port 8080/4317/4318/8123, first-run setup (S22)
- ClickStack — All-in-one deployment (เข้าถึง 2026-07-22) — storage ไม่ persistent, ต้อง mount volume, และ ไม่เหมาะกับ production (S23)
- ClickStack — Ingesting data: OpenTelemetry (เข้าถึง 2026-07-22) — header
authorization+ ingestion API key จาก Team Settings → API Keys (S24) - Docker Hub — clickhouse/clickstack-all-in-one tags (ตรวจ 2026-07-22, push 2026-07-17) — tag ปัจจุบัน
2.31.0(latest/2ชี้มาที่นี่) (S31)
เช็กความเข้าใจ — บทที่ 2
ข้อ 1 / 3ทำไม code ในบทนี้ยังต้องมี builder.Logging.AddOpenTelemetry(...) ทั้งที่เรียก .UseOtlpExporter() ไปแล้ว?