Log-Based Metrics and Logging without Limits
ไอเดียหลักในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียหลักในหนึ่งประโยค”Logging without Limits คือชื่อที่ Datadog ใช้เรียกการแยก log ingestion ออกจาก log indexing และผลลัพธ์ที่สำคัญที่สุดคือคุณ generate log-based metric หรือส่ง log เข้า Archive ได้จาก ingest stream ทั้งหมด — แม้แต่ log ที่ Exclusion Filter drop ทิ้งจากทุก index ก็ตาม
Log-based metric: วัด log ที่คุณไม่ได้ index
หัวข้อที่มีชื่อว่า “Log-based metric: วัด log ที่คุณไม่ได้ index”log-based metric ถูกคำนวณจาก ingest stream เต็ม ๆ — จุดเดียวกับที่ Exclusion Filter ในบทที่แล้วทำงานอยู่ — ไม่ใช่จากสิ่งที่ index ไปแล้ว นี่คือประเด็นสำคัญทั้งหมด log-based metric ยังทำงานต่อได้แม้กับ log ที่ไม่เคยเข้า index ไหนเลย
log-based metric มีสองรูปแบบที่ต้องรู้:
- metric แบบ COUNT นับว่ามี log กี่ตัวที่ match filter query เช่น
@status:error - metric แบบ DISTRIBUTION aggregate attribute เชิงตัวเลขของ log เช่น
@durationให้ percentile กับ average เหมือน distribution metric ทั่วไป
{ "data": { "id": "logs.page.load.count", "attributes": { "compute": { "aggregation_type": "distribution", "path": "@duration" }, "filter": { "query": "service:web* AND @http.status_code:[200 TO 299]" }, "group_by": [ { "path": "@http.status_code", "tag_name": "status_code" } ] } }}ตัวอย่างนี้นิยาม distribution metric บน attribute @duration scope ไปที่ request ที่สำเร็จของ service web* แล้ว group ตาม status code log-based metric ถูกคำนวณที่ 10-second granularity และเก็บไว้ 15 เดือน — นานกว่า retention_days ของ index ส่วนใหญ่มาก และราคาถูกกว่ามาก เพราะ metric เป็น aggregate เล็ก ๆ ไม่ใช่ log body เต็ม ๆ แหล่งอ้างอิงจาก https://docs.datadoghq.com/logs/log_configuration/logs_to_metrics/ (fetch 2026-09-30): log-based metric ให้คุณ “visualize trends and anomalies over all of your log data at 10s granularity for 15 months” และถูกสร้างจาก ingest stream เต็ม ๆ แม้ exclusion filter จะจำกัดสิ่งที่ถูก index นี่เป็นข้อเท็จจริงจากเอกสาร ไม่ได้รันจริง เพราะคอร์สนี้ไม่มี Datadog account
ผลตอบแทนในทางปฏิบัติคือ คุณตั้ง Exclusion Filter ของ index ให้ drop log level:debug ทิ้งทั้งหมดได้ (ประหยัด cost การ index) แล้วยังเก็บ log-based metric แบบ COUNT ของจำนวน debug log ต่อ service ต่อนาทีไว้ได้กว่าปี คุณเสียความสามารถอ่าน log line แต่ละบรรทัดหลังจาก log พวกนั้น age out หรือถูก exclude ไป แต่ไม่เคยเสีย trend หรือ anomaly signal ไปเลย
Log Archive: stream เต็มในราคาถูก ระยะยาว
หัวข้อที่มีชื่อว่า “Log Archive: stream เต็มในราคาถูก ระยะยาว”Log Archive ส่ง log stream ที่แทบจะเต็มและไม่ถูก exclude ไปยัง storage ที่คุณเป็นเจ้าของเอง — S3, GCS หรือ Azure Blob — ไม่ว่า Exclusion Filter ของ index ไหนจะทำอะไรอยู่ก็ตาม การ archive เกิดขึ้นเป็นอิสระจากการตัดสินใจเรื่อง indexing นั่นคือสิ่งที่ทำให้ archive เหมาะกับ compliance และ retention ระยะยาว log ตัวหนึ่งถูก exclude จากทุก index ได้ (ถูก ไม่ต้องจ่ายค่า search เต็มราคา) และยังไปลง archive ได้ (ถูก, ระยะยาว, ค้นหาไม่ได้ถ้าไม่ทำขั้นตอนเพิ่ม)
archive ค้นหาใน Logs Explorer ตรง ๆ ไม่ได้ แต่ตาม https://docs.datadoghq.com/logs/log_configuration/rehydrating/ (fetch 2026-09-30) มีอีกทางที่ถูกกว่า Rehydration: “Archive Search … streams results in real time directly from your archive without re-indexing, and charges only for the data scanned.” archive เป็นเป็น cold storage ใน cloud account ของคุณเอง คิดราคาตามอัตราของ cloud storage ของคุณเอง นั่นคือเหตุผลตรง ๆ ที่ทำให้ archive คุ้มค่าพอจะเก็บแทบทุกอย่างไว้เป็นปี ๆ ในจุดที่ full index retention ทำไม่ได้
Rehydration: re-index ชิ้นส่วนของอดีตชั่วคราว
หัวข้อที่มีชื่อว่า “Rehydration: re-index ชิ้นส่วนของอดีตชั่วคราว”พอคุณต้องสืบสวนอะไรบางอย่างที่อยู่ใน archive จริง ๆ — เช่น level:debug ที่ระเบิดขึ้นรอบ ๆ incident เมื่อสี่เดือนก่อน — คุณใช้ Rehydration คือเอาชิ้นส่วนของ archive ที่ scope ด้วยช่วงเวลามา re-index ใหม่ ปกติจะเข้า temporary index เพื่อให้ค้นหาได้อีกครั้งใน Logs Explorer นานเท่าที่คุณต้องการ Rehydration ถูกออกแบบให้ scope ด้วยช่วงเวลา (และมักด้วย query ด้วย) แทนที่จะ replay archive ทั้งก้อน เพราะการ re-index คือสิ่งที่เสียเงิน — คุณจ่ายเพื่อทำให้ชิ้นส่วนนั้นค้นหาได้อีกครั้ง ไม่ใช่จ่ายเพื่อเก็บไว้ เพราะนอนอยู่ใน archive อย่างปลอดภัยอยู่แล้ว
flowchart LR
A[Full ingest stream] --> B[Log-based metric\nCOUNT or DISTRIBUTION\n10s granularity, 15 months]
A --> C[Log Archive\nS3 / GCS / Azure Blob]
A --> D{Index Exclusion Filter}
D -->|kept| E[Indexed: searchable,\nfull price, retention_days]
D -->|excluded| F[Not in this index]
C -.->|Rehydration: time-scoped| G[Temporary index\nsearchable again] LogsMetricCreateRequest ที่ validate จริง
หัวข้อที่มีชื่อว่า “LogsMetricCreateRequest ที่ validate จริง”สร้าง distribution metric ที่ร่างไว้ด้านบนเป็น JSON จริง แล้ว validate กับ LogsMetricCreateRequest ของ datadog_api_client==2.61.0 — ชื่อจริงตรงกับสเปคเป๊ะ ๆ
// (log-based metric config, not a harness-collected JSON dir -- see caveat below){ "data": { "id": "logs.page.load.count", "type": "logs_metrics", "attributes": { "compute": { "aggregation_type": "distribution", "path": "@duration", "include_percentiles": true }, "filter": { "query": "service:web* AND @http.status_code:[200 TO 299]" }, "group_by": [ { "path": "@http.status_code", "tag_name": "status_code" } ] } }}$ python -c "from datadog_api_client.v2.model.logs_metric_create_request import LogsMetricCreateRequestimport jsonpayload = json.load(open('metric.json'))LogsMetricCreateRequest(**payload, _check_type=True)print('validated OK')"validated OKinspect.signature ยืนยันว่า aggregation_type มีค่าจริงแค่สองค่า {'count', 'distribution'} และ include_percentiles อยู่บน compute เท่านั้น — รูปร่างเดียวกับที่ SpansMetricCreateRequest ของ APM ในคอร์สนี้ใช้ pattern ที่สอดคล้องกันทั่ว client ไม่ใช่เรื่องบังเอิญ ละ compute ไปทั้งหมดจะ fail ทันทีด้วย error จริงที่เจาะจง
# @expect-error LogsMetricCreateAttributes.__init__() missing 1 required positional argument: 'compute'from datadog_api_client.v2.model.logs_metric_create_request import LogsMetricCreateRequestpayload = {"data": {"type": "logs_metrics", "id": "broken", "attributes": {}}}LogsMetricCreateRequest(**payload, _check_type=True)Archive ที่ validate แล้ว — และ field จริงที่บทนี้ไม่ได้คาดไว้
หัวข้อที่มีชื่อว่า “Archive ที่ validate แล้ว — และ field จริงที่บทนี้ไม่ได้คาดไว้”สร้าง S3 archive config จริง แล้ว validate กับ LogsArchiveCreateRequest
// (log archive config, not a harness-collected JSON dir -- see caveat below){ "data": { "type": "archives", "attributes": { "name": "payments-api archive", "query": "service:payments-api", "destination": { "type": "s3", "bucket": "my-log-archive-bucket", "path": "/payments-api", "integration": { "account_id": "<your-aws-account-id>", "role_name": "<your-role-name>" } }, "rehydration_max_scan_size_in_gb": 100, "rehydration_tags": ["team:payments"] } }}$ python -c "from datadog_api_client.v2.model.logs_archive_create_request import LogsArchiveCreateRequestimport jsonpayload = json.load(open('archive.json'))LogsArchiveCreateRequest(**payload, _check_type=True)print('validated OK')"validated OKfield จริงสองตัวที่บทนี้ไม่รู้ว่าต้องคาดไว้จนกว่าจะเช็ค model: rehydration_max_scan_size_in_gb กับ rehydration_tags อยู่บน attributes ของ archive เองตรง ๆ ไม่ใช่บน model แยกต่างหากเฉพาะ Rehydration นั่นแปลว่า Rehydration behavior บางส่วนถูก pre-configure ไว้ตอนสร้าง archive แล้ว — cap ว่า rehydration job ที่รันกับ archive นี้ scan ข้อมูลได้เยอะแค่ไหน และ tag default ที่ใส่ให้ log ที่ rehydrate — ไม่ใช่สิ่งที่คิดขึ้นใหม่ทุกครั้งที่มีคน rehydrate ชิ้นส่วนหนึ่ง destination ที่พัง (ขาด field ที่จำเป็น bucket) fail ทันทีด้วย error จริงที่เจาะจง
# @expect-error LogsArchiveDestinationS3.__init__() missing 1 required positional argument: 'bucket'from datadog_api_client.v2.model.logs_archive_destination_s3 import LogsArchiveDestinationS3LogsArchiveDestinationS3(type='s3', integration={'account_id': '<your-aws-account-id>', 'role_name': '<your-role-name>'})สิ่งที่ client model บังคับและไม่บังคับ
หัวข้อที่มีชื่อว่า “สิ่งที่ client model บังคับและไม่บังคับ”ลอง probe ด้วย _check_type=True กับ LogsMetricCreateRequest และ LogsArchiveCreateRequest aggregation_type โฆษณาใน allowed_values ว่ามีแค่ {'distribution', 'count'} แต่ constructor ยอมรับ sum และ distribution ที่ไม่มี path ก็ผ่านเช่นกัน Python type ผิดและ argument ที่ขาดจะ fail แต่ค่า enum ผิดและความหมายผิดไม่ fail ตัวตัดสินจริงคือ server ซึ่งคอร์สนี้ไม่มี
$ python probe.py # datadog_api_client 2.61.0, _check_type=True, run on this Macnofilter VALIDbad agg 'sum' VALID <- enum not enforced at constructiondist, no path VALID <- distribution without a path also passess3 type 'ftp' VALID <- destination type not enforced eitherscan size "big" ApiTypeError Invalid type for variable 'rehydration_max_scan_size_in_gb'. Required value type is one of [NoneType, int] and passed type was strno name TypeError LogsArchiveCreateRequestAttributes.__init__() missing 1 required positional argument: 'name'Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”🐛 หา bug ในโค้ดนี้
# AI comment: "Track debug log volume per service without indexing debug logs"from datadog_api_client.v2.model.logs_metric_create_request import LogsMetricCreateRequest
payload = { "data": { "type": "logs_metrics", "id": "logs.debug.count", "attributes": { "compute": {"aggregation_type": "count"}, "group_by": [{"path": "service"}], }, }}LogsMetricCreateRequest(**payload, _check_type=True)เฉลย
โค้ดนี้ validate ผ่านสะอาด — compute มีอยู่พร้อม aggregation_type จริง group_by well-formed bug เป็นเรื่อง semantic รูปแบบเดียวกับกับดัก exclusion-filter ของบทที่แล้ว: ไม่มี block filter เลย metric ตัวนี้เลยนับทุก log ที่มาถึง ingest stream group ตาม service ไม่ใช่แค่ debug log อย่างที่ AI comment ตั้งใจไว้ ทางแก้คือเพิ่ม scope ที่ขาดไป: "filter": {"query": "level:debug"} ไม่มีอะไรตรงนี้ throw เลย เพราะ filter ที่ไม่มีอยู่เป็นค่าที่ valid (แปลว่า “match ทุกอย่าง”) ซึ่งเป็นเหตุผลตรง ๆ ที่ bug ประเภทนี้รอดจาก model validation ไปได้ — พิสูจน์ด้านบน model เช็ครูปร่าง ไม่เช็ค scope
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Logging without Limits ทำงานได้เพราะ ingestion สร้าง stream เดียวที่ consumer อิสระหลายตัวดึงของตัวเองออกมาได้แยกกัน — log-based metric (พิสูจน์ด้านบน LogsMetricCreateRequest ของตัวเอง) กับ Log Archive (พิสูจน์ด้านบน LogsArchiveCreateRequest ของตัวเอง) ไม่ใช่ “view” เข้าไปใน index พวกมันคือ destination แยกต่างหากที่ป้อนจากจุดต้นน้ำเดียวกัน ก่อนที่ Exclusion Filter ของ index ไหนจะได้โหวตเลยด้วยซ้ำ นั่นคือเหตุผลเชิงกลไกทั้งหมดที่การ exclude level:debug จาก index ไม่กระทบทั้งสองอย่างเลย: exclusion filter อยู่ downstream ของจุดที่ stream นี้แยกทาง ไม่ใช่ upstream ของมัน
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”- Log-based metric เทียบกับ Log Archive เทียบกับ index — log-based metric (พิสูจน์ด้านบน ถูก, 10s granularity, 15 เดือน) ให้ aggregate ที่นิยามไว้ล่วงหน้าตลอดไป แลกกับการเสีย log line แต่ละบรรทัดไป archive (พิสูจน์ด้านบน ถูก, cloud storage ของคุณเอง) เก็บทุกอย่างไว้แต่ค้นหาไม่ได้ถ้าไม่ Rehydrate index (full price) ค้นหาได้ทันทีแต่แพงที่สุด และเป็นตัวเดียวที่มี retention ceiling ที่คุณจ่ายตรง ๆ
rehydration_max_scan_size_in_gbที่แน่นเทียบกับหลวม — พิสูจน์ด้านบน นี่คือ field จริงระดับ archive: cap ที่แน่นป้องกัน rehydration job ที่ใหญ่และแพงโดยไม่ตั้งใจ แลกกับการที่การสืบสวนขนาดใหญ่ที่ถูกต้องอาจต้องขอปรับ cap ขึ้นก่อน- นิยาม log-based metric เทียบกับพึ่ง Log Analytics บน Exclusion Filter ที่ผ่อนลง — metric (พิสูจน์ด้านบน) ถูกและถาวร แต่ล็อคอยู่กับ
compute/group_byที่นิยามไว้ล่วงหน้า การผ่อน Exclusion Filter ให้ Log Analytics แบบ ad-hoc เต็มรูปแบบ แลกกับ cost การ index เต็มราคา ตราบใดที่ยังผ่อนอยู่
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “log-based metric ที่ไม่มี
filterนับแค่ log ที่ตั้งใจไว้” ไม่ใช่ — พิสูจน์ด้านบนในตัวอย่าง SpotTheBug: blockfilterที่ไม่มีอยู่เป็นค่า valid แปลว่า “match ทุกอย่าง” เป็น scope bug จริงแบบเงียบ ๆ ที่ผ่าน model validation ได้สะอาด - “การตั้งค่า Rehydration ถูกกำหนดใหม่ทุกครั้งที่ rehydrate” ไม่ใช่ — พิสูจน์ด้านบน:
rehydration_max_scan_size_in_gbกับrehydration_tagsเป็น field จริงบน attributes ของ archive เอง ตั้งไว้ตอนสร้าง archive ไม่ใช่คิดขึ้นใหม่ต่อ rehydration job - “JSON ของ log-based metric กับ archive ได้ automatic model validation เหมือน monitor/dashboard/pipeline” ไม่ใช่ใน harness ของคอร์สนี้เอง — พิสูจน์ด้านบน: ทั้ง
logs-metrics/และlogs-archives/ไม่อยู่ใน list directory ที่ harness เก็บรวบรวม fence ทั้งสองตัวนี้เลยถูก validate ด้วยมือ ไม่ใช่โดยnpm run verify - “
_check_type=Trueจะ rejectaggregation_typeหรือ destination type ของ archive ที่ไม่ถูกต้อง” ไม่ใช่ — พิสูจน์ด้านบน:sumและftpconstruct ได้สะอาด มัน reject แค่ Python type ผิด ("big"แทน int) กับ argument ที่ขาด ไม่เกินนั้น