GoCasts آموزش Go به زبان ساده

بیش از ۱۰۰۰ شرکت‌کننده یادگیری Go و Backend رو از امروز شروع کن
ثبت‌نام دوره + تیم‌سازی

نوشتن Prometheus Exporter با گولنگ - آموزش کامل

Prometheus استاندارد مانیتورینگ در دنیای Cloud-Native است. یکی از کاربردهای رایج Go برای DevOps Engineers، نوشتن Exporter های سفارشی برای expose کردن متریک‌های اپلیکیشن یا زیرساخت است.

در این آموزش یاد می‌گیرید چطور یک Prometheus Exporter بنویسید.

Prometheus چگونه کار می‌کند؟

┌─────────────┐     scrape      ┌──────────────┐
│  Prometheus │ ──────────────> │   Exporter   │
│   Server    │    /metrics     │  (Go App)    │
└─────────────┘                 └──────────────┘
      │
      │ query
      ▼
┌─────────────┐
│   Grafana   │
└─────────────┘
  1. Exporter متریک‌ها را در endpoint /metrics expose می‌کند
  2. Prometheus هر چند ثانیه این endpoint را scrape می‌کند
  3. Grafana از Prometheus query می‌گیرد و dashboard نمایش می‌دهد

انواع متریک در Prometheus

نوع کاربرد مثال
Counter مقادیر افزایشی تعداد درخواست‌ها
Gauge مقادیر متغیر دمای CPU
Histogram توزیع مقادیر زمان پاسخ
Summary خلاصه آماری percentile ها

راه‌اندازی پروژه

mkdir myexporter && cd myexporter
go mod init myexporter

# نصب کتابخانه Prometheus
go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promhttp

Exporter ساده: Hello World

package main

import (
    "log"
    "net/http"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

// تعریف یک Counter
var requestsTotal = prometheus.NewCounter(
    prometheus.CounterOpts{
        Name: "myapp_requests_total",
        Help: "Total number of requests",
    },
)

func init() {
    // ثبت متریک
    prometheus.MustRegister(requestsTotal)
}

func handler(w http.ResponseWriter, r *http.Request) {
    // افزایش counter با هر درخواست
    requestsTotal.Inc()
    w.Write([]byte("Hello, World!"))
}

func main() {
    http.HandleFunc("/", handler)
    http.Handle("/metrics", promhttp.Handler())

    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
# اجرا
go run main.go

# در ترمینال دیگر
curl localhost:8080/
curl localhost:8080/metrics | grep myapp

خروجی /metrics:

# HELP myapp_requests_total Total number of requests
# TYPE myapp_requests_total counter
myapp_requests_total 5

Exporter کامل: متریک‌های HTTP API

package main

import (
    "log"
    "math/rand"
    "net/http"
    "time"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    // Counter با labels
    httpRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Namespace: "myapp",
            Name:      "http_requests_total",
            Help:      "Total HTTP requests",
        },
        []string{"method", "endpoint", "status"},
    )

    // Histogram برای زمان پاسخ
    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Namespace: "myapp",
            Name:      "http_request_duration_seconds",
            Help:      "HTTP request duration in seconds",
            Buckets:   []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1},
        },
        []string{"method", "endpoint"},
    )

    // Gauge برای درخواست‌های فعال
    httpRequestsInFlight = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Namespace: "myapp",
            Name:      "http_requests_in_flight",
            Help:      "Number of HTTP requests currently in flight",
        },
    )
)

func init() {
    prometheus.MustRegister(httpRequestsTotal)
    prometheus.MustRegister(httpRequestDuration)
    prometheus.MustRegister(httpRequestsInFlight)
}

// Middleware برای جمع‌آوری متریک‌ها
func metricsMiddleware(next http.HandlerFunc, endpoint string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // افزایش درخواست‌های فعال
        httpRequestsInFlight.Inc()
        defer httpRequestsInFlight.Dec()

        // Wrapper برای گرفتن status code
        wrapper := &responseWrapper{ResponseWriter: w, statusCode: 200}

        // اجرای handler اصلی
        next.ServeHTTP(wrapper, r)

        // ثبت متریک‌ها
        duration := time.Since(start).Seconds()
        statusStr := http.StatusText(wrapper.statusCode)

        httpRequestsTotal.WithLabelValues(
            r.Method, endpoint, statusStr,
        ).Inc()

        httpRequestDuration.WithLabelValues(
            r.Method, endpoint,
        ).Observe(duration)
    }
}

