Log Collection and Processing Pipelines
ไอเดียหลักในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียหลักในหนึ่งประโยค”log จะเข้า Datadog ได้เพราะ Agent ถูกบอกผ่าน logs: config block ว่าอ่านจากไหน แล้วติด label service/source อะไร และพอ log มาถึง Processing Pipeline จะ parse กับ enrich log แต่ละตัวก่อนที่จะไปถึง index
บอก Agent ว่าให้มองที่ไหน
หัวข้อที่มีชื่อว่า “บอก Agent ว่าให้มองที่ไหน”Agent ไม่ได้ไปตามหา log เองลอย ๆ — ทุก log source ต้องระบุชัดเจน คุณเพิ่ม logs: block เข้าไป โดยปกติจะอยู่ใน conf.d integration config หรือ logs config ของ Agent เอง แล้วบอกว่า source เป็นประเภทไหน, path (หรือ port สำหรับ tcp) อยู่ที่ไหน และ label สองตัวที่สำคัญมากในขั้นตอนถัดไป คือ service กับ source
logs: - type: file path: /my/app/file.log service: payments-api source: javatype— Agent อ่าน log แบบไหนfileคือ tail ไฟล์บน disk ส่วนค่าที่ใช้บ่อยอื่น ๆ คือtcp,udpและdockerสำหรับเก็บ log จาก containerpath— log อยู่ที่ไหน สำหรับ typefileเป็น path ของไฟล์ตรง ๆ ไม่ใช่ queryservice— ชื่อ service ที่สร้าง log นี้ขึ้นมา เป็น tagserviceตัวเดียวกับที่ใช้ทุกที่ใน Datadog (metric, trace) ที่เป็นตัวที่ทำให้ log line, trace และ metric ผูกกลับไปหา deployable unit เดียวกันได้source— เป็น technology identifier ไม่ใช่ label แบบข้อความอิสระ การตั้งsource: java(หรือnginx,postgresqlฯลฯ) จะบอก Datadog ว่าให้แนบ pipeline ที่ built-in เฉพาะ source นั้น ให้อัตโนมัติ เพราะ Datadog มี Grok parsing rule สำเร็จรูปให้ framework/service ยอดนิยมหลายสิบตัว
พอ Agent อ่าน config นี้ในรอบ reload ถัดไป จะเริ่ม tail /my/app/file.log แล้วส่งแต่ละบรรทัดไปที่ intake ของ Datadog พร้อม tag service:payments-api และ source:java
Processing Pipeline: parse และ enrich หลัง intake
หัวข้อที่มีชื่อว่า “Processing Pipeline: parse และ enrich หลัง intake”Processing Pipeline คือ list ของ processor ที่เรียงลำดับไว้ รันกับ log ทุกตัวที่มาถึง intake และ match query filter ของ pipeline นั้น ขั้นตอนนี้เกิดหลังจาก log ออกจาก host มาถึง Datadog แล้ว และก่อนที่จะถูกเขียนลง index ไหน ๆ — หน้าที่ของตัวเองคือ parse ข้อความที่ไม่มีโครงสร้างให้เป็น attribute ที่มีโครงสร้าง แล้ว enrich log เท่านั้น ไม่ใช่การตัดสินว่า log จะถูกเก็บไว้หรือไม่
pipeline ถูก scope ด้วย filter เช่น service:my-service เพื่อให้มีแค่ log จาก service หรือ source ที่ตั้งใจไว้เท่านั้นที่ผ่านเข้ามา คุณมี pipeline หลายตัวทำงานพร้อมกันได้ แต่ละตัว scope ไปคนละ service หรือ source รวมถึง pipeline built-in ที่ source: java, source: nginx ฯลฯ แนบให้อัตโนมัติด้วย
processor ที่พบบ่อยใน pipeline เรียงตามลำดับที่รัน:
- Grok Parser — apply Grok pattern กับ raw log message เพื่อดึง attribute ที่มีโครงสร้างออกจากข้อความที่ไม่มีโครงสร้าง เช่น ดึง
duration,http.status_code, หรือuser.idออกจากบรรทัดที่เดิมเป็น string ยาว ๆ ก้อนเดียว - Date Remapper — บอก Datadog ว่า attribute ไหนที่ parse ออกมาแล้วคือ timestamp ทางการของ log นี้ (แทนที่จะ fallback ไปใช้เวลา intake) ซึ่งสำคัญกับการวาง log ให้ถูกตำแหน่งบน timeline
- Status Remapper — map attribute ที่ parse ออกมา (เช่น field ข้อความที่มีคำว่า
ERRORหรือWARN) เข้ากับ fieldstatus(severity) มาตรฐานของ Datadog เพื่อให้ view และ facet ที่อิงตาม severity ทำงานสอดคล้องกันทุก service
Pipeline: "payments-api logs"filter: service:payments-api
1. Grok Parser -> extract duration, http.status_code, user.id from message2. Date Remapper -> use parsed `timestamp` attribute as the log's official time3. Status Remapper -> map parsed `level` attribute to Datadog's status fieldflowchart LR
A["logs: block in Agent config\ntype/path/service/source"] --> B[Agent tails the file]
B --> C[Intake]
C --> D{Pipeline filter matches?}
D -->|yes| E[Grok Parser]
E --> F[Date Remapper]
F --> G[Status Remapper]
G --> H["Parsed, enriched log\n(not yet indexed)"]
D -->|no match| H พิสูจน์: Agent จริงที่ tail ไฟล์จริงจริง ๆ
หัวข้อที่มีชื่อว่า “พิสูจน์: Agent จริงที่ tail ไฟล์จริงจริง ๆ”mount config ด้านบน (เป็น conf.d/mylogs.d/conf.yaml บวก block type: tcp อีกตัว) เข้า container datadog/agent:7 ที่ pull มาสด ๆ จริง (DD_API_KEY ปลอม DD_DD_URL ชี้ไป local port ที่ปิดอยู่ — ไม่มีอะไรส่งไปไหนเลย) แล้วรัน agent configcheck
logs: - type: file path: /var/log/lesson/app.log service: payments-api source: java log_processing_rules: - type: exclude_at_match name: exclude_healthcheck_lines pattern: 'GET /healthz'$ docker exec dd-logs agent configcheck=== mylogs check ===Configuration provider: fileConfiguration source: file:/etc/datadog-agent/conf.d/mylogs.d/conf.yamlLog Config:logs: - log_processing_rules: - name: exclude_healthcheck_lines pattern: GET /healthz type: exclude_at_match path: /var/log/lesson/app.log service: payments-api source: java type: fileconfig ถูกยอมรับโดยไม่มี error เลย เพื่อพิสูจน์ว่า Agent กำลัง tail จริง ไม่ใช่แค่ parse config เฉย ๆ append บรรทัดจริงเข้าไปในไฟล์ที่ mount ไว้ แล้วเช็ค section Logs Agent ของ agent status ก่อนและหลัง
$ echo "2026-09-29 10:00:05 ERROR duration=812ms user=42 checkout failed" >> app.log$ docker exec dd-logs agent status | grep -A9 'mylogs$' mylogs ------ - Type: file Path: /var/log/lesson/app.log Service: payments-api Source: java Status: OK Bytes Read: 65 Pipeline Latency: Average Latency: 1.001441792sBytes Read: 65 คือจำนวน byte จริงเป๊ะของบรรทัดที่เพิ่งเพิ่มเข้าไป — หลักฐานจริงที่วัดได้ว่า Agent อ่าน content ใหม่ ไม่ใช่แค่ echo config นิ่ง ๆ config block ตัวที่สองสำหรับ type: tcp (ประเภทที่สามของการเก็บ log นอกจาก file/docker) ก็ configcheck ผ่านสะอาดกับ Agent ตัวเดียวกันที่รันอยู่
logs: - type: tcp port: 10518 service: checkout-web source: nginx$ docker exec dd-logs agent configcheck=== mytcplogs check ===Log Config:logs: - port: 10518 service: checkout-web source: nginx type: tcpประเภทที่สี่ type: docker (container log) ไม่ขอพูดซ้ำที่นี่ — The Datadog Agent and Integrations พูดถึง container-log Autodiscovery (annotation ad.datadoghq.com/<container>.logs บน Pod template) ไว้ลึกแล้ว concept logs: เดียวกันนี้ใช้ได้เหมือนกัน แค่ขับเคลื่อนด้วย annotation แทนไฟล์ conf.d แบบ static
Pipeline JSON ที่ validate แล้ว — และช่องว่างจริงใน harness ของคอร์สนี้เอง
หัวข้อที่มีชื่อว่า “Pipeline JSON ที่ validate แล้ว — และช่องว่างจริงใน harness ของคอร์สนี้เอง”สร้าง pipeline สามตัว processor ตามที่ร่างไว้ด้านบนเป็น JSON จริง แล้ว validate กับ datadog_api_client==2.61.0 ที่ติดตั้งจริง โดย model LogsPipeline
{ "name": "payments-api logs", "filter": { "query": "service:payments-api" }, "is_enabled": true, "processors": [ { "type": "grok-parser", "name": "Parse payments log line", "is_enabled": true, "grok": { "match_rules": "PaymentsRule %{date(\"yyyy-MM-dd HH:mm:ss\"):timestamp} %{word:level} duration=%{number:duration}ms user=%{number:user.id}" } }, { "type": "date-remapper", "name": "Set official timestamp", "is_enabled": true, "sources": ["timestamp"] }, { "type": "status-remapper", "name": "Map level to status", "is_enabled": true, "sources": ["level"] }, { "type": "attribute-remapper", "name": "Rename duration", "is_enabled": true, "sources": ["duration"], "target": "http.response_time_ms", "source_type": "attribute", "target_type": "attribute" } ]}$ python -c "from datadog_api_client.v1.model.logs_pipeline import LogsPipelineimport jsonpayload = json.load(open('payments-api.json'))LogsPipeline(**payload, _check_type=True)print('validated OK')"validated OKลำดับสำคัญ: remapper ที่มาก่อน parser ไม่มีอะไรให้ remap
หัวข้อที่มีชื่อว่า “ลำดับสำคัญ: remapper ที่มาก่อน parser ไม่มีอะไรให้ remap”processor สามตัวด้านบนเรียงตามลำดับที่ต้องรัน: Grok Parser ก่อน (เป็นตัวเดียวที่แปลง string message ดิบให้เป็น attribute ที่มีชื่ออย่าง timestamp กับ level ขึ้นมาเลย) แล้วค่อย Date Remapper กับ Status Remapper (มีอะไรให้ remap ก็ต่อเมื่อ attribute พวกนั้นมีอยู่แล้วเท่านั้น) สลับลำดับ — Date Remapper มาก่อน Grok Parser — pipeline ก็ยัง validate ผ่านเป็น LogsPipeline ที่ well-formed สมบูรณ์ (ทั้งคู่เป็น processor object ที่ถูกต้องตามรูปแบบ ไม่มีอะไรใน model ที่รู้ได้ว่าต้องรันตามลำดับ) แต่ตอน runtime Date Remapper จะหา attribute timestamp ที่ยังไม่มีอยู่ เพราะ Grok Parser ยังไม่ได้รันสร้างมันขึ้นมา
Pipeline processors, order swapped (broken):1. Date Remapper -> looks for `timestamp` attribute — does not exist yet2. Grok Parser -> creates `timestamp` — too late, remapper already ran3. Status Remapper -> looks for `level` — also does not exist yetนี่คือ bug เชิง semantic ล้วน ๆ — processor แต่ละตัวเป็น JSON ที่ valid เอง LogsPipeline validate ทั้งไฟล์โดยไม่บ่นอะไรเลย วิธีเดียวที่จะจับได้คือต้องรู้ (หรือทดสอบ) ว่าลำดับ processor มีความหมาย ไม่ใช่แค่ list เฉย ๆ
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”🐛 หา bug ในโค้ดนี้
{"name": "orders logs","filter": { "query": "service:orders-api" },"is_enabled": true,"processors": [ { "type": "date-remapper", "name": "Set timestamp", "is_enabled": true, "sources": ["timestamp"] }, { "type": "status-remapper", "name": "Map level", "is_enabled": true, "sources": ["level"] }, { "type": "grok-parser", "name": "Parse message", "is_enabled": true, "grok": { "match_rules": "OrdersRule %{date(\"yyyy-MM-dd HH:mm:ss\"):timestamp} %{word:level} order=%{number:order.id}" } }]}เฉลย
JSON นี้ well-formed สมบูรณ์ — processor object ทุกตัวมี field ที่จำเป็นครบ และ LogsPipeline(**payload, _check_type=True) validate ผ่านไม่มีคำบ่นเลย (พิสูจน์ด้านบน model เช็ครูปร่าง ไม่เช็คลำดับ) bug ที่พิสูจน์ด้านบนด้วยการไล่ดูว่าแต่ละ processor ต้องการอะไรจริง ๆ: Date Remapper กับ Status Remapper รันก่อน Grok Parser ที่จะสร้าง attribute timestamp กับ level ที่มันกำลังมองหา remapper ทั้งสองตัวไม่มีผลอะไรเลยแบบเงียบ ๆ กับทุก log ที่ pipeline นี้ประมวลผล — ไม่มี error ที่ไหนเลย มีแค่ log ที่ timestamp ทางการยังเป็น “เวลา intake” แทนที่จะเป็นเวลาที่ฝังอยู่ในข้อความ และ facet status ไม่เคยถูกตั้งจาก level ที่ parse ได้เลย ทางแก้คือสลับลำดับ array: Grok Parser ก่อน แล้วค่อยสอง remapper
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Processing Pipeline ไม่ใช่ชุด rule อิสระที่เห็น input เดียวกันหมด — มันคือสายพานที่แต่ละสถานีมี input แค่สิ่งที่สถานีก่อนหน้าทิ้งไว้ให้เท่านั้น string message ดิบเป็นสิ่งเดียวที่รับประกันว่ามีอยู่ตอน pipeline เริ่มทำงาน attribute อื่น ๆ ทั้งหมด (timestamp, level, อะไรก็ตามที่ Grok Parser ดึงออกมา) มีอยู่ก็ต่อเมื่อ processor ก่อนหน้าสร้างมันขึ้นมาแล้วเท่านั้น model LogsPipeline — พิสูจน์ด้านบน — validate ว่าแต่ละสถานี well-built เองเท่านั้น มันไม่มีทางรู้ และไม่พยายามจะรู้ ว่าสถานีที่ 3 ต้องพึ่งอะไรที่มีแค่สถานีที่ 5 สร้างขึ้นมา
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”exclude_at_matchฝั่ง Agent เทียบกับ Processing Pipeline — การทิ้งบรรทัดบน host (จากlog_processing_rulesในlogs:block) ฟรีแต่ย้อนกลับไม่ได้ ส่วน Processing Pipeline รันเสมอ (ข้ามไม่ได้) การตัดสินใจกรองจึงควรอยู่ที่ขั้น index/exclusion-filter ในบทถัดไป ไม่ใช่ที่นี่ — pipeline ของบทนี้แค่ parse กับ enrich ไม่เคย drop เลย- Structural JSON validation เทียบกับความถูกต้องเชิง semantic — พิสูจน์ด้านบนสองครั้ง (ช่องว่างของ processor ที่ type เป็น Union และ bug เรื่อง order dependency):
LogsPipelineที่ validate ผ่านบอกแค่ว่ารูปร่างถูก ไม่ใช่ว่า pipeline ทำสิ่งที่คุณตั้งใจ ทั้งคู่ต้องคิดแยกต่างหากว่าแต่ละ processor ต้องการ input อะไรจริง ๆ source: java(pipeline built-in) เทียบกับ pipeline ที่เขียนเอง — pipeline built-in เฉพาะ source ไม่ต้องเขียนเอง Datadog ดูแลให้ตามที่ format ของ framework เปลี่ยนไป แลกกับความ generic ส่วน pipeline ที่เขียนเองแบบด้านบนตรงกับ format log ของคุณเป๊ะ แลกกับที่คุณต้องดูแลมันไปตลอด
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “Processing Pipeline ตัดสินว่า log จะถูกเก็บไว้หรือไม่” ไม่ใช่ — บทนี้เปิดเรื่องด้วยความต่างนี้ตั้งแต่แรก และยังคงยืนยันได้จากหลักฐาน JSON ด้านบน: pipeline parse และ enrich log ทุกตัวที่ match filter ของมัน ส่วนว่า log จะรอดพอที่จะถูก index หรือไม่เป็นการตัดสินใจคนละเรื่อง อยู่ในบทถัดไป
- “ถ้า JSON ของ pipeline validate ผ่าน pipeline นั้นก็ถูกต้อง” ไม่ใช่ — พิสูจน์ด้านบนสองครั้ง:
LogsPipelineที่มี processor เรียงผิดลำดับ validate ผ่านสะอาดแต่ไม่มีผลอะไรเลยแบบเงียบ ๆ และ processor ที่ซ้อนอยู่ผิดรูปแบบ fallback ไปเป็น type ทั่วไปแบบเงียบ ๆ แทนที่จะ raise - “Grok syntax ถูกเช็คก่อนสร้าง pipeline” ไม่ใช่จาก tooling ของคอร์สนี้ — พิสูจน์ด้านบน:
LogsGrokParserRulesต้องการแค่match_rulesเป็น string ที่ไม่ว่างเปล่า ยอมรับ Grok ที่ syntax ผิดโดยไม่บ่นเลย