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

Agent-Collected Metrics และ Checks

metric ทุกตัวที่ขึ้นบน host ใน Datadog มาจากสองทาง คือ core check ของ Agent ที่เก็บอัตโนมัติ หรือ integration (หรือ custom) check ที่เรียก check API method ชุดหนึ่งตาม collection interval คงที่ บทนี้เขียน, mount และรัน custom Python 3 check จริงข้างใน Agent container จริง เพื่อพิสูจน์ว่าแต่ละ method ทำอะไรและเมื่อไหร่จริง ๆ

ทันทีที่ Agent start จะรัน core check กลุ่มหนึ่งโดยไม่ต้อง config อะไรเลย เช่น cpu, memory, disk, network, io และอื่น ๆ อีกไม่กี่ตัว สิ่งเหล่านี้คือที่มาของ metric ที่เห็นบน host dashboard ทันทีโดยไม่ต้องติดตั้งอะไรเพิ่ม เช่น system.cpu.user, system.cpu.system, system.mem.used, system.mem.free, system.disk.used, system.net.bytes_rcvd เป็นต้น นี่คือเหตุผลที่ Agent ที่เพิ่งติดตั้งใหม่มี data ไหลเข้ามาแล้วก่อนที่จะไป config integration ตัวไหนเลยด้วยซ้ำ — ภาพรวมระดับ host มีให้อยู่แล้วเสมอ

Integration tile เทียบกับ core check — ความต่างเชิง structure จริง ไม่ใช่แค่เรื่อง UI

หัวข้อที่มีชื่อว่า “Integration tile เทียบกับ core check — ความต่างเชิง structure จริง ไม่ใช่แค่เรื่อง UI”

core check ถูก compile เข้าไปใน Go binary ของ Agent ตรง ๆ — ไม่ใช่ Python package และไม่มีทางที่จะ “ติดตั้ง” หรือ “ถอด” cpu/memory/disk แยกจากการ upgrade ตัว Agent เอง Integration (Postgres, Redis, NGINX, Kafka และอีกหลายร้อยตัว — “tile” ที่เห็นใน Datadog app marketplace) ต่างออกไป — มันคือ Python wheel ที่ version แยกกัน bundle มากับ image ของ Agent แต่จัดการแยกได้ด้วยคำสั่ง agent integration จริง:

$ docker exec dd-fnd agent integration freeze
datadog-activemq==5.3.0
datadog-aerospike==5.5.0
...
datadog-nginx==10.0.0
datadog-postgres==23.11.0
datadog-redisdb==8.9.0

ทุกบรรทัด datadog-<name>==<version> คือ integration package จริงที่ version แยกกันเป็นเอกเทศ (agent integration install <name>==<version> upgrade แค่ integration ตัวนั้นตัวเดียว โดยไม่แตะ Agent binary หรือ integration อื่นเลย) — ยืนยันว่า datadog-redisdb คือชื่อ package จริงของ Redis integration (ตรงกับข้อค้นพบเรื่องชื่อ check redisdb จากบท Autodiscovery ไม่ใช่เรื่องบังเอิญ — integration เดียวกัน ชื่อเดียวกัน แค่พิสูจน์คนละมุม) check จะ active ก็ต่อเมื่อมี config file ให้ check นั้น (ปกติอยู่ที่ conf.d/<integration>.d/conf.yaml) แล้ว Agent detect เจอ config นั้นในรอบ check ถัดไป — ไม่มี auto-discovery ว่า “มี postgres รันอยู่ตรงนี้” ถ้าไม่มี config นั้น

นอกจาก integration ที่มาพร้อม Agent แล้ว checks.d/ คือที่ที่ custom check ของคุณเองไป — ไฟล์ Python 3 ที่ Agent load ตรง ๆ คู่กับ conf.d/<name>.d/conf.yaml แบบเดียวกับที่ integration ถูก config ตัวอย่างจริง รันได้จริง ครบทุกส่วน — metric type ห้าตัว, init_config และ min_collection_interval:

