OpenTelemetry: Unified Observability Framework

OpenTelemetry (OTel) เป็นมาตรฐาน Observability ที่รวม Traces, Metrics และ Logs ไว้ในเครื่องมือเดียว ก่อนหน้านี้ผู้พัฒนาต้องใช้ SDK หลายตัว เช่น Prometheus client สำหรับ metrics, Jaeger client สำหรับ traces, Fluentd สำหรับ logs — แต่ละตัวมี API และ format ต่างกัน ทำให้ต้อง instrument code ซ้ำซ้อนและผูกติดกับ vendor เฉพาะราย

OTel แก้ปัญหานี้ด้วยการสร้าง API และ SDK กลางที่ทุกภาษาใช้ร่วมกัน เขียน code ครั้งเดียวแล้วส่งข้อมูลไปยัง backend ใดก็ได้ เช่น Jaeger, Zipkin, Prometheus, Grafana Tempo, Datadog หรือ New Relic บทความนี้จะอธิบายสถาปัตยกรรม วิธีการ instrument แอปพลิเคชัน และการติดตั้ง Collector เพื่อใช้งานจริง

OpenTelemetry คืออะไร

OTel เป็นโครงการ open-source ภายใต้ CNCF (Cloud Native Computing Foundation) เกิดจากการรวม OpenTracing และ OpenCensus เข้าด้วยกันในปี 2019 เป้าหมายหลักคือสร้างมาตรฐานเดียวสำหรับการเก็บ telemetry data ทุกประเภท ลดการผูกติดกับ vendor และช่วยให้ทีม DevOps สามารถเปลี่ยน backend ได้โดยไม่ต้องแก้โค้ดแอปพลิเคชัน

3 เสาหลักของ Observability

  • Traces — ติดตาม request ที่เดินทางผ่านหลาย service พร้อม latency แต่ละจุด
  • Metrics — ข้อมูลตัวเลขที่วัดได้ เช่น request rate, error rate, CPU, memory
  • Logs — ข้อความบันทึกเหตุการณ์พร้อม timestamp ที่ใช้ debug ปัญหาเฉพาะจุด

สถาปัตยกรรม OpenTelemetry

ระบบนี้แบ่งออกเป็น 3 ส่วนหลัก ทำงานประสานกันเพื่อส่งข้อมูลจากแอปพลิเคชันไปยัง observability backend

  • API — interface ที่นักพัฒนาเรียกใช้ในโค้ด เช่น tracer.startSpan() ไม่ผูกติดกับ implementation ใดๆ
  • SDK — implementation ของ API พร้อมความสามารถเสริม เช่น sampling, batching, exporter
  • Collector — service กลางที่รับข้อมูลจากแอป แปลง format และส่งต่อไป backend ต่างๆ

OTLP Protocol

OTLP (OpenTelemetry Protocol) คือ protocol มาตรฐานสำหรับส่งข้อมูล telemetry ระหว่าง SDK, Collector และ backend รองรับทั้ง gRPC (port 4317) และ HTTP (port 4318) ออกแบบให้มีประสิทธิภาพสูงและ schema ยืดหยุ่น backend ใหม่ๆ ส่วนใหญ่รองรับ OTLP โดยตรง เช่น Grafana Tempo, Honeycomb และ Datadog

ติดตั้ง OpenTelemetry SDK ใน Node.js

การ instrument แอปพลิเคชัน Node.js เริ่มจากการติดตั้ง SDK และ auto-instrumentation package ซึ่งจะทำ instrument library ยอดนิยมอัตโนมัติ เช่น Express, HTTP, gRPC, PostgreSQL

npm install @opentelemetry/api @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-metrics-otlp-http

สร้างไฟล์ tracing.js เพื่อ initialize SDK ก่อน require module อื่นๆ

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');

