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

Unified Service Tagging ในทางปฏิบัติ

ตั้ง env, service, และ version ครั้งเดียวบน deployable unit หนึ่งตัว แล้ว triple เดียวกันนั้นแหละที่ทำให้ metrics dashboard, APM service page และ Logs Explorer เห็นตรงกันว่ากำลังดู service ไหนอยู่

เอา deployable unit เดียว checkout-service มาแท็กด้วย DD_ENV=production, DD_SERVICE=checkout-service, และ DD_VERSION=2.4.1 แทนที่จะ hardcode สามค่านี้ไว้หลายที่ pattern Kubernetes label/downward-API เดียวกันจาก Foundations ตั้งค่าครั้งเดียวบน pod แล้ว reflect เข้า environment variable

apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-service
spec:
template:
metadata:
labels:
tags.datadoghq.com/env: "production"
tags.datadoghq.com/service: "checkout-service"
tags.datadoghq.com/version: "2.4.1"
spec:
containers:
- name: checkout-service
image: registry.example.com/checkout-service:2.4.1
env:
- name: DD_ENV
valueFrom:
fieldRef:
fieldPath: metadata.labels['tags.datadoghq.com/env']
- name: DD_SERVICE
valueFrom:
fieldRef:
fieldPath: metadata.labels['tags.datadoghq.com/service']
- name: DD_VERSION
valueFrom:
fieldRef:
fieldPath: metadata.labels['tags.datadoghq.com/version']

label คือ single source of truth downward API reflect มันเข้าไปใน DD_ENV/DD_SERVICE/DD_VERSION ทำให้ environment variable พวกนี้ตรงกับสิ่งที่ Kubernetes — และ autodiscovery ของ Agent — เห็นบน pod อยู่แล้วเสมอ เปลี่ยน label ครั้งเดียว แล้วทุก consumer ที่อยู่ downstream ได้ค่าใหม่ใน rollout ถัดไป

  • Agent’s infrastructure/container metrics — autodiscovery แท็ก container กับ infra metric ตรงจาก label ของ pod ทำให้ metric CPU, memory, request-rate ของ pod checkout-service พก env, service, และ version โดยไม่ต้อง config แยก
  • tracer initialization สำหรับ APM — dd-trace อ่าน DD_ENV, DD_SERVICE, และ DD_VERSION จาก environment อัตโนมัติ ทำให้ span ทุกตัวที่มันสร้างถูกแท็กด้วย triple เดียวกัน
// dd-trace picks up DD_ENV, DD_SERVICE, and DD_VERSION from the environment automatically
require('dd-trace').init({
logInjection: true
});
  • log integration จากบทก่อนหน้า — เพราะ log injection อ่าน active service configuration ตัวเดียวกับที่ tracer ใช้ ฟิลด์ dd.env, dd.service, และ dd.version ที่แปะลงบน log ทุกบรรทัดก็คือ triple ตัวเดียวกันนี้ด้วย

การ pivot ทำงานได้ก็เพราะ สตริง เหมือนกันเป๊ะทั้งสามผลิตภัณฑ์ — และปรากฏว่าไม่มีอะไรใน client library normalize สตริงนั้นให้เราเลย รัน dd-trace ด้วย DD_ENV=Prod (P ตัวใหญ่) แล้ว dump config จริงที่ resolve แล้วของ tracer แทนที่จะสมมติว่ามันถูก lowercase ให้:

$ DD_ENV=Prod DD_SERVICE=checkout-Service DD_VERSION=2.4.1 node -e "
const t = require('dd-trace').init({ startupLogs: false });
console.log('tags:', JSON.stringify(t._tracer._config.tags));
"
tags: {"service":"checkout-Service","env":"Prod","version":"2.4.1", ...}

