DevOps

Мониторинг и observability для Go-сервисов: метрики, трейсинг и логи в единой экосистеме

Ruslan Ismailov Опубликовано 14 мин чтения
М

Введение: три столпа observability

Observability — это способность понять внутреннее состояние системы по её внешним выходным данным. Для production Go-сервисов это означает три обязательных компонента:

  • Метрики — числовые показатели состояния системы во времени (CPU, RPS, latency, error rate).
  • Трейсинг — трассировка пути запроса через все сервисы и компоненты.
  • Логи — структурированные записи событий с контекстом.

Без каждого из этих столпов вы работаете вслепую. Метрики говорят «что-то сломалось», логи — «почему», а трейсинг — «где именно». В 2026 году стандартом де-факто стал OpenTelemetry, а связка Prometheus + Grafana + Loki + Tempo закрывает все три направления в единой экосистеме. В этой статье мы пройдём весь путь от инструментирования кода до деплоя в Kubernetes.

Инструментирование Go-приложения: Prometheus-клиент и кастомные метрики

Первый шаг — добавить метрики прямо в код сервиса. Официальный клиент Prometheus для Go позволяет экспортировать счётчики, гистограммы, гейджи и саммари.

// go get github.com/prometheus/client_golang/prometheus
// go get github.com/prometheus/client_golang/prometheus/promhttp

package main

import (
    "net/http"
    "time"

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

var (
    httpRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "path", "status"},
    )

    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: prometheus.DefBuckets,
        },
        []string{"method", "path"},
    )
)

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

func metricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriter{w, http.StatusOK}
        next.ServeHTTP(rw, r)
        duration := time.Since(start).Seconds()

        statusStr := http.StatusText(rw.status)
        httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, statusStr).Inc()
        httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/metrics", promhttp.Handler())
    mux.Handle("/api/v1/orders", metricsMiddleware(http.HandlerFunc(ordersHandler)))
    http.ListenAndServe(":8080", mux)
}

Эндпоинт /metrics — стандартная точка сбора метрик для Prometheus. Используйте labels осторожно: высокая кардинальность (например, user_id в label) убивает производительность Prometheus.

Распределённый трейсинг с OpenTelemetry в Go — практика 2026 года

OpenTelemetry (OTel) стал единым стандартом для трейсинга и метрик. В 2026 году SDK стабилизировался, и интеграция в Go-сервисы стала прямолинейной.

// go get go.opentelemetry.io/otel
// go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
// go get go.opentelemetry.io/otel/sdk/trace

package telemetry

import (
    "context"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)

func InitTracer(serviceName, otlpEndpoint string) (*sdktrace.TracerProvider, error) {
    exporter, err := otlptracehttp.New(
        context.Background(),
        otlptracehttp.WithEndpoint(otlpEndpoint),
        otlptracehttp.WithInsecure(),
    )
    if err != nil {
        return nil, err
    }

    res := resource.NewWithAttributes(
        semconv.SchemaURL,
        semconv.ServiceName(serviceName),
        semconv.ServiceVersion("1.0.0"),
    )

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(res),
        sdktrace.WithSampler(sdktrace.AlwaysSample()),
    )
    otel.SetTracerProvider(tp)
    return tp, nil
}

