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

OTLP กับมาตรฐานเปิด

OTLP เป็น wire format ไม่ใช่ฟีเจอร์ของ Datadog จึงมีตัวแปลสองตัวอยู่ระหว่าง OpenTelemetry SDK กับหน้าจอ Datadog — OTLP receiver ของ Agent และ Datadog exporter ของ Collector (หรือเส้นทาง OTLP HTTP ที่ใหม่กว่า) — และแต่ละตัวใช้ การ map semantic convention ชุดเดียวกัน (service.name เป็น service, http.request.method เป็น http.method, span kind กับ db.system เป็น operation name และ span type) คำถามที่มีประโยชน์จึงไม่ใช่ “รับ OTLP ไหม” แต่คือ “มันเปลี่ยน attribute ของฉันเป็นอะไร”

flowchart LR
  SDK["App with OpenTelemetry SDK"] -->|"OTLP grpc :4317 or http :4318"| AG["Agent OTLP receiver: otlp_config"]
  SDK -->|"OTLP"| COL["OpenTelemetry Collector"]
  AG --> MAP1["Semantic mapping and sampling in the Agent"]
  MAP1 --> INT["Datadog intake"]
  COL -->|"datadog exporter, or otlp_http exporter"| MAP2["Semantic mapping in exporter or intake"]
  MAP2 --> INT
จาก OpenTelemetry SDK ถึง Datadog

docs บอกตรง ๆ: “OTLP ingestion is off by default” (OTLP ingestion by the Datadog Agent) container แบบ default:

$ docker exec dd-internals agent status
OTLP
====
Status: Not enabled
Collector status: Not running
$ curl -X POST -H 'Content-Type: application/json' --data @otlp_trace.json http://127.0.0.1:14318/v1/traces
curl: (52) Empty reply from server # HTTP 000: nothing is listening behind the published port

ไม่มีอะไร listen ดังนั้น OTel SDK ที่ชี้มาที่ Agent จะ export ไม่สำเร็จ (SDK ออกแบบให้ log แล้วทิ้ง) เมื่อเปิดแล้วจะมีสองบรรทัด Status ที่ความหมายต่างกัน และความต่างนั้นคือเรื่องทั้งหมดของหัวข้อถัดไป

agent config --all ใน container ที่รันอยู่แสดง setting ที่สำคัญกับค่า default:

otlp_config:
metrics:
enabled: true
histograms:
mode: distributions
sums:
cumulative_monotonic_mode: to_delta
resource_attributes_as_tags: false
logs:
enabled: false
traces:
enabled: true
internal_port: 5003
span_name_as_resource_name: false
probabilistic_sampler:
sampling_percentage: 100
receiver:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318

(ตัดออก: block ของ batch, keepalive และ CORS สอง endpoint แสดงเป็น 0.0.0.0 เพราะ env ของ container นี้ตั้งไว้ ตัว receiver ไม่มี endpoint จนกว่าคุณจะตั้ง) docs ให้ปุ่มสองตัวที่เปิดมัน: “For gRPC (localhost:4317): DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT; for HTTP (localhost:4318): DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT” และประโยคที่พลาดง่าย: “These must be passed to both the core Agent and trace Agent processes.” log ปิดเป็น default “to prevent unexpected logs billing.”

ทีนี้หลักฐานว่า configcheck ไม่ได้ตรวจเรื่องนี้ datadog.yaml ที่ผิดหนึ่งคำ protocol แทน protocols:

# @expect-error otlpreceiver.Config has invalid keys: protocol
api_key: <your-api-key>
otlp_config:
receiver:
protocol:
http:
endpoint: 0.0.0.0:4318

เริ่มด้วยไฟล์นั้น (container แบบ --network none) แล้วสามคำสั่งที่คนใช้ตรวจ:

