Урок 25: Метрики Prometheus и Grafana

Урок 25. Метрики Prometheus и Grafana

🔄 Node.js → Go (ключевые аналоги):

📋 Что изучаем

📦 Инициализация проекта

mkdir go-metrics && cd go-metrics
go mod init go-metrics

# Устанавливаем зависимости
go get github.com/prometheus/client_golang
go get github.com/google/uuid
go mod tidy

# Запускаем Prometheus (опционально)
docker run -d --name prometheus \
  -p 9090:9090 \
  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \
  prom/prometheus

💻 Файл: prometheus.yml

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'go-metrics-app'
    static_configs:
      - targets: ['host.docker.internal:8080']

💻 Файл: internal/metrics/metrics.go

package metrics

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

// ╔══════════════════════════════════════════════════════════╗
// ║  МЕТРИКИ (используем promauto — авто-регистрация)      ║
// ╚══════════════════════════════════════════════════════════╝

var (
    // ==========================================
    // HTTP метрики
    // ==========================================

    // HTTPRequestsTotal — СЧЁТЧИК общего количества HTTP-запросов.
    // CounterVec — счётчик с лейблами (method, path, status).
    HTTPRequestsTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "path", "status"},
    )

    // HTTPRequestDuration — ГИСТОГРАММА длительности запросов.
    // Buckets — границы для распределения (в секундах).
    HTTPRequestDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
        },
        []string{"method", "path"},
    )

    // HTTPRequestsInFlight — GAUGE текущего количества запросов в обработке
    HTTPRequestsInFlight = promauto.NewGauge(
        prometheus.GaugeOpts{
            Name: "http_requests_in_flight",
            Help: "Current number of HTTP requests being served",
        },
    )

    // HTTPResponseSize — ГИСТОГРАММА размера ответа
    HTTPResponseSize = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_response_size_bytes",
            Help:    "HTTP response size in bytes",
            Buckets: prometheus.ExponentialBuckets(100, 2, 12), // 100, 200, 400, ... 409600
        },
        []string{"method", "path"},
    )

    // ==========================================
    // Бизнес-метрики
    // ==========================================

    // OrdersCreatedTotal — количество созданных заказов
    OrdersCreatedTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "orders_created_total",
            Help: "Total number of orders created",
        },
        []string{"status"}, // success, failed
    )

    // OrdersAmountTotal — общая сумма заказов
    OrdersAmountTotal = promauto.NewCounter(
        prometheus.CounterOpts{
            Name: "orders_amount_total",
            Help: "Total amount of orders in USD",
        },
    )

    // UsersRegisteredTotal — количество регистраций
    UsersRegisteredTotal = promauto.NewCounter(
        prometheus.CounterOpts{
            Name: "users_registered_total",
            Help: "Total number of user registrations",
        },
    )

    // ActiveUsers — GAUGE активных пользователей
    ActiveUsers = promauto.NewGauge(
        prometheus.GaugeOpts{
            Name: "active_users_current",
            Help: "Current number of active users",
        },
    )

    // ==========================================
    // Метрики БД
    // ==========================================

    // DBQueriesTotal — счётчик запросов к БД
    DBQueriesTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "db_queries_total",
            Help: "Total number of database queries",
        },
        []string{"operation", "status"}, // select/insert/update/delete, success/error
    )

    // DBQueryDuration — гистограмма времени запросов
    DBQueryDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "db_query_duration_seconds",
            Help:    "Database query duration in seconds",
            Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1},
        },
        []string{"operation"},
    )

    // DBPoolConnections — GAUGE соединений в пуле
    DBPoolConnections = promauto.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: "db_pool_connections",
            Help: "Database pool connections",
        },
        []string{"state"}, // idle, active, total
    )

    // ==========================================
    // Метрики Redis
    // ==========================================

    // CacheHitsTotal — попадания в кэш
    CacheHitsTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "cache_hits_total",
            Help: "Total number of cache hits/misses",
        },
        []string{"result"}, // hit, miss
    )

    // ==========================================
    // Метрики приложения
    // ==========================================

    // AppVersion — версия приложения (info-метрика)
    AppVersion = promauto.NewGauge(
        prometheus.GaugeOpts{
            Name: "app_info",
            Help: "Application information",
            ConstLabels: prometheus.Labels{
                "version": "1.0.0",
                "language": "go",
            },
        },
    )
)