app/my_check.py
from datadog_checks.base import AgentCheck
class MyCheck(AgentCheck):
__NAMESPACE__ = 'myservice'
def check(self, instance):
threshold = self.instance.get('threshold', 100)
label = self.init_config.get('label', 'default')
self.gauge('connections.active', 42, tags=[f'label:{label}'])
self.count('requests.total', 137, tags=[f'label:{label}'])
self.monotonic_count('requests.lifetime_total', 90210, tags=[f'label:{label}'])
self.rate('errors.rate', 3, tags=[f'label:{label}'])
self.histogram('query.duration', 0.045, tags=[f'label:{label}'])
if threshold < 50:
self.service_check('threshold_low', AgentCheck.WARNING)
else:
self.service_check('threshold_low', AgentCheck.OK)
conf.d/my_check.d/conf.yaml
init_config:
label: orders
instances:
- threshold: 42
min_collection_interval: 30

mount เข้า datadog/agent:7 container จริง (checks.d/my_check.py กับ conf.d/my_check.d/conf.yaml ทั้งคู่ bind-mount แบบ read-only) แล้วรันด้วยคำสั่งเดียวกัน agent check <name> จากบทที่แล้ว:

$ docker exec dd-mycheck agent check my_check
...
"metric": "myservice.connections.active", "type": "gauge", ...
"metric": "myservice.requests.total", "type": "count", ...
"metric": "myservice.query.duration.max", "type": "gauge", ...
"metric": "myservice.query.duration.median", "type": "gauge", ...
"metric": "myservice.query.duration.avg", "type": "gauge", ...
"metric": "myservice.query.duration.count", "type": "rate", ...
"metric": "myservice.query.duration.95percentile", "type": "gauge", ...
=== Service Checks ===
[{ "check": "myservice.myservice.threshold_low", "status": 1, ... }]
my_check (unversioned)
----------------------
Total Runs: 1
Metric Samples: Last Run: 5, Total: 5
Check has run only once, if some metrics are missing you can try again
with --check-rate to see any other metric if available.

output จริงนี้เผยสองเรื่องที่ควรหยุดดูให้ดี

histogram ระเบิดเป็น derived metric ห้าตัวจริง พิสูจน์ที่นี่อีกครั้ง

หัวข้อที่มีชื่อว่า “histogram ระเบิดเป็น derived metric ห้าตัวจริง พิสูจน์ที่นี่อีกครั้ง”

self.histogram('query.duration', 0.045, ...) — เรียกครั้งเดียว — ให้ metric จริงห้าตัว: .max, .median, .avg, .count (เก็บเป็น rate) และ .95percentile พฤติกรรม client-side aggregation แบบเดียวกับที่ document ไว้ในบท DogStatsD ยืนยันที่นี่สำหรับ check API โดยเฉพาะ ด้วย check จริงแทนที่จะเป็น datagram

rate กับ monotonic_count ต้องการรันรอบที่สอง — พิสูจน์ด้วยการรันซ้ำผ่าน --check-rate

หัวข้อที่มีชื่อว่า “rate กับ monotonic_count ต้องการรันรอบที่สอง — พิสูจน์ด้วยการรันซ้ำผ่าน --check-rate”

สังเกตว่า myservice.errors.rate กับ myservice.requests.lifetime_total หายไป จาก output ของรอบแรกด้านบน — ไม่ใช่ error แต่เป็นผลจริงของสิ่งที่ method สองตัวนี้คำนวณ: rate ต้องการ sample สองตัวที่มี timestamp ถึงจะคำนวณ delta ตามเวลาที่ผ่านไปได้ และ monotonic_count ต้องการค่า cumulative ก่อนหน้าถึงจะได้ count-since-last-run คำใบ้ของ Agent เองที่ท้าย output — “Check has run only once, if some metrics are missing you can try again with —check-rate” — บอกตรงนี้เป๊ะ ๆ รันซ้ำด้วย flag นั้น (ซึ่งรัน check สองครั้งติดกัน) ทำให้ทั้งสองปรากฏในรอบที่สอง:

$ docker exec dd-mycheck agent check my_check --check-rate
[... รอบแรก: metric 7 ตัวเดียวกับด้านบน ยังไม่มี rate/monotonic_count ...]
[... รอบที่สอง ...]
"metric": "myservice.connections.active", ...
"metric": "myservice.requests.total", ...
"metric": "myservice.requests.lifetime_total", "type": "count", ...
"metric": "myservice.errors.rate", "type": "gauge", ...
[... derived metric 5 ตัวของ histogram อีกครั้ง ...]

