ข้ามไปยังเนื้อหา

Structured Logging and Log Facets

structured log (JSON) ทำให้ Processing Pipeline ดึง attribute แบบมี type ได้เชื่อถือได้ แทนที่จะพึ่ง Grok parsing ของข้อความอิสระที่เปราะบาง และ attribute พวกนั้นกลายเป็น Facet ที่ filter, group และ aggregate ได้ใน Logs Explorer — แต่ทำได้เฉพาะ log ที่เข้า index จริง ๆ เท่านั้น

Grok Parser ดึง attribute ออกจากบรรทัดที่ไม่มีโครงสร้างแบบ 2024-01-15 ERROR user=42 duration=812ms checkout failed ได้ แต่ pattern ทุกตัวคือการเดิมพันว่า log จะคงรูปแบบนั้นเป๊ะ ๆ ตลอด แค่เพิ่มคำ, สลับตำแหน่ง field หรือมี service หนึ่งที่ log ต่างจากอีก service เล็กน้อย Grok pattern ก็หยุด match แบบเงียบ ๆ — หรือแย่กว่านั้นคือ match ผิด

structured log หลบปัญหานี้ไปเลย เพราะ field มีชื่อและ type อยู่แล้วก่อนที่ Datadog จะเห็นด้วยซ้ำ:

{
"timestamp": "2024-01-15T10:22:31Z",
"level": "error",
"service": "checkout-api",
"user": { "id": 42 },
"duration_ms": 812,
"message": "checkout failed"
}

ตรงนี้ Processing Pipeline ไม่ต้องใช้ Grok Parser เลยสำหรับ field ที่เป็น key ที่มีชื่ออยู่แล้ว — level, service, user.id, duration_ms มาเป็น attribute แบบมี type ให้อัตโนมัติ Grok ยังมีประโยชน์กับส่วนของ message ที่ยังเป็นข้อความอิสระอยู่ (field message เอง หรือ log จากระบบที่คุณคุม format ไม่ได้) แต่เส้นทางที่เชื่อถือได้สำหรับอะไรก็ตามที่คุณคุมได้ คือ log เป็น JSON ตั้งแต่แรก

พอ log มี attribute ที่มีโครงสร้างแล้ว — ไม่ว่าจะดึงมาด้วย Grok หรือเป็น JSON field แต่แรก — Datadog แปลง attribute นั้นให้เป็น Facet ได้ คือ field ที่ Logs Explorer มองว่า filter, group และ chart ได้ เหมือนกับที่เว็บไซต์ทั่วไป facet ผลการค้นหา การ facet บน status_code ทำให้ filter เป็น status_code:500 ได้ หรือ group ผลค้นหาตาม status_code เพื่อดู breakdown ส่วนการ facet บน service ทำให้แบ่ง query ไหนก็ได้ตาม service ที่สร้าง log แต่ละตัว

structure ที่สอดคล้องกันข้าม service คือสิ่งที่ทำให้ facet มีประโยชน์จริงในระดับ organization ถ้า service หนึ่งเรียก field นี้ว่า status_code อีกตัวเรียก httpStatus และอีกตัวฝังไว้ในข้อความที่ไม่ได้ parse คุณ facet ข้ามทั้งสามตัวให้สอดคล้องกันไม่ได้ นี่คือเหตุผลที่การ remap สำคัญ ในการทำให้ log มีรูปร่างสอดคล้องกัน:

  • Status Remapper — map field severity ที่แต่ละ service เรียกไม่เหมือนกัน (level, severity, log.level, …) เข้ากับ facet status มาตรฐานของ Datadog เพื่อให้ “ขอดู error ทั้งหมดจากทุก service” เป็น query เดียว แทนที่จะเป็น query ละ service
  • Message Remapper — กำหนดว่า attribute ไหนคือ message หลักที่มนุษย์อ่านได้ของ log เพื่อให้ Logs Explorer แสดงคอลัมน์ message ที่สมเหตุสมผล ไม่ว่า service ต้นทางจะเรียก field นั้นว่าอะไร

นี่คือประเด็นที่ผูกกลับไปหาบทที่แล้วโดยตรง Log Analytics — การ breakdown แบบ group-by, computed measure และ aggregation อื่น ๆ บน facet — ทำงานได้เฉพาะกับ log ที่ถูก index จริง ๆ เท่านั้น Exclusion Filter ที่กัน log level:debug ไม่ให้เข้า index ไม่ได้แค่ซ่อน log พวกนั้นจากการค้นหาตรง ๆ เท่านั้น แต่ยังหมายความว่า log พวกนั้นปรากฏใน Log Analytics aggregation ไหนไม่ได้เลย เพราะไม่มีอะไรถูก index ให้ aggregate