Prod กับ checkout-Service ออกไปทาง wire ตรงตามที่พิมพ์เป๊ะ — dd-trace ไม่ lowercase หรือ normalize อะไรฝั่ง client เลย เอกสาร tagging ของ Datadog เอง (docs.datadoghq.com/getting_started/tagging/) ระบุตรง ๆ ว่า “tag key ตัวเดียวมีหลายค่าได้ ถ้าถูก config ต่างกันในแต่ละ source” — platform ไม่ merge env:Prod กับ env:production เข้าเป็นค่าเดียว มันอยู่คู่กันเป็นสองค่าแยกกันใต้ key เดียวกัน ดังนั้นถ้า Kubernetes label ใน manifest ข้างบนเป็น tags.datadoghq.com/env: "Prod" ในขณะที่ service ที่จัดการด้วย Terraform ของเพื่อนร่วมทีมข้าง ๆ ตั้ง DD_ENV=production (lowercase) สอง service นี้ไม่ fail แบบดัง ๆ — พวกมันแค่ไปจบที่ facet env คนละอันเงียบ ๆ dashboard ที่ scope เป็น env:production จะแสดงแค่ service เดียวเงียบ ๆ ไม่แสดงอีกตัว; APM service page สำหรับ env:production ก็ไม่ list service ที่ mismatch เลย นี่คือบั๊กประเภทเดียวกับ typo logInjection จากบทก่อนหน้า: ไม่มี exception มีแต่ filter ที่กีดกันข้อมูลออกไปเงียบ ๆ โดยที่ไม่มีใครตั้งใจ เอกสาร tagging ของ Datadog มีระบุแยกไว้ว่า tag key ถูก normalize เป็น lowercase (“Span tags and metric tags are normalized to lowercase, so avoid using camel case in tag keys”) — แต่กฎนั้นพูดถึง key (env, service) ไม่ใช่ value ซึ่งเป็นช่องว่างที่พิสูจน์ข้างบนนี้เผยออกมาเป๊ะ

unified service tagging เป็นแบบต่อ-deployable-unit; Agent เองก็มี list tags: ของตัวเองที่ครอบทั้ง host ใน datadog.yaml ใช้กับทุก metric/check ที่ Agent เก็บเองบน host นั้น แยกอิสระจาก DD_ENV/DD_SERVICE/DD_VERSION ของ service ตัวไหนตัวหนึ่ง:

datadog.yaml
api_key: "<DD_API_KEY>"
hostname: probe-host
tags:
- team:checkout
- datacenter:local-probe

รัน container datadog/agent:7 จริง (docker run --name dd-corr, DD_API_KEY เป็น fake ศูนย์ล้วน, DD_DD_URL ชี้ไปที่ local port ที่ปิดอยู่เลยไม่มีอะไรถูกส่งออกไปจริง) พร้อม mount ไฟล์นี้เป๊ะ แล้วรัน agent status:

$ docker exec dd-corr agent status
===============
Agent (v7.83.3)
===============
...
Hostname
========
hostname: probe-host
host tags:
datacenter:local-probe
team:checkout

Agent จริง output จริง: entry tags: สองตัวใน datadog.yaml โผล่มาเป็น “host tags” เป๊ะตัวอักษร — ใช้ทั้ง host โดยรวม (ไม่ใช่แทนที่) ไม่ว่า service ไหนบน host นั้นจะตั้ง env/service/version ของตัวเองยังไง

Kubernetes label ที่ใช้ก่อนหน้านี้ในบทนี้มี equivalent สำหรับ Docker Compose/plain Docker ตรง ๆ — container label com.datadoghq.tags.env/com.datadoghq.tags.service/com.datadoghq.tags.version (docs.datadoghq.com/getting_started/tagging/unified_service_tagging/) เริ่ม container ธรรมดาด้วย label พวกนั้นแล้วชี้ Autodiscovery ของ dd-corr Agent ตัวเดิมที่ใช้ docker.sock ไปที่มัน — ไม่มี config file ไม่มี annotation มีแค่ label บน docker run:

$ docker run -d --name labeled-app \
--label com.datadoghq.tags.env=production \
--label com.datadoghq.tags.service=checkout-service \
--label com.datadoghq.tags.version=2.4.1 \
alpine:3 sleep 300
$ docker exec dd-corr agent tagger-list | grep -A2 'container_name:labeled-app'
Tags: [container_id:e0f42036... container_name:labeled-app docker_image:alpine:3
env:production ... service:checkout-service ... version:2.4.1 ...]