gauge, count และ histogram ทั้งหมดส่งอะไรบางอย่างในรอบแรก; rate กับ monotonic_count ทำแบบนั้นไม่ได้เชิง structure และการรัน agent check ครั้งเดียวจริง (ไม่มี --check-rate) จะทำให้ดูเหมือนสอง method นี้พังเงียบ ๆ ทั้งที่จริง ๆ มันทำงานตามที่ออกแบบไว้เป๊ะ

  • gauge — รายงานค่า ณ ขณะนั้น เหมือนเข็มวัดน้ำมัน สนใจแค่ค่าล่าสุดใน interval นั้น ส่งตั้งแต่รอบแรก
  • count — รายงานจำนวนดิบที่เกิดขึ้นระหว่างรอบ check นั้น Agent ไม่หารด้วยเวลาให้ ส่งตั้งแต่รอบแรก
  • monotonic_count — คล้าย count แต่สำหรับค่าที่ระบบต้นทางเพิ่มขึ้นเรื่อย ๆ เท่านั้น (counter สะสม เช่น จำนวน request รวมตั้งแต่ process เริ่ม) Agent คำนวณ delta ตั้งแต่รอบก่อนให้เอง พิสูจน์ด้านบน: ต้องการรอบที่สองถึงจะส่งอะไรออกมา
  • rate — คล้าย count แต่ Agent หารด้วยเวลาที่ผ่านไประหว่างรอบ run ให้ metric มาถึง Datadog แบบ normalize เป็น per-second แล้ว พิสูจน์ด้านบน: ต้องการรอบที่สองถึงจะส่งอะไรออกมา
  • histogram — รายงาน distribution ของค่าที่เห็นระหว่างรอบ check หนึ่งครั้ง ซึ่ง Agent จะ aggregate แบบ client-side ออกมาเป็น derived metric หลายตัว (พิสูจน์ด้านบน: .avg, .max, .median, .95percentile, .count) — พฤติกรรม aggregate แบบ client-side นี้จะเจอซ้ำอีกครั้งใน DogStatsD histogram ที่บทถัดไป

configcheck error จริง: instances: เป็น dict อีกครั้ง — กฎเดียวกัน แค่ config ของ check นี้เอง

หัวข้อที่มีชื่อว่า “configcheck error จริง: instances: เป็น dict อีกครั้ง — กฎเดียวกัน แค่ config ของ check นี้เอง”

ความผิดพลาด dict-เทียบ-list จากบทที่แล้วไม่ได้เฉพาะเจาะจงกับ integration ที่มาพร้อม Agent เท่านั้น — มันทำให้ config ของ custom check พังแบบเดียวกันเป๊ะ ด้วย error class เดียวกันจริง:

# @expect-error conf.d/my_check.d/conf.yaml -- instances: is a mapping, not a []map
init_config:
label: orders
instances:
threshold: 42
min_collection_interval: 30
$ docker exec dd-mycheck-broken agent configcheck
=== Configuration errors ===
my_check: yaml: unmarshal errors:
line 5: cannot unmarshal !!map into []integration.RawMap

init_config: — มีให้เห็นบ่อย แต่ไม่ได้ required จริงตาม schema

หัวข้อที่มีชื่อว่า “init_config: — มีให้เห็นบ่อย แต่ไม่ได้ required จริงตาม schema”

ควรแก้ตรงนี้แทนที่จะพูดตามสมมติฐานทั่วไป: ต่างจาก instances: (พิสูจน์ด้านบนว่า error หนักเมื่อรูปแบบผิด) Agent จริงยอมให้ init_config: หายไปทั้งหมด จาก YAML ได้ — self.init_config จะ resolve เป็น dict เปล่า และ .get('label', 'default') fallback ได้เรียบร้อย ทดสอบจริงกับ container ของบทนี้เองที่ลบ key init_config: ออกทั้งหมด (agent configcheck โชว์ error ศูนย์ตัว agent check my_check ยังรันได้และส่ง 5 metric ตามปกติ) คำแนะนำเชิงปฏิบัติยังคงเดิม — เขียน init_config: ชัดเจนเสมอสำหรับอะไรก็ตามที่ check ของคุณพึ่งพาจริง และเข้าถึงมันแบบระวัง ด้วย .get(...) ไม่ใช่ self.init_config['key'] เพราะไม่มีอะไรใน schema รับประกันว่า key (หรือทั้ง block) จะมีอยู่