type responseWrapper struct {
    http.ResponseWriter
    statusCode int
}

func (w *responseWrapper) WriteHeader(code int) {
    w.statusCode = code
    w.ResponseWriter.WriteHeader(code)
}

// Handlers
func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    // شبیه‌سازی latency تصادفی
    delay := time.Duration(rand.Intn(100)) * time.Millisecond
    time.Sleep(delay)

    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"status": "success"}`))
}

func slowHandler(w http.ResponseWriter, r *http.Request) {
    // شبیه‌سازی درخواست کند
    time.Sleep(500 * time.Millisecond)
    w.Write([]byte("Slow response"))
}

func main() {
    // Register handlers با middleware
    http.HandleFunc("/health",
        metricsMiddleware(healthHandler, "/health"))
    http.HandleFunc("/api",
        metricsMiddleware(apiHandler, "/api"))
    http.HandleFunc("/slow",
        metricsMiddleware(slowHandler, "/slow"))

    // Metrics endpoint
    http.Handle("/metrics", promhttp.Handler())

    log.Println("Server starting on :8080")
    log.Println("Metrics available at :8080/metrics")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Exporter برای سیستم: متریک‌های سرور

package main

import (
    "log"
    "net/http"
    "os"
    "runtime"
    "time"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    // اطلاعات اپلیکیشن
    appInfo = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{
            Namespace: "myapp",
            Name:      "info",
            Help:      "Application information",
        },
        []string{"version", "go_version", "hostname"},
    )

    // Uptime
    appUptime = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Namespace: "myapp",
            Name:      "uptime_seconds",
            Help:      "Application uptime in seconds",
        },
    )

    // Goroutines
    goRoutines = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Namespace: "myapp",
            Name:      "goroutines",
            Help:      "Number of goroutines",
        },
    )

    // Memory
    memoryAlloc = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Namespace: "myapp",
            Name:      "memory_alloc_bytes",
            Help:      "Allocated memory in bytes",
        },
    )

    memorySys = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Namespace: "myapp",
            Name:      "memory_sys_bytes",
            Help:      "System memory in bytes",
        },
    )

    // GC
    gcPauseDuration = prometheus.NewSummary(
        prometheus.SummaryOpts{
            Namespace:  "myapp",
            Name:       "gc_pause_seconds",
            Help:       "GC pause duration",
            Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
        },
    )
)

var startTime = time.Now()

func init() {
    prometheus.MustRegister(appInfo)
    prometheus.MustRegister(appUptime)
    prometheus.MustRegister(goRoutines)
    prometheus.MustRegister(memoryAlloc)
    prometheus.MustRegister(memorySys)
    prometheus.MustRegister(gcPauseDuration)
}

func collectMetrics() {
    hostname, _ := os.Hostname()
    appInfo.WithLabelValues("1.0.0", runtime.Version(), hostname).Set(1)

    for {
        // Uptime
        appUptime.Set(time.Since(startTime).Seconds())

        // Goroutines
        goRoutines.Set(float64(runtime.NumGoroutine()))

        // Memory stats
        var m runtime.MemStats
        runtime.ReadMemStats(&m)
        memoryAlloc.Set(float64(m.Alloc))
        memorySys.Set(float64(m.Sys))

        time.Sleep(5 * time.Second)
    }
}

func main() {
    // شروع collector در background
    go collectMetrics()

    http.Handle("/metrics", promhttp.Handler())

    log.Println("Exporter running on :9090")
    log.Fatal(http.ListenAndServe(":9090", nil))
}

Exporter سفارشی: مانیتورینگ سرویس خارجی

یک exporter برای مانیتورینگ یک API خارجی:

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

// Collector interface را پیاده‌سازی می‌کند
type APICollector struct {
    apiURL string

    // متریک‌ها
    up            *prometheus.Desc
    responseTime  *prometheus.Desc
    usersTotal    *prometheus.Desc
    ordersTotal   *prometheus.Desc
}

// API Response structure
type APIStatus struct {
    Status     string `json:"status"`
    UsersCount int    `json:"users_count"`
    OrdersCount int   `json:"orders_count"`
}

func NewAPICollector(url string) *APICollector {
    return &APICollector{
        apiURL: url,
        up: prometheus.NewDesc(
            "external_api_up",
            "Is the external API up",
            nil, nil,
        ),
        responseTime: prometheus.NewDesc(
            "external_api_response_time_seconds",
            "Response time of the external API",
            nil, nil,
        ),
        usersTotal: prometheus.NewDesc(
            "external_api_users_total",
            "Total users from external API",
            nil, nil,
        ),
        ordersTotal: prometheus.NewDesc(
            "external_api_orders_total",
            "Total orders from external API",
            nil, nil,
        ),
    }
}

// Describe متریک‌ها را توصیف می‌کند
func (c *APICollector) Describe(ch chan<- *prometheus.Desc) {
    ch <- c.up
    ch <- c.responseTime
    ch <- c.usersTotal
    ch <- c.ordersTotal
}

// Collect متریک‌ها را جمع‌آوری می‌کند
func (c *APICollector) Collect(ch chan<- prometheus.Metric) {
    start := time.Now()

    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Get(c.apiURL)

    duration := time.Since(start).Seconds()

    if err != nil {
        ch <- prometheus.MustNewConstMetric(c.up, prometheus.GaugeValue, 0)
        ch <- prometheus.MustNewConstMetric(c.responseTime, prometheus.GaugeValue, duration)
        return
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        ch <- prometheus.MustNewConstMetric(c.up, prometheus.GaugeValue, 0)
        ch <- prometheus.MustNewConstMetric(c.responseTime, prometheus.GaugeValue, duration)
        return
    }

    ch <- prometheus.MustNewConstMetric(c.up, prometheus.GaugeValue, 1)
    ch <- prometheus.MustNewConstMetric(c.responseTime, prometheus.GaugeValue, duration)

    // Parse response
    var status APIStatus
    if err := json.NewDecoder(resp.Body).Decode(&status); err == nil {
        ch <- prometheus.MustNewConstMetric(c.usersTotal, prometheus.GaugeValue, float64(status.UsersCount))
        ch <- prometheus.MustNewConstMetric(c.ordersTotal, prometheus.GaugeValue, float64(status.OrdersCount))
    }
}

func main() {
    // ثبت collector سفارشی
    collector := NewAPICollector("https://api.example.com/status")
    prometheus.MustRegister(collector)

    http.Handle("/metrics", promhttp.Handler())

    log.Println("Exporter running on :9091")
    log.Fatal(http.ListenAndServe(":9091", nil))
}

تنظیمات Prometheus

prometheus.yml:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'myapp'
    static_configs:
      - targets: ['localhost:8080']

  - job_name: 'myapp-system'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'external-api'
    scrape_interval: 30s
    static_configs:
      - targets: ['localhost:9091']

Docker Compose برای تست

version: '3.8'

services:
  myexporter:
    build: .
    ports:
      - "8080:8080"

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

Query‌های Prometheus مفید

# نرخ درخواست در ثانیه
rate(myapp_http_requests_total[5m])

# نرخ درخواست به تفکیک endpoint
sum(rate(myapp_http_requests_total[5m])) by (endpoint)

# میانگین زمان پاسخ
histogram_quantile(0.95, rate(myapp_http_request_duration_seconds_bucket[5m]))

# درخواست‌های با خطا
rate(myapp_http_requests_total{status!="OK"}[5m])

# Uptime
myapp_uptime_seconds

# Memory usage
myapp_memory_alloc_bytes / 1024 / 1024  # MB

بهترین شیوه‌ها

شیوه توضیح
Namespace استفاده کنید myapp_ prefix
Label‌های معنادار method, endpoint, status
Help text کامل توضیح واضح متریک
Unit در نام _seconds, _bytes
از Histogram استفاده کنید به جای Summary برای aggregation
Label‌های با cardinality بالا نه مثلاً user_id

ساختار پروژه کامل

myexporter/
├── main.go
├── collector/
│   ├── http.go
│   ├── system.go
│   └── custom.go
├── Dockerfile
├── docker-compose.yml
├── prometheus.yml
└── go.mod

Dockerfile

FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 go build -o exporter .

FROM alpine:3.18
COPY --from=builder /app/exporter /exporter
EXPOSE 8080
CMD ["/exporter"]

جمع‌بندی

با Prometheus client library می‌توانید:

  • متریک‌های سفارشی برای اپلیکیشن‌های خود بسازید
  • سرویس‌های خارجی را مانیتور کنید
  • متریک‌های سیستمی جمع‌آوری کنید
  • با Grafana داشبورد بسازید

این یکی از کاربردهای عملی Go برای DevOps است که مستقیماً در کار روزانه استفاده می‌شود.


مقالات مرتبط

منابع

بیش از ۱۰۰۰ شرکت‌کننده یادگیری Go و Backend رو از امروز شروع کن
ثبت‌نام دوره + تیم‌سازی