Ваше приложение работает, но вы уверены, что знаете, что внутри?
Представьте: вы запустили микросервис на Go в продакшн. Он не падает, не выдаёт 500-х ошибок, но пользователи жалуются, что страница грузится «как-то медленно». Вы лезете в мониторинг — CPU 30%, память в норме. Что делать? Без observability в Go вы — капитан корабля без карты и компаса.
Observability (наблюдаемость) — это не просто «логи». Это три кита: трейсинг, метрики и логи. Вместе они дают вам суперсилу: понимать, что происходит в системе, даже если вы не ожидали проблемы. В этой статье мы разберём каждый из этих аспектов на примерах кода на Go. Вы не просто прочитаете теорию — вы получите готовые рецепты, которые можно внедрить уже сегодня.
Почему Go — идеальный язык для observability?
Go создавался для бэкенд-систем, где важна производительность и надёжность. Стандартная библиотека (net/http, database/sql) уже умеет отдавать метрики и поддерживать контексты для трейсинга. А экосистема OpenTelemetry Go и Prometheus Go делает внедрение observability простым и элегантным.
Но главное — в Go нет «магии». Вы пишете код, который можно измерить, трассировать и логировать без боли. Давайте посмотрим, как это делается.
Трейсинг в Go: OpenTelemetry в действии
Трейсинг (трассировка) — это история одного запроса от входа в систему до выхода. Он показывает, сколько времени занял каждый шаг: запрос к БД, вызов внешнего API, обработка данных. В Go трейсинг строится на контекстах (context.Context).
Настройка OpenTelemetry SDK
Установим пакеты:
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \
go.opentelemetry.io/otel/sdk/trace
Теперь инициализируем трейсер:
package main
import (
"context"
"log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/trace"
)
func initTracer() {
exporter, err := otlptracehttp.New(context.Background())
if err != nil {
log.Fatal(err)
}
tp := trace.NewTracerProvider(
trace.WithBatcher(exporter),
trace.WithSampler(trace.AlwaysSample()),
)
otel.SetTracerProvider(tp)
}
Создаём спан (span) в HTTP-обработчике
Вот как выглядит трейсинг в реальном коде:
func userHandler(w http.ResponseWriter, r *http.Request) {
tracer := otel.Tracer("user-service")
ctx, span := tracer.Start(r.Context(), "getUser")
defer span.End()
// Имитация запроса к БД
user, err := getUserFromDB(ctx, r.URL.Query().Get("id"))
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
http.Error(w, err.Error(), 500)
return
}
span.SetAttributes(
attribute.String("user.id", user.ID),
attribute.Int64("user.age", int64(user.Age)),
)
// Вложенный спан для внешнего вызова
_, childSpan := tracer.Start(ctx, "callAnalyticsAPI")
defer childSpan.End()
callAnalyticsAPI(user)
w.Write([]byte("OK"))
}
Каждый спан содержит имя, время начала и конца, атрибуты и статус. В Jaeger или Grafana Tempo вы увидите полную карту запроса.
Практический совет
Не забывайте передавать контекст через всё приложение. В Go это делается автоматически, если вы используете context.Context в сигнатурах функций. OpenTelemetry Go сам подхватит контекст из HTTP-запроса, если вы используете middleware.
Метрики: Prometheus и экспорт в Go
Метрики — это цифры, которые говорят о состоянии системы: RPS, latency, количество ошибок, использование памяти. Prometheus Go — стандарт де-факто.
Создаём собственные метрики
Установите клиентскую библиотеку:
go get github.com/prometheus/client_golang/prometheus
Пример: считаем количество запросов и их длительность.
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: "Duration of HTTP requests in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
)
func init() {
prometheus.MustRegister(httpRequestsTotal)
prometheus.MustRegister(httpRequestDuration)
}
Middleware для автоматического сбора метрик
Сделаем простой middleware, который оборачивает любой HTTP-обработчик:
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &responseWriter{ResponseWriter: w}
next.ServeHTTP(wrapped, r)
duration := time.Since(start).Seconds()
httpRequestsTotal.WithLabelValues(
r.Method, r.URL.Path, strconv.Itoa(wrapped.status),
).Inc()
httpRequestDuration.WithLabelValues(
r.Method, r.URL.Path,
).Observe(duration)
})
}
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
Теперь добавьте эндпоинт /metrics для Prometheus:
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", nil)
Проверьте: curl http://localhost:8080/metrics — вы увидите все метрики.
Что ещё можно измерять?
- Количество активных горутин
- Размер очереди сообщений
- Время выполнения внешних вызовов (например, к Redis или PostgreSQL)
- Количество ошибок валидации
Логирование: от текста к структуре
Логирование в Go прошло долгий путь от простого log.Println до структурированных логов с уровнями и контекстом. Сегодня стандарт — zerolog (быстрый) или slog (из коробки в Go 1.21+).
Почему slog?
Начиная с Go 1.21, пакет log/slog встроен в стандартную библиотеку. Он поддерживает уровни (DEBUG, INFO, WARN, ERROR), структурированные поля и кастомные обработчики.
package