Мониторинг и observability для Go-сервисов: метрики, трейсинг и логи в единой экосистеме
Введение: три столпа 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: 15sGrafana 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. Подробнее обо мне →