เชื่อม Log เข้ากับ Trace
ไอเดียหลักในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียหลักในหนึ่งประโยค”tracer รู้อยู่แล้วว่า span ไหน active อยู่ตอนโค้ดกำลังจัดการ request หนึ่ง ดังนั้น log injection ก็แค่ tracer ส่ง trace_id (กับ span_id) ของ span ที่ active นั้นให้ logging library ของเรา ทำให้ log ทุกบรรทัดที่เขียนระหว่าง request นั้นพก identifier ที่ใช้ตามหา trace ต้นทางกลับไปได้
ถ้าไม่มี: log บรรทัดหนึ่งที่ไปต่อไม่ได้
หัวข้อที่มีชื่อว่า “ถ้าไม่มี: log บรรทัดหนึ่งที่ไปต่อไม่ได้”2026-07-08T10:14:02Z ERROR Failed to charge card for order 48213log บรรทัดนี้บอกแค่ว่ามีอะไรพัง — ไม่บอกว่า downstream call ไหนช้า service ไหนใน chain เป็นคนโยน error ออกมาจริง ๆ หรือมีอะไรอื่นเกิดขึ้นอีกใน request เดียวกัน ถ้าไม่มี identifier ที่ประทับอยู่ทั้งใน log และ trace การ correlate สองอย่างนี้เข้าด้วยกันจะกลายเป็นการเดาจาก timestamp แล้วหวังว่าจะไม่มีอย่างอื่นเกิดบน host เดียวกันในวินาทีเดียวกัน
กลไกทั่วไป: log integration อ่าน active span
หัวข้อที่มีชื่อว่า “กลไกทั่วไป: log integration อ่าน active span”tracer ทุกตัวเก็บ span ที่ active อยู่ไว้ใน context ที่ scope ตาม request — thread-local ถ้า runtime เป็นแบบ threaded, async-local (หรือ continuation-local) ถ้า runtime เป็นแบบ event-loop log injection คือ hook เล็ก ๆ ที่เสียบเข้ากับ logging library ของเรา — อาจเป็น formatter, processor หรือ logger method ที่ patch ไว้ — รันทุกครั้งที่มี log record แล้วถาม tracer ว่า “ตอนนี้ span ไหน active อยู่” ถ้ามี จะ copy trace_id กับ span_id ของ span นั้นแปะเข้าไปใน record ก่อนปล่อยออกไป เปิดใช้งานครั้งเดียว แล้ว log statement ทุกจุดที่มีอยู่แล้วในโค้ดเบสได้ประโยชน์ทันที โดยไม่ต้องแตะทีละบรรทัด
ตัวอย่างจริงหนึ่งภาษา: Node.js
หัวข้อที่มีชื่อว่า “ตัวอย่างจริงหนึ่งภาษา: Node.js”require('dd-trace').init({ logInjection: true});option ตัวนี้คือขั้นตอน integration ทั้งหมดสำหรับ service ที่เขียนด้วย Node.js และใช้ dd-trace พอตั้งค่านี้แล้ว log ทุกอันที่ปล่อยผ่าน logging library ที่รองรับ (pino, winston, bunyan) จะได้ trace context ที่ active อยู่แปะเข้าไปใน structured output อัตโนมัติ — ไม่ต้องแก้ logger.info(...) หรือ logger.error(...) ทีละจุดในแอปเลย
พิสูจน์: option key ที่ไม่รู้จักเงียบกลายเป็น no-op ไม่ใช่ error
หัวข้อที่มีชื่อว่า “พิสูจน์: option key ที่ไม่รู้จักเงียบกลายเป็น no-op ไม่ใช่ error”จุดนี้สำคัญเพราะ object literal ธรรมดาของ JavaScript ไม่เคยปฏิเสธ key ที่มันไม่รู้จัก — dd-trace แค่ไม่มองมันเลย ติดตั้ง [email protected] (ตรงกับเวอร์ชันที่คอร์สนี้ pin ไว้) ในสแครชแล้ว dump config ที่ resolve จริงของ tracer สามแบบ — t._tracer._config.logInjection คือค่า config ภายในของ dd-trace เองที่ merge เสร็จแล้ว และ DD_TRACE_STARTUP_LOGS (default true ตาม supported-configurations.json) ยังปริ้นท์ภาพเดียวกันออกมาเองโดยไม่ต้องขอตอน init() เป็น log บรรทัด DATADOG TRACER CONFIGURATION - ด้วย เลยไม่จำเป็นต้องรู้จัก private field นั้นก็เห็นได้:
$ node -e "const t=require('dd-trace').init({logInjection:false,service:'probe'}); console.log(t._tracer._config.logInjection)"DATADOG TRACER CONFIGURATION - {...,"log_injection_enabled":false,...}camelCase logInjection:false -> false # ทำตามที่สั่งจริง
$ node -e "const t=require('dd-trace').init({log_injection:false,service:'probe'}); console.log(t._tracer._config.logInjection)"DATADOG TRACER CONFIGURATION - {...,"log_injection_enabled":true,...}snake_case log_injection:false -> true # ถูกเพิกเฉยเงียบ ๆ — ยังเป็นค่า default เดิมที่ไม่ถูกแตะ
$ node -e "const t=require('dd-trace').init({service:'probe'}); console.log(t._tracer._config.logInjection)"no logInjection key at all -> true # true คือ default จริงไม่ว่าจะแบบไหนเพราะ default จริงคือ true อยู่แล้ว log_injection: true ในตัวอย่างเวอร์ชันเก่าของบทนี้จึง “ดูเหมือนทำงาน” โดยบังเอิญล้วน ๆ — แต่ใครก็ตามที่ copy pattern สะกดผิดตัวเดียวกันไปเขียน log_injection: false โดยตั้งใจจะ ปิด injection (ความต้องการจริงที่พบบ่อย เช่น service ที่ห้ามให้ trace_id หลุดเข้า log destination ที่แชร์ออกนอกองค์กร) จะไม่ได้ผลลัพธ์แบบนั้นเลย โหมด --options ของ npm run verify มีไว้เพื่อจับ key ที่หลอนแบบนี้ก่อนขึ้น production โดยเฉพาะ — มันเช็ค key ระดับบนสุดทุกตัวใน tracer.init({...}) fence ที่ถูก collect กับ .d.ts ที่ติดตั้งจริง — fence ข้างบน (logInjection: true) ผ่าน; log_injection: true จะไม่มีทางถูก collect ว่าถูกต้องตั้งแต่แรก
Trace context propagation: สอง service ตกลงกันได้ยังไงว่าเป็น trace เดียวกัน
หัวข้อที่มีชื่อว่า “Trace context propagation: สอง service ตกลงกันได้ยังไงว่าเป็น trace เดียวกัน”log injection ตอบคำถามว่า “log บรรทัดนี้เป็นส่วนหนึ่งของ trace ไหน” อีกคำถามที่เกี่ยวข้องกันคือ trace เองอยู่เป็น trace เดียวข้าม network call ระหว่างสอง service ได้ยังไง — คำตอบคือชุด HTTP header เล็ก ๆ ที่ tracer inject ตอนขาออกแล้ว extract ตอนขาเข้า รัน http.get() ที่ instrument ด้วย dd-trace ไปยัง local HTTP server ที่ทำหน้าที่แค่ปริ้นท์ header ที่รับมา — ไม่มีอะไรออกจากเครื่องนี้เลย และ DD_TRACE_AGENT_URL ชี้ไปที่ local port ที่ปิดอยู่เลยไม่มี agent จริงเข้ามาเกี่ยวข้องด้วย:
$ DD_TRACE_PROPAGATION_STYLE=datadog,tracecontext node propagation.js--- headers received by downstream server ---x-datadog-trace-id: 5586879601662099183x-datadog-parent-id: 703201510884566073x-datadog-sampling-priority: 1x-datadog-tags: _dd.p.tid=6abbb93000000000,_dd.p.dm=-0traceparent: 00-6abbb930000000004d889565abf1feef-09c2460c65cae439-01tracestate: dd=t.dm:-0;t.tid:6abbb93000000000;s:1;p:09c2460c65cae439x-datadog-trace-id กับ x-datadog-parent-id คือ propagation format ของ Datadog เอง; traceparent/tracestate คือมาตรฐาน W3C Trace Context ทั้งสองแบบติดไปกับ request ขาออกเดียวกันเพราะ DD_TRACE_PROPAGATION_STYLE ถูกตั้งเป็น datadog,tracecontext — tracer ตัวเดียว inject (และรับ) ได้มากกว่าหนึ่ง format พร้อมกัน ซึ่งนี่แหละที่ทำให้มันอยู่ตรงกลางระหว่าง service เก่าที่ใช้ Datadog อย่างเดียวกับ service ใหม่ที่ instrument ด้วย OpenTelemetry ได้
ค่าที่ DD_TRACE_PROPAGATION_STYLE รับได้จริง ๆ อ่านตรงจาก parsers.js ของแพ็กเกจที่ติดตั้งจริง (VALID_PROPAGATION_STYLES) — datadog, tracecontext, b3, b3 single header (เป็น alias ของ b3 ตั้งแต่ major version 6 เป็นต้นไป), b3multi, baggage, none เอกสาร library-config ของ Node.js (docs.datadoghq.com/tracing/trace_collection/library_config/nodejs/) ยืนยัน default: datadog,tracecontext,baggage เปลี่ยน style แล้ว wire format เปลี่ยนไปเลย — trace เดียวกัน header คนละแบบ:
$ DD_TRACE_PROPAGATION_STYLE=b3multi node propagation.jsx-b3-traceid: 6abbb93e00000000041b1cd0943a09acx-b3-spanid: 2a31fba7df01f8bdx-b3-sampled: 1x-b3-parentspanid: 041b1cd0943a09ac
$ DD_TRACE_PROPAGATION_STYLE=b3 node propagation.jsb3: 6abbb93e00000000256681dd9f945d47-24d519d1a7e5f9c8-1-256681dd9f945d47
$ DD_TRACE_PROPAGATION_STYLE=none node propagation.js--- headers received by downstream server --- # (ไม่มีอะไรเลย -- ไม่มี propagation header ถูก inject เลย)รันสุดท้ายนี่คือกับดักในทางปฏิบัติ: ถ้าสอง service ใน request path เดียวกันตั้ง DD_TRACE_PROPAGATION_STYLE ที่ไม่มี format ไหนตรงกันเลย (ตัวหนึ่ง inject แค่ b3multi อีกตัว extract แค่ datadog) trace จะแตกเป็นสอง trace แยกกันเงียบ ๆ ตรง hop นั้น — ไม่มี error ที่ไหนเลย มีแค่ service map ที่มีรูโหว่ นี่คือฝาแฝดระดับ network ของบั๊ก logInjection ข้างบน: mismatch ที่ไม่ throw อะไรเลย มีแต่ข้อมูลหายไป
convention dd.*: identity ที่ติดไปกับ log บรรทัดนั้น
หัวข้อที่มีชื่อว่า “convention dd.*: identity ที่ติดไปกับ log บรรทัดนั้น”{ "level": "error", "message": "Failed to charge card for order 48213", "dd": { "trace_id": "6746734253672554013", "span_id": "3993638273767594310", "env": "production", "service": "checkout-service", "version": "2.4.1" }}tracer ของ Datadog รวมฟิลด์ที่ inject เข้าไปเหล่านี้ไว้ใต้ namespace dd.* dd.trace_id กับ dd.span_id คือ key ที่ใช้ pivot — ชี้ไปที่ trace และ span ตัวที่กำลังรันอยู่ตอนบรรทัดนี้ถูกเขียน dd.env, dd.service และ dd.version คือ unified service tagging triple ที่ติดมาด้วย หมายความว่า log บรรทัดเดียวนี้พก identity มากพอที่จะถูกค้นเจอผ่าน structured logging กับ facet ที่สร้างไว้ใน module Log Management ได้ด้วย — ไม่ได้แค่ correlate กับ trace ได้ แต่ยังเป็น log event ที่ facet ได้เต็มรูปแบบในตัวเอง รันโค้ด Node.js แบบเดียวกันจริง — winston ตั้งค่า logInjection: true กับ JSON formatter, เรียก logger.error() หนึ่งครั้งข้างใน span ที่ active อยู่ — แล้วมันออกมาเป็นรูปนี้เป๊ะโดยไม่ต้องตั้งฟิลด์เองเลย:
$ node logcorrelate.js{"dd":{"env":"production","service":"checkout-service","span_id":"5160495945473134754","trace_id":"6abbb93800000000479dc3b88ec510a2","version":"2.4.1"},"level":"error","message":"Failed to charge card for order 48213"}ภาษา Python เทียบเท่า: DD_LOGS_INJECTION กับ ddtrace-run
หัวข้อที่มีชื่อว่า “ภาษา Python เทียบเท่า: DD_LOGS_INJECTION กับ ddtrace-run”ddtrace (Python) ใช้ environment variable แทน key แบบ programmatic สำหรับเรื่องนี้: DD_LOGS_INJECTION ติดตั้ง ddtrace==4.15.2 ตัวที่ pin ไว้ในสแครช venv แล้ว introspect config object จริงที่รันอยู่แทนการเดาจากความจำ — น่าสังเกตว่าชื่อ attribute เปลี่ยนไประหว่าง major version ของแพ็กเกจเอง ซึ่งเป็นแบบเดียวกับสิ่งที่ training data ของ AI model มักล้าสมัยไปแล้ว:
$ python -c "from ddtrace import config; print(config._logs_injection)"True # default จริง เวอร์ชันที่ pin นี้
$ DD_LOGS_INJECTION=false python -c "from ddtrace import config; print(config._logs_injection)"False
$ DD_LOGS_INJECTION=true python -c "from ddtrace import config; print(config._logs_injection)"Trueการตั้ง config attribute ในโค้ดหลัง import ไม่ใช่ path การเปิดใช้งานที่เอกสารบอก — กลไกจริงคือ patch การ format record ของโมดูล logging มาตรฐาน และ patch นั้นถูกใส่ผ่าน ddtrace-run (หรือ ddtrace.patch_all()) ไม่ใช่แตะ config ตรง ๆ รัน script ที่ใช้ logging ธรรมดา format string อ้าง %(dd.trace_id)s/%(dd.span_id)s/%(dd.service)s/%(dd.env)s/%(dd.version)s ผ่าน ddtrace-run พร้อม DD_LOGS_INJECTION=true:
$ DD_LOGS_INJECTION=true DD_SERVICE=checkout-service DD_ENV=production DD_VERSION=2.4.1 \ ddtrace-run python logcorrelate.py2026-09-29 20:18:51,330 ERROR [__main__] [dd.service=checkout-service dd.env=productiondd.version=2.4.1 dd.trace_id=6abbbabb000000005b560a95a242be0fdd.span_id=8465665182175932103] - Failed to charge card for order 48213ไอเดียเดียวกับ logInjection: true ของ Node, ชื่อฟิลด์ dd.* เหมือนกัน กลไกเปิดใช้งานต่างกัน: env var บวก wrapper ddtrace-run (หรือ ddtrace.patch_all() ในโปรเซสเดียวกัน) แทนที่จะเป็น key ข้างใน tracer.init() ddtrace-run --help ยืนยันว่า binary นี้มีจริงและมีเอกสารรองรับ ไม่ใช่ entry point ที่หลอนขึ้นมา
payoff: คลิกเดียว ไม่ต้องค้นเอง
หัวข้อที่มีชื่อว่า “payoff: คลิกเดียว ไม่ต้องค้นเอง”ก่อนมี log injection log บรรทัดระดับ error หนึ่งบรรทัดอยู่โดดเดี่ยว ไม่มีทางง่าย ๆ ที่จะเห็น request รอบข้าง — อะไรเรียก service นี้ service นี้เรียกอะไรต่อ เวลาไปหมดตรงไหน หลังมี log injection เพราะ log พก dd.trace_id ไปด้วย Datadog ใส่ปุ่ม pivot “view trace” ลงบน log บรรทัดนั้นใน Logs Explorer ได้เลย คลิกเดียวแล้วไปถึง distributed trace ตัวเป๊ะ ๆ ที่มีทุก span จากทุก service ที่มีส่วนร่วมใน request นั้น ซึ่ง span พวกนั้นไม่เคยเห็น log เลยด้วยซ้ำ
flowchart LR A[Incoming request] --> B[Tracer starts span] B --> C["App code calls logger.error(...)"] C --> D["Log injection hook reads active span"] D --> E["Log emitted with dd.trace_id, dd.span_id, dd.env/service/version"] E --> F[Datadog Log Management] F -->|click dd.trace_id| G[APM distributed trace view]
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”🐛 หา bug ในโค้ดนี้
// AI comment: "Enable log-trace correlation for our checkout service"require('dd-trace').init({service: 'checkout-service',env: 'production',log_injection: true});
// elsewhere, for a service that must NOT leak trace_id into a// log destination shared outside the org:// require('dd-trace').init({ log_injection: false });เฉลย
โค้ดนี้ compile และรันได้โดยไม่มี warning สักตัว — object literal ธรรมดาไม่เคยปฏิเสธ key ที่มันไม่รู้จัก บั๊กที่พิสูจน์แล้วข้างบนด้วย config dump จริงของ tracer: log_injection ไม่ใช่ option ของ dd-trace — key จริงคือ logInjection (camelCase, ระดับ env var คือ DD_LOGS_INJECTION) เพราะ default จริงของ log injection คือ true อยู่แล้ว snippet แรก “ทำงาน” โดยบังเอิญล้วน ๆ — แต่ snippet ที่สอง ซึ่งมีไว้เพื่อ ปิด injection ด้วยเหตุผลด้าน compliance โดยเฉพาะ กลับไม่ทำอะไรเลย: service ยังคง inject trace_id/span_id เข้าไปในทุก log บรรทัดเงียบ ๆ ตรงข้ามกับผลลัพธ์ที่ comment ของ AI บอกว่าตั้งใจจะป้องกัน การแก้คือ logInjection: true / logInjection: false — และโหมด --options ของ harness คอร์สนี้เองจะจับสิ่งนี้ได้ก่อนขึ้น production จริง เพราะมันเช็ค key เทียบกับ .d.ts จริงของแพ็กเกจที่ pin ไว้
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”มองว่า log injection กับ trace propagation เป็นไอเดียเดียวกันแค่ scope ต่างกัน Log injection คือ tracer ส่ง identifier ของ span ที่ active อยู่ตอนนั้น ไปด้านข้าง ให้ logging library ที่รันอยู่ในโปรเซสเดียวกัน Propagation headers คือ tracer ส่ง identifier ของ trace เดียวกันนั้น ไปข้างหน้า ข้าม network ไปยังโปรเซสถัดไป ทั้งคู่คือ “แปะ active trace context ลงบนอะไรบางอย่างที่ไม่ใช่ตัว trace เอง” — อันหนึ่งแปะลง log record อีกอันแปะลง HTTP request — และทั้งคู่พังแบบเดียวกันเป๊ะเมื่อตั้งค่าผิด: ไม่ throw exception มีแต่ช่องโหว่ log ที่ไม่มี dd.trace_id ก็แค่ดูเหมือน log ธรรมดา request ที่ไม่มี (หรือ mismatch) propagation header ก็แค่เริ่ม trace ใหม่แทนที่จะต่อจาก trace เดิม ทั้งคู่ไม่ throw อะไรเลย ทั้งคู่จะโผล่มาแค่ในรูปแบบ “pivot ที่ควรจะมีมันหายไป”
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”logInjection: true(หรือDD_LOGS_INJECTION=true) เทียบกับปล่อยไว้เฉย ๆ — เปิดใช้งานคือ flag เดียวและไม่มีต้นทุนที่ระดับ network เลย (ข้อมูลไม่ได้ออกจากโปรเซสต่างไปจากเดิม) แต่หมายความว่าtrace_id/span_idจะติดไปกับ log ทุกบรรทัด ซึ่งสำคัญถ้า log พวกนั้นเคยถูกส่งไปที่ไหนนอกการควบคุมของ Datadog org ตัวเอง- หลาย format ใน
DD_TRACE_PROPAGATION_STYLEพร้อมกัน เทียบกับแค่อันเดียว — injectdatadog,tracecontextพร้อมกัน (พิสูจน์แล้วข้างบน) ทำให้ service คุยกับทั้ง tracer แบบ Datadog-only เก่าและ tracer มาตรฐาน W3C ใหม่ได้ แลกกับ header ที่ใหญ่ขึ้นนิดหน่อยและอีกหนึ่งอย่างที่ต้องคุมให้ตรงกันทุก service ใน request path - debug ด้วยการ dump config จริง (
DD_TRACE_STARTUP_LOGS, introspect_config) เทียบกับเชื่อโค้ดที่เราเขียนเอง — อ่าน config ที่ resolve จริงของ tracer ช้ากว่าการสมมติว่าinit()ทำตามที่เราตั้งใจ แต่เป็นทางเดียวที่จับ key ที่ถูกเพิกเฉยเงียบ ๆ อย่างlog_injectionได้ก่อนขึ้น production
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “option ของ
tracer.init()ที่สะกดผิด throw หรือ warn” ไม่จริง — พิสูจน์แล้วข้างบน:log_injection: true/log_injection: falseรันสะอาดทั้งคู่ ไม่มี error ไม่มี warning ไม่มีอะไรขึ้นใน stderr เลย object literal ของ JavaScript ไม่เคยตรวจสอบ key ของมันกับอะไรทั้งนั้น - “
log_injection: trueในตัวอย่างเก่าของบทนี้ถูกต้องเพราะมันทำงาน” ไม่จริง — พิสูจน์แล้วข้างบน: มัน “ทำงาน” ก็เพราะtrueเป็น default ที่ไม่ถูกแตะอยู่แล้ว การพิมพ์ผิดแบบเดียวกันเป๊ะกับlog_injection: falseที่ตั้งใจจะปิด injection กลับไม่มีผลอะไรเลย - “propagation header ของ trace มีแค่ format เดียวตายตัว” ไม่จริง — พิสูจน์แล้วข้างบน:
DD_TRACE_PROPAGATION_STYLEเลือกได้ทั้ง header แบบ Datadog-native, W3Ctraceparent/tracestate, B3 (single หรือ multi-header) หรือnoneและ tracer ตัวเดียว inject ได้หลาย format พร้อมกันบน request เดียวกัน - “ถ้า propagation ตั้งค่าผิด จะเห็น error” ไม่จริง — พิสูจน์แล้วข้างบน: ตั้ง
DD_TRACE_PROPAGATION_STYLE=noneรันสะอาดสนิทโดยไม่มี propagation header ถูกส่งเลย service ปลายทางก็แค่เริ่ม trace ใหม่ที่ไม่เชื่อมกัน ไม่มีอะไร throw