package observability
import (
"context"
"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.21.0"
)
func InitTracing(ctx context.Context, serviceName string) (*sdktrace.TracerProvider, error) {
exp, err := otlptracegrpc.New(ctx)
if err != nil {
return nil, err
}
res, err := resource.New(ctx, resource.WithAttributes(semconv.ServiceName(serviceName)))
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
return tp, nil
}
To get useful traces, you need propagation and a real exporter. I set a global TextMapPropagator (TraceContext + Baggage) so inbound headers connect spans across services. Then I configure an OTLP exporter and a batch span processor so tracing overhead stays low. I also explicitly set a sampler: ParentBased(TraceIDRatioBased(0.1)) is a common starting point that respects upstream sampling and keeps costs predictable. The other key piece is resource attributes like service.name, which is how traces are grouped in most backends. Once this is initialized, you can start spans in handlers with otel.Tracer("...").Start(ctx, ...) and get end-to-end visibility without special log parsing.