agent tagger-list (command จริงที่ dump ทุกอย่างที่ tagger ของ Agent resolve ไว้สำหรับทุก entity ที่มันเห็น) แสดง env:production, service:checkout-service, และ version:2.4.1 แปะบน container ล้วน ๆ จาก Docker label ของมัน — convention สามคีย์เดียวกับ Kubernetes label แปลงสำหรับ deployment แบบ plain-Docker หรือ Compose ที่ไม่มี Kubernetes downward API มา reflect label เข้า env var

unified service tagging ตั้งค่า default ให้ทั้งโปรเซส แต่ span เดียวยังประกาศ service ต่างออกไปได้ — มีประโยชน์กับ span ของ database client ที่ควรจะ roll up ใต้ identity ของ database เองแทนที่จะเป็นของแอปที่เรียก:

$ DD_SERVICE=checkout-service DD_ENV=production node -e "
const tracer = require('dd-trace').init({ startupLogs: false });
tracer.trace('pg.query', { service: 'checkout-postgres' }, (span) => {
console.log(JSON.stringify(span.context()._tags));
});
"
{"service.name":"checkout-postgres","service":"checkout-service","env":"production", ...}

tag service.name ของ span เองคือ checkout-postgres (override ที่ส่งเข้า option ของ tracer.trace()) ในขณะที่ tag service ของทั้งโปรเซสยังเป็น checkout-service อยู่ข้างใต้ — เป็นข้อยกเว้นเฉพาะจุดของกฎ “ตั้งครั้งเดียว” ข้างบน ไม่ใช่การขัดแย้งกัน: span ส่วนใหญ่ในโปรเซสยัง default ไปที่ identity ทั้งโปรเซสอยู่ มีแค่ span เดียวที่เลือก opt out ถึงจะพก service ต่างออกไป

พอ triple ไหลเข้าทั้งสามผลิตภัณฑ์ การเดินตาม incident ครั้งหนึ่งจะหน้าตาแบบนี้: เริ่มที่ metrics dashboard scope เป็น service:checkout-service env:production เห็น CPU, latency, request rate ไต่สูงขึ้น กระโดดไปที่ APM service page สำหรับ service, env, และ version เดียวกัน เห็น trace latency, error rate, และเพื่อนบ้านของ service นี้บน service map กระโดดอีกครั้งไปที่ Logs Explorer ที่ pre-filter ด้วย triple เดียวกัน เห็น log บรรทัดที่ตรงเป๊ะ — รวมถึง error ที่แท็กด้วย dd.trace_id จากบทก่อนหน้า — สำหรับ version นั้น ใน environment นั้น

เทียบกับสถานะก่อนหน้า: metric แท็ก service:checkout, Java tracer แท็ก service:checkout-svc, และ log แท็ก application:checkout ไม่มีอะไรตรงกันเลย แล้วใครก็ตามที่ debug ต้องจำสามชื่อต่างกันตอนย้ายไปมาระหว่างผลิตภัณฑ์ พอแท็กสม่ำเสมอแล้ว สตริงเหมือนกันทุกที่ UI ของทั้งสามผลิตภัณฑ์เลย pre-filter ไปที่ identity เดียวกันได้ และ saved view ในผลิตภัณฑ์หนึ่งอธิบาย slice ของโลกเดียวกันเป๊ะในอีกผลิตภัณฑ์หนึ่ง

flowchart LR
  T["env=production\nservice=checkout-service\nversion=2.4.1"] --> Agent[Agent autodiscovery]
  T --> Tracer[Tracer init]
  T --> LogInj[Log injection]
  Agent --> M[Metrics dashboard]
  Tracer --> A[APM service page]
  LogInj --> L[Logs Explorer]
  M -.pivot.-> A
  A -.pivot.-> L
  L -.pivot.-> M
