40. Distributed Tracing и корреляция запросов
Распределённая трассировка под микроскопом
«Самым эффективным инструментом отладки по-прежнему остается глубокое размышление в сочетании с разумно расставленными операторами печати — но когда ваша система разбросана по сотне серверов через весь континент, вам нужен способ помочь этим записям отыскать друг друга».
— Брайан Керниган
Представьте почтовую службу, доставляющую заказное международное отправление. Если посылка теряется или задерживается на две недели, отправителю мало пользы от сводной статистики: «в сентябре 98% писем доставлены вовремя». Ему не поможет и ворох разрозненных журналов пятидесяти почтовых отделений разных стран. Всё, что ему нужно — это трек-номер (Tracking Number), наклеенный на конверт в момент отправки. Просканировав этот единый номер, почтовая система выстраивает хронологическую ленту пути: когда письмо покинуло терминал в Нью-Йорке, через какой транзитный хаб в Амстердаме проследовало, сколько часов пролежало на таможне и в какой конкретно момент курьер нажал кнопку дверного звонка.
В распределённых микросервисных системах роль такого универсального трек-номера выполняет распределённая трассировка (Distributed Tracing).
Когда высокоуровневые метрики сообщают, что задержка $p99$ внезапно выросла с 80 до 650 мс, а процент ошибок поднялся до 2.5%, они лишь констатируют факт катастрофы (39. Observability в архитектуре. Metrics, Logs, Traces). Логи отдельных сервисов тонут в тысячах строк, фиксируя лишь локальные микрозадержки. Где именно запрос провёл лишние полсекунды? В ожидании мьютекса в Go-рантайме, в медленном SQL-запросе с Full Table Scan или на этапе TLS-хендшейка к внешнему платёжному шлюзу?
В этой лекции мы разберём внутреннюю анатомию трейсов и спанов, стандарты передачи контекста сквозь сетевой стек (W3C Trace Context), реализуем промышленную инструментацию на базе OpenTelemetry в Go, построим корреляцию с log/slog и детально исследуем механику сэмплирования, защищающую наши сервера от перегрузки самой телеметрией.
Trace, Span и контекст
Архитектурно распределённая трассировка представляет собой направленный ациклический граф (DAG), вершинами которого являются логические операции, а рёбрами — причинно-следственные связи между ними:
flowchart TD
subgraph Trace["Distributed Trace (Trace ID: 4bf92f3577b34da6a3ce929d0e0e4736)"]
SpanA["Span A: API Gateway<br>HTTP POST /order (120ms)<br>Parent: none (Root Span)"]
SpanB["Span B: Order Service<br>gRPC CreateOrder (105ms)<br>Parent: Span A"]
SpanC["Span C: User Service<br>gRPC ValidateUser (25ms)<br>Parent: Span B"]
SpanD["Span D: Inventory Service<br>gRPC CheckStock (15ms)<br>Parent: Span B"]
SpanE["Span E: PostgreSQL Repo<br>SQL INSERT INTO orders (60ms)<br>Parent: Span B"]
end
SpanA --> SpanB
SpanB --> SpanC
SpanB --> SpanD
SpanB --> SpanE
classDef root fill:#1e1b4b,stroke:#818cf8,stroke-width:2px,color:#f8fafc;
classDef child fill:#131d33,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
classDef leaf fill:#0f172a,stroke:#10b981,stroke-width:2px,color:#f8fafc;
class SpanA root;
class SpanB child;
class SpanC,SpanD,SpanE leaf;
В терминах стандарта OpenTelemetry эта модель состоит из следующих ключевых сущностей:
- Trace (Трейс): Полная история прохождения одного внешнего запроса сквозь всю экосистему сервисов. Трейс идентифицируется глобально уникальным 128-битным числом —
Trace ID(в hex-формате: 32 символа). - Span (Спан): Атомарный кирпичик трейса. Это именованный непрерывный отрезок времени, представляющий одну логическую операцию (обработку HTTP-запроса, выполнение транзакции в БД, вызов внешнего RPC-метода или обработку сообщения из очереди Kafka).
- Span Context (Контекст спана): Минимальный набор метаданных, необходимый для идентификации спана в распределённой сети:
Trace ID: Общий для всех спанов в рамках одной цепочки вызовов.Span ID: Уникальный 64-битный идентификатор текущего спана (16 hex-символов).Parent Span ID: Идентификатор родительского спана, породившего текущую операцию (у корневого Root Span он пуст).Trace Flags: 8-битная битовая маска, определяющая параметры трейсинга (младший бит01указывает, что трейс сэмплирован —Sampled).
- Атрибуты (Attributes) и События (Events):
- Attributes: Типизированные пары «ключ-значение», описывающие операцию (
http.method = "POST",db.system = "postgresql",order.id = "ord-9921"). - Events: Временные отметки (аннотации) внутри спана с сообщением и временным штампом, фиксирующие моментальные вехи (например,
"cache_miss","serialization_start").
- Attributes: Типизированные пары «ключ-значение», описывающие операцию (
- Status (Статус): Результат выполнения (
Unset,Ok,Error). При возникновении паники или возврате ошибки спан помечается статусом ошибки с записью стектрейса черезspan.RecordError(err).
Визуально в интерфейсах систем визуализации (Jaeger, Grafana Tempo) трейс представляется в виде каскадной диаграммы Ганта (Waterfall view), где мгновенно виден критический путь (Critical Path) запроса:
gantt
title Каскадное исполнение трейса (Waterfall View)
dateFormat X
axisFormat %s мс
section Gateway
HTTP POST /orders (Span A) :active, a1, 0, 120
section Order Service
gRPC CreateOrder (Span B) :crit, b1, 10, 115
section Downstream
ValidateUser (Span C) :c1, 15, 40
CheckStock (Span D) :d1, 42, 57
SQL INSERT orders (Span E) :crit, e1, 60, 110
Как контекст путешествует между сервисами
Чтобы распределённая трассировка работала, Trace ID и Span ID должны бесшовно передаваться через границы процессов, сетевых сокетов и протоколов. Этот процесс называется контекстной пропагацией (Context Propagation).
Исторически существовал зоопарк несовместимых проприетарных форматов: B3 (разработанный Twitter для Zipkin), Jaeger Native (uber-trace-id), AWS X-Ray, Datadog. В современной архитектуре стандартом де-юре и де-факто стал открытый стандарт консорциума W3C Trace Context, поддерживаемый всеми облачными провайдерами и рантаймами.
Спецификация W3C Trace Context
Стандарт определяет два ключевых HTTP-заголовка:
-
traceparent(обязательный заголовок):
Имеет строго фиксированный формат из 4 полей, разделённых дефисом:TEXT traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^ версия Trace ID Parent Span ID флаги- Версия (Version):
00(текущая версия стандарта). - Trace ID: 32 hex-символа (16 байт).
- Parent Span ID: 16 hex-символов (8 байт), представляющий ID вызывающего спана.
- Trace Flags: 2 hex-символа (1 байт). Флаг
01означает, что вызывающая сторона приняла решение сохранить этот трейс (sampled).
- Версия (Version):
-
tracestate(опциональный заголовок):
Список пар «вендор-состояние» (rojo=123,congo=456), позволяющий пробрасывать специфичные метаданные различных систем трассировки сквозь гетерогенные прокси без потери контекста. -
W3C Baggage (
baggage):
Механизм сквозной передачи произвольных бизнес-метрик сквозь всю цепочку вызовов (например,user_tier=enterprise,region=eu-central). В отличие от атрибутов спана, которые остаются локальными, элементы Baggage автоматически сериализуются в HTTP-заголовки и доезжают до самого последнего сервиса в цепочке.
В рантайме Go за сериализацию и парсинг заголовков отвечает абстракция propagation.TextMapPropagator:
package tracing
import (
"context"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
// Инжектирование контекста трассировки в исходящий HTTP-запрос
func InjectTraceContext(ctx context.Context, req *http.Request) {
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
}
// Извлечение контекста трассировки из входящего HTTP-запроса
func ExtractTraceContext(r *http.Request) context.Context {
return otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
}
Инструментация Go-сервиса с OpenTelemetry
OpenTelemetry (OTel) предоставляет четкое разделение между API (стабильные интерфейсы для расстановки спанов без привязки к вендорам) и SDK (конкретная реализация, управляющая аллокациями, батчингом, сэмплированием и сетевым экспортом).
1. Инициализация TracerProvider и OTLP-экспортёра
Входная точка инициализации трассировки настраивается в main.go и обеспечивает корректный graceful shutdown при остановке сервиса:
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)
func initTracer(ctx context.Context, serviceName string) (*sdktrace.TracerProvider, error) {
// Создаем OTLP-экспортер, отправляющий трейсы по gRPC в Jaeger / OpenTelemetry Collector
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("jaeger:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("failed to create OTLP trace exporter: %w", err)
}
// Описываем наш сервис в соответствии со спецификацией Semantic Conventions
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceNameKey.String(serviceName),
semconv.ServiceVersionKey.String("1.0.0"),
semconv.DeploymentEnvironmentKey.String("production"),
),
)
if err != nil {
return nil, fmt.Errorf("failed to create resource: %w", err)
}
// Настраиваем TracerProvider: фоновый батчер + сэмплер
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter,
sdktrace.WithBatchTimeout(5*time.Second),
sdktrace.WithMaxExportBatchSize(512),
),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.AlwaysSample()), // Для dev/staging; в prod настраивается Head-based сэмплер!
)
// Регистрируем глобальный провайдер и стандартные W3C пропогаторы
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{}, // W3C traceparent
propagation.Baggage{}, // W3C baggage
))
return tp, nil
}
2. Инструментация HTTP-сервера и роутера
Библиотека go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp предоставляет готовую middleware-обертку для стандартного мультиплексора или любого совместимого HTTP-роутера:
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
tp, err := initTracer(ctx, "order-service")
if err != nil {
log.Fatalf("Init tracer failed: %v", err)
}
defer func() {
// Обязательный сброс оставшихся в буфере спанов при завершении работы процесса
if err := tp.Shutdown(context.Background()); err != nil {
log.Printf("Error shutting down tracer: %v", err)
}
}()
mux := http.NewServeMux()
mux.HandleFunc("/order", CreateOrderHandler)
// otelhttp оборачивает весь мультиплексор:
// 1. Автоматически извлекает W3C заголовки из r.Header
// 2. Создает Root или Child Span с именем операции "server"
// 3. Фиксирует статус ответа и длительность
instrumentedHandler := otelhttp.NewHandler(mux, "order-service")
log.Println("Server listening on :8080...")
http.ListenAndServe(":8080", instrumentedHandler)
}
3. Создание спанов в прикладной бизнес-логике
Внутри хендлера разработчик может создавать дочерние спаны для детализации ресурсоемких операций:
package main
import (
"context"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
type Order struct {
ID string
Amount int64
}
func CreateOrderHandler(w http.ResponseWriter, r *http.Request) {
// Контекст запроса уже обогащен родительским спаном от otelhttp
ctx := r.Context()
tracer := otel.Tracer("order-service")
order := Order{ID: "ord-1029", Amount: 5000}
// Создаём дочерний спан для сохранения заказа в базу данных
ctx, span := tracer.Start(ctx, "SaveOrderToDB",
trace.WithSpanKind(trace.SpanKindInternal),
)
defer span.End()
if err := saveOrder(ctx, order); err != nil {
// Фиксация ошибки в спане
span.SetStatus(codes.Error, "failed to persist order")
span.RecordError(err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
// Обогащаем спан семантическими бизнес-атрибутами
span.SetAttributes(
attribute.String("order.id", order.ID),
attribute.Int64("order.amount", order.Amount),
)
w.WriteHeader(http.StatusCreated)
}
func saveOrder(ctx context.Context, order Order) error {
// Симуляция обращения к базе данных с передачей контекста
return nil
}
4. Инструментация gRPC сервисов
Для gRPC-взаимодействия OpenTelemetry предоставляет перехватчики и хендлеры: otelgrpc.NewServerHandler() для сервера и otelgrpc.NewClientConn() (или современный otelgrpc.NewClientHandler()) для клиента через пакет go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc:
import (
"google.golang.org/grpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
// Инструментация gRPC-сервера
func StartGRPCServer() *grpc.Server {
// В современных версиях OTel рекомендуется otelgrpc.NewServerHandler(),
// но интерцепторы остаются повсеместно распространенным стандартом
s := grpc.NewServer(
grpc.StatsHandler(otelgrpc.NewServerHandler()),
grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
)
return s
}
// Инструментация клиентского gRPC-соединения
func DialOrderService(target string) (*grpc.ClientConn, error) {
return grpc.Dial(target,
grpc.WithInsecure(),
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
)
}
Корреляция логов и трейсов
Изолированный трейс показывает форму графа и задержки, но не объясняет внутренних причин сбоя. Изолированный лог содержит подробное сообщение об ошибке, но не дает представления о том, какой пользователь инициировал транзакцию.
Связывание логов и трейсов через единый Trace ID замыкает контур наблюдаемости. Для этого в Go создается middleware, который извлекает активный спан из контекста и внедряет идентификаторы трассировки в контекстный логгер log/slog:
package middleware
import (
"context"
"log/slog"
"net/http"
"go.opentelemetry.io/otel/trace"
)
type contextKey string
const loggerKey contextKey = "slog_logger"
// LoggingMiddleware связывает активный трейс OpenTelemetry со структурированным логгером slog
func LoggingMiddleware(baseLogger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
span := trace.SpanFromContext(r.Context())
logger := baseLogger
// Если запрос трассируется, добавляем TraceID и SpanID во все последующие логи
if span.IsRecording() {
traceID := span.SpanContext().TraceID().String()
spanID := span.SpanContext().SpanID().String()
logger = logger.With(
slog.String("trace_id", traceID),
slog.String("span_id", spanID),
)
}
// Кладём логгер в контекст запроса
ctx := context.WithValue(r.Context(), loggerKey, logger)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// FromContext извлекает настроенный логгер из контекста
func FromContext(ctx context.Context) *slog.Logger {
if logger, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
return logger
}
return slog.Default()
}
Теперь каждый вызов slog.InfoContext(ctx, ...) автоматически включает trace_id и span_id. Это позволяет дежурному инженеру в интерфейсе Grafana перейти от одного лога с ошибкой прямо в окно визуализации графа трейса всего за один клик мыши!
Mechanical Sympathy: влияние трейсинга на рантайм Go
Трассировка — самый тяжелый из всех столпов Observability. Если метрика инкрементирует 64-битное число в памяти, а лог форматирует одну строку, то спан — это сложный составной объект с временными метками, коллекциями атрибутов, синхронизацией и сетевым вводом-выводом.
1. Анатомия аллокаций в куче (Heap Allocations)
- Каждый вызов
tracer.Start(ctx, ...)создает новую структуруsdktrace.span, выделяет срез под атрибутыattribute.KeyValueи оборачиваетcontext.Contextновым интерфейсным узломvalueCtx. - Если сервис обрабатывает 50 000 RPS и на каждый запрос генерирует по 10 спанов, рантайм Go вынужден выделять и очищать 500 000 объектов в секунду!
- Это резко увеличивает частоту срабатывания сборщика мусора (GC Stop-The-World фазы и фоновая маркировка CPU), приводя к непредсказуемым выбросам хвостовой латентности ($p99$).
2. Фоновый экспорт через BatchSpanProcessor
- OpenTelemetry SDK предотвращает блокировку сетевых хендлеров с помощью фонового батчера (
BatchSpanProcessor). - При вызове
span.End()спан отправляется в неблокирующий кольцевой буфер в памяти (Go-канал с ограниченной емкостью). - Отдельная системная горутина накапливает спаны в пачки (
MaxExportBatchSize = 512) и выполняет gRPC-вызов по протоколу OTLP. Если сеть до коллектора деградирует, буфер переполняется, и спаны молча отбрасываются (с инкрементом системного счетчикаdropped_spans_total), не замедляя клиентский трафик.
3. Стратегии сэмплирования (Head-based vs Tail-based)
Сэмплирование — фундаментальный архитектурный рычаг, позволяющий срезать 90–99% нагрузки от телеметрии без потери видимости критических проблем:
flowchart TD
subgraph HeadBased["Head-Based Sampling (На входе в систему)"]
H_Req["Входящий запрос"] --> H_Decide{"Случайное число < 5%?"}
H_Decide -->|"Да"| H_Sample["Заголовок: traceparent: ...-01<br>Трассируются ВСЕ сервисы"]
H_Decide -->|"Нет"| H_Drop["Заголовок: traceparent: ...-00<br>Спаны НЕ создаются"]
end
subgraph TailBased["Tail-Based Sampling (На уровне OTel Collector)"]
T_Req["100% запросов поступают в коллектор"] --> T_Buf["Кольцевой буфер памяти (30 сек)"]
T_Buf --> T_Eval{"Запрос завершился ошибкой (5xx)<br>ИЛИ latency > 500ms?"}
T_Eval -->|"Да"| T_Save["Сохранить трейс в Jaeger / Tempo"]
T_Eval -->|"Нет"| T_Discard["Отбросить трейс"]
end
classDef head fill:#1e1b4b,stroke:#818cf8,stroke-width:2px,color:#f8fafc;
classDef tail fill:#131d33,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
class H_Req,H_Decide,H_Sample,H_Drop head;
class T_Req,T_Buf,T_Eval,T_Save,T_Discard tail;
-
Head-Based Sampling (Сэмплирование в точке входа):
Решение о трассировке принимается на самом первом шлюзе (API Gateway) по детерминированному алгоритму (например, хешированиеTraceIDпо модулю).
Если флаг установлен, дочерние сервисы уважают его через правилоParentBased:Go // Конфигурация: 5% трафика трассируется, дочерние сервисы следуют за родителем sampler := sdktrace.ParentBased( sdktrace.TraceIDRatioBased(0.05), )Плюс: Нулевые накладные расходы на отброшенные 95% запросов.
Минус: Редкая критическая ошибка или всплеск задержки, случившийся в неотсэмплированном запросе, будет безвозвратно потерян. -
Tail-Based Sampling (Сэмплирование по результату):
Все сервисы трассируют 100% трафика, но спаны отправляются на локальный агент OpenTelemetry Collector. Коллектор собирает все спаны одного трейса в буфере в течение 10–30 секунд и принимает решение: если в цепочке был хотя бы один HTTP 500, спан со статусомErrorили общее время превысило $p99$, трейс сохраняется в долгосрочное хранилище (Jaeger/Tempo). Все рядовые быстрые запросы отбрасываются.
Плюс: 100% гарантия фиксации всех багов и аномалий.
Минус: Требует выделения существенной памяти под буферы OpenTelemetry Collector.
Типичные архитектурные проблемы и их решения
-
Обрыв цепочки контекста (Broken Context Chain):
Самая распространенная ошибка в Go — создание нового фонового контекстаcontext.Background()или запуск горутины без проброса контекста:Go // КАТАСТРОФА: обрыв цепочки трейсинга! go func() { sendEmail(context.Background(), order) // Создаст изолированный новый трейс! }() // ПРАВИЛЬНО: проброс или отделение с сохранением метаданных go func(ctx context.Context) { // Если горутина должна жить дольше HTTP-запроса, используем context.WithoutCancel (Go 1.21+) asyncCtx := context.WithoutCancel(ctx) sendEmail(asyncCtx, order) }(r.Context())Аналогично, если исходящий HTTP-клиент создан без
otelhttp.NewTransport(http.DefaultTransport), W3C-заголовки не попадут в сокет, и downstream-сервис начнет новый Root Span. -
Гранулярный спам (Too Many Fine-Grained Spans):
Оборачивание каждой элементарной функции (fmt.Sprintf, парсинг строки, проверка условия) в отдельный спан. Это создает визуальный мусор в UI и колоссальный оверхед по памяти.
Правило архитектора: Спаны создаются на границах компонентов (сетевые вызовы, шина сообщений, дисковые операции) и вокруг крупных логических этапов бизнес-процесса. -
Утечка спанов при паниках и таймаутах:
Если в методе забытdefer span.End(), спан никогда не будет закрыт и отправлен в коллектор. Всегда вызывайтеdefer span.End()сразу после вызоваtracer.Start(). При этом важно убедиться, что экспортёр успевает отправить данные до завершения процесса (graceful shutdown с вызовомtp.Shutdown()/tp.Shutdown(ctx)).
Связь с архитектурой и паттернами
- API Gateway и BFF (35. API Gateway и BFF): Служат естественной точкой входа (Root Span) для всех клиентских обращений, генерируют W3C
traceparentи реализуют политику Head-based сэмплирования. - Clean Architecture (14. Clean Architecture и Dependency Rule):
context.Contextпронизывает все слои системы от контроллеров до доменных сущностей и репозиториев, не нарушая инверсию зависимостей. - SLA, SLO и SLI (4. SLA, SLO, SLI и как они влияют на дизайн): Распределённые трейсы предоставляют математически точные данные для аудита нарушений соглашений об уровне доступности и декомпозиции задержек по сервисам.
Антипаттерны распределённой трассировки
- Хранение секретов и PII в атрибутах спанов:
Передача номеров кредитных карт, паролей пользователей или сессионных JWT вspan.SetAttributes(). Трейсы индексируются и доступны сотням инженеров на дашбордах, что превращает такую телеметрию в критическую уязвимость. - Перегрузка Baggage тяжелыми объектами:
Использование W3C Baggage для передачи крупных JSON-структур или бинарных данных. Заголовкиbaggageпутешествуют во всех исходящих HTTP-запросах, раздувая сетевые заголовки и рискуя превысить лимит веб-серверов на размер HTTP-хидера (обычно 8 КБ). - Отсутствие Graceful Shutdown провайдера:
Завершение приложения поos.Exit(0)без вызоваtp.Shutdown(ctx). Последние спаны, находящиеся в кольцевом буфере памяти, гарантированно теряются, скрывая причины инцидента, вызвавшие падение сервиса.
Итог
Распределённая трассировка превращает слепой туман микросервисной архитектуры в кристально прозрачную карту. Стандарт W3C Trace Context обеспечивает универсальный язык передачи контекста сквозь любые платформы и языки, а OpenTelemetry в Go даёт элегантные абстракции для связывания контекста с сетевым стеком и структурированными логами log/slog.
Осознанное управление сэмплированием и бережное отношение к аллокациям памяти в рантайме гарантируют, что система наблюдения останется легкой, быстрой и надежной опорой для инженеров в моменты самых серьезных испытаний.
Теперь, когда мы научились контролировать надежность и наблюдать за жизненным циклом запросов, мы переходим к обработке непрерывных потоков информации. В следующей лекции: 41. Data Pipeline и потоковая обработка.