const sdk = new NodeSDK({
  serviceName: 'order-service',
  traceExporter: new OTLPTraceExporter({
    url: 'http://otel-collector:4318/v1/traces',
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: 'http://otel-collector:4318/v1/metrics',
    }),
    exportIntervalMillis: 10000,
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

รันแอปพลิเคชันพร้อม preload tracing.js ด้วย -r flag

node -r ./tracing.js app.js

Manual Instrumentation

สำหรับโค้ดเฉพาะทางที่ auto-instrumentation ไม่ครอบคลุม สามารถสร้าง span เองเพื่อวัด latency ของ business logic เช่น การคำนวณราคา หรือการเรียก external API ที่ไม่ได้ใช้ HTTP client มาตรฐาน

const { trace, SpanStatusCode } = require('@opentelemetry/api');
const tracer = trace.getTracer('order-service');

async function processOrder(orderId) {
  return tracer.startActiveSpan('processOrder', async (span) => {
    try {
      span.setAttribute('order.id', orderId);
      const result = await calculateTotal(orderId);
      span.setAttribute('order.total', result.total);
      return result;
    } catch (err) {
      span.recordException(err);
      span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
      throw err;
    } finally {
      span.end();
    }
  });
}

Python Instrumentation

Python รองรับ OpenTelemetry เช่นเดียวกับ Node.js ใช้ opentelemetry-instrument สำหรับ auto-instrumentation ไม่ต้องแก้โค้ดเลย

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install

opentelemetry-instrument \
  --traces_exporter otlp \
  --metrics_exporter otlp \
  --service_name payment-service \
  --exporter_otlp_endpoint http://otel-collector:4317 \
  python app.py

ติดตั้ง OpenTelemetry Collector

Collector เป็นตัวกลางที่รับข้อมูลจากแอปแล้วส่งต่อไป backend ช่วยลดการเชื่อมต่อโดยตรงระหว่างแอปและ backend ให้ประโยชน์ทั้งในด้าน performance (batching, compression) และความยืดหยุ่น (เปลี่ยน backend ได้โดยไม่ต้อง restart แอป)

ไฟล์ otel-collector-config.yaml ตัวอย่างที่รับ OTLP แล้วส่งต่อ Jaeger (traces) และ Prometheus (metrics)

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 10s
    send_batch_size: 1024
  memory_limiter:
    check_interval: 1s
    limit_mib: 512

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true
  prometheus:
    endpoint: 0.0.0.0:8889
  logging:
    loglevel: info

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp/jaeger, logging]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [prometheus, logging]

รัน Collector ด้วย Docker

docker run -d --name otel-collector \
  -p 4317:4317 -p 4318:4318 -p 8889:8889 \
  -v $(pwd)/otel-collector-config.yaml:/etc/otel/config.yaml \
  otel/opentelemetry-collector-contrib:latest \
  --config=/etc/otel/config.yaml

Context Propagation

การส่ง trace context ข้าม service ต้องใช้ HTTP header มาตรฐาน เช่น traceparent ตาม W3C Trace Context Specification ซึ่ง SDK จะใส่และอ่าน header เหล่านี้อัตโนมัติเมื่อใช้ auto-instrumentation ผลลัพธ์คือ span ของ service A และ service B จะเชื่อมกันเป็น trace เดียว แม้จะเป็นคนละ process

Best Practices

  • Service Name ต้องชัดเจน — ใช้ชื่อที่สั้น เช่น payment-api ไม่ใช่ server
  • Attribute ระวัง cardinality — ห้ามใส่ user_id หรือ request_id เป็น metric label เพราะจะทำให้ backend overload
  • Sampling ก่อนส่ง — ตั้ง head-based sampling ที่ SDK เพื่อลด traffic ไป Collector ใช้ tail-based ที่ Collector เพื่อตัดสินใจจาก trace ทั้งก้อน
  • Resource Attributes — ระบุ service.version, deployment.environment เสมอ ช่วย filter ได้ละเอียดขึ้น
  • Collector มี 2 tier — Agent Collector อยู่ข้างแอป (sidecar/DaemonSet), Gateway Collector เป็น central service ที่ส่งต่อ backend

สรุป

OTel เป็นมาตรฐาน Observability ที่รวม traces, metrics และ logs เข้าด้วยกัน ลดการผูกติดกับ vendor ทำให้เปลี่ยน backend ได้ง่าย การ instrument ด้วย auto-instrumentation ช่วยให้เริ่มใช้งานได้รวดเร็วโดยไม่ต้องแก้โค้ดมาก ส่วนการติดตั้ง Collector เป็นตัวกลางช่วยจัดการ routing, batching และ buffering อย่างมีประสิทธิภาพ

แนะนำให้เริ่มจาก traces ก่อนเพราะเห็นผลลัพธ์ชัดเจนที่สุด แล้วค่อยขยายไป metrics และ logs ตามลำดับ การใช้ OTLP protocol และ W3C Trace Context จะช่วยให้ระบบ compatible กับ backend ในอนาคตได้โดยไม่ต้องเปลี่ยนโค้ด