ถ้าต้องการ slice-and-dice field บน log ที่ปกติถูก Exclusion Filter drop ทิ้ง คุณมีสามทางเลือกเดียวกับที่บทก่อนให้ไว้แล้ว มาใช้กับความต้องการนี้โดยเฉพาะ:

  1. ปิด Exclusion Filter ชั่วคราวหรือทำให้แคบลง เพื่อให้ log ที่ต้องวิเคราะห์ถูก index จริง
  2. generate log-based metric แทน — COUNT หรือ DISTRIBUTION ที่คำนวณจาก ingest stream เต็ม ๆ ให้ trend ได้โดยไม่ต้อง index log เลย
  3. Rehydrate ช่วงเวลาที่เกี่ยวข้องจาก Log Archive เข้า temporary index รันการวิเคราะห์ แล้วปล่อยให้ temporary index หมดอายุไป
Want to group-by @http.status_code on logs currently excluded by level:debug filter?
-> Option 1: relax the Exclusion Filter (costs indexing $$, gets full Log Analytics)
-> Option 2: log-based metric on @http.status_code (cheap, but only pre-defined aggregations)
-> Option 3: Rehydrate the time range (temporary cost, full Log Analytics for that window)
flowchart LR
  A["JSON log\nlevel, service, duration_ms"] --> B[Processing Pipeline\nStatus Remapper, Message Remapper]
  B --> C[Typed attributes]
  C --> D[Facets in Logs Explorer\nfilter / group / chart]
  D --> E{Log actually indexed?}
  E -->|yes| F[Log Analytics: group-by,\ncomputed measures work]
  E -->|no, excluded| G[No Log Analytics\nuse log-based metric or Rehydration]
จาก structured field ไปเป็น facet แล้วไปเป็น analytics

structured logging กับ trace correlation เป็นกลไกเดียวกันมองจากสองมุม — Connecting Logs and Traces พิสูจน์ field dd.* ที่ inject เข้ามาไว้ลึกแล้ว หลักฐานของบทนี้เอง ใช้ pino (Node) กับ dd-trace ที่ pin ไว้ แสดง block dd.* เดียวกันที่มาถึงเป็น JSON key ที่มี type อยู่แล้วโดยธรรมชาติ ไม่ใช่อะไรที่ Grok pattern ต้องดึงออกมา

src/pino-structured-log.js
const tracer = require('dd-trace').init({ service: 'checkout-api', logInjection: true });
const pino = require('pino')();
tracer.trace('checkout.request', () => {
pino.error({ user: { id: 42 }, duration_ms: 812 }, 'checkout failed');
});
$ node src/pino-structured-log.js
{"level":50,"time":1790690518779,"pid":18822,"hostname":"...","user":{"id":42},"duration_ms":812,
"msg":"checkout failed","dd":{"trace_id":"6abbc4d6000000007c4f928caf80a6a5",
"span_id":"8957539316800726693","service":"checkout-api"}}

user.id กับ duration_ms เป็น JSON field ที่มี type จริง — พิสูจน์ด้านบน ไม่ต้องมี Grok Parser มาดึงออกมาเลย มาเป็น nested object กับตัวเลขตามลำดับ ตรงกับข้อได้เปรียบเรื่องความเชื่อถือได้ที่บทนี้เปิดเรื่องมาด้วยเป๊ะ ฝั่ง Python ที่เทียบเท่ากัน (DD_LOGS_INJECTION ของ ddtrace ป้อนเข้า JSON formatter ที่เขียนเอง แทนที่จะเป็น log line แบบ %-style) ให้รูปร่าง dd.* เดียวกัน ผลลัพธ์จริง

$ DD_LOGS_INJECTION=true DD_SERVICE=checkout-api DD_ENV=production DD_VERSION=2.4.1 python json_log_test.py
{"level": "ERROR", "message": "checkout failed",
"dd": {"trace_id": "6abbc4dd000000009bb491e69f700f86", "span_id": "13978525130697882112",
"service": "checkout-api", "env": "production", "version": "2.4.1"}}

Reserved attribute: ชื่อที่ Datadog ปฏิบัติเป็นพิเศษอยู่แล้ว

หัวข้อที่มีชื่อว่า “Reserved attribute: ชื่อที่ Datadog ปฏิบัติเป็นพิเศษอยู่แล้ว”