$ agent configcheck ; echo "configcheck exit $?"
configcheck exit 0
$ agent status
OTLP
====
Status: Enabled
Collector status: Closed
$ grep "OTLP ingest" /var/log/datadog/agent.log
CORE | ERROR | (comp/otelcol/collector/impl-pipeline/pipeline.go:116 in func1) | Error running the OTLP ingest pipeline: failed to get config: cannot unmarshal the configuration: decoding failed due to the following error(s):
'receivers' error reading configuration for "otlp": decoding failed due to the following error(s):
'otlpreceiver.Config' has invalid keys: protocol

agent configcheck ลิสต์ config ของ check ที่ resolve แล้วจาก conf.d/ มันไม่เคยดู otlp_config ของ datadog.yaml และ exit 0 agent status บอก Status: Enabled (มี setting อยู่) แต่ Collector status: Closed (pipeline ตายตอน startup) มีแค่บรรทัดสุดท้ายของ log ที่บอกชื่อ key ที่ผิด สองบรรทัดที่ต่างกันจึงมีความหมายต่างกัน และ gate --agent ของ harness ซึ่งรัน configcheck บน fence # datadog.yaml ของบทเรียน พิสูจน์ว่า Agent เริ่มทำงาน กับไฟล์นั้นได้ ไม่ได้พิสูจน์ว่า OTLP ใช้งานได้ ให้อ่าน Collector status และ grep log เมื่อสะกด protocols ถูก (ทางแก้ ที่ harness collect):

datadog.yaml
api_key: <your-api-key>
otlp_config:
receiver:
protocols:
http:
endpoint: 0.0.0.0:4318
logs:
enabled: false

สามคำสั่งเดิมให้ configcheck exit 0, Status: Enabled, Collector status: Running

ค่า env default ใน docs คือ localhost:4317 และ localhost:4318 และคำแนะนำสำหรับ container คือ: “If running in a containerized environment, use 0.0.0.0 instead of localhost to ensure the server is available on non-local interfaces.” ผมตั้ง gRPC เป็น localhost:4317 และ HTTP เป็น 0.0.0.0:4318 ใน container เดียว อ่าน socket ที่ listen จาก /proc/net/tcp แล้วเชื่อมต่อจาก container ตัวที่สอง:

listening (inside the Agent container): 127.0.0.1:4317 [::]:4318
from another container, to the Agent's IP: 4317 ConnectionRefusedError [Errno 111] Connection refused
4318 open

Agent ที่อยู่ใน pod หรือ container ของตัวเองด้วย bind แบบ default จะเพื่อนบ้านเข้าถึงไม่ได้ docs เพิ่มข้อจำกัดที่ config แก้ไม่ได้: telemetry จาก collector หรือแอปบน host หนึ่งส่งไปยัง Agent บนอีก host ไม่ได้ (“The supported setup is an ingesting Agent deployed on every OpenTelemetry-data generating host”) — topology ที่รองรับคือหนึ่ง Agent ต่อหนึ่ง host

หนึ่ง trace สอง span ส่งเป็น OTLP/HTTP JSON resource มี service.name=checkout-otel, service.version=1.4.2, deployment.environment.name=lab; server span คือ GET /orders/{id} พร้อม http.request.method=GET, http.route=/orders/{id}, http.response.status_code=500, status Error (“upstream failed”); ตัวลูกคือ client span ที่มี db.system=postgresql และ db.statement สิ่งที่ไปถึง fake intake (/api/v0.2/traces, ถอดรหัสแล้ว):

tracer_version 'otlp-' env lab hostname internals-host app_version 1.4.2
chunk priority 1 tags {_dd.p.dm: -9, _dd.otlp_sr: 1.00}
span service=checkout-otel name=http.server.request resource="GET /orders/{id}" type=web error=1 duration 50 ms
meta http.method=GET http.route=/orders/{id} span.kind=server env=lab version=1.4.2
error.msg="upstream failed" otel.status_code=Error otel.status_description="upstream failed"
otel.scope.name=lab.instrumentation otel.trace_id=5b8efff798038103d269b633813fc60c
metric http.status_code=500.0 _top_level=1.0
span service=checkout-otel name=postgresql.query resource="SELECT * FROM orders WHERE id = ?" type=sql duration 30 ms
meta db.system=postgresql db.statement="SELECT * FROM orders WHERE id = $1" sql.query="SELECT * FROM orders WHERE id = ?"