// Использование в хендлере:
func ordersHandler(w http.ResponseWriter, r *http.Request) {
    tracer := otel.Tracer("orders-service")
    ctx, span := tracer.Start(r.Context(), "GetOrders")
    defer span.End()

    orders, err := db.GetOrders(ctx)
    if err != nil {
        span.RecordError(err)
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
    // ...
}

Трейсы отправляются в Grafana Tempo через OTLP-коллектор. Важно пробрасывать context.Context через все вызовы — это основа распределённого трейсинга в Go. Для автоматической инструментации HTTP-клиентов и баз данных используйте официальные contrib-пакеты OTel.

Структурированное логирование: slog, zerolog и интеграция с Loki

С Go 1.21 в стандартную библиотеку вошёл log/slog — структурированный логгер. Для высоконагруженных сервисов предпочтительнее zerolog из-за нулевого аллоцирования.

// zerolog
package main

import (
    "os"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func main() {
    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
    log.Logger = zerolog.New(os.Stdout).With().
        Timestamp().
        Str("service", "orders-service").
        Str("env", "production").
        Logger()

    log.Info().
        Str("method", "GET").
        Str("path", "/api/v1/orders").
        Int("status", 200).
        Dur("duration", duration).
        Msg("request completed")
}

Логи в формате JSON из stdout собирает Promtail (или Grafana Alloy в 2026 году) и отправляет в Grafana Loki. В Loki логи индексируются по labels (service, env, level), а полнотекстовый поиск работает по содержимому. Важно: не создавайте лишних labels в Loki — та же проблема высокой кардинальности, что и в Prometheus.

Для корреляции логов с трейсами добавляйте trace_id и span_id в каждый лог-запись:

import "go.opentelemetry.io/otel/trace"

func logWithTrace(ctx context.Context, msg string) {
    span := trace.SpanFromContext(ctx)
    sc := span.SpanContext()
    log.Info().
        Str("trace_id", sc.TraceID().String()).
        Str("span_id", sc.SpanID().String()).
        Msg(msg)
}

Хранение метрик с Redis для агрегации в реальном времени

Prometheus хорошо справляется с долгосрочным хранением, но для агрегации в реальном времени — например, rate limiting, leaderboards или sliding window counters — удобен Redis. Связка Go + Redis позволяет считать метрики прямо в памяти и периодически сбрасывать их в Prometheus через кастомный коллектор.

// go get github.com/redis/go-redis/v9

package metrics

import (
    "context"
    "github.com/redis/go-redis/v9"
    "github.com/prometheus/client_golang/prometheus"
)

type RedisMetricsCollector struct {
    rdb     *redis.Client
    counter *prometheus.Desc
}

func NewRedisMetricsCollector(rdb *redis.Client) *RedisMetricsCollector {
    return &RedisMetricsCollector{
        rdb: rdb,
        counter: prometheus.NewDesc(
            "realtime_orders_processed_total",
            "Orders processed (from Redis counter)",
            []string{"region"}, nil,
        ),
    }
}

func (c *RedisMetricsCollector) Describe(ch chan<- *prometheus.Desc) {
    ch <- c.counter
}

func (c *RedisMetricsCollector) Collect(ch chan<- prometheus.Metric) {
    ctx := context.Background()
    val, _ := c.rdb.Get(ctx, "orders:processed:eu").Float64()
    ch <- prometheus.MustNewConstMetric(c.counter, prometheus.CounterValue, val, "eu")
}

Redis используется также для кэширования результатов тяжёлых Prometheus-запросов на дашбордах и для хранения временных агрегатов между перезапусками сервиса.

Деплой стека мониторинга в Kubernetes: Prometheus Operator, Grafana, Tempo

В production-окружении с Kubernetes рекомендуется использовать kube-prometheus-stack (Helm-чарт), который разворачивает Prometheus Operator, Grafana и набор готовых алертов.

# values.yaml для kube-prometheus-stack
prometheus:
  prometheusSpec:
    retention: 30d
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: fast-ssd
          resources:
            requests:
              storage: 100Gi

grafana:
  enabled: true
  adminPassword: "changeme"
  additionalDataSources:
    - name: Loki
      type: loki
      url: http://loki:3100
    - name: Tempo
      type: tempo
      url: http://tempo:3200

alertmanager:
  enabled: true

Для автоматического обнаружения сервисов используйте ServiceMonitor — CRD Prometheus Operator:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: orders-service
  namespace: production
spec:
  selector:
    matchLabels:
      app: orders-service
  endpoints:
    - port: http
      path: /metrics
      interval: 15s

Grafana Tempo разворачивается отдельно и принимает трейсы по OTLP. В Grafana настраивается корреляция: из дашборда метрик можно перейти к логам через Loki, а из логов — к трейсу в Tempo по trace_id. Это и есть единая экосистема observability.

Алертинг: настройка правил и интеграция с мессенджерами

Alertmanager маршрутизирует алерты в Slack, Telegram, PagerDuty и другие системы. Пример PrometheusRule:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: orders-service-alerts
spec:
  groups:
    - name: orders.rules
      rules:
        - alert: HighErrorRate
          expr: |
            rate(http_requests_total{status="Internal Server Error"}[5m])
            / rate(http_requests_total[5m]) > 0.05
          for: 2m
          labels:
            severity: critical
          annotations:
            summary: "High error rate on {{ $labels.path }}"
            description: "Error rate is {{ $value | humanizePercentage }}"

        - alert: SlowResponseTime
          expr: |
            histogram_quantile(0.99,
              rate(http_request_duration_seconds_bucket[5m])
            ) > 1.0
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "P99 latency above 1s"

Конфиг Alertmanager для Telegram:

route:
  group_by: ['alertname', 'severity']
  receiver: 'telegram'

receivers:
  - name: 'telegram'
    telegram_configs:
      - bot_token: '${TELEGRAM_BOT_TOKEN}'
        chat_id: -1001234567890
        message: |
          🚨 *{{ .GroupLabels.alertname }}*
          {{ range .Alerts }}{{ .Annotations.summary }}{{ end }}

Практический пример: полный observability-стек для микросервиса на Go

Ниже — docker-compose для локальной разработки, поднимающий весь стек одной командой.

version: '3.8'

services:
  orders-service:
    build: ./services/orders
    ports:
      - "8080:8080"
    environment:
      - OTLP_ENDPOINT=otel-collector:4318
      - REDIS_URL=redis:6379
    depends_on:
      - redis
      - otel-collector

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    volumes:
      - ./otel-config.yaml:/etc/otel/config.yaml
    command: ["--config=/etc/otel/config.yaml"]
    ports:
      - "4318:4318"

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

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning

  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"

  tempo:
    image: grafana/tempo:latest
    command: ["-config.file=/etc/tempo.yaml"]
    volumes:
      - ./tempo.yaml:/etc/tempo.yaml
    ports:
      - "3200:3200"

  promtail:
    image: grafana/promtail:latest
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./promtail-config.yaml:/etc/promtail/config.yaml
    command: -config.file=/etc/promtail/config.yaml

Конфиг OTel Collector (otel-config.yaml):

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

exporters:
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true
  prometheus:
    endpoint: 0.0.0.0:8889

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlp/tempo]
    metrics:
      receivers: [otlp]
      exporters: [prometheus]

После запуска docker compose up -d открывайте Grafana на localhost:3000. Подключите источники данных Prometheus, Loki и Tempo, настройте корреляцию — и у вас готов полноценный observability-стек для разработки.

Заключение

Построить observability для Go-микросервисов сегодня проще, чем кажется: OpenTelemetry унифицирует трейсинг и метрики, zerolog/slog закрывают структурированное логирование, а связка Prometheus + Grafana + Loki + Tempo обеспечивает единое окно для анализа. Redis органично дополняет стек для агрегации в реальном времени. В Kubernetes Prometheus Operator и ServiceMonitor автоматизируют обнаружение сервисов и сбор метрик. Начните с локального docker-compose, выработайте привычки инструментирования, затем перенесите стек в production — и ваши Go-сервисы станут по-настоящему прозрачными.

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →