Трассировка на Go

от автора

Всем привет, этой мой первый пост на данной платформе, прошу любить и жаловать.

Трассировка — это важный инструмент для мониторинга и диагностики микросервисов. Она позволяет понять, как запросы проходят через систему, где возникают узкие места, и как взаимодействуют различные компоненты приложения. В этой статье я расскажу про свой опыт, как интегрировал трассировку в сервис на Go, использующий GORM.

1. Основы трассировки с OpenTelemetry

OpenTelemetry — это популярная платформа для сбора, обработки и экспорта метрик, логов и трассировок. Пример настройки OpenTelemetry:

exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(traceConfig.Url))) if err != nil {    ... }  tp := tracesdk.NewTracerProvider(     tracesdk.WithBatcher(exp),     tracesdk.WithResource(resource.NewWithAttributes(         semconv.SchemaURL,         semconv.ServiceNameKey.String(traceConfig.ServiceName),         attribute.String("environment", "production"),         attribute.Int64("ID", 1),     )), )

2. Интеграция с GORM

Для интеграции трассировки с GORM важно перехватывать события до и после выполнения SQL-запросов. Это позволяет собирать информацию о времени выполнения запросов, количестве затронутых строк и возможных ошибках.

Пример плагина для GORM:

 func MiddleWareGormTrace() gorm.Plugin { return &GormTracing{} }  type GormTracing struct { }  func (g *GormTracing) Name() string { return "" }  func (p *GormTracing) Initialize(db *gorm.DB) error { tracer := tracer.TraceClient  if tracer == nil || !tracer.IsEnabled { return nil } db.Callback().Create().Before("gorm:before_create").Register("gormotel:before_create", p.before(tracer)) db.Callback().Query().Before("gorm:before_query").Register("gormotel:before_query", p.before(tracer)) db.Callback().Delete().Before("gorm:before_delete").Register("gormotel:before_delete", p.before(tracer)) db.Callback().Update().Before("gorm:before_update").Register("gormotel:before_update", p.before(tracer)) db.Callback().Row().Before("gorm:before_row").Register("gormotel:before_row", p.before(tracer)) db.Callback().Raw().Before("gorm:before_raw").Register("gormotel:before_raw", p.before(tracer)) db.Callback().Create().After("gorm:after_create").Register("gormotel:after_create", p.after) db.Callback().Query().After("gorm:after_query").Register("gormotel:after_query", p.after) db.Callback().Delete().After("gorm:after_delete").Register("gormotel:after_delete", p.after) db.Callback().Update().After("gorm:after_update").Register("gormotel:after_update", p.after) db.Callback().Row().After("gorm:after_row").Register("gormotel:after_row", p.after) db.Callback().Raw().After("gorm:after_raw").Register("gormotel:after_raw", p.after)  return nil }   func (p *PluginTrace) before(tracer *tracer.Tracer) func(*gorm.DB) {     return func(db *gorm.DB) {         ctx, span := tracer.CreateSpan(db.Statement.Context, "[DB]")         db.InstanceSet("otel:span", span)         db.Statement.Context = ctx     } }  func (p *PluginTrace) after(db *gorm.DB) {     if spanVal, ok := db.InstanceGet("otel:span"); ok {         if span, ok := spanVal.(trace.Span); ok {             defer span.End()              span.SetAttributes(                 attribute.String(span2.AttributeDBStatement, db.Statement.SQL.String()),                 attribute.String(span2.AttributeDBTable, db.Statement.Table),                 attribute.Int64(span2.AttributeDbRowsAffected, db.RowsAffected),             )              if db.Error != nil {                 span.RecordError(db.Error)                 span.SetStatus(trace2.StatusCodeError, db.Error.Error())             }         }     } }

Вот пример как внедрить в GORM:

dbClient, err := database.GetGormConnection( database.DbConfig{ Driver:             database.MySql, Host:               app.dbConfig.Host, User:               app.dbConfig.User, Password:           app.dbConfig.Password, Db:                 app.dbConfig.Db, Port:               app.dbConfig.Port, SslMode:            false, Logging:            app.dbConfig.Logging, MaxOpenConnections: app.dbConfig.MaxOpenConnections, MaxIdleConnections: app.dbConfig.MaxIdleConnections, }, )   if err != nil {  return err } dbClient.Use(gormtracing.MiddleWareGormTrace())