tag สามตัวชุดเดียว ใช้ได้สามผลิตภัณฑ์

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

k8s/checkout-service.yaml
metadata:
labels:
tags.datadoghq.com/env: "Production"
tags.datadoghq.com/service: "checkout-service"
tags.datadoghq.com/version: "2.4.1"
---
# k8s/checkout-worker.yaml (AI comment: "same env as checkout-service")
metadata:
labels:
tags.datadoghq.com/env: "production"
tags.datadoghq.com/service: "checkout-worker"
tags.datadoghq.com/version: "2.4.1"
เฉลย

ทั้งสอง manifest ดูสอดคล้องกันแวบแรก และทั้งคู่เป็น YAML ที่ valid สมบูรณ์ — ไม่มีอะไรตรงนี้ throw error หรือทำให้ kubectl apply fail บั๊กที่พิสูจน์แล้วข้างบนด้วย tracer config dump จริง: "Production" กับ "production" ไม่ใช่ tag value เดียวกัน checkout-service กับ checkout-worker จะไปจบที่ facet env คนละอันใน Datadog dashboard, monitor, หรือ SLO ที่ scope เป็น env:production จะรวม checkout-worker เข้าไปเงียบ ๆ และตัด checkout-service ออก — service ที่คนส่วนใหญ่น่าจะตั้งใจรวมเข้าไปมากที่สุด การแก้เป็นเรื่องกลไกล้วน ๆ: เลือก casing convention เดียว (lowercase คือ default ที่ปลอดภัยที่สุด ตรงกับที่ Datadog ปฏิบัติกับ tag key) แล้วใช้ให้เหมือนกันทุกที่ที่ tags.datadoghq.com/env (หรือ DD_ENV) ถูกตั้ง ทุก manifest ทุกไฟล์ Terraform ทุก label ใน Dockerfile ทั้งองค์กร