// ╔══════════════════════════════════════════════════════════╗
// ║  ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ                               ║
// ╚══════════════════════════════════════════════════════════╝

// RecordOrderCreated — записывает метрику создания заказа
func RecordOrderCreated(success bool, amount float64) {
    status := "success"
    if !success {
        status = "failed"
    }
    OrdersCreatedTotal.WithLabelValues(status).Inc()
    if success {
        OrdersAmountTotal.Add(amount)
    }
}

// RecordDBQuery — записывает метрику запроса к БД
func RecordDBQuery(operation string, duration float64, err error) {
    status := "success"
    if err != nil {
        status = "error"
    }
    DBQueriesTotal.WithLabelValues(operation, status).Inc()
    DBQueryDuration.WithLabelValues(operation).Observe(duration)
}

// RecordCacheResult — записывает попадание/промах кэша
func RecordCacheResult(hit bool) {
    result := "miss"
    if hit {
        result = "hit"
    }
    CacheHitsTotal.WithLabelValues(result).Inc()
}

💻 Файл: internal/middleware/metrics.go

package middleware

import (
    "net/http"
    "strconv"
    "time"

    "go-metrics/internal/metrics"
)

// MetricsMiddleware — собирает HTTP-метрики
func MetricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // Увеличиваем счётчик активных запросов
        metrics.HTTPRequestsInFlight.Inc()
        defer metrics.HTTPRequestsInFlight.Dec()

        // Оборачиваем ResponseWriter
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}

        // Выполняем запрос
        next.ServeHTTP(wrapped, r)

        // Собираем метрики
        duration := time.Since(start).Seconds()
        status := strconv.Itoa(wrapped.statusCode)
        path := r.URL.Path // В реальности — нормализованный путь (не /users/123, а /users/{id})

        metrics.HTTPRequestsTotal.WithLabelValues(r.Method, path, status).Inc()
        metrics.HTTPRequestDuration.WithLabelValues(r.Method, path).Observe(duration)
        metrics.HTTPResponseSize.WithLabelValues(r.Method, path).Observe(float64(wrapped.bytesWritten))
    })
}

