Golang трассировка Open Telemetry: как лучше расставлять трассировки, чтобы не загрязнять код?
Добавление трассировки (tracing
) в код — мощный инструмент для анализа производительности и диагностики проблем, но важно сделать это аккуратно, чтобы код оставался читаемым и трассировка была полезной. Вот рекомендации по расстановке трассировок, их именованию и поддержанию чистоты кода.
1. Где расставлять трассировки?
1.1. Высокоуровневые операции
Создавайте основные спаны (Span
) для ключевых операций:
- Обработка входящих запросов (HTTP, gRPC и т. д.).
- Выполнение бизнес-логики.
- Взаимодействие с базами данных, кэшем или другими внешними системами.
Пример:
1 2 3 |
ctx, span := tracer.Start(ctx, "HandleHTTPRequest") defer span.End() |
1.2. Внешние вызовы
Добавляйте трассировку при взаимодействии с внешними системами:
- HTTP-запросы.
- Вызовы RPC (gRPC, Kafka, etc.).
- Чтение/запись в базу данных.
Это помогает отслеживать задержки и выявлять узкие места.
1.3. Длительные или критические операции
Если функция содержит долгие или критически важные блоки, добавляйте вложенные спаны внутри них.
Пример:
1 2 3 4 5 6 |
ctx, span := tracer.Start(ctx, "ProcessData") defer span.End() processPartA(ctx) processPartB(ctx) |
1.4. Места, где могут возникнуть ошибки
Если есть потенциально проблемные участки кода (например, сложные алгоритмы, нестабильные зависимости), добавляйте спаны и события, чтобы фиксировать их.
2. Как минимизировать загрязнение кода трассировкой?
2.1. Используйте обёртки и хелперы
Создайте утилитарные функции для работы с трассировкой.
Пример:
1 2 3 4 |
func StartSpan(ctx context.Context, tracer trace.Tracer, name string) (context.Context, trace.Span) { return tracer.Start(ctx, name) } |
Теперь вместо дублирования вызова Start
:
1 2 3 |
ctx, span := tracer.Start(ctx, "OperationName") defer span.End() |
Вы можете писать:
1 2 3 |
ctx, span := StartSpan(ctx, tracer, "OperationName") defer span.End() |
2.2. Интеграция с middleware
Вместо ручной трассировки в каждом обработчике, интегрируйте трассировку в middleware.
Пример для HTTP:
1 2 3 4 5 6 7 8 9 |
func tracingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx, span := tracer.Start(r.Context(), "HTTP "+r.Method) defer span.End() next.ServeHTTP(w, r.WithContext(ctx)) }) } |
2.3. Инкапсулируйте трассировку в библиотеках
Если вы пишете универсальные функции или используете библиотеки, интегрируйте трассировку там. Например:
- Трассировка запросов к базе данных.
- Трассировка выполнения фоновых задач.
3. Именование спанов
3.1. Следуйте иерархии
Имена должны отражать иерархию операций. Например:
"HTTP GET /api/v1/resource"
для входящих HTTP-запросов."Database Query: SELECT * FROM users"
для запросов к БД."BusinessLogic: CalculateScore"
для выполнения бизнес-логики.
3.2. Будьте информативными
Имена должны быть конкретными:
- Используйте глагол:
"ProcessOrder"
,"FetchData"
. - Указывайте ресурс:
"Database Query: users table"
.
3.3. Добавляйте параметры только если это безопасно
Например:
- Хорошо:
"HTTP POST /api/v1/user/{id}"
. - Плохо:
"HTTP POST /api/v1/user/123"
, так как это может утекать в логи и нарушать безопасность.
4. Что включать в атрибуты?
4.1. Общие атрибуты
Для всех спанов:
http.method
,http.url
для HTTP-запросов.db.system
,db.statement
для запросов к БД.rpc.method
,rpc.service
для RPC.
Пример:
1 2 3 4 5 |
span.SetAttributes( attribute.String("http.method", r.Method), attribute.String("http.url", r.URL.String()), ) |
4.2. Контекстные атрибуты
Добавляйте ключевые параметры, которые помогают понять, что произошло:
- Идентификаторы:
order.id
,user.id
. - Состояния:
task.status
,operation.phase
.
4.3. Ошибки
Если возникает ошибка, фиксируйте её:
1 2 3 |
span.RecordError(err) span.SetStatus(codes.Error, err.Error()) |
5. Уровень детализации трассировки
5.1. Оценивайте производительность
Не добавляйте слишком много спанов для мелких операций, чтобы избежать снижения производительности.
5.2. Делайте трассировку конфигурируемой
Позвольте настраивать уровень детализации через параметры или переменные окружения:
"minimal"
: Только ключевые операции."detailed"
: Полная трассировка.
6. Пример структуры трассировки
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package main import ( "context" "log" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" ) func main() { tracer := otel.Tracer("example-service") // Главный контекст ctx, span := tracer.Start(context.Background(), "MainOperation") defer span.End() HandleRequest(ctx, tracer) } func HandleRequest(ctx context.Context, tracer trace.Tracer) { ctx, span := tracer.Start(ctx, "HandleRequest") defer span.End() ProcessData(ctx, tracer) } func ProcessData(ctx context.Context, tracer trace.Tracer) { ctx, span := tracer.Start(ctx, "ProcessData") defer span.End() // Имитация выполнения log.Println("Processing data...") } |