3. Обработка HTTP-запросов

Трассировка HTTP-запросов позволяет отслеживать путь запроса через все слои приложения. Для этого важно использовать middleware, который будет создавать спан для каждого входящего запроса и записывать важные метаданные. При этом не стоит использовать данный middleware на все запросы, на моем горьком опыте были сервисы которые записывали health-чекеры.

Пример middleware для Gin:

func (t *Tracer) MiddleWareTrace() gin.HandlerFunc {     return func(c *gin.Context) {         if t == nil || !t.cfg.IsTraceEnabled {             c.Next()             return         }          parentCtx, span := t.CreateSpan(c.Request.Context(), "["+c.Request.Method+"] "+c.FullPath())         defer span.End()          c.Request = c.Request.WithContext(parentCtx)         c.Next()          // Обработка ошибок для сервисов использующих sdk         excep := c.Keys["exception"]         switch v := excep.(type) {         case *exception.AppException:             span.SetAttributes(attribute.Int(span2.AttributeRespHttpCode, v.Code))             if v.Error != nil {                 span.SetAttributes(attribute.String(span2.AttributeRespErrMsg, v.Error.Error()))             }         default:             span.SetAttributes(attribute.Int(span2.AttributeRespHttpCode, c.Writer.Status()))         }     } }

Вот пример как внедрить MW

v1 := router.Group("/banner/v1") v1.Use(tracer.MiddleWareTrace())

Вот полный код клиента трассировки:

Данную переменную var TraceClient *Tracer вытащил в глобал, только потому, что есть реализация HTTP-Builder-а , где на каждый запрос я создаю свой спан. У нас в Go сервисах реализована слоистая архитектура, и пришлось бы данного клиента прокидывать в каждый слой для трассировки http запросов в сторонние сервис

package tracer  import ( "bytes" "context" "fmt" "github.com/gin-gonic/gin" "github.com/gookit/goutil/netutil/httpctype" "github.com/gookit/goutil/netutil/httpheader" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/jaeger" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" tracesdk "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.7.0" trace2 "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" "io" "net/http" "strings" )  var TraceClient *Tracer  const AttributeReqBody = "request.body"  const ( AttributeRespHttpCode = "http.status_code" AttributeRespErrMsg   = "error.message" )  type TraceConfig struct { IsTraceEnabled    bool   `mapstructure:"TRACE_IS_ENABLED"` Url               string `mapstructure:"TRACE_URL"` ServiceName       string `mapstructure:"TRACE_SERVICE_NAME"` IsHttpBodyEnabled bool   `mapstructure:"TRACE_IS_HTTP_BODY_ENABLED"` }  type Tracer struct { tp          *tracesdk.TracerProvider cfg         *TraceConfig IsEnabled   bool ServiceName string }  // InitTraceClient - создание клиента трассировки func InitTraceClient() (*Tracer, error) { t := &Tracer{} // config init if err := t.initTraceConfig(); err != nil { return nil, err }  if !t.cfg.IsTraceEnabled { return t, nil }  // Create the Jaeger exporter exp, err := jaeger.New( jaeger.WithCollectorEndpoint( jaeger.WithEndpoint(t.cfg.Url), ), )  if err != nil { return nil, err }  tp := tracesdk.NewTracerProvider( //tracesdk.WithSampler(), // Always be sure to batch in production. tracesdk.WithBatcher(exp), // Record information about this application in a Resource. tracesdk.WithResource(resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String(t.cfg.ServiceName), //attribute.String("environment", "development"), //attribute.Int64("ID", 1), )), )  otel.SetTracerProvider(tp) otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { //error handler }))  t.tp = tp TraceClient = t  return t, nil }  // Shutdown - func (t *Tracer) Shutdown(ctx context.Context) error { fmt.Println("shutdown") return t.tp.Shutdown(ctx) }  // InjectHttpTraceId -  записывает  trace id  в запрос, требует  *http.Request func (t *Tracer) InjectHttpTraceId(ctx context.Context, req *http.Request) { otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) }  // MiddleWareTrace -  мидлвар который записывает трассировку // при этом контекст спана записывается в c.Request . В хэндлере рекомендуется передавать ctx.Request.Context() в слой ниже, или другую функцию func (t *Tracer) MiddleWareTrace() gin.HandlerFunc { return func(c *gin.Context) { if t == nil || !t.cfg.IsTraceEnabled { c.Next()  return }  parentCtx, span := t.CreateSpan(c.Request.Context(), "["+c.Request.Method+"] "+c.FullPath(), "middleware") defer span.End()  // парсинг body if t.cfg.IsHttpBodyEnabled { // нет смысла копировать тело запроса при наличии файла if !strings.HasPrefix(c.GetHeader(httpheader.ContentType), httpctype.MIMEDataForm) { bodyBytes, _ := io.ReadAll(c.Request.Body) c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))  span.SetAttributes(attribute.String(AttributeReqBody, string(bodyBytes))) } }  c.Request = c.Request.WithContext(parentCtx) c.Next()  // парсинг ошибок { excep := c.Keys["exception"]  switch v := excep.(type) { case *exception.AppException: span.SetAttributes(attribute.Int(AttributeRespHttpCode, v.Code)) if v.Error != nil { span.SetAttributes(attribute.String(AttributeRespErrMsg, v.Error.Error())) } default: span.SetAttributes(attribute.Int(AttributeRespHttpCode, c.Writer.Status())) } } } }  // CreateSpan - Создает родительский спан,и возвращает контекст, этот контекст нужен для дочернего спана. // В случае если в ctx нет контекста родителя то создается контекст родителя // Не забыть вызывать span.End() func (t *Tracer) CreateSpan(ctx context.Context, name string, fun string) (context.Context, trace2.Span) { if t == nil || t.tp == nil { return context.Background(), noop.Span{} }  return t.tp.Tracer(t.ServiceName).Start(ctx, name) }  // CreateSpanWithCustomTraceId -  экспериментальный метод, создаем спан на основе кастомного трайс айди func (t *Tracer) CreateSpanWithCustomTraceId(ctx context.Context, traceId, name string) (context.Context, trace2.Span, error) { tId, err := trace2.TraceIDFromHex(traceId)  if err != nil { return nil, noop.Span{}, err }  spanContext := trace2.NewSpanContext(trace2.SpanContextConfig{ TraceID: tId, })  ctx1 := trace2.ContextWithSpanContext(ctx, spanContext) ctx1, span := t.tp.Tracer(t.ServiceName).Start(ctx1, name)  return ctx1, span, nil }  // initTraceConfig -  инициализирует конфиг трассировки, читает  из файла  .env переменки func (t *Tracer) initTraceConfig() error { if err := config.ReadEnv(); err != nil { return err }  traceCfg := &TraceConfig{} err := config.InitConfig(traceCfg)  if err != nil { return err }  t.cfg = traceCfg t.ServiceName = traceCfg.ServiceName t.IsEnabled = traceCfg.IsTraceEnabled  return nil } 

4. Какие лучшие практики для продакшн-среды выявил:

Когда трассировка интегрирована и работает, важно учитывать следующие моменты для использования в продакшн-среде:

  • Минимизируйте нагрузку: Используйте батчинг и асинхронную отправку данных, чтобы минимизировать влияние на производительность приложения.

  • Соблюдайте конфиденциальность данных: Не записывайте чувствительные данные в атрибуты или логи трассировки.

  • Регулярный мониторинг: Следите за объемом данных, отправляемых на трассировку, чтобы избежать избыточной генерации данных и переполнения системы мониторинга.

  • Установите ограничения на количество данных: Ограничьте глубину и количество трассировок, особенно для высоконагруженных сервисов. На продакшн-среде мы реализовали запись трейсов 1 из 5. То есть только 1 трейс из 5 запишется в базу, остальные игнорируем.

Заключение

Трассировка — это мощный инструмент для мониторинга микросервисов. Данным инструментом повысили наблюдаемость наших сервисов, но и упростили диагностику и устранение неполадок.

P.S. В создание спанов главное правильно передавать контексты, иначе в админке увидите не точную картину ваших трейсов, не будет вложенности. Я к сожалению потратил очень много времени чтобы выявить это.

Данный пост написан для обмена опытом, мой опыт — это лишь один из возможных подходов, и приглашаю Вас к конструктивному обсуждению.


ссылка на оригинал статьи https://habr.com/ru/articles/841380/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *