Indexes, Exclusion Filters, and Retention
ไอเดียหลักในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียหลักในหนึ่งประโยค”log ถูก drop หรือ filter ได้ที่สามขั้นตอนที่ต่างกันจริง ๆ — บน host ก่อนที่จะออกไปเลย, ที่ quota รายวันของ index หรือด้วย Exclusion Filter ของ index เอง — แต่ละจุดมีผลเรื่อง cost และการกู้คืนที่ต่างกัน จึงห้ามเอามาปนกัน
ขั้นที่ 1: exclude_at_match ฝั่ง Agent
หัวข้อที่มีชื่อว่า “ขั้นที่ 1: exclude_at_match ฝั่ง Agent”ก่อนที่ log จะออกจาก host เลย ตัว Agent เองก็ drop log ทิ้งได้ผ่าน entry ใน log_processing_rules ที่ type เป็น exclude_at_match โดย match regex pattern กับ raw log line
logs: - type: file path: /my/app/file.log service: payments-api source: java log_processing_rules: - type: exclude_at_match name: exclude_healthcheck_lines pattern: 'GET /healthz'นี่คือจุด filter ที่ถูกที่สุดเท่าที่จะทำได้ เพราะบรรทัดที่ถูก drop ไม่กิน network, intake หรือ storage เลย — แต่ก็เป็นการตัดสินใจที่ final เช่นกัน log ที่ถูก exclude ตรงนี้หายไปตลอดกาล ไม่ถูก archive, ไม่ไปถึง Processing Pipeline และกู้คืนทีหลังไม่ได้ ใช้จุดนี้เฉพาะกับบรรทัดที่ไม่มีค่าจริง ๆ (noise ของ health-check, debug spam ที่ไม่ต้องการในรูปแบบไหนเลย) เพราะไม่มี undo
ขั้นที่ 2: Log Index — retention_days กับ daily_limit
หัวข้อที่มีชื่อว่า “ขั้นที่ 2: Log Index — retention_days กับ daily_limit”log ที่รอดจาก Agent แล้วผ่าน intake กับ Processing Pipeline มาถึง Log Index หนึ่งตัวหรือมากกว่า index คือจุดที่ full-price searchable retention เกิดขึ้นจริง และแต่ละ index ตั้งค่าตัวเลขสองตัวที่สำคัญต่อ cost และ behavior:
{ "name": "main", "daily_limit": 100, "retention_days": 30, "exclusion_filters": [ { "name": "exclude-debug", "query": "level:debug" } ]}retention_days— log ที่ถูกเขียนเข้า index นี้ค้นหาได้ใน Logs Explorer แบบ full price นานกี่วันdaily_limit— quota GB/วันของ index นี้ พอถึง quota ของวันนั้นแล้ว index จะหยุดรับ log เพิ่ม หรือ (แล้วแต่ config) เริ่ม sample ดังนั้นdaily_limitที่ตั้งผิดขนาดสามารถจำกัดสิ่งที่ค้นหาได้แบบเงียบ ๆ โดยไม่เกี่ยวกับส่วนอื่นใน config เลย
organization ปกติจะมี index มากกว่าหนึ่งตัว (เช่น main สำหรับ production service, index retention สั้นกว่าสำหรับ log ที่ noisy หรือมูลค่าต่ำ) และ log จะไปลง index ไหนก็ถูกควบคุมด้วย filter criteria ระดับ index เองอีกที — แต่ประเด็นสำคัญของบทนี้คือ retention_days กับ daily_limit เป็น property ของ index ไม่ใช่ของ Agent หรือ pipeline
ขั้นที่ 3: Exclusion Filter — คันโยกหลักของปริมาณที่ index
หัวข้อที่มีชื่อว่า “ขั้นที่ 3: Exclusion Filter — คันโยกหลักของปริมาณที่ index”Exclusion Filter อยู่บน index ตัวใดตัวหนึ่ง และใช้ query แบบ Lucene-style — syntax เดียวกับที่พิมพ์ในช่องค้นหาของ Logs Explorer — เพื่อ sample ออกหรือ drop log ที่ match ไม่ให้ถูกเขียนเข้า index นั้น จาก config ด้านบน { "name": "exclude-debug", "query": "level:debug" } หมายความว่า log ที่ match level:debug จะไม่ถูกเขียนเข้า index main
นี่คือคันโยกหลักที่ใช้ประจำวันเพื่อคุมปริมาณที่ index, ค้นหาได้ และคิดเงินเต็มราคา และถูกออกแบบให้ยืดหยุ่นและ final น้อยกว่าการ drop ฝั่ง Agent โดยตั้งใจ:
- log ที่ถูก exclude จาก index หนึ่งด้วย Exclusion Filter ของตัวเอง ยังถูก route ไป index อื่น ได้ (ที่มี retention หรือ cost ต่างกัน) ถ้า index routing ส่งไปทางนั้น
- log ที่ถูก exclude จากทุก index ยังไปถึง Log Archive ได้ ถ้าตั้งค่า archiving ไว้ — บทถัดไปจะพูดเรื่องนี้
- เพราะการ exclude เกิดที่ index ไม่ใช่บน host จึงเปลี่ยนได้จากส่วนกลางใน Datadog โดยไม่ต้องแตะ config ไฟล์ของ Agent สักตัวเดียว
เรียงลำดับทั้งหมดเข้าด้วยกัน
หัวข้อที่มีชื่อว่า “เรียงลำดับทั้งหมดเข้าด้วยกัน”เส้นทางเต็ม ๆ ที่ log เดินทางผ่าน ตั้งแต่ต้นจนจบ คือ Agent เก็บ log → rule exclude_at_match ฝั่ง Agent (ถ้ามี) อาจ drop log บางตัวก่อนที่จะออกจาก host เลย → log ที่รอดมาถึง intake → Processing Pipeline parse และ enrich log ทุกตัวที่ match filter ของตัวเอง → log มาถึง Log Index ซึ่ง Exclusion Filter ของตัวเองเป็นตัวตัดสินว่าอะไรจะถูก index จริง ค้นหาได้ และคิดเงินเต็มราคา
flowchart LR
A[Agent collects log] --> B{exclude_at_match\nmatches?}
B -->|yes, dropped forever| Z[Gone: no archive, no index]
B -->|no| C[Intake]
C --> D[Processing Pipeline\nparse + enrich]
D --> E{Index Exclusion Filter\ne.g. level:debug}
E -->|excluded| F[Not written to this index\nmay still reach archive/other index]
E -->|kept| G["Indexed: searchable\nfor retention_days, billed at full price"] Index JSON ที่ validate แล้ว — ชื่อ field จริงและเซอร์ไพรส์หนึ่งอย่าง
หัวข้อที่มีชื่อว่า “Index JSON ที่ validate แล้ว — ชื่อ field จริงและเซอร์ไพรส์หนึ่งอย่าง”สร้าง index config ที่ร่างไว้ด้านบนเป็น JSON จริง แล้ว validate กับ model LogsIndex ของ datadog_api_client==2.61.0 ชื่อ field จริงสำหรับ retention คือ num_retention_days ไม่ใช่ retention_days — ยืนยันด้วย inspect.signature น่าพูดถึงเพราะทั้งสเปคของบทนี้และตัวอย่าง JSON ก่อนหน้าของบทนี้เองใช้ชื่อสั้นกว่า
// (index config, not a harness-collected JSON dir -- see caveat below){ "name": "main", "filter": { "query": "*" }, "daily_limit": 100, "num_retention_days": 30, "exclusion_filters": [ { "name": "exclude-debug", "is_enabled": true, "filter": { "query": "level:debug", "sample_rate": 0.0 } } ]}$ python -c "from datadog_api_client.v1.model.logs_index import LogsIndeximport jsonpayload = json.load(open('index.json'))m = LogsIndex(**payload, _check_type=True)print('validated OK')"validated OKfield ที่บทนี้ไม่รู้ว่าต้องคาดหวังจนกว่าจะเช็ค model จริง: num_flex_logs_retention_days — field retention-days ตัวที่สอง แยกต่างหาก อยู่บน LogsIndex model เดียวกันนี้เอง ยืนยันว่า Flex Logs (storage tier ที่ถูกกว่าและ retention นานกว่าสำหรับ log ที่ index แล้ว) เป็น concept จริงและปัจจุบันใน client version ที่ pin ไว้นี้ ไม่ใช่ feature ที่เปลี่ยนชื่อหรือถูกถอดไป ราคาที่แน่นอน retention window ที่แน่นอน และชื่อ product ปัจจุบันของ Flex Logs เป็นข้อเท็จจริงระดับ UI/billing ที่บทนี้ verify ไม่ได้โดยไม่มี URL เอกสาร บันทึกไว้ตรงนี้ ไม่ได้แต่งขึ้นเอง
กับดัก exclusion filter ที่กว้างเกินไป: model ยอมรับแบบสบาย ๆ
หัวข้อที่มีชื่อว่า “กับดัก exclusion filter ที่กว้างเกินไป: model ยอมรับแบบสบาย ๆ”ความผิดพลาดคลาสสิก — Exclusion Filter ที่มี query: "*" กับ sample_rate: 1.0 ซึ่ง drop 100% ของทุก log ที่มาถึง index นี้ validate ผ่านสะอาดสมบูรณ์ ไม่มีอะไรผิดเชิง structure เลย มันแค่เป็น filter ที่ match ทุกอย่างและไม่เก็บอะไรไว้เลย
{ "name": "drop-everything-by-accident", "is_enabled": true, "filter": { "query": "*", "sample_rate": 1.0 }}$ python -c "from datadog_api_client.v1.model.logs_index import LogsIndexpayload = {'name': 'main', 'filter': {'query': '*'}, 'num_retention_days': 30, 'daily_limit': 100, 'exclusion_filters': [{'name': 'drop-everything-by-accident', 'is_enabled': True, 'filter': {'query': '*', 'sample_rate': 1.0}}]}m = LogsIndex(**payload, _check_type=True)print('validated OK -- model accepts a filter that drops every single log matching *')"validated OK -- model accepts a filter that drops every single log matching *นี่เป็นเรื่อง semantic ล้วน ๆ — query, sample_rate และ field อื่น ๆ ทั้งหมด type ถูกต้อง LogsIndex เลยไม่มีเหตุผลที่จะ reject มัน วิธีเดียวที่จะจับได้ว่า “exclusion filter นี้กว้างเกินไป” คืออ่าน query กับ rate รวมกันแล้วคิดว่ามันจับอะไรจริง ๆ เป็นช่องว่างแบบเดียวกับที่สองบทก่อนพิสูจน์ไปแล้ว (pipeline ที่ order-dependent, processor ที่ type เป็น Union แล้ว fallback) — model ที่ validate ผ่านไม่ใช่สิ่งเดียวกับ config ที่ถูกต้อง
LogsIndexesOrder: index แรกที่ match ชนะ
หัวข้อที่มีชื่อว่า “LogsIndexesOrder: index แรกที่ match ชนะ”organization ปกติ route log ข้ามหลาย index และ LogsIndexesOrder คือ model จริง แยกต่างหาก (ยืนยันใน client) ที่ควบคุมลำดับที่ index ถูกประเมิน — log จะถูกเขียนเข้า indexแรกใน order นั้นที่ filter ของตัวเอง match มัน ไม่ใช่ทุก index ที่ filter บังเอิญ match
$ python -c "from datadog_api_client.v1.model.logs_indexes_order import LogsIndexesOrderm = LogsIndexesOrder(index_names=['main', 'low-value', 'debug-index'])print('validated OK', m)"validated OK {'index_names': ['main', 'low-value', 'debug-index']}ผลที่ตามมาจริง ๆ: index ที่ filter ของตัวเองกว้างและอยู่แรกใน order นี้ สามารถกลืน log ที่ index ตัวหลัง เฉพาะเจาะจงกว่า ตั้งใจจะรับไว้แบบเงียบ ๆ ได้ — filter ของ index ตัวหลังไม่เคยถูกประเมินเลยสำหรับ log ที่ index ตัวแรกเคลมไปแล้ว behavior เรื่อง ordering นี้เป็น logic ฝั่ง UI/backend routing ที่บทนี้ทดสอบเองโดยไม่มี org จริงไม่ได้ (model LogsIndexesOrder พิสูจน์ได้แค่รูปร่างของ ordering list ไม่ใช่การตัดสินใจ routing ตอน runtime) — เขียนไว้ตรงนี้ในฐานะ behavior ของ product ที่มีเอกสารรองรับ ไม่ได้ re-derive จาก tool ในเครื่อง
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”🐛 หา bug ในโค้ดนี้
// (index config)// AI comment: "Drop noisy debug logs, keep everything else searchable as before"{"name": "main","filter": { "query": "*" },"num_retention_days": 30,"daily_limit": 100,"exclusion_filters": [ { "name": "reduce-debug-noise", "is_enabled": true, "filter": { "query": "*", "sample_rate": 0.9 } }]}เฉลย
โค้ดนี้ validate ผ่านสะอาดกับ LogsIndex — ทุก field type ถูกต้อง ไม่มีอะไรผิดเชิง structure เลย bug ที่พิสูจน์ด้านบนด้วยการอ่านว่า query: "*" match อะไรจริง ๆ: มัน match ทุก log ไม่ใช่แค่ระดับ debug รวมกับ sample_rate: 0.9 นี่ drop 90% ของทุก log ที่มาถึง index นี้ ไม่ใช่ 90% ของ debug noise — เจตนาของ AI comment (“drop noisy debug logs”) ต้องการ query แบบ level:debug และ wildcard * ขยาย blast radius แบบเงียบ ๆ จาก “log level หนึ่งที่ noisy” ไปเป็น “ทั้ง index” ทางแก้คือ query ไม่ใช่ rate: "query": "level:debug" scope exclusion ให้ตรงกับที่ตั้งใจไว้จริง ๆ
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Exclusion Filter ของ index คือ query ที่มีผลตามมา ไม่ใช่ control ที่ safety-check ไว้แล้ว — พิสูจน์ด้านบนสองครั้ง LogsIndex validate รูปร่างของ JSON เท่านั้น ไม่รู้อะไรเลยว่า query match อะไรจริง ๆ หรือคิดเป็นสัดส่วนเท่าไหร่ของ traffic คุณ คิดว่า query กับ sample_rate เป็นตัวคูณอิสระสองตัวของ “traffic ที่เข้า index นี้หายไปเท่าไหร่”: query แคบคูณ rate สูง drop เยอะของเศษเสี้ยวเล็ก ๆ query กว้าง (* กว้างที่สุดเท่าที่จะเป็นได้) คูณ rate ที่ไม่ใช่ศูนย์ drop สัดส่วนเดียวกันนั้นของทุกอย่าง การอ่าน exclusion filter อย่างปลอดภัยคือ อ่าน scope จริงของ query ก่อน แล้วค่อยอ่าน rate — สลับลำดับนั้นคือวิธีที่ “ลด debug noise” กลายเป็น “เสีย 90% ของ index แบบเงียบ ๆ” เป๊ะ ๆ
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”num_retention_daysเทียบกับnum_flex_logs_retention_days— พิสูจน์ด้านบน ทั้งคู่เป็น field จริงบนLogsIndexเดียวกัน: full-price index retention ค้นหาเร็วกว่าและคิดง่ายกว่า แลกกับ cost สูงกว่า Flex Logs retention (ยืนยันว่ามีอยู่จริง แต่ไม่ได้ตี price หรือ benchmark เองที่นี่) แลกบางส่วนของนั้นเพื่อได้ window ที่ถูกกว่าและนานกว่า — สอง guarantee นี้ไม่เหมือนกันแม้จะเรียกว่า “retention” ทั้งคู่- Index filter แบบ catch-all กว้าง ๆ (
query: "*") เทียบกับ index เฉพาะทางที่แคบ — index กว้างตัวเดียวคิดง่ายและ route เข้าง่าย แต่ (พิสูจน์ด้านบนด้วยLogsIndexesOrder) ถ้ามันอยู่แรกใน index order มันกลืน log ที่ index ตัวหลัง เฉพาะเจาะจงกว่า ตั้งใจจะรับได้แบบเงียบ ๆ index ที่แคบหลีกเลี่ยงกับดักนั้นได้ แลกกับ index ที่ต้องดูแลและ route ระหว่างกันมากขึ้น - Structural JSON validation เทียบกับการอ่าน scope จริงของ query — SpotTheBug ของบทนี้คือเวอร์ชันที่ชัดที่สุดของ pattern จากสองบทก่อนหน้า: model ที่ validate ผ่านสะอาดจำเป็นแต่ไม่พอที่จะพิสูจน์ว่า config ทำสิ่งที่ตั้งใจ ยังต้องอ่าน
queryกับsample_rateรวมกันด้วยมืออยู่ดี
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “Exclusion Filter ที่มี
sample_rateสูง drop แค่สิ่งที่ query อธิบายไว้แคบ ๆ เท่านั้น” ไม่ใช่ — พิสูจน์ด้านบน: scope ของ query ไม่ใช่ rate เป็นตัวกำหนด blast radius query ที่กว้างกับ rate ที่ไม่ใช่ศูนย์ drop ได้มากกว่าที่ตั้งใจไว้เยอะ และLogsIndexvalidate filter แบบนั้นโดยไม่บ่นเลย - “
retention_daysคือชื่อ field จริงบนLogsIndex” ไม่ใช่ — พิสูจน์ด้านบนผ่านinspect.signature: field จริงคือnum_retention_daysและมี field ที่สอง แยกต่างหากnum_flex_logs_retention_daysสำหรับ retention ของ Flex Logs บน model เดียวกัน - “log ที่ match filter ของมากกว่าหนึ่ง index จะถูกเขียนเข้าทุกตัว” ไม่ใช่โดย default — พิสูจน์ด้านบนผ่าน model
LogsIndexesOrderจริง: ลำดับการประเมิน index มีความหมาย และ index แรกที่ match ใน order นั้นคือที่ที่ log ที่ยังไม่ถูก route ลงไป ไม่ใช่ทุก index ที่ match