การแปลแต่ละอย่างเป็นกฎ และทุกอย่างตรงกับตารางใน docs (semantic mapping): service.name เป็น service, service.version เป็น version, deployment.environment.name เป็น env, http.request.method เป็น http.method, http.response.status_code เป็น http.status_code (และวัดได้ว่ากลายเป็น metric ตัวเลข ไม่ใช่ string tag) ส่วนที่เหลือเป็นการอนุมานของ Agent ที่ docs อธิบายเป็นตรรกะ span type ตามลำดับความสำคัญ — span.type ที่ระบุชัดก่อน แล้ว “Span kind server” เป็น web — จึงเป็นเหตุผลที่ server span เป็น web และ client span ของ postgresql เป็น sql operation name http.server.request และ resource GET /orders/{id} ถูกคำนวณ ไม่ได้ copy: resource คือ method บวก http.route (run แบบ convention เก่าด้านล่างคง span name OTel เดิมแต่ได้ resource เป็น GET) และ resource ของ DB span คือ statement ที่ obfuscate $1 เป็น ? ไม่ใช่ span name SELECT orders ของ OTel trace เก็บต้นกำเนิด OTel ไว้เป็น tag otel.*

สามรูปแบบ แต่ละอันรันจริง:

old semantic conventions (http.method, http.url, http.status_code, deployment.environment):
span name=http.server.request resource="GET" env=lab http.status_code=500.0
# no http.route, so the resource is only the method; deployment.environment still maps to env
otlp_config.traces.span_name_as_resource_name=true:
span name=GET_orders_id resource="GET /orders/{id}"
span name=SELECT_orders resource="SELECT orders"
otlp_config.traces.probabilistic_sampler.sampling_percentage=25, 400 traces posted:
chunks forwarded 98 _dd.p.dm=-9 _dd.otlp_sr=0.25

ทั้งชื่อ attribute เก่าและใหม่ไปถึง env/http.method บน 7.83.3 (footnote ใน docs บอกว่า deployment.environment.name “Requires Datadog Agent 7.58.0+, Datadog Exporter v0.110.0+, or Datadog OTLP intake”) ถ้าไม่มี http.route resource ของ server span ยุบเหลือแค่ method — สาเหตุคลาสสิกของ resource GET ก้อนยักษ์ในหน้า service sampler คือจุดเชื่อมกลับไปที่ The Trace Sampling Pipeline: OTLP trace ไม่เคยผ่าน head sampler ของ Datadog tracer ดังนั้น otlp_config.traces.probabilistic_sampler ของ Agent คือสิ่งที่ sample มัน (98 จาก 400 คือ 24.5% เมื่อตั้ง 25%; mechanism -9 ตัวเดียวกับ probabilistic sampler ของ Agent ในบทนั้น บวก _dd.otlp_sr ที่บันทึก rate) docs ลิสต์ข้อจำกัดที่ตามมา: “Data from applications instrumented with OpenTelemetry SDK cannot be used in some Datadog proprietary products, such as App and API Protection, Continuous Profiler, and Ingestion Rules.” OTLP ผ่าน Agent ไม่มี DD_TRACE_SAMPLING_RULES และไม่มี rate จากหน้า Ingestion Control

ส่ง OTLP metric สามชนิด แล้ว export รอบสองหลังสิบวินาที พร้อม resource attribute service.name และ deployment.environment.name:

OTLP gauge lab.queue.depth = 42 {queue=jobs}
-> series gauge 42.0 tags [queue:jobs, service:checkout-otel, env:lab, instrumentation_scope:lab.meter, instrumentation_scope_version:n/a]
OTLP cumulative monotonic sum lab.requests = 10, then 25 {route=home}
-> first export: no point at all
-> second export: series count 15.0 (a delta, 25 - 10)
OTLP delta histogram lab.latency count 4, sum 1060, min 10, max 1000, bounds [50, 500], buckets [2,1,1]
-> /api/beta/sketches cnt 4 min 10.0 max 1000.0 sum 1060.0 k=[1590, 1739] n=[3, 1]