attribute ชื่อบางตัวเป็น reserved — UI และ processing ของ Datadog รู้อยู่แล้วว่าต้องทำอะไรกับมันทันทีที่โผล่ใน log ไม่ว่า structured หรือไม่: host (เครื่อง/container ไหนสร้าง log นี้) service (unified service tagging เดียวกับ trace/metric) status (severity สิ่งที่ Status Remapper เขียนเข้าไป) message (ข้อความหลักที่มนุษย์อ่านได้ สิ่งที่ Message Remapper เขียนเข้าไป) timestamp (เวลาทางการของ log สิ่งที่ Date Remapper เขียนเข้าไป) และ trace_id/dd.trace_id (จุด pivot correlation ที่พิสูจน์ด้านบน) เพราะเป็น reserved structured log ที่ตั้งชื่อ field เองเป็น host, service, status, message หรือ timestamp จะถูกดึงค่าไปใช้อัตโนมัติ ไม่ต้องมี remapper เลย — remapper จากบทที่แล้วมีไว้เพื่อ log ที่ไม่ได้ใช้ชื่อพวกนี้เป๊ะ ๆ เท่านั้น

เช็ค datadog_api_client==2.61.0 ที่ติดตั้งจริงหา model สำหรับสร้าง log Facet บน attribute — ไม่มี model แบบนั้นเลย ทุก model facet ที่มีอยู่ใน client ตัวนี้เป็นของ product อื่น (Service Catalog search, Incident search, CSM host facet, Model Lab) หรือเป็นการอ่านข้อมูล facet (facet_info_request/facet_info_response) ไม่ใช่การ define log Facet ใหม่

$ python -c "
import pkgutil, datadog_api_client.v2.model as m
print([n for _, n, _ in pkgutil.iter_modules(m.__path__) if 'facet' in n])
" | tr ',' '\n' | grep -i "logs\|create"
# (no output -- no logs-specific facet CREATE model exists in either v1 or v2)

การสร้าง log Facet เป็น UI-only operation ใน Datadog (Logs Explorer → attribute → “Create facet”) ไม่ใช่สิ่งที่ API client ที่ pin ไว้นี้มี model ให้ — ตรงกับที่สเปคคาดไว้เองว่าน่าจะเป็น UI-only ตอนนี้ยืนยันแล้วด้วยการเช็ค module list จริงของ client แบบละเอียด ไม่ใช่แค่สันนิษฐาน

ผูกกลับไปหา field source: java/source: nginx ของบทที่แล้ว: การตั้งค่า source ที่รู้จักบน logs: block ก็เป็นตัวกระตุ้นให้ Datadog auto-install Processing Pipeline built-in ของ integration ที่ match ด้วย — คุณไม่ต้องสร้างหรือแนบมันเอง นี่คือกลไกเดียวกัน ไม่ใช่กลไกใหม่: tag source เดียวกันที่ใช้ label log สำหรับ filter ก็เป็นค่าเดียวกันที่ logic การติดตั้ง pipeline ของ Datadog อ่านเพื่อตัดสินว่าจะ wire Grok rule built-in ตัวไหนให้อัตโนมัติตั้งแต่ log ตัวแรกที่มี source นั้นมาถึง

🐛 หา bug ในโค้ดนี้

src/log-user-object.js
// AI comment: "Log full user context as structured data for facets"
const pino = require('pino')();
function logCheckoutFailure(user, durationMs) {
pino.error({
user: user, // the whole user object -- id, email, address, payment_method, ...
duration_ms: durationMs,
}, 'checkout failed');
}
เฉลย

โค้ดนี้สร้าง JSON ที่ structured จริง ๆ — พิสูจน์ด้านบน output ของ pino เป็นข้อมูลจริงที่มี type พร้อม facet ได้ ไม่มีอะไรตรงนี้ throw หรือ fail แบบเงียบ ๆ เหมือน bug ก่อนหน้าในคอร์สนี้ ปัญหาเป็นเรื่อง design ไม่ใช่ crash: การ log object user ทั้งก้อนทำให้ทุก field ของมัน (email, address, payment_method, อะไรก็ตามที่ object นั้นมีอยู่) ถูก nest เข้าไปใน log line เป็น attribute ซึ่ง (a) อาจรั่ว PII เข้า log index ที่ engineer ส่วนใหญ่คิดว่ามีแค่ id กับ duration_ms แบบเงียบ ๆ และ (b) สร้าง attribute ที่ facet ได้หนึ่งตัวต่อ field ที่ object มีอยู่ตอนนี้ โดยไม่มีขั้นตอน review เลย — field ที่ถูกเพิ่มเข้า object user เดือนหน้าจะกลายเป็น log attribute ที่ facet ได้ใหม่โดยอัตโนมัติ โดยไม่มีใครตัดสินใจว่านั่นเป็นความคิดที่ดี ทางแก้คือวินัยเดียวกับ code review เรื่อง structured logging ทั่วไป: log field ที่ต้องการ facet จริง ๆ (user.id) ไม่ใช่ object ที่คุณคุม shape ไม่ได้เต็มที่

