Observability на максимум: как обеспечить наблюдаемость в микросервисной архитектуре
- пятница, 13 февраля 2026 г. в 00:00:07
Всем привет! Меня зовут Максим, я Go-разработчик в Wildberries & Russ. В высоконагруженных системах сотни сервисов взаимодействуют ежесекундно, и любой малейший простой системы напрямую влияет на прибыль бизнеса. Чтобы уметь быстро находить причины и устранять их за короткие сроки придуманы инструменты, обеспечивающие наблюдаемость приложения. Сегодня поговорим о том, как обеспечить observability и почему без нее жизнь продукта превращается в «черный ящик».
Зачастую, в разговорах про observability говорят про Prometheus и Grafana. Это сильное заблуждение, ведь Prometheus это лишь верх айсберга, который не может показать всей картины. Действительно, Prometheus это очень мощный инструмент для мониторинга систем, но мониторинг != observability.
Так из чего же состоит этот монстр?
В основе любой системы наблюдаемости фигурируют 3 основных компонента: Metrics, Logging, Tracing. Важно отметить, что одно наличие этих компонентов не обеспечивает единую правильную картину. Необходимо обеспечить работу компонент как единый механизм, который помогает командам не только видеть симптомы проблем, но и понимать их причины.
Первый и самый частый компонент, который присутствует в observability системах - это мониторинг. Мониторинг отвечает нам на вопрос «что сломалось?». Это отправная точка разработчика при анализе проблем в системе. Мониторинг представляет агрегированные числовые данные о состоянии системы. Это может быть загруженность CPU, количество 5xx ошибок API, задержка ответа (latency) API.
Из популярных инструментов, которые используются для мониторинга можно отметить Prometheus, Grafana, Thanos, VictoriaMetrics.
В блоге Grafana существуют 3 популярных подхода для сбора метрик, которые покрывают все случаи мониторинга системы.
USE
Этот метод лучше всего подходит для аппаратных ресурсов инфраструктуры, таких как CPU, память и сетевые устройства. Метод создан для анализа сервера и может использоваться для быстрого выявления узких мест или ошибок, связанных с нехваткой ресурсов.
Сам метод состоит из 3 метрик:
U: Utilization — метрика, показывающая насколько ресурс занят в моменте
S: Saturation — метрика, которая показывает объем отложенной работы, которую система не смогла выполнить мгновенно и поставила в очередь.
E: Errors — процент ошибок во времени
RED
В отличие от метода USE, который больше сфокусирован на железе и ресурсах, RED идеально подходит для продуктовых команд. Его метрики ориентированы не на инфраструктуру, а на пользовательский опыт.
Как и USE, RED состоит из 3 метрик:
R: Rate — количество запросов в секунду
E: Errors — доля ошибок в запросах
D: Duration — задержка ответа сервиса
4 Golden Signals
Данный метод популяризовала команда SRE из Google в своей статье. Суть заключается в том, что 4 основные метрики покрывают 80% наблюдаемости системы. Можно сказать, что это золотой стандарт мониторинга.
Метод, как не странно, состоит из 4 метрик:
Latency — время, которое требуется на обработку запроса
Traffic — метрика, показывающая нагрузку на систему, к примеру RPS
Errors — доля запросов, завершившихся с ошибкой
Saturation — считаем, насколько наша система забита
В данной статье остановимся на последнем подходе, так как он покрывает больше метрик для продуктовых команд, чем RED.
Все примеры будут приведены на языке Go, но для понимания я буду оставлять комментарии. Будем использовать библиотеку github.com/prometheus/client_golang/prometheus
Итак, 4 Golden Signals на практике должны содержать следующие метрики:
http_requests_total — будет отвечать за метрику Traffic и Errors. Для того, чтобы считать эту метрику необходимо ее создать в проекте.
var requests = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "http_requests_total", Help: "Total number of HTTP requests.", }, []string{"code", "method", "path"}, )
Здесь Counter Vector — это тип метрики, значение которой может только расти или сбрасываться до нуля. А значения в срезе называются Labels. В дальнейшем мы сможем фильтровать и группировать данные по этим меткам.
Для подсчета метрики Latency необходимо добавить новую переменную http_request_latency_seconds
var latency = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_latency_seconds", Help: "Latency of HTTP requests.", Buckets: prometheus.DefBuckets, }, []string{"code", "method", "path"}, )
Здесь Histogram Vector измеряет длительность событий. Главным отличием Histogram от Counter являются бакеты. Гистограмма не просто хранит среднее число, она раскладывает каждый запрос по бакетам в зависимости от его длительности. Это нужно для подсчета скорости ответа в худших случаях (95 или 99 перцентиль), а не среднем по больнице.
Отлично, метрики созданы, но пока что никак не используются. Чтобы собирать метрики, необходимо зарегистрировать их и внедрить в хэндлеры. Реализуем middleware для подсчета метрик.
type metricsWriter struct { http.ResponseWriter statusCode int } func (rw *metricsWriter) WriteHeader(code int) { rw.statusCode = code rw.ResponseWriter.WriteHeader(code) } func NewMetricsMiddleware() func(next http.Handler) http.Handler { prometheus.MustRegister(latency) prometheus.MustRegister(requests) return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rw := &metricsWriter{ResponseWriter: w} next.ServeHTTP(rw, r) statusCode := rw.statusCode method := r.Method path := r.URL.Path code := strconv.Itoa(statusCode) duration := time.Since(start).Seconds() latency.WithLabelValues(code, method, path).Observe(duration) requests.WithLabelValues(code, method, path).Inc() }) } }
А что с Saturation?
По умолчанию prometheus библиотека собирает информацию про go_goroutines и process_cpu_seconds_total . Этих метрик достаточно для наблюдаемости загруженности CPU и утечки горутин.
Метрики пишутся, а как нам их собирать?
Чтобы понять, как работает сборка метрик, необходимо окунуться вглубь устройства работы Prometheus.
Важное замечание про кардинальность метрик: При добавлении лейблов (например, path), важно быть осторожным. Никогда не передавайте туда уникальные значения, такие как ID пользователя или полный URL с параметрами (/orders/12345). Каждая уникальная комбинация лейблов создает новый временной ряд в памяти Prometheus. Если у вас миллион пользователей, Prometheus быстро вычерпает лимиты памяти и упадет. Все динамические части путей должны быть заменены на шаблоны (например, /orders/:id)

Prometheus это система мониторинга с открытым исходным кодом. Prometheus собирает и хранит метрики в виде временных рядов, то есть информация о метриках хранится с меткой времени, в которое она была записана, а также с необязательными парами ключ-значение, называемыми метками.
Основной принцип работы Prometheus это Pull-модель. Prometheus сервер использует scraping mode, раз в какое-то время настроенное на сервере он делает обычный HTTP GET запрос к сервису по эндпоинту /metrics, парсит текст и сохраняет данные в свою базу (TSDB).
Grafana - это мощный инструмент для визуализации собранных метрик. Для каждого сервиса строится свой дашборд с графиками метрик. Grafana выступает клиентом для базы Prometheus, исполняя запросы на языке PromQL и преобразовывает сырые данные временных рядов в наглядные графики.

Для построения дашборда выполним следующие запросы:
Traffic:
sum(rate(http_requests_total[1m])) by (path)
Latency:
histogram_quantile(0.95, sum(rate(http_request_latency_seconds_bucket[1m])) by (le, path))
Errors:
(sum(rate(http_requests_total{code=~"4..|5.."}[1m])) / sum(rate(http_requests_total[1m]))) * 100
Метрики настроены, стало понятнее и прозрачнее наблюдать за нашей системой, но в работе разработчика смотреть весь день на графики точно не стоит :) Но как в таком случае быстро реагировать на резкие всплески активности, увеличение времени отклика или роста количества ошибок? На помощь приходят Grafana Alerts.
В Grafana Alerts удобно написать запрос в PromQL, выставить пороговое значение и добавить Webhook URL для нашего сервиса уведомлений (Telegram, Slack, Mattermost и тд.).
Итак, наш выдуманный разработчик интегрировал мониторинг в свою работу и теперь может отслеживать нестандартное поведение приложений. В один из дней ему приходит алерт о том, что приложение стало дольше отвечать, вырос Latency. Разработчик смотрит в график и, действительно, с 200 миллисекунд график вырос до 1 секунды. Проблема в том, что на этом этапе наш разработчик не может выяснить где конкретно тормозит запрос. Графики в Prometheus или Grafana говорят нам,"что случилось?", но они молчат о том, "почему это произошло?".
На помощь мониторингу приходит дополнительный компонент observability - трейсинг. При помощи трейсинга мы можем отследить полный execution path в распределенной системе. Трейсинг показывает как запрос проходит через все компоненты системы, включая их взаимодействие и задержки на каждом этапе. Без трейсинга отладка ошибок в микросервисной архитектуре превращается в ад, где разработчику придется в ручную анализировать все сетевые вызовы. Трейсинг укажет не только сервис, который тормозит, но и конкретный метод/функцию в которой достигается максимальная задержка.
Из популярных инструментов для трейсинга обычно используют OpenTelemetry, Jaeger, Tempo, OpenTracing.
Чтобы понять, как работает трейсинг, взглянем, как он устроен под капотом. У каждого трейса есть уникальный TraceID в формате UUIDv4. Этот ID передается от сервиса к сервису в заголовках запроса.
Трейс состоит из спанов (Spans). Спан — это единичная операция, может быть представлена в виде API-вызова, SQL-запроса к базе данных.
Спан обычно содержит в себе следующую информацию:
SpanID
TraceID
Имя спана
ParentID чтобы собирать из спанов полноценный трейс
Время начала и конца спана
Дополнительный атрибуы в виде пары ключ-значение
В настоящее время OpenTelemetry стал стандартом разработки. Это целая экосистема, объединяющяя в себе протоколы и набор инструментов для генерации, сбора и экспорта данных телеметрии: трейсов, метрик и логов. OpenTelemetry заслуживает отдельной статьи, но здесь мы сосредоточимся на его роли в контексте трейсинга, подробнее прочитать про OTel можно в официальной документации.
Чтобы успешно разобраться в том, как правильно интегрировать трейсинг в ваши проекты, необходимо разобраться в основных компонентах трейсинга.
Tracer Provider - это главный компонент трейсинга, чья основная задача создавать и выдавать экземпляры трассировщиков. В типичном сценарии провайдер настраивается всего один раз при старте приложения и его жизненный цикл равен жизненному циклу приложения.
При создании провайдера фактически собирается конфигурация всей системы мониторинга, подключая к нему все необходимые компоненты.
Пример настройки трейсинг провайдера
func newTracerProvider(endpoint, serviceName string) (*trace.TracerProvider, error) { client := otlptracehttp.NewClient( otlptracehttp.WithEndpoint(cendpoint), otlptracehttp.WithInsecure(), ) exporter, err := otlptrace.New(context.Background(), client) if err != nil { return nil, err } res := resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String(serviceName), ) tracerProvider := trace.NewTracerProvider( trace.WithBatcher( exporter, trace.WithBatchTimeout(5*time.Second), ), trace.WithResource(res), ) return tracerProvider, nil }
Можно заметить, что в основе Tracer Provider'a лежат компоненты Exporter и Resource.
Exporter отвечает за передачу собранных трейсов из приложения во внешнюю систему для их хранения и анализа. Они переводят внутренние данные OpenTelemetry в формат, который понимает конкретный backend. В данном примере за backend возьмем Jaeger.
В чем здесь плюс?
Код становится независимым от выбранного решения. Поменяв всего одну строчку в конфиге, можно спокойно переехать с того же Jaeger на Zipkin или Tempo.
Resource это метадата, которая идентифицирует инициатора создания трейса.
Tracer Provider создан, трейсы собираются и отправляются в Jaeger, но полной картины execution path все еще нет, не хватает важного компонента Propagator.
Propagation - это механизм, который позволяет связать воедино действия, происходящие в разных микросервисах. Без него каждый сервис видел бы только свой локальный кусок работы, а весь трейс рассыпался бы на несвязанные фрагменты.
func newPropagator() propagation.TextMapPropagator { return propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, ) }
По сути, это процесс склейки пути запроса, когда он проходит через множество систем, баз данных и очередей.
Процесс состоит из двух фаз, которые происходят на границах сервисов
Injection
Когда сервис А отправляет запрос сервису Б, он собирает ID текущей трассировки и другие данные (контекст) в заголовки протокола (например, HTTP-заголовки).
func Inject(ctx context.Context, req *http.Request) { otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) }
Extraction
Когда сервис Б получает запрос, он разбирает эти заголовки и создает свои спаны как продолжение уже существующей цепочки.
func Extract(req *http.Request, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { ctx := otel.GetTextMapPropagator().Extract(req.Context(), propagation.HeaderCarrier(req.Header)) return otel.Tracer("default_tracer").Start(ctx, name, opts...) }
После того, как инициализировали трейсинг и научились его собирать, взглянем на диаграммы. Будем пользоваться Jaeger UI.

Диаграмма представляет из себя waterfall-диаграмму, где каждый ее уровень отображает время жизни каждого спана. Таким образом можно легко определять, какой из компонентов тормозит.
Теперь наш разработчик сможет отследить через трейсы, что конкретно повлияло на замедление системы.
Итак, разработчик нашел, в каком месте тормозит приложение, но вот еще один нюанс, АПИ отвечает медленно при определенных параметрах запроса, которые не показаны в трейсе. В такой ситуации выручают логи приложения.
В настоящее время логирование это не просто stdout в произвольном формате, а структурированный подход. Хорошим форматом для вывода логов является JSON из-за своей структуры.
Типичный пример лога
{ "time": "2026-01-20T12:00:01Z", "level": "ERROR", "msg": "failed to process payment", "user_id": "12345", "trace_id": "a1b2c3d4...", "span_id": "e5f6g7h8..." }
В распределенных системах очень важно, чтобы трейс был связан с логом. Такая корреляция помогает быстро искать ошибки, а в следствии их устранять. Инструментарий OpenTelemetry позволяет получать TraceID из спана для его вывода в лог.
С помощью log/slog и go.opentelemetry.io/otel/trace создадим обработчик логов, который автоматически достает TraceID и SpanID из контекста.
type OtelHandler struct { slog.Handler } func (h *OtelHandler) Handle(ctx context.Context, r slog.Record) error { if ctx == nil { return h.Handler.Handle(ctx, r) } span := trace.SpanFromContext(ctx) if span.SpanContext().IsValid() { r.AddAttrs( slog.String("trace_id", span.SpanContext().TraceID().String()), slog.String("span_id", span.SpanContext().SpanID().String()), ) } return h.Handler.Handle(ctx, r) }
Теперь достаточно пробрасывать контекст при логировании, обработчик сам дополнит лог ID спана и трейса.
Помимо инструментов OpenTelemetry и форматирования через slog, эти логи нужно транспортировать, индексировать и отображать, иначе смысла в этих логах немного.
Из популярных инструментов есть ELK стэк, также довольно популярен PLG (Promtail + Loki + Grafana). Но у всех этих инструментов одна логика.
Emission
Процесс генерации, когда приложение пишет в stdout
Collection
Рядом с приложением находится агент, который следит за файлами логов или слушает поток данных и мгновенно перехватывает новые записи.
Ingestion and Storage
Агент отправляет логи в базу данных, где далее они индексируются для быстрого поиска в последующем
Visualization
Конечный продукт, который помогает удобно пользоваться поиском и находить нужные логи.
Подводя итоги, важно понимать, что Observability это не что-то модное, а необходимость в больших распределенных системах. Когда система не похожа на черный ящик, то возникает меньше вопросов к ней, а значит быстрее решаются проблемы, что в следствии ведет к меньшим потерям бизнеса.
В то же время, не стоит зацикливаться на Observability, так как чрезмерное количество трейсов и логов может привести к огромным нагрузкам на базы данных, что тоже не есть хорошо.