conf.yaml ของบทนี้ตั้งค่า min_collection_interval: 30 (เป็นสองเท่าของ default ประมาณ 15 วินาทีจาก section built-in checks) agent status จริง เช็คหลังจาก container start ไปสองสามนาที (ระหว่างนั้นบทนี้กำลังทำงานอย่างอื่นอยู่ ไม่ได้ trigger check มือซ้ำ):

$ docker exec dd-mycheck agent status
my_check (unversioned)
----------------------
Total Runs: 3
Metric Samples: Last Run: 5, Total: 15

รัน scheduled สามครั้ง (metric sample รวม 15 ÷ 5 ต่อรอบ = 3 รอบ) ในช่วงประมาณหนึ่งนาทีครึ่งของ container uptime สอดคล้องกับ schedule ~30 วินาที ไม่ใช่ default ~15 วินาที — ตัว scheduler ของ Agent เอง ไม่ใช่แค่ config file เท่านั้น ที่ honor ค่า override นี้จริง

flowchart LR
  A[Agent process] -->|runs every ~15s default| B[Core checks: cpu, memory, disk, network]
  A -->|runs every min_collection_interval| C[Integration / custom checks: postgres, redis, my_check, ...]
  B -->|self.gauge / self.rate| D[system.* metrics]
  C -->|gauge / count / monotonic_count / rate / histogram| E[integration.* / custom metrics]
  D --> F[Datadog backend]
  E --> F
metric ของ host มาจากไหน

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

from datadog_checks.base import AgentCheck
class MyCheck(AgentCheck):
__NAMESPACE__ = 'myservice'
def check(self, instance):
threshold = self.instance.get('threshold', 100)
if threshold < 50:
self.service_check('myservice.threshold_low', AgentCheck.WARNING)
else:
self.service_check('myservice.threshold_low', AgentCheck.OK)
เฉลย

ดูรอบคอบดี — __NAMESPACE__ คือ class attribute จริงที่ document ไว้ของ AgentCheck ที่ auto-prefix ทุกชื่อ metric/service-check และ 'myservice.threshold_low' ดูเป็นชื่อ service check ที่สมเหตุสมผล อธิบายตัวเองได้ bug ที่พิสูจน์ด้วย output จริงของ container ในบทนี้ด้านบน: __NAMESPACE__ = 'myservice' เพิ่ม myservice. นำหน้าทุกชื่อที่ส่งเข้า self.service_check(...) (และ self.gauge/self.count/ฯลฯ) อัตโนมัติอยู่แล้ว — การเติม prefix มือซ้ำอีกทีทำให้เกิดชื่อ service check ที่ถูก double จริง myservice.myservice.threshold_low เห็นตรงตัวใน output agent check ของบทนี้เองด้านบน มันไม่ error แค่สร้างชื่อ check ที่ไม่มีใครคิดจะค้นหาแบบเงียบ ๆ วิธีแก้: พอตั้ง __NAMESPACE__ แล้ว ส่งชื่อเปล่า ๆ ทุกที่ (self.service_check('threshold_low', ...)) แล้วปล่อยให้ class attribute จัดการ prefix เอง