นั่นคือ cumulative_monotonic_mode: to_delta และ histograms.mode: distributions ทำงาน resource attribute ถูกยกเป็น unified-service tag (service, env) ทั้งที่ resource_attributes_as_tags เป็น false (setting นั้นคุม resource attribute ตัวอื่น) และ instrumentation_scope ถูกเพิ่มเป็น tag โดย default สองผลที่ควรรู้ก่อนสร้าง dashboard ค่าแรกของ cumulative counter เป็น baseline และถูกทิ้ง ช่วงแรกหลัง restart จึงว่าง และ histogram มาถึงเป็น DDSketch ที่สร้างจาก explicit bucket — sketch key ที่ต่างกันมีแค่สองตัวสำหรับ OTel bucket สามอัน percentile จากมันจึงละเอียดเท่า explicitBounds ของคุณเท่านั้น (ดู DogStatsD and Aggregation ว่า sketch ตอบ percentile อย่างไร)

docs แนะนำสอง setup ของ Collector ในปัจจุบัน และ config ที่ AI เขียนมักปนกัน จาก Set Up the OpenTelemetry Collector: config ที่ “tested with the OpenTelemetry Collector Contrib distribution v0.154.0” ซึ่งใช้ OTLP HTTP exporter (“Sends telemetry to Datadog’s OTLP intake endpoints”, endpoint https://otlp.<YOUR_DD_SITE>), span_metrics connector สำหรับ RED metrics และ Datadog extension จาก Configure the Datadog Exporter and Connector: “For new OpenTelemetry Collector configurations, Datadog recommends the OTLP HTTP exporter and span_metrics connector setup. The Datadog Exporter and Datadog Connector remain supported, and existing configurations do not need to migrate.” ดังนั้น datadog exporter เป็น legacy แต่ยังรองรับ และเป็นตัวที่บท Phase 3 กับ config เดิมส่วนใหญ่ใช้ รูปแบบของมัน validate จริง:

otel/collector-datadog-exporter.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: localhost:4317
http:
endpoint: localhost:4318
processors:
batch:
send_batch_max_size: 100
send_batch_size: 10
timeout: 10s
connectors:
datadog/connector:
exporters:
datadog/exporter:
api:
site: datadoghq.com
key: ${env:DD_API_KEY}
service:
pipelines:
metrics:
receivers: [otlp, datadog/connector]
processors: [batch]
exporters: [datadog/exporter]
traces:
receivers: [otlp]
processors: [batch]
exporters: [datadog/connector, datadog/exporter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [datadog/exporter]

otelcol-contrib validate กับไฟล์นี้พิมพ์แค่บรรทัด debug และ exit 0 สามตัวแปรที่พัง ใช้คำสั่งเดียวกัน:

api.key renamed to api_key exit 1 Error: exporters::datadog/exporter: api.key is not set
exporter id "datadogexporter" exit 1 Error: failed to get config: cannot unmarshal the configuration: decoding failed due to the following error(s):
'exporters' unknown type: "datadogexporter" for id: "datadogexporter" (valid values: [logzio bmchelix clickhouse ...
connector removed from traces exporters
(still a receiver in metrics) exit 1 Error: failed to build pipelines: connector "datadog/connector" used as receiver in [metrics] pipeline but not used in any supported exporter pipeline

ต่างจาก configcheck ของ Agent, validate ของ Collector สร้าง pipeline graph จริง จึงจับความผิดเรื่อง wiring ได้ — อันสุดท้ายถูกจับเพราะทั้ง validate และการเริ่มรันจริงสร้าง pipeline (container ที่เริ่มด้วยไฟล์เดียวกัน exit 1 พร้อมข้อความเดียวกันตอนที่ผมกลับมาดูหลังสิบวินาที) connector ถูกตั้งใจให้เป็น exporter ของ traces pipeline และ receiver ของ metrics pipeline: มันแปลง span เป็น trace metrics กับ exporter แบบ legacy log ตอน start ของ 0.161.0 บอกว่า “Trace metrics are now disabled in the Datadog Exporter by default. To continue receiving Trace Metrics, configure the Datadog Connector or disable the feature gate.” — connector คือสิ่งที่สร้าง APM trace metrics จาก span ของคุณ

คิดว่า OTLP คือ wire เดียว ผู้บริโภคสองแบบที่ฟีเจอร์ต่างกัน ผู้บริโภคฝั่ง Agent แปลง OTLP เป็นรูป span และ metric แบบ native ของ Datadog แล้วทำตัวเหมือน Agent ทั่วไป: มี sampler, tag, forwarder ของตัวเอง และรันบน host ผู้บริโภคฝั่ง Collector อยู่ในโครงสร้างพื้นฐานของคุณ และ component ของ Datadog แปลระหว่างทางออก ในทั้งสองแบบ สิ่งสำคัญคือ ตาราง mapping: attribute ที่ตรง convention กลายเป็น first-class (service, env, http.method) ที่ไม่ตรงยังเป็น raw tag และสิ่งที่ mapping ต้องการแต่ SDK ไม่ส่ง (http.route, deployment.environment.name) จะปรากฏทีหลังเป็นชื่อ resource ที่เสื่อมสภาพหรือ env ที่หายไป การเปิดดู payload อย่างข้างบนเร็วกว่าอ่าน dashboard ใด ๆ

ทางเลือกข้อดีข้อเสีย
OTLP เข้า Agentendpoint ในเครื่องเดียว; ใช้ tagging, sampler และ forwarder ของ Agent; ไม่ต้องรัน Collector เพิ่มตาม docs ผลิตภัณฑ์ Datadog บางตัวใช้กับแอปที่ instrument ด้วย OTel ไม่ได้; topology ที่รองรับคือหนึ่ง Agent ต่อ host
Collector กับ datadog exporterประมวลผลส่วนกลาง (filter, tail sampling ใน gateway) ก่อนถึง Datadog; pipeline ที่ไม่ผูก vendorคุณเป็นเจ้าของ config, version และการ upgrade ของ Collector; ต้อง wire connector ไม่งั้น trace metrics หาย
Collector กับ otlp_http exporter (คำแนะนำปัจจุบันของ docs)ใช้ OTLP intake ของ Datadog; exporter ง่ายกว่ารูปแบบ config ต่างจากตัวอย่างเดิมส่วนใหญ่; ตรวจขีดจำกัด intake ที่ docs ลิสต์
Datadog tracer แทน OTel SDKฟีเจอร์ native (Ingestion Rules, profiler, App and API Protection)instrumentation เฉพาะ vendor
bind 0.0.0.0container และ pod อื่นเข้าถึงได้docs เองบอกว่า localhost ปลอดภัยกว่านอก container
span_name_as_resource_nameresource ตาม span name ของคุณเปลี่ยน operation name ด้วย (GET_orders_id ข้างบน); ตรวจ monitor ที่อิง operation name ใหม่
  • “agent configcheck validate OTLP config ของฉันแล้ว” มัน exit 0 กับไฟล์ที่ OTLP pipeline ตายแล้ว (Collector status: Closed) ให้ดู agent status และ log
  • “Status: Enabled แปลว่า OTLP ใช้งานได้” แปลว่ามี setting อยู่ Collector status: Running คือสถานะที่ใช้งานได้
  • “OTLP เปิดเมื่อ Agent เปิด” ปิดเป็น default; container default ปฏิเสธการเชื่อมต่อ
  • “OTLP span ถูกเก็บตามที่ส่ง” มันถูก map: http.request.method เป็น http.method, span kind บวก attribute เป็น span type และ operation name
  • “cumulative counter ปรากฏตั้งแต่ export แรก” ค่าแรกเป็น baseline และไม่ได้ point; ครั้งที่สองได้ delta 15
  • “OTLP trace ใช้ DD_TRACE_SAMPLING_RULES” ไม่มี Datadog tracer เกี่ยวข้อง; OTLP probabilistic sampler ของ Agent ทำงานแทน (98 จาก 400 ที่ 25%)
  • “otelcol validate ตรวจแค่ syntax ของ YAML” ใน 0.161.0 มันสร้าง pipeline และปฏิเสธ connector ที่ wire ผิดด้วย exit 1

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

# datadog.yaml -- "enable OTLP so our OpenTelemetry services can send traces"
api_key: <your-api-key>
otlp_config:
receiver:
protocol:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
เฉลย

protocol ต้องเป็น protocols Agent จริงรับไฟล์นี้, agent configcheck exit 0 และ agent status พิมพ์ Status: Enabled — แต่ OTLP pipeline ไม่เคยเริ่ม (Collector status: Closed) และหลักฐานมีแค่บรรทัดเดียวใน agent.log: 'otlpreceiver.Config' has invalid keys: protocol OTel SDK ทุกตัวเชื่อมต่อไม่ได้ สิ่งที่ควรจำคือควรเชื่อคำสั่งไหน: Collector status: Running block ที่แก้แล้ว ตรงกับที่ได้ Running ข้างบนพอดี:

# corrected datadog.yaml
api_key: <your-api-key>
otlp_config:
receiver:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318

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

otel/collector.yaml
receivers:
otlp:
protocols:
http:
endpoint: localhost:4318
processors:
batch:
connectors:
datadog/connector:
exporters:
datadog/exporter:
api:
site: datadoghq.com
key: ${env:DD_API_KEY}
service:
pipelines:
metrics:
receivers: [otlp, datadog/connector]
processors: [batch]
exporters: [datadog/exporter]
traces:
receivers: [otlp]
processors: [batch]
exporters: [datadog/exporter]
เฉลย

connector เป็น receiver ของ metrics pipeline แต่ไม่มีอะไรป้อนมัน: มันหายไปจาก exporters ของ traces pipeline otelcol-contrib 0.161.0 จริงปฏิเสธที่จะเริ่ม: Error: failed to build pipelines: connector "datadog/connector" used as receiver in [metrics] pipeline but not used in any supported exporter pipeline (exit 1 ทั้งจาก validate และการเริ่มรันจริง) ทางแก้คือเพิ่ม datadog/connector ใน exporter ของ traces เพื่อให้ trace metrics ที่สร้างจาก span ไปถึง metrics pipeline ไฟล์ที่ทำงานได้เต็มคือ collector-datadog-exporter.yaml ข้างบนซึ่ง validate ได้ exit 0 อย่า “แก้” error ด้วยการลบ connector ทุกที่: log ตอน start ของ Collector รุ่นนี้บอกว่า “Trace metrics are now disabled in the Datadog Exporter by default” ถ้าไม่มี connector exporter ก็ไม่สร้างมันอีกแล้ว (ผมไม่ได้รันตัวแปรนั้นเพื่อสังเกตฝั่ง metric)

container datadog/agent:7 แบบ default ถูกส่ง OTLP/HTTP trace ด้วย curl ไปที่ port ที่ publish ไว้ agent status กับ curl แสดงอะไร?
datadog.yaml มี otlp_config.receiver.protocol (เอกพจน์) พร้อม http endpoint Agent จริงแสดงอะไร?
OTLP server span หนึ่งตัวมาถึงพร้อม kind=server, http.request.method=GET, http.route=/orders/{id}, status Error Agent forward อะไร?
cumulative monotonic OTLP sum ถูก export เป็น 10 และสิบวินาทีต่อมาเป็น 25 อะไรไปถึง intake ด้วย cumulative_monotonic_mode แบบ default?
ทำไม Collector config ที่ลิสต์ datadog/connector เป็น receiver ของ metrics pipeline แต่ไม่ใช่ exporter ของ traces pipeline จึงล้มเหลว และอะไรพิสูจน์?
ข้อใดเกี่ยวกับ sampling ของ OTLP trace ที่ส่งเข้า Agent ได้รับการสนับสนุนจากบทนี้?