Continuous Profiler และ Error Tracking
ไอเดียหลักในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียหลักในหนึ่งประโยค”tracing บอกคุณว่าตรงไหนที่ช้าหรือ fail — service ไหน, span ไหน — ส่วน Continuous Profiler กับ Error Tracking บอกคุณว่าทำไม ลงลึกถึงฟังก์ชันที่กิน CPU หรือ root cause ที่ถูก dedup มาจากกอง stack trace ที่ซ้ำกัน
Continuous Profiler: code-level visibility ที่ always-on
หัวข้อที่มีชื่อว่า “Continuous Profiler: code-level visibility ที่ always-on”Continuous Profiler รันตลอดเวลาบน production ด้วย overhead ต่ำ เก็บ CPU time, wall-time และ allocation ระดับโค้ด — ฟังก์ชันและ call stack แต่ละตัว ไม่ใช่แค่ aggregate ระดับ service เพราะ always-on แทนที่จะเป็นอะไรที่คุณเปิดทีหลังตอน investigate incident ข้อมูลจึงพร้อมอยู่แล้วตั้งแต่ก่อนคุณต้องการด้วยซ้ำ
profiler ผูก sample ของตัวเองกลับไปหา service, endpoint และแม้แต่ trace ที่เกิดขึ้นด้วย ความเชื่อมโยงนี้เองที่ทำให้ profiler มีประโยชน์ตอน debug request ที่ช้า — แทนที่จะเดาว่าส่วนไหนของ checkout-web ที่แพง คุณกระโดดจาก trace ที่ช้าตรงเข้าไปหา flame graph ว่าฟังก์ชันไหนกำลังรันอยู่และนานแค่ไหนระหว่าง request นั้น
# ddtrace's profiler starts alongside the tracer — no separate instrumentation neededfrom ddtrace.profiling import Profiler
prof = Profiler()prof.start()พอทั้ง tracer และ profiler รันพร้อมกัน span ที่ช้าใน trace หนึ่งจะพาคุณตรงไปที่ flame graph ระดับฟังก์ชันของช่วงเวลานั้นพอดี — tracing จำกัดให้เหลือ span ไหน profiler จำกัดให้เหลือ บรรทัดโค้ดไหน
พิสูจน์ว่า env var ของ profiler ถูกอ่านจริง — และ profile ในเครื่องจริง ไม่ต้องมี Agent
หัวข้อที่มีชื่อว่า “พิสูจน์ว่า env var ของ profiler ถูกอ่านจริง — และ profile ในเครื่องจริง ไม่ต้องมี Agent”DD_PROFILING_ENABLED เป็น option จริงมีเอกสารรองรับในทั้งสอง client ที่ pin ไว้ — .d.ts ของ dd-trace ประกาศ profiling?: boolean พร้อม @env DD_PROFILING_ENABLED และ supported-configurations.json ของ package ที่ติดตั้งจริงเอง list ทั้งตระกูล: DD_PROFILING_CPU_ENABLED, DD_PROFILING_ALLOCATION_ENABLED, DD_PROFILING_HEAP_ENABLED, DD_PROFILING_CODEHOTSPOTS_ENABLED, DD_PROFILING_TIMELINE_ENABLED, DD_PROFILING_UPLOAD_PERIOD และตัวที่น่าทดสอบตรง ๆ ที่สุด — DD_PROFILING_EXPORTERS (default agent)
profiler ต้องมี live Agent connection ถึงจะพิสูจน์ได้ว่ากำลังเก็บข้อมูลจริงไหม ไม่ต้อง — DD_PROFILING_EXPORTERS=file คือ exporter implementation จริงตัวที่สอง (profiling/exporters/file.js คู่กับ exporters/agent.js ใน package ที่ติดตั้งจริง) และมันเขียน profile ลงดิสก์ในเครื่องตรง ๆ รันฟังก์ชันที่กิน CPU ภายใต้มัน ไม่มี Agent ไม่มี network เลย
const tracer = require('dd-trace').init({ service: 'checkout-web', profiling: true, logInjection: false });
function busy() { let x = 0; const end = Date.now() + 3000; while (Date.now() < end) x += Math.sqrt(x + 1); return x;}busy();setTimeout(() => process.exit(0), 1000);$ DD_PROFILING_ENABLED=true DD_PROFILING_EXPORTERS=file \ DD_PROFILING_PPROF_PREFIX=./pprof-out/ DD_PROFILING_UPLOAD_PERIOD=2 \ node src/profile-test.js$ ls pprof-out/events_worker_0_20260929T135419Z.pprofspace_worker_0_20260929T135419Z.pprofwall_worker_0_20260929T135419Z.pprofprofile file จริง อยู่บนดิสก์ ไม่มี account หรือ Agent ของ Datadog เข้ามาเกี่ยวข้องเลย format .pprof คือ protobuf ที่ compress ด้วย zstd — decompress wall-time profile แล้ว grep raw byte หาชื่อฟังก์ชันของ script นี้เองยืนยันว่ามันจับ sample ระดับโค้ดจริง ไม่ใช่ placeholder data
$ zstd -d pprof-out/wall_worker_0_20260929T135419Z.pprof -o wall.decoded && strings wall.decoded | grep busy(anonymous:L#1711:C#37)/private/tmp/.../src/profile-test.jsbusybusy — ฟังก์ชันของบทนี้เอง — อยู่ในนั้นจริง ๆ ตรงกับ claim ที่ว่า profiler จับ “individual functions and call stacks ไม่ใช่แค่ service-level aggregate” เป๊ะ
ฝั่ง Python ทำงานแบบเดียวกัน แต่ config accessor ไม่ใช่ ddtrace.config.profiling — มันโยน AttributeError จริงใน ddtrace==4.15.2 ที่ pin ไว้
$ python -c "from ddtrace import config; print(config.profiling)"AttributeError: <class '...Config'> object has no attribute profiling, profiling is not a valid configurationaccessor จริงอยู่ใน module แยกต่างหาก และ env var จริงสำหรับ export ไฟล์ในเครื่องคือ DD_PROFILING_OUTPUT_PPROF (ไม่ใช่ DD_PROFILING_EXPORTERS=file — อันนั้นเฉพาะ Node)
$ DD_PROFILING_ENABLED=true python -c \ "from ddtrace.internal.settings.profiling import config as pconfig; print(pconfig.enabled, pconfig.output_pprof)"True None
$ DD_PROFILING_ENABLED=true DD_PROFILING_OUTPUT_PPROF=./pprof-out-py/profile \ python app/profile_test.py$ ls pprof-out-py/profile.<pid>.1.info.jsonprofile.<pid>.1.internal_metadata.jsonprofile.<pid>.1.pprofinfo.json เองก็เป็นหลักฐานว่า env var ถูกอ่านจริง — มันคือ settings dump เต็มรูปแบบที่ profiler เขียนออกมา และ output_pprof ในนั้นอ่านค่ากลับมาตรงกับ path ที่ตั้งไว้เป๊ะ
$ python -m json.tool pprof-out-py/profile.*.1.info.json | grep output_pprof"output_pprof": "./pprof-out-py/profile"Error Tracking: เปลี่ยน stack trace ที่ซ้ำกันให้เป็น issue เดียว
หัวข้อที่มีชื่อว่า “Error Tracking: เปลี่ยน stack trace ที่ซ้ำกันให้เป็น issue เดียว”Error Tracking เอา error event ที่มาจาก trace และ log แล้ว group กับ dedup error ที่คล้ายกันให้เป็น issue เดียวอัตโนมัติ แทนที่จะปล่อยให้คุณต้องมาสังเกตเองว่า NullPointerException ตัวเดิมเกิดขึ้นไปแล้ว 482 ครั้งตลอดหนึ่งสัปดาห์ของ trace แต่ละ issue มี lifecycle ที่คุณจัดการได้ชัดเจน — ปกติคือ new, triaged และ resolved — error ที่เกิดซ้ำ ๆ จึงกลายเป็นสิ่งเดียวที่ทีมของคุณ track จนปิดได้ แทนที่จะเป็น event หลายร้อยตัวที่ดูเหมือนไม่เกี่ยวกันเลย
{ "issue": { "status": "new", "occurrences": 482, "first_seen": "2026-07-01T10:03:00Z", "last_seen": "2026-07-08T09:12:00Z", "example_trace_ids": ["6141ee2f9a4d4b2e8f3c1a90", "9af23cd1b8e7f4a6c2d5e901"] }}สังเกต field example_trace_ids — Error Tracking เก็บ link กลับไปหา example trace จริงของ issue นั้นไว้ด้วย คุณจึงกระโดดจาก “issue นี้เกิดไปแล้ว 482 ครั้ง” ตรงเข้าไปหา occurrence จริงหนึ่งตัวเพื่อดู context ของ request รอบ ๆ ได้
จาก “ตรงไหน” ไปสู่ “ทำไม”
หัวข้อที่มีชื่อว่า “จาก “ตรงไหน” ไปสู่ “ทำไม””สองเครื่องมือนี้รวมกันแล้วปิดช่องว่างที่ tracing เพียงอย่างเดียวทิ้งไว้
- Tracing — service ไหนและ span ไหนที่ช้าหรือ fail
- Continuous Profiler — ฟังก์ชันไหนระดับโค้ดที่กิน CPU/wall-time/allocation ระหว่าง span ที่ช้านั้น
- Error Tracking — span ที่ fail นั้นอยู่ใน issue ที่ dedup แล้วอันไหน และ issue นั้นอยู่ที่จุดไหนใน lifecycle new/triaged/resolved
ไม่มีเครื่องมือไหนแทนที่ tracing ทั้งคู่เริ่มจาก trace หรือ span แล้วลงลึกไปอีกระดับกว่าที่ “service ไหน” จะพาไปถึงได้
flowchart LR Trace[Slow or failing trace] --> Where[Tracing: which service, which span] Where --> Profiler[Continuous Profiler: which function, CPU/wall-time/allocations] Where --> ErrorTracking[Error Tracking: deduplicated issue, new/triaged/resolved] Profiler --> WhySlow[Why it is slow] ErrorTracking --> WhyFailing[Why it is failing]
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”🐛 หา bug ในโค้ดนี้
# AI comment: "Enable profiling and route it through the tracer's own config"from ddtrace import config, tracer
config.profiling = True
def handle_request(): with tracer.trace("checkout.handle_request"): ...เฉลย
ดูเหมือนเป็นการต่อยอด pattern เดียวกับที่ใช้ตั้งค่า tracer ทั่วทั้งคอร์สนี้ — ตั้ง attribute บน ddtrace.config จบ มันโยน AttributeError จริงทันทีที่ module นี้ถูก import พิสูจน์ด้านบน: config.profiling ไม่มีอยู่ใน ddtrace==4.15.2 ที่ pin ไว้ profiling ใน client ตัวนี้ไม่ใช่ attribute แบบ config.<x> เลย มันคือ object Profiler แยกต่างหาก (from ddtrace.profiling import Profiler; Profiler().start()) ที่เริ่มแยกจาก tracer ตั้งค่าผ่าน env var ของ module ตัวเอง (DD_PROFILING_ENABLED, DD_PROFILING_OUTPUT_PPROF และเพื่อน ๆ) ไม่ใช่ผ่าน ddtrace.config
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”tracing กับ profiling ต่าง sample process เดียวกันที่กำลังรันอยู่ แต่คนละความละเอียดและคนละตารางเวลา span คือการวัดแบบตั้งใจ — มีอะไรบางอย่างในโค้ดพูดตรง ๆ ว่า “จับเวลา operation นี้ ติด tag ชื่อนี้” ส่วน profiler sample เป็นแบบไม่ตั้งใจ — พิสูจน์ด้านบน มันเดิน call stack จริงของ while loop ธรรมดาที่ไม่เคยสร้าง span แม้แต่ตัวเดียว แล้วยังจับ busy ได้ตามชื่อ นี่คือเหตุผลที่สองอย่างนี้เข้ากันได้ดีมาก tracing บอกคุณว่าเมื่อไหร่ที่มีอะไรช้า (span ที่มี duration ยาว) ส่วน profiler — sample ต่อเนื่องไม่ว่าจะมีอะไรเรียก tracer.trace() หรือไม่ — มีโอกาสสูงมากที่จะจับสิ่งที่ CPU กำลังทำอยู่ในช่วงเวลานั้นไว้แล้ว ไม่ว่าโค้ดข้างในจะคิดจะวัดตัวเองหรือเปล่าก็ตาม
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”- Agent exporter เทียบกับ file exporter — พิสูจน์ด้านบน file exporter ไม่ต้องมี infrastructure เลยและเหมาะกับการ investigate ในเครื่อง หรืออย่างที่บทนี้ใช้ เพื่อพิสูจน์ output ของ profiler เองโดยไม่ต้องมี account ส่วน Agent exporter คือสิ่งที่ deployment จริงใช้ batch และส่ง profile ต่อเนื่องให้โผล่คู่กับ trace ใน UI
- Always-on profiling เทียบกับ on-demand profiling — always-on (ท่าเริ่มต้นของ profiler) แปลว่าข้อมูลมีอยู่แล้วตั้งแต่ก่อนคุณต้องการ แลกกับ overhead พื้นหลังต่ำ ๆ ตลอดเวลา ส่วน profiler ที่เปิดแค่ตอน incident ไม่มีต้นทุนตอน steady-state เลย แต่บอกคุณไม่ได้ว่าเกิดอะไรขึ้นก่อนที่คุณจะสังเกตเห็นปัญหา
- การ group อัตโนมัติของ Error Tracking เทียบกับ triage ด้วยมือ — dedup 482 occurrence ให้เหลือ issue เดียว (จากตัวอย่าง JSON ด้านบน) ประหยัดความสนใจได้จริง แต่ logic การ group บางครั้งอาจรวม error ที่จริง ๆ แยกกัน หรือแยก error ที่จริง ๆ root cause เดียวกันแค่ stack ต่างกันนิดหน่อย — ควรเช็ค
example_trace_idsของ issue เป็นครั้งคราวแทนที่จะเชื่อตัวเลขเฉย ๆ
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “Continuous Profiler ต้องมี live Agent connection ถึงจะพิสูจน์ได้ว่าทำงาน” ไม่ใช่ — พิสูจน์ด้านบน:
DD_PROFILING_EXPORTERS=file(Node) และDD_PROFILING_OUTPUT_PPROF(Python) ทั้งคู่เขียนไฟล์.pprofจริงที่ inspect ได้ลงดิสก์ในเครื่องตรง ๆ ไม่มี Agent หรือ account เข้ามาเกี่ยวข้องเลย - “
ddtrace.config.profilingคือวิธีเช็คหรือตั้ง profiling state ใน Python” ไม่ใช่ — พิสูจน์ด้านบนด้วยAttributeErrorจริง: profiling อยู่ใน objectddtrace.profiling.Profilerของตัวเองและ settings module ของตัวเอง ไม่ได้อยู่บน namespaceddtrace.configที่ใช้ร่วมกัน - “profiler sample มีอยู่แค่กับโค้ดที่ห่อด้วย span เท่านั้น” ไม่ใช่ — พิสูจน์ด้านบน:
whileloop ธรรมดาที่ไม่มี tracer call เลยยังโผล่ตามชื่อฟังก์ชัน (busy) ใน wall-time profile จริงที่จับได้