type responseWriter struct {
    http.ResponseWriter
    statusCode   int
    bytesWritten int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

func (rw *responseWriter) Write(b []byte) (int, error) {
    n, err := rw.ResponseWriter.Write(b)
    rw.bytesWritten += n
    return n, err
}

💻 Файл: cmd/server/main.go

package main

import (
    "context"
    "errors"
    "fmt"
    "math/rand"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/prometheus/client_golang/prometheus/promhttp"
    "go-metrics/internal/metrics"
    "go-metrics/internal/middleware"
)

func main() {
    fmt.Println("📊 Сервер с Prometheus-метриками на :8080")

    // Инициализируем версию приложения
    metrics.AppVersion.Set(1)

    mux := http.NewServeMux()

    // Эндпоинт для метрик Prometheus
    mux.Handle("/metrics", promhttp.Handler())

    // Health check
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("OK"))
    })

    // Бизнес-эндпоинт: создание заказа
    mux.HandleFunc("POST /api/orders", func(w http.ResponseWriter, r *http.Request) {
        // Имитация бизнес-логики
        success := rand.Float64() > 0.2 // 80% успех
        amount := float64(rand.Intn(10000)) / 100 // $0-$99.99

        metrics.RecordOrderCreated(success, amount)

        if success {
            w.WriteHeader(http.StatusCreated)
            fmt.Fprintf(w, `{"status":"created","amount":%.2f}`, amount)
        } else {
            w.WriteHeader(http.StatusInternalServerError)
            w.Write([]byte(`{"status":"error"}`))
        }
    })

    // Бизнес-эндпоинт: регистрация
    mux.HandleFunc("POST /api/register", func(w http.ResponseWriter, r *http.Request) {
        metrics.UsersRegisteredTotal.Inc()
        metrics.ActiveUsers.Inc() // В реальности — через фоновый сбор

        w.WriteHeader(http.StatusCreated)
        w.Write([]byte(`{"status":"registered"}`))
    })

    // Эндпоинт с имитацией запроса к БД
    mux.HandleFunc("GET /api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
        // Имитация запроса к БД
        dbStart := time.Now()
        time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
        dbDuration := time.Since(dbStart).Seconds()

        dbErr := error(nil)
        if rand.Float64() < 0.1 {
            dbErr = errors.New("timeout")
        }

        metrics.RecordDBQuery("select", dbDuration, dbErr)
        metrics.RecordCacheResult(rand.Float64() > 0.3) // 70% cache hit

        if dbErr != nil {
            http.Error(w, "DB error", http.StatusInternalServerError)
            return
        }

        id := r.PathValue("id")
        fmt.Fprintf(w, `{"user":"%s"}`, id)
    })

    // Применяем middleware
    handler := middleware.MetricsMiddleware(mux)

    server := &http.Server{
        Addr:    ":8080",
        Handler: handler,
    }

    // Graceful shutdown
    go func() {
        fmt.Println("✅ Сервер запущен")
        if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
            os.Exit(1)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    fmt.Println("\n🛑 Выключение...")
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    server.Shutdown(ctx)
    fmt.Println("✅ Остановлен")
}

🚀 Запуск и тестирование

# Запуск сервера
go run ./cmd/server/main.go

# Проверка метрик (другой терминал)
curl http://localhost:8080/metrics

# Генерация нагрузки
for i in {1..100}; do
  curl -s http://localhost:8080/api/users/$i > /dev/null &
  curl -s -X POST http://localhost:8080/api/orders > /dev/null &
done
wait

# Смотрим метрики после нагрузки
curl -s http://localhost:8080/metrics | grep -E "http_requests_total|orders_created_total|db_queries_total"

# Запуск Prometheus (в Docker)
docker run -d --name prometheus \
  -p 9090:9090 \
  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \
  prom/prometheus

# Открыть Prometheus: http://localhost:9090
# Запросы в PromQL:
#   rate(http_requests_total[1m])
#   histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[1m]))
#   sum(rate(orders_created_total[1m])) by (status)

📊 Типы метрик Prometheus

ТипПоведениеПримерPromQL
CounterТолько растёт (сбрасывается при рестарте)http_requests_totalrate(http_requests_total[1m])
GaugeРастёт и падаетhttp_requests_in_flightavg_over_time(memory_bytes[5m])
HistogramРаспределение по bucket’амhttp_request_duration_secondshistogram_quantile(0.95, rate(...))
SummaryКвантили на клиентеrequest_latency_secondsrequest_latency_seconds{quantile="0.95"}

📊 Основные PromQL-запросы

МетрикаPromQL
RPS (запросов в секунду)rate(http_requests_total[1m])
Процент ошибокrate(http_requests_total{status=~"5.."}[1m]) / rate(http_requests_total[1m]) * 100
P95 latencyhistogram_quantile(0.95, rate(http_request_duration_seconds_bucket[1m]))
Активные соединенияdb_pool_connections{state="active"}
Cache hit raterate(cache_hits_total{result="hit"}[1m]) / rate(cache_hits_total[1m]) * 100
⚠️ Типичные ошибки:
💡 Практический совет:

💡 Best practices от сеньоров:

🔑 Ключевые концепции

Что нужно запомнить из этого урока:

💡 Для Node.js разработчика:

← Предыдущий урок Следующий урок →