การเขียน Custom Prometheus Exporter เป็นเรื่องจำเป็นเมื่อระบบที่ต้อง monitor ไม่มี exporter สำเร็จรูป หรือเมื่อต้องการเก็บ business metric เฉพาะทาง เช่น จำนวน order ต่อวินาที, latency ของ internal API, หรือสถานะของ job queue — บทความนี้จะลงลึกถึงวิธีเขียน custom exporter ระดับ production ที่เสถียร ปลอดภัย และ scale ได้
เราจะดูทั้งแบบ Python (prometheus_client) และ Go (prometheus/client_golang) ที่เป็นภาษาหลักของ exporter ส่วนใหญ่ พร้อมแนวทางการทำ unit test, การ package เป็น binary/container, และการ instrument ข้อมูลภายในของ exporter เองเพื่อดูว่า exporter แข็งแรงแค่ไหน
เมื่อไหร่ต้องเขียน Custom Exporter
- ระบบ internal ที่ไม่ได้ expose metric ในรูปแบบ Prometheus เช่น legacy system, mainframe
- Business metric ที่ไม่สามารถดึงจาก infrastructure ทั่วไป เช่น จำนวนสมาชิกใหม่ต่อชั่วโมง
- External API ที่ต้องการ monitor SLA เช่น latency ของ partner API
- Job queue, message broker ที่ไม่มี standard exporter ที่ตรงกับ setup
- Custom hardware, IoT device ที่ต้องดึงข้อมูลผ่าน serial/SNMP/API เฉพาะ
หลักการออกแบบ Exporter
Exporter ที่ดีต้องทำตามหลัก stateless — ไม่เก็บข้อมูลระยะยาว ทุกครั้งที่ถูก scrape จะไปดึงข้อมูลสด ๆ จากต้นทาง ให้ Prometheus เป็นตัวเก็บ time series แทน
- Scrape-driven: เก็บข้อมูลเมื่อถูก scrape เท่านั้น ไม่ poll เป็นระยะเอง (ยกเว้น metric ที่ต้นทางช้า)
- Cardinality-aware: จำกัดจำนวน label combination ให้ไม่เกินหลักพัน ต่อ exporter
- Fail-open: ถ้า source ล่ม ให้ return metric ที่ว่างเปล่าพร้อม
up = 0แทน error 500 - Observable: เปิด metric ของ exporter เองด้วย เช่น
scrape_duration,scrape_errors_total
Python Exporter (Production Ready)
ตัวอย่างนี้เป็น custom exporter ที่ดึง metric จาก internal REST API — รวม scrape timeout, error handling, และ instrument ตัวเอง:
from prometheus_client import start_http_server, Gauge, Counter, Histogram, REGISTRY
from prometheus_client.core import GaugeMetricFamily
import requests
import time
import logging
logging.basicConfig(level=logging.INFO)
log = logging.getLogger('api_exporter')
SCRAPE_DURATION = Histogram('exporter_scrape_duration_seconds',
'Duration of scrape to source API')
SCRAPE_ERRORS = Counter('exporter_scrape_errors_total',
'Total scrape errors', ['error_type'])
class ApiCollector:
def __init__(self, api_url, timeout=5):
self.api_url = api_url
self.timeout = timeout
def collect(self):
orders = GaugeMetricFamily('business_orders_total',
'Total orders by status',
labels=['status'])
up = GaugeMetricFamily('source_up', 'Source API reachable', value=0)
with SCRAPE_DURATION.time():
try:
r = requests.get(f"{self.api_url}/stats",
timeout=self.timeout)
r.raise_for_status()
data = r.json()
for status, count in data.get('orders', {}).items():
orders.add_metric([status], count)
up = GaugeMetricFamily('source_up', 'Source API reachable', value=1)
except requests.Timeout:
SCRAPE_ERRORS.labels(error_type='timeout').inc()
log.warning('source API timeout')
except requests.RequestException as e:
SCRAPE_ERRORS.labels(error_type='request').inc()
log.error(f'source API error: {e}')
yield orders
yield up
if __name__ == '__main__':
REGISTRY.register(ApiCollector('http://api.internal:8080'))
start_http_server(9200)
log.info('exporter listening on :9200')
while True:
time.sleep(60)
จุดสำคัญคือใช้ GaugeMetricFamily แทน Gauge global ทั่วไป เพราะจะไม่คงค่าจาก scrape ก่อน — ถ้า source ล่ม metric จะหายไปแทนที่จะค้างค่าเก่า ทำให้ alert ทำงานได้ถูกต้อง
Go Exporter
Go เป็นภาษาทางการของ Prometheus และเป็นตัวเลือกที่ดีสำหรับ exporter ที่ต้องการ performance สูงและ binary เดียว — ตัวอย่างพื้นฐาน:
package main
import (
"log"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
type collector struct {
ordersTotal *prometheus.Desc
sourceUp *prometheus.Desc
}
func newCollector() *collector {
return &collector{
ordersTotal: prometheus.NewDesc(
"business_orders_total",
"Total orders by status",
[]string{"status"}, nil,
),
sourceUp: prometheus.NewDesc(
"source_up",
"Source API reachable",
nil, nil,
),
}
}
func (c *collector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.ordersTotal
ch <- c.sourceUp
}
func (c *collector) Collect(ch chan<- prometheus.Metric) {
orders, err := fetchOrders()
if err != nil {
log.Printf("fetch error: %v", err)
ch <- prometheus.MustNewConstMetric(c.sourceUp, prometheus.GaugeValue, 0)
return
}
ch <- prometheus.MustNewConstMetric(c.sourceUp, prometheus.GaugeValue, 1)
for status, count := range orders {
ch <- prometheus.MustNewConstMetric(c.ordersTotal,
prometheus.CounterValue, count, status)
}
}
func fetchOrders() (map[string]float64, error) {
// HTTP call, database query, etc.
return map[string]float64{
"pending": 42, "completed": 1024,
}, nil
}
func main() {
prometheus.MustRegister(newCollector())
http.Handle("/metrics", promhttp.Handler())
srv := &http.Server{
Addr: ":9200", ReadTimeout: 10 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
Go client ใช้ Collect/Describe interface ซึ่งเป็น pattern ที่แนะนำสำหรับ scrape-driven exporter — metric ถูกสร้างใหม่ทุกครั้งที่ Prometheus scrape ทำให้ไม่มีการ leak state
Unit Testing Exporter
Exporter ที่ใช้ใน production ต้องมี test coverage เพื่อให้แน่ใจว่า metric ที่ expose ถูกต้อง — ใช้ testutil ของ prometheus client library:
# test_exporter.py
from prometheus_client import REGISTRY
from prometheus_client.core import GaugeMetricFamily
def test_collector_returns_metric(requests_mock):
requests_mock.get('http://api.internal:8080/stats',
json={'orders': {'pending': 5, 'completed': 100}})
from exporter import ApiCollector
collector = ApiCollector('http://api.internal:8080')
metrics = {m.name: m for m in collector.collect()}
assert 'business_orders' in metrics
pending = [s for s in metrics['business_orders'].samples if s.labels['status']=='pending'][0]
assert pending.value == 5
สำหรับ Go ใช้ testutil.CollectAndCompare ที่เปรียบเทียบ metric output กับ expected text format ได้โดยตรง — ช่วยจับ regression เมื่อชื่อ metric หรือ label เปลี่ยน
Docker Image
FROM python:3.12-slim AS base
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY exporter.py .
USER nobody
EXPOSE 9200
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --spider http://localhost:9200/metrics || exit 1
CMD ["python", "-u", "exporter.py"]
รันด้วย user nobody ไม่ใช่ root เพื่อความปลอดภัย — และเพิ่ม HEALTHCHECK เพื่อให้ orchestrator เช่น Kubernetes ตรวจสอบความพร้อมของ exporter ได้
Systemd Service
# /etc/systemd/system/api-exporter.service
[Unit]
Description=API Metrics Exporter
After=network.target
[Service]
Type=simple
User=exporter
Group=exporter
ExecStart=/usr/local/bin/api-exporter
Restart=always
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Security hardening ของ systemd เช่น NoNewPrivileges, ProtectSystem, PrivateTmp ลดผลกระทบถ้ามีช่องโหว่ใน exporter — เป็น baseline ของ service modern
Instrument ตัว Exporter เอง
Metric ที่ควร expose จาก exporter เองเสมอ:
| Metric | Type | ประโยชน์ |
|---|---|---|
exporter_scrape_duration_seconds | Histogram | ตรวจสอบว่า exporter ช้าหรือไม่ |
exporter_scrape_errors_total | Counter | Alert ถ้า error spike ขึ้น |
source_up | Gauge | บอกว่า source reachable หรือไม่ |
exporter_build_info | Gauge | Version tracking แบบ Grafana-friendly |
exporter_last_scrape_timestamp | Gauge | ตรวจว่า scrape ยังทำงานปกติ |
Best Practices
- ตั้ง timeout ของ source call ให้น้อยกว่า
scrape_timeoutของ Prometheus (default 10s) มิฉะนั้น scrape จะค้าง - ใช้
prometheus.NewGaugeVecแทนprometheus.NewGaugeที่ต้องประกาศทุก label combination ล่วงหน้า - จัดการ concurrency ด้วย sync.Mutex หรือ asyncio.Lock ถ้ามี state ร่วมกันระหว่าง scrape
- อย่าเก็บ metric เป็น histogram กับ bucket ที่ไม่เหมาะสม — default bucket มักไม่ตรงกับ workload จริง
- Version exporter ด้วย
build_infometric — ช่วย debug เมื่อ Grafana dashboard พัง - Logging ให้ ideal level (WARN ขึ้นไปสำหรับ production) — ไม่ต้อง log ทุก scrape
- ใช้ graceful shutdown — รับ SIGTERM แล้ว close HTTP server อย่าง clean ก่อนจบ process
- Release ด้วย semver และใช้ CI pipeline ทำ test + build image อัตโนมัติ
เผยแพร่ให้คนอื่นใช้
ถ้า exporter ที่เขียนมีประโยชน์ต่อชุมชน พิจารณา open-source ผ่าน GitHub — ตามกฎ Prometheus community แนะนำให้:
- ใส่
READMEอธิบาย metric ทุกตัวที่ expose - ใช้ license เช่น Apache 2.0, MIT ที่เข้ากับ Prometheus ecosystem
- มี Grafana dashboard ตัวอย่างใน
dashboard/directory - Publish Docker image ไปที่ Docker Hub หรือ GitHub Container Registry
- ใส่ตัวอย่าง scrape config ใน README
สรุป
Custom exporter ทำให้ Prometheus ครอบคลุมทุกระบบที่ทีมใช้งาน ตั้งแต่ business metric ไปจนถึง legacy system ที่ไม่เคยคิดว่า monitor ได้ การเขียน exporter ระดับ production ต้องการความใส่ใจทั้งด้าน performance, security, และ observability ของตัว exporter เอง — แต่พอทำให้ครบถ้วน จะได้เครื่องมือที่เสถียรและ scale ได้จริง
เมื่อมี exporter ครบแล้ว ขั้นตอนถัดไปคือการเขียน PromQL query และ alert rule ที่ใช้ metric เหล่านี้ให้เกิดประโยชน์สูงสุด