ชื่อ attribute ที่ reserved คือสัญญา ไม่ใช่แค่ convention — พิสูจน์ด้านบน service/status/message/timestamp ถูกดึงไปใช้อัตโนมัติทันทีที่ structured log ใช้ชื่อพวกนั้นเป๊ะ ๆ โดยไม่ต้องตั้งค่า remapper เลย เพราะ processing ของ Datadog เองคอยมองหาอยู่แล้ว ทุกอย่างที่คุณ log นอกจากนั้นเป็นแค่ data เฉย ๆ จนกว่าจะมีอะไรบางอย่าง — การดึงของ Grok Parser หรือชื่อ key ของ JSON เอง — ให้ชื่อที่ระบบ Facet ของ Datadog แขวน filter/group/chart ได้ การ structure log ให้ดีคือ ในความหมายจริง ๆ การตัดสินใจล่วงหน้าว่างาน remapper ของคุณเองจะเป็นยังไง: log service/status/message ตรง ๆ แล้วสอง จาก สาม processor ที่พบบ่อยที่สุดใน pipeline ของบทที่แล้วก็ไม่จำเป็นสำหรับ log source นั้นอีกต่อไป

  • JSON key แบบ native เทียบกับ attribute ที่ Grok ดึงออกมา — พิสูจน์ด้านบน (user.id, duration_ms มาแบบมี type อยู่แล้ว): JSON ไม่ต้องดูแล pattern และไม่มีทางหยุด match แบบเงียบ ๆ Grok ยังจำเป็นสำหรับ log จากระบบที่คุณคุม format ไม่ได้ แลกกับความเสี่ยงที่แค่ framework เปลี่ยน version เดียวก็ทำให้พังแบบเงียบ ๆ ได้
  • Log object ทั้งก้อนเทียบกับ field เฉพาะเจาะจง — พิสูจน์ด้านบนในตัวอย่าง SpotTheBug: log object user ทั้งก้อนเขียนโค้ดน้อยกว่าตอนนี้ แต่สร้าง facet surface area ที่ไม่ได้ review และ drift ไปเรื่อย ๆ พร้อมความเสี่ยง PII รั่วจริง ส่วนการ log field ที่ตั้งชื่อไว้เขียนโค้ดมากกว่าตอนนี้ แต่ได้ attribute set ที่เสถียรและ audit ได้ในอนาคต
  • Reserved attribute เทียบกับชื่อที่ตั้งเอง — ใช้ชื่อ reserved ของ Datadog (service, status, message, timestamp) แปลว่าไม่ต้องตั้งค่า remapper เลย แลกกับที่โค้ด logging ของทุก service ต้องตกลงใช้ชื่อนั้นเป๊ะ ๆ ชื่อที่ตั้งเองเป็นธรรมชาติกว่าต่อทีม แต่ต้องมี Status/Message/Date Remapper มา normalize รวมศูนย์
  • “การสร้าง Facet ทำผ่าน public API ได้ เหมือน monitor หรือ dashboard” ไม่ใช่ — พิสูจน์ด้านบนด้วยการเช็ค client ที่ติดตั้งจริงแบบละเอียด: ไม่มี model datadog_api_client สำหรับสร้าง log Facet เลยทั้ง v1 และ v2 เป็น action ของ UI ใน Logs Explorer
  • “Reserved attribute อย่าง service กับ status ต้องมี remapper ถึงจะทำงาน” ไม่ใช่ — พิสูจน์ด้านบนตามนิยาม: structured log ที่ตั้งชื่อ field เป็น service/status/message/timestamp อยู่แล้วจะถูกจดจำอัตโนมัติ remapper มีไว้สำหรับ log ที่ใช้ชื่อ fieldอื่น
  • “Log object ทั้งก้อนเหมือนกับ log field ที่มีประโยชน์ของมัน” ไม่ใช่ — พิสูจน์ด้านบน: ทุก field ที่ object มีอยู่กลายเป็น log data และ attribute ที่ facet ได้ ไม่ว่าจะตั้งใจหรือไม่ ซึ่งเป็นความเสี่ยง PII และ facet-sprawl จริง ไม่ใช่แค่เรื่อง style
ทำไม JSON field ที่เป็น native ถึงเชื่อถือได้กว่า field ที่ Grok parse จากข้อความอิสระ
Facet ใน Logs Explorer คืออะไร
Status Remapper ทำอะไรให้กับหลาย service พร้อมกัน
field หนึ่งถูก exclude จาก index ด้วย Exclusion Filter Log Analytics group-by บน field นั้นจะเป็นยังไง
datadog_api_client ที่ติดตั้งจริง (v1 หรือ v2) มี model สำหรับสร้าง log Facet บน attribute ไหม
structured log ตั้งชื่อ field severity เป็น status เป๊ะ ๆ แทนที่จะเป็น level หรือ severity เกิดอะไรขึ้นโดยไม่มี Status Remapper ตั้งไว้เลย