Сервис, который нельзя наблюдать, нельзя и эксплуатировать. Наблюдаемость — три опоры (логи, трейсы, метрики) плюс health-checks для оркестратора. В Go всё это собирается из стандартной библиотеки (slog) и зрелых пакетов экосистемы, без отдельного фреймворка.
Структурные логи: slog
С версии 1.21 в стандартной библиотеке есть slog — структурное логирование. Логи пишут парами ключ-значение и в продакшене выводят как JSON, чтобы их можно было искать и фильтровать.
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
slog.Info("product_created", "product_id", product.ID, "price", product.Price)
Ключевое поле — идентификатор запроса, проставленный middleware в context: по нему строки одного запроса сшиваются. Логгер с привязанными к запросу полями достают из контекста и передают в Handler. Важно правило: лог — это событие с полями, а не форматированная строка.
Трейсинг: OpenTelemetry
Трейс показывает путь запроса через сервис и дальше — в базу, в соседний сервис — и где сколько времени ушло. Стандарт — OpenTelemetry; для net/http есть готовая обёртка otelhttp, которая добавляет span на каждый запрос.
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
handler := otelhttp.NewHandler(router, "http")
Дальше инструментируют pgx и HTTP-клиентов, чтобы трейс был сквозным; экспортируют трейсы в коллектор (Jaeger, Tempo) настройкой экспортёра, не кодом обработчиков. Идентификатор запроса из логов сшивается с трейсом — вместе они отвечают на «что случилось с этим конкретным запросом».
Метрики: Prometheus
Метрики — числа во времени: количество запросов, задержки, ошибки. Стандарт — Prometheus, который забирает их с эндпоинта /metrics. В Go это prometheus/client_golang.
import "github.com/prometheus/client_golang/prometheus/promhttp"
r.Handle("/metrics", promhttp.Handler())
Базовые метрики (число и длительность запросов) навешивают middleware-обёрткой; свои бизнес-счётчики (например, созданных заказов) объявляют через client_golang и инкрементируют в коде. Метрики отвечают на «что происходит в целом», трейсы — на «что с этим запросом».
Health-checks
Оркестратору (Kubernetes) нужны два разных сигнала, и путать их опасно. liveness — жив ли процесс (провал → перезапусти); прост и ни от чего не зависит. readiness — готов ли принимать трафик (провал → не шли запросы); тут уместно проверить базу.
r.Get("/health/live", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Get("/health/ready", func(w http.ResponseWriter, r *http.Request) {
if err := pool.Ping(r.Context()); err != nil {
http.Error(w, "not ready", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
})
Ошибка — делать liveness зависимым от базы: упала база, Kubernetes решит, что процесс мёртв, и начнёт перезапускать, хотя перезапуск чужую базу не лечит. Liveness — про себя, readiness — про готовность.
Где это в UCP
Наблюдаемость — не украшение перед продом, а часть определения «готово»: сервис, который нельзя продиагностировать, не доведён до конца. Три опоры отвечают на разные вопросы, health-checks дают оркестратору правду о состоянии. Это тот же набор, что Actuator, Micrometer и трейсинг в Spring-биндинге, только стандартной библиотекой Go и пакетами экосистемы. Для продукт-инженера, который владеет сервисом до пользователя, наблюдаемость закрывает последний участок пути: увидеть, что выкаченное работает, и быстро понять, если нет — в том числе когда ошибка дошла до клиента.