ลองนึกภาพ scheduler ของ Agent เป็น พื้นโรงงานที่มีสถานีสองแบบ — สถานี core check ถูกเชื่อมติดกับตัวอาคารเลย (Go binary) คุณเปลี่ยนตัวใดตัวหนึ่งไม่ได้โดยไม่ปรับปรุงอาคารทั้งหลัง (upgrade Agent) สถานี integration กับ custom check เป็นโต๊ะทำงานแบบ modular ที่ยึดกับพื้น (Python package, checks.d/) แต่ละตัวสลับและ version แยกกันได้ (พิสูจน์ด้านบนผ่าน agent integration freeze) ทุกสถานีรายงานตามนาฬิกาของตัวเอง (min_collection_interval พิสูจน์ด้านบนผ่านจำนวนรอบจริง) และสองในห้ารูปแบบการรายงาน — rate กับ monotonic_count — คือคนงานที่บอกได้แค่ “เปลี่ยนไปเท่าไหร่ตั้งแต่ฉันดูครั้งล่าสุด” ซึ่งหมายความว่าครั้งแรกที่ถูกถาม พวกเขาไม่มีอะไรจะเทียบ เลยไม่พูดอะไรเลยอย่างถูกต้อง

  • core check เทียบกับเขียน custom check สำหรับข้อมูลเดียวกัน — core check (cpu/memory/disk) ไม่ต้อง config เลยและมากับทุก Agent แต่คุณเพิ่มเองไม่ได้; custom check ให้ควบคุมเต็มที่ว่าจะวัดอะไร แลกกับการเขียนและดูแลโค้ด Python จริงที่รันบนทุก host
  • count/gauge/histogram เทียบกับ rate/monotonic_count — สามตัวแรกคิดตามง่ายกว่า (สิ่งที่ส่งใกล้เคียงกับสิ่งที่เห็น) และทำงานตั้งแต่รอบแรก; rate/monotonic_count ทำคณิตศาสตร์ที่มีประโยชน์ให้ (normalize per-second, delta-จาก-cumulative) แต่พิสูจน์ด้านบนว่าให้อะไรที่มีความหมายไม่ได้จนกว่าจะมี data point ที่สอง
  • HISTOGRAM ผ่าน check API เทียบกับ DISTRIBUTION ผ่าน DogStatsD (บทถัดไป) — histogram ของ check API aggregate แบบ client-side ต่อ host เดียวกับ trade-off ที่บท DogStatsD ครอบสำหรับ HISTOGRAM type ของมันเอง; check ไม่มีอะไรเทียบเท่า DISTRIBUTION ของ DogStatsD ที่ aggregate ฝั่ง server — ถ้าต้องการ percentile ข้าม host จริงจาก check data ต้องส่งผ่าน DogStatsD จากข้างใน check แทนที่จะใช้ self.histogram
  • “core check กับ integration check เป็นของแบบเดียวกัน แค่บางตัวติดตั้งมาให้แล้ว” ผิด — พิสูจน์ด้านบนผ่าน agent integration freeze: integration คือ Python package จริงที่ version แยกกันเป็นเอกเทศ (datadog-postgres==23.11.0); core check (cpu, memory, disk) compile เข้าไปใน Go binary ของ Agent ไม่มี version string เทียบเท่าหรือทาง install/remove แยกเลย
  • “ถ้า rate/monotonic_count ไม่ปรากฏใน output ของ agent check แปลว่า check พัง” ผิด — พิสูจน์ด้านบน: นี่คือ behavior ที่คาดไว้เชิง structure ในรอบแรกของ check ที่ใช้สอง method นี้; --check-rate (หรือแค่รอ scheduled run รอบที่สองของ Agent เอง) คือสิ่งที่ทำให้มันปรากฏ
  • “init_config: เป็น key ที่ required เหมือน instances:” ไม่เชิง — พิสูจน์ด้านบน: Agent จริงยอมให้ init_config: หายไปทั้งหมดโดยไม่มี configcheck error เลย; instances: ที่รูปแบบผิดเป็น dict แทนที่จะเป็น list ตรงกันข้าม คือ configcheck error จริง หนักทุกครั้ง
  • “การตั้ง __NAMESPACE__ เป็นแค่ documentation ควรเขียนชื่อ metric เต็มเองเพื่อความชัดเจน” ผิด — พิสูจน์ด้านบนด้วยชื่อ service check ที่ double จริงใน output ของบทนี้เอง: __NAMESPACE__ เติม prefix ให้ทุกชื่ออัตโนมัติจริง ดังนั้นการเขียน prefix ซ้ำทำให้เกิด bug จริง ไม่ใช่แค่ความชัดเจนที่ซ้ำซ้อน
ควรใช้ metric type แบบไหนใน check เพื่อรายงานจำนวน active connection ปัจจุบัน
พิสูจน์ด้านบนผ่าน docker exec agent integration freeze จริง ความต่างเชิง structure ระหว่าง core check (cpu, memory, disk) กับ integration check (postgres, redis, nginx) คืออะไร
พิสูจน์ด้านบนด้วยการรัน agent check my_check ครั้งเดียวจริง (ไม่มี --check-rate) จาก check ที่ส่ง gauge, count, monotonic_count, rate และ histogram metric type ไหนที่ปรากฏจริงใน output
พิสูจน์ด้านบนด้วยการลบ key init_config: ออกทั้งหมดจาก conf.yaml จริง เกิดอะไรขึ้น
พิสูจน์ด้านบนด้วยชื่อ service check ที่ double จริง (myservice.myservice.threshold_low) ใน output จริงของ agent check ในบทนี้เอง อะไรเป็นสาเหตุ
collection interval default ของ Agent check คือเท่าไร และ override ยังไง