มองว่า triple env/service/version เป็นเหมือนรหัสผ่านที่แชร์กันข้ามสามระบบที่ไม่เคยคุยกันตรง ๆ Metrics, APM, และ Logs คือสามผลิตภัณฑ์แยกกัน มี storage และ query engine แยกกัน เหตุผลเดียวที่ dashboard ในตัวหนึ่ง pre-filter ไปที่ “สิ่งเดียวกัน” กับ service page ในอีกตัวได้ก็เพราะทั้งสามบังเอิญได้รับสตริงเดียวกันเป๊ะ ไม่มีอะไรบังคับความเหมือนนั้นจากส่วนกลางเลย ไม่มี registry ที่บอกว่า “พวกนี้คือ service เดียวกัน” มันทำงานได้ด้วย convention ล้วน ๆ — convention เดียวกับที่บทนี้แสดงให้เห็นว่าไหลผ่าน Kubernetes label, Docker label, และ environment variable — และมันพังตรงจุดที่ convention นั้นถูกใช้ไม่สม่ำเสมอ เงียบ ๆ ไม่มี error ที่ไหนใน pipeline เลย

  • Kubernetes label + downward API เทียบกับ Docker label (com.datadoghq.tags.*) — pattern downward API ให้ single source of truth ตัวเดียว (pod label) แล้ว reflect เข้า env var ให้ container ไหนก็ได้ใน pod อัตโนมัติ; Docker/Compose label (พิสูจน์แล้วข้างบนผ่าน agent tagger-list) ไม่ต้องมีกลไกแบบ downward-API เลย แต่ทำงานได้แค่ตอนที่ Agent เข้าถึง Docker socket สำหรับ Autodiscovery และไม่ reflect เข้า environment ของแอปเองอัตโนมัติ
  • identity ทั้งโปรเซส เทียบกับ service override เฉพาะ span — ปล่อยให้ทุก span ใช้ service ทั้งโปรเซสทำให้ service map เรียบง่าย หนึ่ง node ต่อหนึ่ง deployable unit; override service บาง span (พิสูจน์แล้วข้างบน span pg.query แท็ก checkout-postgres) ให้ service map แม่นยำกว่า (database โผล่เป็น node ของตัวเอง) แลกกับทุกคนที่อ่านต้องรู้จัก convention นี้ก่อนถึงจะเข้าใจ service map
  • บังคับ tag value เป็น lowercase ด้วย convention เทียบกับด้วย tooling — กฎ lowercase-only ที่เขียนไว้ในเอกสารไม่มีต้นทุนอะไรเลย แต่ไม่มีอะไรหยุด Terraform module ใหม่หรือไฟล์ YAML ที่ copy-paste มาจากทำผิดกฎ (พิสูจน์แล้วข้างบน); CI lint step ที่ปฏิเสธค่า tags.datadoghq.com/env/DD_ENV/DD_SERVICE/DD_VERSION ที่มีตัวพิมพ์ใหญ่ปนอยู่จับได้ก่อน merge แลกกับอีกหนึ่ง check ที่ทุก deploy ต้องผ่าน
  • “Datadog merge env:Prod กับ env:production เข้าเป็นค่าเดียวอัตโนมัติ” ไม่จริง — พิสูจน์แล้วข้างบนด้วย tracer config dump จริงและเอกสาร tagging ของ Datadog เอง: casing ที่ mismatch ทำให้เกิด tag value สองอันที่อยู่คู่กันใต้ key เดียวกัน ไม่ใช่ค่าเดียวที่ merge
  • “tag value ถูก lowercase เหมือนกับ tag key” ไม่จริง — เอกสารของ Datadog ระบุว่า tag key ถูก normalize เป็น lowercase; เอกสารไม่ได้อ้างแบบนั้นกับ value เลย และการพิสูจน์ฝั่ง client ข้างบนแสดงว่า dd-trace ส่งค่า env/service/version ตามที่ config ไว้เป๊ะ รวมตัวพิมพ์ใหญ่ด้วย
  • “ตั้ง service บน span เดียว เปลี่ยน identity ของ service ทั้งโปรเซส” ไม่จริง — พิสูจน์แล้วข้างบน: override service ใน option ของ tracer.trace() เปลี่ยนแค่ tag service.name ของ span นั้นตัวเดียว ค่า DD_SERVICE ทั้งโปรเซสไม่ถูกแตะและยังใช้กับ span อื่นทุกตัว
  • “Container label เป็นของ Kubernetes อย่างเดียว plain Docker/Compose ไม่มี equivalent” ไม่จริง — พิสูจน์แล้วข้างบน: label com.datadoghq.tags.env/service/version ของ Docker ถูก Autodiscovery ของ Agent ที่ใช้ docker.sock จับได้เหมือน Kubernetes pod label เป๊ะ แค่ผ่านกลไกคนละแบบ (ไม่ต้องมี downward API)
ในตัวอย่าง Kubernetes ทำไม DD_ENV, DD_SERVICE, และ DD_VERSION ถึงตั้งผ่าน fieldRef ไปที่ pod label แทนที่จะ hardcode string ตรง ๆ
พอ DD_ENV/DD_SERVICE/DD_VERSION ถูกตั้งใน environment ของ container แล้ว tracer initialization ต้องทำอะไรเพิ่มเพื่อรับค่าเหล่านั้น
tracer หนึ่งเริ่มด้วย DD_ENV=Prod บน service หนึ่ง และ DD_ENV=production บนอีก service หนึ่ง เกิดอะไรขึ้นจริง พิสูจน์แล้วข้างบนด้วย tracer config dump จริง
อะไรที่ทำให้การ pivot แบบ "ไม่ต้อง cross-reference มือ" ระหว่าง metrics, APM, และ logs พัง
container หนึ่งเริ่มด้วย --label com.datadoghq.tags.service=checkout-service โดยไม่มี Datadog config อื่นเลย output จริงของ agent tagger-list พิสูจน์ว่าเกิดอะไรขึ้น
payoff จริง ๆ ของ env/service/version triple ที่สอดคล้องกันทั่ว metrics, APM, และ logs คืออะไร