@opentelemetry/sdk-node → go.opentelemetry.io/otel
@opentelemetry/instrumentation-http → otelhttp
trace.getTracer('my-service') → otel.Tracer("my-service")
span.setAttribute('key', 'val') → span.SetAttributes(attribute.String("key", "val"))
context.with(trace.setSpan(ctx, span)) → контекст в Go (передаётся явно)
mkdir go-tracing && cd go-tracing
go mod init go-tracing
# Устанавливаем OpenTelemetry
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/sdk/trace
go get go.opentelemetry.io/otel/exporters/jaeger
go get go.opentelemetry.io/otel/exporters/stdout/stdouttrace
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
go get go.opentelemetry.io/otel/attribute
go get go.opentelemetry.io/otel/codes
go mod tidy
# Запускаем Jaeger (all-in-one)
docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 14268:14268 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest
internal/tracing/tracing.gopackage tracing
import (
"context"
"fmt"
"log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)
// Config — конфигурация трассировки
type Config struct {
ServiceName string
Environment string
JaegerURL string // http://localhost:14268/api/traces
}
// InitTracer — инициализирует глобальный трейсер
func InitTracer(cfg Config) (*sdktrace.TracerProvider, error) {
// 1. Создаём экспортер в Jaeger
exporter, err := jaeger.New(
jaeger.WithCollectorEndpoint(
jaeger.WithEndpoint(cfg.JaegerURL),
),
)
if err != nil {
return nil, fmt.Errorf("create jaeger exporter: %w", err)
}
// 2. Создаём ресурс (метаданные сервиса)
res := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(cfg.ServiceName),
semconv.DeploymentEnvironment(cfg.Environment),
attribute.String("version", "1.0.0"),
)
// 3. Создаём TracerProvider
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
// Sampler: AlwaysSample (для dev), TraceIDRatioBased (для prod)
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
// 4. Устанавливаем глобальный TracerProvider
otel.SetTracerProvider(tp)
log.Printf("✅ Трассировка настроена: сервис=%s, env=%s", cfg.ServiceName, cfg.Environment)
return tp, nil
}
// Shutdown — корректное завершение (отправка оставшихся спанов)
func Shutdown(ctx context.Context, tp *sdktrace.TracerProvider) error {
if err := tp.Shutdown(ctx); err != nil {
return fmt.Errorf("shutdown tracer: %w", err)
}
log.Println("✅ Трассировка завершена")
return nil
}
// ╔══════════════════════════════════════════════════════════╗
// ║ ХЕЛПЕРЫ ДЛЯ СОЗДАНИЯ СПАНОВ ║
// ╚══════════════════════════════════════════════════════════╝
// StartDBSpan — создаёт спан для запроса к БД
func StartDBSpan(ctx context.Context, operation, query string) (context.Context, Span) {
tracer := otel.Tracer("database")
ctx, span := tracer.Start(ctx, fmt.Sprintf("DB %s", operation))
span.SetAttributes(
attribute.String("db.system", "postgresql"),
attribute.String("db.operation", operation),
attribute.String("db.statement", query),
)
return ctx, &otelSpan{span: span}
}
// StartCacheSpan — создаёт спан для запроса к кэшу
func StartCacheSpan(ctx context.Context, operation, key string) (context.Context, Span) {
tracer := otel.Tracer("cache")
ctx, span := tracer.Start(ctx, fmt.Sprintf("Cache %s", operation))
span.SetAttributes(
attribute.String("cache.system", "redis"),
attribute.String("cache.operation", operation),
attribute.String("cache.key", key),
)
return ctx, &otelSpan{span: span}
}
// StartExternalSpan — создаёт спан для внешнего API-запроса
func StartExternalSpan(ctx context.Context, method, url string) (context.Context, Span) {
tracer := otel.Tracer("external")
ctx, span := tracer.Start(ctx, fmt.Sprintf("HTTP %s %s", method, url))
span.SetAttributes(
attribute.String("http.method", method),
attribute.String("http.url", url),
)
return ctx, &otelSpan{span: span}
}
// ╔══════════════════════════════════════════════════════════╗
// ║ Span — обёртка для удобства ║
// ╚══════════════════════════════════════════════════════════╝
type Span interface {
End()
RecordError(err error)
SetAttributes(attrs ...attribute.KeyValue)
AddEvent(name string, attrs ...attribute.KeyValue)
}
type otelSpan struct {
span oteltrace.Span
}
func (s *otelSpan) End() {
s.span.End()
}
func (s *otelSpan) RecordError(err error) {
s.span.RecordError(err)
s.span.SetStatus(codes.Error, err.Error())
}
func (s *otelSpan) SetAttributes(attrs ...attribute.KeyValue) {
s.span.SetAttributes(attrs...)
}
func (s *otelSpan) AddEvent(name string, attrs ...attribute.KeyValue) {
s.span.AddEvent(name, oteltrace.WithAttributes(attrs...))
}
import oteltrace "go.opentelemetry.io/otel/trace"
cmd/server/main.gopackage main
import (
"context"
"errors"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go-tracing/internal/tracing"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("🔍 Запуск сервера с трассировкой...")
// 1. Инициализируем трассировку
tp, err := tracing.InitTracer(tracing.Config{
ServiceName: "user-service",
Environment: "development",
JaegerURL: "http://localhost:14268/api/traces",
})
if err != nil {
log.Fatalf("Трассировка: %v", err)
}
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tracing.Shutdown(ctx, tp)
}()
// 2. Создаём HTTP-обработчики
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users/{id}", getUserHandler)
// 3. Оборачиваем ВЕСЬ роутер в otelhttp (автоматические спаны!)
handler := otelhttp.NewHandler(mux, "user-service",
otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return fmt.Sprintf("%s %s", r.Method, r.URL.Path)
}),
)
server := &http.Server{Addr: ":8080", Handler: handler}
go func() {
log.Println("✅ Сервер на http://localhost:8080")
log.Fatal(server.ListenAndServe())
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("🛑 Выключение...")
server.Shutdown(context.Background())
}
// getUserHandler — обработчик с РУЧНОЙ трассировкой
func getUserHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := r.PathValue("id")
// Получаем трейсер (автоматически создан otelhttp)
tracer := otel.Tracer("user-service")
// Создаём дочерний спан
ctx, span := tracer.Start(ctx, "getUserHandler")
defer span.End()
span.SetAttributes(
attribute.String("user.id", id),
attribute.String("handler.type", "http"),
)
// 1. Проверка кэша (дочерний спан)
user, fromCache, err := getFromCacheOrDB(ctx, id)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if fromCache {
span.AddEvent("cache_hit", attribute.String("user.id", id))
}
// 2. Обогащение данных (дочерний спан)
enrichCtx, enrichSpan := tracer.Start(ctx, "enrichUserData")
enrichSpan.SetAttributes(attribute.String("user.id", id))
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond) // Имитация
enrichSpan.End()
_ = enrichCtx
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"user":"%s","from_cache":%v}`, user, fromCache)
}
// getFromCacheOrDB — имитация запроса к кэшу и БД
func getFromCacheOrDB(ctx context.Context, id string) (string, bool, error) {
// Спан для проверки кэша
cacheCtx, cacheSpan := tracing.StartCacheSpan(ctx, "GET", fmt.Sprintf("user:%s", id))
defer cacheSpan.End()
// Имитация: 70% — cache hit
if rand.Float64() < 0.7 {
cacheSpan.SetAttributes(attribute.Bool("cache.hit", true))
return fmt.Sprintf("User-%s", id), true, nil
}
cacheSpan.SetAttributes(attribute.Bool("cache.hit", false))
_ = cacheCtx
// Cache miss — идём в БД
dbCtx, dbSpan := tracing.StartDBSpan(ctx, "SELECT",
fmt.Sprintf("SELECT * FROM users WHERE id = '%s'", id))
defer dbSpan.End()
// Имитация запроса к БД
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
if rand.Float64() < 0.1 {
err := errors.New("database timeout")
dbSpan.RecordError(err)
return "", false, err
}
dbSpan.AddEvent("row_found", attribute.String("user.id", id))
_ = dbCtx
return fmt.Sprintf("User-%s", id), false, nil
}
cmd/client/main.go — HTTP-клиент с трассировкойpackage main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"time"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go-tracing/internal/tracing"
)
func main() {
// Инициализируем трассировку
tp, _ := tracing.InitTracer(tracing.Config{
ServiceName: "api-gateway",
Environment: "development",
JaegerURL: "http://localhost:14268/api/traces",
})
defer tp.Shutdown(context.Background())
// HTTP-клиент с автоматической трассировкой
client := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
tracer := otel.Tracer("api-gateway")
for i := 0; i < 5; i++ {
ctx, span := tracer.Start(context.Background(), "fetchUserFromGateway")
span.SetAttributes(attribute.Int("iteration", i+1))
userID := fmt.Sprintf("%d", i+1)
url := fmt.Sprintf("http://localhost:8080/api/users/%s", userID)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := client.Do(req)
if err != nil {
span.RecordError(err)
log.Printf("❌ Ошибка: %v", err)
span.End()
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
log.Printf("✓ %s", string(body))
span.End()
time.Sleep(500 * time.Millisecond)
}
log.Println("✅ Клиент завершил работу")
}
# Запускаем Jaeger
docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 14268:14268 \
jaegertracing/all-in-one:latest
# Запускаем сервер
go run ./cmd/server/main.go
# Запускаем клиент (в другом терминале)
go run ./cmd/client/main.go
# Открываем Jaeger UI
open http://localhost:16686
# Выбираем сервис "user-service" и смотрим трейсы!
| Компонент | Пакет | Назначение |
|---|---|---|
| TracerProvider | go.opentelemetry.io/otel/sdk/trace | Управление трейсерами |
| Exporter (Jaeger) | .../exporters/jaeger | Отправка трейсов в Jaeger |
| Exporter (OTLP) | .../exporters/otlp/otlptrace | Отправка трейсов по OTLP (gRPC) |
| otelhttp | .../instrumentation/net/http/otelhttp | Авто-инструментация HTTP |
| Sampler | sdktrace.AlwaysSample() | Какие трейсы сохранять |
| Resource | resource.NewWithAttributes() | Метаданные сервиса |
💡 Best practices от сеньоров:
💡 Для Node.js разработчика: