สำรวจ & visualize ใน HyperDX
ห้าบทที่ผ่านมาคุณ ปล่อยสัญญาณ ออกมาครบ — auto-trace end-to-end (บท2), custom span ที่ข้าม RabbitMQ เป็น trace เดียว (บท3), RED metric ที่ไม่ระเบิด cardinality (บท4), และ log ที่ correlate เข้า trace อัตโนมัติ (บท5) ตอนนี้ store ของคุณเต็มไปด้วย telemetry ที่สัมพันธ์กัน แต่ยัง ถามมันไม่เป็น บทนี้คือบทที่คุณเปิด HyperDXHyperDXUI ของ ClickStack สำหรับ search (Lucene)/SQL/dashboard/alert/trace waterfall เหนือ store เดียวกัน — บทเดียวที่ผูกกับ sink โดยตั้งใจArchitecture แล้วเริ่ม ตั้งคำถามที่คุณไม่ได้เตรียมไว้ก่อน
ตลอดคอร์สเราย้ำเส้นความซื่อสัตย์ข้อ (A): “สิ่งที่คุณ instrument คือ OpenTelemetry — มาตรฐานเปิดที่เป็นกลางต่อ vendor. ClickStack เป็นแค่ ‘ปลายทาง’ (sink) ที่เราเลือกใช้ให้จับต้องได้ ไม่ใช่ตัวคอร์ส.” — code instrument ใน C# ไม่ผูกกับปลายทางเลย สลับ sink เป็น Jaeger/Grafana/Datadog ได้โดยไม่แก้ code แต่บทนี้คือข้อยกเว้นที่ตั้งใจ Lucene syntax, ชื่อตาราง SQL, ชนิด tile ของ dashboard — พวกนี้เป็นของ HyperDX เฉพาะตัว ถ้าวันหนึ่งคุณย้ายไป Grafana/Tempo ทักษะการตั้งคำถาม (pivot จาก metric → trace → log) จะติดตัวไป แต่ ไวยากรณ์ ต้องเรียนใหม่ เราพูดตรงๆ ตรงนี้: บทนี้ไม่ portable และนั่นคือความตั้งใจ
บทนี้ query telemetry ที่บท2–บท5 ปล่อยจาก platform Order (Ordering/Kitchen/Delivery/Payment) — repo kaen-food-ordering (กำลังจัดทำ) ใช้ order-placement load script จากบท2 รันไว้สักพักก่อน เพื่อให้ store มี trace/log/metric ให้ค้นจริง ทุก query ในบทนี้อ้าง service.name และ attribute ที่คุณตั้งเองในบทก่อน (order.id, payment.outcome) — ไม่มีอะไรใหม่ต้อง instrument เพิ่ม บทนี้ล้วนๆ คือ อ่าน สิ่งที่ปล่อยไว้แล้ว
เป้าหมาย: ตอบ ‘unknown-unknowns’ ไม่ใช่เปิด dashboard ที่เดาไว้
หัวข้อที่มีชื่อว่า “เป้าหมาย: ตอบ ‘unknown-unknowns’ ไม่ใช่เปิด dashboard ที่เดาไว้”ย้อนเส้นความซื่อสัตย์ข้อ (B): monitoring ตอบคำถามที่คุณ รู้ล่วงหน้าว่าจะถาม — dashboard ที่สร้างไว้ก่อน ส่วน observability คือความสามารถ ตั้งคำถามใหม่ กับข้อมูลที่ระบบปล่อยไว้แล้ว บทนี้คือที่ที่ความต่างนั้นจับต้องได้จริง
สถานการณ์: มีคนบ่นว่า “ออเดอร์ #4821 ใช้เวลา 40 วิ” คุณ ไม่มี dashboard ชื่อ “ทำไมออเดอร์นี้ช้า” ไว้ก่อน — เพราะไม่มีใครคาดเหตุนี้ได้ แต่คุณมีทุก span/log/metric ที่ platform ปล่อยไว้แล้วใน 30 นาทีที่ผ่านมา งานของบทนี้คือ ขุด คำตอบออกมาจากข้อมูลดิบนั้น ผ่านสี่เครื่องมือของ HyperDX: Lucene search, ClickHouse SQL, trace waterfall, และ dashboard
สองพื้นผิว query เหนือ store เดียว
หัวข้อที่มีชื่อว่า “สองพื้นผิว query เหนือ store เดียว”หัวใจของ HyperDX คือทุกสัญญาณ (traces/logs/metrics) ถูกเก็บเป็นแถว columnar กว้างๆ ใน ClickHouse — store เดียว ไม่ใช่สามคลังแยกกัน (นี่คือ Wide EventWide Eventเหตุการณ์ structured ที่มี attribute จำนวนมาก (คือสแปนนั่นเอง) เป็นหน่วยข้อมูลหลักของ Obs 2.0Architecture ที่เราคุยกันมาตั้งแต่บท1/บท5: structured event ที่มี attribute กว้างๆ ก็คือ span นั่นเอง) เหนือ store เดียวกันนั้น HyperDX ให้คุณถามได้สองแบบ (S21):
- Lucene-style search — พิมพ์เร็ว สำหรับ triage: HyperDX transpile มันเป็น ClickHouse SQL
WHEREให้เบื้องหลัง (S27) - ClickHouse SQL เต็มรูป — สำหรับ aggregation/join ที่ Lucene ทำไม่ได้ (p95, group-by, correlate ข้ามตาราง) (S21)
คิดง่ายๆ: Lucene = ตะแกรงร่อนเร็ว, SQL = มีดผ่าตัด เริ่มด้วย Lucene เพื่อแคบขอบเขต แล้วสลับไป SQL เมื่อคำถามซับซ้อนเกินไวยากรณ์ตะแกรง
Lucene search: ตะแกรงร่อนเร็วสำหรับ platform Order
หัวข้อที่มีชื่อว่า “Lucene search: ตะแกรงร่อนเร็วสำหรับ platform Order”Lucene bar เหมาะกับการ แคบผลลัพธ์ ให้เร็วที่สุด ต่อไปนี้คือไวยากรณ์ที่คุณจะใช้จริงกับ telemetry ของ Order platform (S27):
# field match — log level Error เฉพาะ ordering-serviceServiceName:ordering-service SeverityText:Error
# boolean + negation — event ของ kitchen-service ที่ไม่ใช่ระดับ InfoServiceName:kitchen-service AND NOT SeverityText:Info
# quoted phrase — ข้อความ log ตรงตัวBody:"payment declined"
# comparison — span ที่ช้ากว่า 1000 (หน่วยตาม field; ดู ⚠️ ด้านล่าง)Duration:>1000
# existence — เฉพาะ record ที่ *มี* attribute order.id (คือ span/log ของ Order flow)OrderId:*
# wildcard บางส่วน — จับทุกอย่างที่มีคำว่า Timeout*Timeout*ตัวอย่างจริงตอน triage: อยากดูว่ามี log “payment declined” ของ payment-service ในชั่วโมงนี้ไหม พิมพ์ ServiceName:payment-service Body:"payment declined" แล้วปรับ time range ที่มุมขวาบน — ได้ผลใน 2 วินาที ไม่ต้องเขียน SQL
Lucene bar ของ HyperDX ไม่รองรับ regex, fuzzy match, และ wildcard กลางคำ (in-term) — *Timeout* (ขึ้นต้น/ลงท้ายด้วย *) ใช้ได้ แต่ pattern กลางคำแบบ Time*out หรือ regex /ord.*/ ทำไม่ได้ พอเจอเพดานนี้ให้สลับไป ClickHouse SQL ซึ่งมีทั้ง match() (regex), LIKE, และ aggregation เต็มรูป — Lucene คือความเร็ว SQL คือพลัง
ClickHouse SQL: มีดผ่าตัดเหนือ OTel schema
หัวข้อที่มีชื่อว่า “ClickHouse SQL: มีดผ่าตัดเหนือ OTel schema”เมื่อคำถามต้องการ สรุปรวม (p95, count group-by, join ข้าม trace) Lucene หมดแรง — เปิด SQL editor แล้ว query ตรงๆ เหนือ schema ที่ ClickStack collector เขียนไว้ ตารางหลักที่คุณจะใช้ (S26):
otel_traces— ทุก spanotel_logs— ทุก log record (มีTraceId/SpanIdที่ correlate มาจากบท5)otel_metrics_*— metric ห้าตาราง แยกตามชนิด instrument (sum/gauge/histogram/exponential-histogram/summary)
3 detail ต่อไปนี้ ต่างกันได้ ระหว่าง version ClickStack — เราสอนค่าที่พบบ่อย แต่ให้เปิดหน้า schema ใน deployment ของคุณยืนยันก่อน copy-paste (S26):
Durationเป็นUInt64หน่วย nanosecond → ต้องหาร/1e6เพื่อได้ millisecond (บาง query ตัวอย่างข้างล่างพึ่งค่านี้ ถ้า deployment ของคุณเก็บเป็นหน่วยอื่น ตัวเลขจะเพี้ยน)- database เริ่มต้นชื่อ
default— แต่บาง deployment ใช้otelถ้า query ฟ้อง table not found ลองเติม prefixotel.otel_traces SpanAttributes/ResourceAttributes/LogAttributesเป็นชนิดMap→ ใน SQL เข้าถึงด้วย bracketSpanAttributes['http.route']แต่ ใน Lucene bar การอ้าง key ของ Map อาจใช้รูปแบบต่างกัน (แล้วแต่ว่า field ถูก promote เป็น top-level หรือไม่) — ยืนยันกับ search docs ของ version คุณ
Query 1 — p95 latency ต่อ service (คำถาม “service ไหนช้าที่สุดตอนนี้”):
-- p95 latency per service (Duration หน่วยเป็น nanosecond → /1e6 = ms)SELECT ServiceName, quantile(0.95)(Duration)/1e6 AS p95_msFROM otel_tracesWHERE Timestamp >= now() - INTERVAL 1 HOUR AND SpanAttributes['http.route'] = '/orders'GROUP BY ServiceName ORDER BY p95_ms DESC;นี่คือ query ที่ Lucene ทำไม่ได้ — quantile(0.95) เป็น aggregation ที่ต้องมองทุกแถวใน 1 ชั่วโมง แล้วสรุปเป็นตัวเลขเดียวต่อ service สังเกต /1e6 ที่แปลง nanosecond → millisecond และ SpanAttributes['http.route'] ที่ index เข้า Map column
Query 2 — ทุก span ของ trace เดียว (สิ่งที่ป้อนให้ waterfall):
-- all spans of one trace (feeds the waterfall)SELECT Timestamp, ServiceName, SpanName, SpanKind, Duration, StatusCode, ParentSpanId, SpanIdFROM otel_traces WHERE TraceId = '0af7651916cd43dd8448eb211c80319c' ORDER BY Timestamp;ParentSpanId/SpanId คือ2 column ที่ประกอบร่างเป็นต้นไม้ waterfall — แต่ละแถวรู้ว่า parent ของตัวเองคือใคร เรียงตาม Timestamp แล้วคุณจะเห็นลำดับ Ordering → (RabbitMQ) → Kitchen + Delivery ตามที่บท3 สร้างไว้
Trace waterfall: จาก query สู่ภาพ
หัวข้อที่มีชื่อว่า “Trace waterfall: จาก query สู่ภาพ”คุณไม่จำเป็นต้องเขียน Query 2 ด้วยมือทุกครั้ง — ใน HyperDX คลิกที่ trace หนึ่งใบ มันจะ render span waterfall ให้อัตโนมัติ: แต่ละ span เป็นแท่งแนวนอน ความยาว = duration เยื้องเข้าตามความลึก parent-child เห็นทันทีว่าเวลา 40 วิ ไปหมดกับ span ไหน
trace ของ Order flow มี span เยอะ (Ordering server span, EF Core DB span, Wolverine send/receive, Kitchen handler, Delivery handler…) ตอนสแกนหา trace ที่ผิดปกติ ให้ filter ผลลัพธ์เหลือเฉพาะ root span — หนึ่งแถวต่อ1 request ทำให้ไล่สายตาหา request ที่ช้าได้ก่อน แล้วค่อยเจาะเข้า waterfall ของใบนั้น (S49)
flowchart LR
Q["คำถามที่ไม่ได้เตรียมไว้ก่อน<br/>'ทำไมออเดอร์ #4821 ช้า 40 วิ?'"]
subgraph HDX["HyperDX เหนือ store เดียว (ClickHouse)"]
direction TB
L["Lucene search<br/>ตะแกรงร่อนเร็ว<br/>transpile → SQL WHERE"]
S["ClickHouse SQL<br/>มีดผ่าตัด<br/>p95 / group-by / join"]
W["Trace waterfall<br/>คลิก trace → เห็น span tree<br/>filter root-span-only"]
D["Dashboard<br/>tiles + global filter + time range"]
end
A["คำตอบ + pivot ต่อ<br/>ไป all orders ที่ share handler ช้า"]
Q --> L
Q --> S
L --> W
S --> W
W --> A
S --> D
D --> A
classDef q fill:#0e7490,stroke:#155e75,color:#f0fdff;
classDef tool fill:#1e293b,stroke:#0f172a,color:#e2e8f0;
class Q,A q;
class L,S,W,D tool;
คำบรรยายภาพ: คำถามที่ไม่ได้เตรียมไว้ล่วงหน้าเข้าทางซ้าย เลือกได้สองพื้นผิว query เหนือ store เดียวกัน — Lucene สำหรับร่อนเร็ว, SQL สำหรับ aggregation ที่ Lucene ทำไม่ได้ ทั้งคู่ป้อนต่อเข้า trace waterfall (คลิก trace แล้วเห็น span tree, filter เหลือ root span เพื่อสแกน) และ SQL ยังต่อเข้า dashboard ได้ ปลายทางคือคำตอบและการ pivot ต่อไปยังทุกออเดอร์ที่ share ปัญหาเดียวกัน
Dashboard: จาก ad-hoc สู่มุมมองที่ share ได้
หัวข้อที่มีชื่อว่า “Dashboard: จาก ad-hoc สู่มุมมองที่ share ได้”พอคุณ เจอ pattern ที่อยากจับตาซ้ำๆ ให้ปั้นมันเป็น dashboard — HyperDX ให้ tile หลายชนิด: Line/Bar (แนวโน้มตามเวลา), Number (ตัวเลขเดียว เช่น p95 ปัจจุบัน), Table (top-N), Heatmap (การกระจายของ latency), Pie (สัดส่วน error ต่อ context) ทุก tile บนหน้าเดียวกัน share global filter และ time range เดียวกัน — เลื่อน time picker ทีเดียว ทุก tile ขยับตาม (S29)
tile ที่เขียนด้วย SQL ใช้ macro ช่วยให้ chart interactive โดยไม่ต้อง hard-code (S30):
-- SQL chart tile: error count ต่อ context, เคารพ global filter + time range ของ dashboardSELECT ServiceName, count() AS errorsFROM $__sourceTableWHERE StatusCode = 'Error' AND $__filtersGROUP BY ServiceName ORDER BY errors DESC;$__sourceTable ถูกแทนด้วยตารางที่ tile ชี้อยู่ และ $__filters ถูกแทนด้วย global filter + time range ที่ผู้ดู dashboard เลือก ณ ตอนนั้น (convention เดียวกับ ClickHouse Grafana plugin เช่น param {startDateMilliseconds:Int64}) — เขียน query ครั้งเดียว ใช้ได้ทุก filter (S30)
รายการชนิด tile และชื่อ macro ($__filters/$__sourceTable) ข้างบนอ้างจากเอกสาร dashboard + block SQL charting ณ ช่วงที่เขียน (S29, S30) HyperDX ยังพัฒนาต่อเนื่อง — เปิด “Add tile” ใน deployment ของคุณเพื่อดูรายการชนิดจริง และเช็กชื่อ macro ในหน้า SQL chart ก่อน copy-paste เราสอน แนวคิด (tile + shared filter + macro) ให้แน่น ส่วนชื่อเป๊ะๆ ให้ยืนยันกับ UI
Payoff: pivot จาก ‘1 trace ช้า’ ไป ‘ทุกออเดอร์ที่เจอปัญหาเดียวกัน’
หัวข้อที่มีชื่อว่า “Payoff: pivot จาก ‘1 trace ช้า’ ไป ‘ทุกออเดอร์ที่เจอปัญหาเดียวกัน’”นี่คือช่วงที่ทุกอย่างมาบรรจบ และเป็นสิ่งที่ Wide Event / Observability 2.0 ซื้อ ให้คุณจริงๆ:
- คุณเปิด trace ของออเดอร์ #4821 ใน waterfall เห็นว่าเวลา 40 วิไปหมดกับ span เดียว — span ที่ Kitchen context ประมวลผล
OrderConfirmedIntegrationEvent(ชื่อ span ที่ HyperDX แสดงจริงอ่านได้จาก waterfall — ดู ⚠️ ใต้ query) - คุณอยากรู้ทันทีว่า “ออเดอร์อื่นเจอ span ช้าตัวเดียวกันไหม กี่ใบ?” — แต่คุณ ไม่เคยตั้ง metric ชื่อ “kitchen handler duration by order” ไว้ก่อน เพราะไม่มีใครคาดเหตุนี้
- ในโลก metric-only คุณจบตรงนี้ — ถามไม่ได้เพราะไม่ได้ pre-aggregate ไว้ แต่เพราะทุก span คือ wide event ที่เก็บ
order.idเป็น attribute (บท4 บอกไว้: OrderId เป็น span attribute ไม่ใช่ metric label — traces รับ high cardinality ได้) คุณ query มันสดๆ ได้เลย:
-- ทุกออเดอร์ที่ share Kitchen handler ตัวช้า ในชั่วโมงที่ผ่านมา — ไม่มี metric ตั้งไว้ล่วงหน้าSELECT SpanAttributes['order.id'] AS order_id, Duration/1e6 AS msFROM otel_tracesWHERE ServiceName = 'kitchen-service' AND SpanName = 'WhenOrderConfirmed' -- ⚠️ placeholder — Wolverine ตั้งชื่อ span ตามชนิด message ไม่ใช่ชื่อ handler; อ่านชื่อจริงจาก waterfall (ดู callout ใต้ query) AND Duration/1e6 > 5000 AND Timestamp >= now() - INTERVAL 1 HOURORDER BY ms DESC;WhenOrderConfirmed ที่กรองด้านบนคือ ชื่อ handler class ที่เราใส่ให้เห็นภาพ — แต่ Wolverine ตั้งชื่อ span ของการประมวลผล message ตาม ชนิดของ message (ตาม OTel messaging semconv) ไม่ใช่ชื่อ handler และชื่อที่ HyperDX แสดงจริงยัง ต่างได้ตาม version/config ของ Wolverine ถ้า SpanName ที่กรองไม่ตรงกับที่ระบบคุณปล่อยจริง query จะคืน ศูนย์แถวเงียบๆ — คือกับดัก silent-empty-result ที่บทนี้เตือนไว้พอดี ก่อน copy-paste ให้เปิด waterfall ของ trace ช้าจริงสักใบ อ่านค่า SpanName ของ span Kitchen ที่ช้า แล้วเอาค่านั้นมาแทน — หรือกรองด้วยสิ่งที่เสถียรกว่า เช่น ServiceName = 'kitchen-service' บวก SpanAttributes['order.id'] != '' และ Duration threshold โดยไม่พึ่งชื่อ span
คุณเพิ่งตอบคำถามที่ ไม่มีใครเตรียม dashboard ไว้ — เพราะ high-cardinality order.id ถูกเก็บเป็น attribute บน span (ไม่ใช่ label บน metric ที่จะทำ cardinality ระเบิด) คุณจึง pivot จาก หนึ่ง trace ที่ผิดปกติ ไปหา ทุก ออเดอร์ที่ share อาการเดียวกันได้ในบรรทัดเดียว นี่คือความต่างระหว่าง observability (ตั้งคำถามใหม่ได้) กับ monitoring (ตอบได้แค่ที่เดาไว้) ที่จับต้องได้ที่สุดในทั้งคอร์ส
ความสามารถ pivot นี้ ยืม การตัดสินใจจากบทก่อนมาทั้งหมด: บท3 ทำให้ span ของ Kitchen อยู่ใน trace เดียวกับ Ordering ข้าม RabbitMQ, บท4 ตัดสินใจเก็บ order.id เป็น span attribute (ไม่ใช่ metric label), บท5 ทำให้ log ของ payment click กลับเข้า trace ได้ บทนี้แค่ เก็บเกี่ยว — ซึ่งตอกย้ำเส้นข้อ (D): คุณ debug ได้เฉพาะสิ่งที่คุณ instrument ไว้ ถ้าบท4 เผลอทำ order.id เป็น metric label คุณจะทั้งจ่ายเงินแพงและ ยัง ตอบคำถามนี้ไม่ได้อยู่ดี
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”บทนี้คือข้อยกเว้นที่ตั้งใจ — บทเดียวของคอร์สที่ผูกกับ HyperDX เพราะ Lucene syntax/ชื่อตาราง SQL/ชนิด tile เป็นของ HyperDX เฉพาะตัว (ที่เหลือของคอร์ส portable) HyperDX ให้สองพื้นผิว query เหนือ store เดียว: Lucene (ตะแกรงร่อนเร็ว, transpile → SQL WHERE, ไม่มี regex/fuzzy/wildcard กลางคำ) และ ClickHouse SQL (มีดผ่าตัด, aggregation เต็มรูป) — จำไว้ว่า Duration เป็น nanosecond ต้อง /1e6 และ SpanAttributes เป็น Map ต้อง index ด้วย ['key'] trace waterfall เปลี่ยน query เป็นภาพ (filter root-span-only เพื่อสแกน) และ dashboard เก็บ pattern ที่เจอบ่อยเป็น tile ที่ share filter/time range ผ่าน macro $__filters/$__sourceTable และ payoff ที่ใหญ่ที่สุด: เพราะทุก span เป็น wide event ที่เก็บ order.id เป็น attribute คุณ pivot จาก1 trace ช้าไปหาทุกออเดอร์ที่ share ปัญหาเดียวกันได้ โดยไม่ต้องมี metric ตั้งไว้ล่วงหน้า
บทหน้าเราจะเลิกถาม ตอนเกิดเหตุ แล้วหันมาถามว่า “มันพังจริงหรือยัง?” — SLI/SLO/error budget และ burn-rate alert ที่ยิงเมื่ออาการกระทบผู้ใช้จริง ไม่ใช่ noise ตามสาเหตุ
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- ClickStack — Overview (เอกสารทางการ, เข้าถึง 2026-07-22) — 3 component (HyperDX + ClickHouse + OTel Collector), สองพื้นผิว query (Lucene + SQL) เหนือ store เดียว [S21]
- ClickStack — Schemas (เอกสารทางการ, เข้าถึง 2026-07-22) — ตาราง
otel_logs/otel_traces/otel_metrics_*;Durationเป็นUInt64nanosecond;SpanAttributes/ResourceAttributesเป็นMap[S26] - ClickStack — Search (เอกสารทางการ, เข้าถึง 2026-07-22) — Lucene-style syntax ที่ transpile เป็น ClickHouse SQL WHERE; ขอบเขต (ไม่มี regex/fuzzy/in-term wildcard) [S27]
- ClickStack — Dashboards (เอกสารทางการ, เข้าถึง 2026-07-22) — ชนิด tile, global filter, shared time range, drilldown [S29]
- ClickHouse blog — ClickStack SQL charting and alerting (2025) — macro
$__filters/$__sourceTableสำหรับ SQL chart tile [S30] - ClickHouse blog — What’s new in ClickStack (November 2025) (2025-11) — trace waterfall และ filter เหลือ root span เพื่อสแกน [S49]
เช็กความเข้าใจ — บทที่ 6
ข้อ 1 / 3ทำไมบท6 ถึงเป็น 'บทเดียวที่ผูกกับ sink โดยตั้งใจ' ในขณะที่ทั้งคอร์สย้ำว่า OTel portable?