Files
Zach LeslieandGitHub a6f3928b05 Update dskit and adjust memcached interface signature (#5604)
* Update dskit and adjust memcached interface signature

* Generate config manifest
2025-09-03 18:34:46 +02:00

364 lines
12 KiB
Go

package tracing
import (
"context"
"fmt"
"io"
"net"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
otelpyroscope "github.com/grafana/otel-profiling-go"
"github.com/pkg/errors"
"go.opentelemetry.io/contrib/samplers/jaegerremote"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
//nolint:staticcheck
jaegerotel "go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/propagation"
)
const (
envJaegerAgentHost = "JAEGER_AGENT_HOST"
envJaegerTags = "JAEGER_TAGS"
envJaegerSamplerManagerHostPort = "JAEGER_SAMPLER_MANAGER_HOST_PORT"
envJaegerSamplerParam = "JAEGER_SAMPLER_PARAM"
envJaegerSamplerNotParentBased = "JAEGER_SAMPLER_NOT_PARENT_BASED"
envJaegerEndpoint = "JAEGER_ENDPOINT"
envJaegerAgentPort = "JAEGER_AGENT_PORT"
envJaegerSamplerType = "JAEGER_SAMPLER_TYPE"
envJaegerSamplingEndpoint = "JAEGER_SAMPLING_ENDPOINT"
envJaegerReporterMaxQueueSize = "JAEGER_REPORTER_MAX_QUEUE_SIZE"
envJaegerDefaultSamplingServerPort = 5778
envJaegerDefaultUDPSpanServerHost = "localhost"
envJaegerDefaultUDPSpanServerPort = "6831"
jaegerDebugIdHeader string = "Jaeger-Debug-Id"
)
// NewOTelOrJaegerFromEnv is a convenience function to allow OTel tracing configuration via environment variables.
// It will configure Jaeger exporter if any of the following environment variables are set:
// - JAEGER_AGENT_HOST
// - JAEGER_ENDPOINT
// - JAEGER_SAMPLER_MANAGER_HOST_PORT
//
// Otherwise, it will initialize tracing with the OTel auto exporter using the environment variables defined in the OTel docs.
// If you want to explicitly disable OTel auto exporter, you can set the environment variable `OTEL_TRACES_EXPORTER` to `none`.
func NewOTelOrJaegerFromEnv(serviceName string, logger log.Logger, opts ...OTelOption) (io.Closer, error) {
if env, found := findNonEmptyEnv(envJaegerAgentHost, envJaegerEndpoint, envJaegerSamplerManagerHostPort); found {
level.Info(logger).Log("msg", "Configuring tracing with Jaeger exporter", "detected_env_var", env)
return newOTelFromJaegerEnv(serviceName, logger, opts...)
}
if env, found := findNonEmptyEnv("OTEL_TRACES_EXPORTER", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"); found {
level.Info(logger).Log("msg", "Configuring OpenTelemetry tracing", "detected_env_var", env)
return NewOTelFromEnv(serviceName, logger, opts...)
}
// No tracing is configured, so don't initialize the tracer as it would complain on every span about localhost:4718 not accepting traces.
return ioCloser(func() error { return nil }), nil
}
func findNonEmptyEnv(envVars ...string) (string, bool) {
for _, envVar := range envVars {
if value := os.Getenv(envVar); value != "" {
return envVar, true
}
}
return "", false
}
// NewOTelFromJaegerEnv is a convenience function to allow OTel tracing configuration via Jaeger environment variables
//
// Tracing will be enabled if one (or more) of the following environment variables is used to configure trace reporting:
// - JAEGER_AGENT_HOST
// - JAEGER_SAMPLER_MANAGER_HOST_PORT
//
// Deprecated: use NewOTelOrJaegerFromEnv instead as it will automatically configure OTel auto exporter if Jaeger is not configured.
func NewOTelFromJaegerEnv(serviceName string) (io.Closer, error) {
return newOTelFromJaegerEnv(serviceName, log.NewNopLogger())
}
func newOTelFromJaegerEnv(serviceName string, logger log.Logger, options ...OTelOption) (io.Closer, error) {
cfg, err := parseOTelJaegerConfig()
if err != nil {
return nil, errors.Wrap(err, "could not load jaeger tracer configuration")
}
if cfg.samplingServerURL == "" && cfg.agentHostPort == "" && cfg.jaegerEndpoint == "" {
return nil, ErrBlankJaegerTraceConfiguration
}
return cfg.initJaegerTracerProvider(serviceName, logger, options...)
}
// parseJaegerTags Parse Jaeger tags from env var JAEGER_TAGS, example of TAGs format: key1=value1,key2=${value2:value3} where value2 is an env var
// and value3 is the default value, which is optional.
func parseJaegerTags(sTags string) ([]attribute.KeyValue, error) {
pairs := strings.Split(sTags, ",")
res := make([]attribute.KeyValue, 0, len(pairs))
for _, p := range pairs {
k, v, found := strings.Cut(p, "=")
if found {
k, v := strings.TrimSpace(k), strings.TrimSpace(v)
if strings.HasPrefix(v, "${") && strings.HasSuffix(v, "}") {
e, d, _ := strings.Cut(v[2:len(v)-1], ":")
v = os.Getenv(e)
if v == "" && d != "" {
v = d
}
}
if v == "" {
return nil, errors.Errorf("invalid tag %q, expected key=value", p)
}
res = append(res, attribute.String(k, v))
} else if p != "" {
return nil, errors.Errorf("invalid tag %q, expected key=value", p)
}
}
return res, nil
}
type otelJaegerConfig struct {
agentHost string
jaegerEndpoint string
agentPort string
samplerType string
samplingServerURL string
samplerParam float64
samplerNotParentBased bool
jaegerTags []attribute.KeyValue
agentHostPort string
reporterMaxQueueSize int
}
// parseOTelJaegerConfig facilitates initialization that is compatible with Jaeger's InitGlobalTracer method.
func parseOTelJaegerConfig() (otelJaegerConfig, error) {
cfg := otelJaegerConfig{}
var err error
// Parse reporting agent configuration
if e := os.Getenv(envJaegerEndpoint); e != "" {
u, err := url.ParseRequestURI(e)
if err != nil {
return cfg, errors.Wrapf(err, "cannot parse env var %s=%s", envJaegerEndpoint, e)
}
cfg.jaegerEndpoint = u.String()
} else {
useEnv := false
host := envJaegerDefaultUDPSpanServerHost
if e := os.Getenv(envJaegerAgentHost); e != "" {
host = e
useEnv = true
}
port := envJaegerDefaultUDPSpanServerPort
if e := os.Getenv(envJaegerAgentPort); e != "" {
port = e
useEnv = true
}
if useEnv || cfg.agentHostPort == "" {
cfg.agentHost = host
cfg.agentPort = port
cfg.agentHostPort = net.JoinHostPort(host, port)
}
}
// Then parse the sampler Configuration
if e := os.Getenv(envJaegerSamplerType); e != "" {
cfg.samplerType = e
}
if e := os.Getenv(envJaegerSamplerParam); e != "" {
if value, err := strconv.ParseFloat(e, 64); err == nil {
cfg.samplerParam = value
} else {
return cfg, errors.Wrapf(err, "cannot parse env var %s=%s", envJaegerSamplerParam, e)
}
}
if e := os.Getenv(envJaegerSamplingEndpoint); e != "" {
cfg.samplingServerURL = e
} else if e := os.Getenv(envJaegerSamplerManagerHostPort); e != "" {
cfg.samplingServerURL = e
} else if e := os.Getenv(envJaegerAgentHost); e != "" {
// Fallback if we know the agent host - try the sampling endpoint there
cfg.samplingServerURL = fmt.Sprintf("http://%s:%d/sampling", e, envJaegerDefaultSamplingServerPort)
}
// When sampling server URL is set, we use the remote sampler
if cfg.samplingServerURL != "" && cfg.samplerType == "" {
cfg.samplerType = "remote"
}
// Parse tags
cfg.jaegerTags, err = parseJaegerTags(os.Getenv(envJaegerTags))
if err != nil {
return cfg, errors.Wrapf(err, "could not parse %s", envJaegerTags)
}
// Parse reporter max queue size
if e := os.Getenv(envJaegerReporterMaxQueueSize); e != "" {
if value, err := strconv.Atoi(e); err == nil {
cfg.reporterMaxQueueSize = value
} else {
return cfg, errors.Wrapf(err, "cannot parse env var %s=%s", envJaegerReporterMaxQueueSize, e)
}
}
if e := os.Getenv(envJaegerSamplerNotParentBased); e != "" {
if value, err := strconv.ParseBool(e); err == nil {
cfg.samplerNotParentBased = value
} else {
return cfg, errors.Wrapf(err, "cannot parse env var %s=%s", envJaegerSamplerNotParentBased, e)
}
}
return cfg, nil
}
// initJaegerTracerProvider initializes a new Jaeger Tracer Provider.
func (cfg otelJaegerConfig) initJaegerTracerProvider(serviceName string, logger log.Logger, option ...OTelOption) (io.Closer, error) {
var otelCfg config
for _, opt := range option {
opt.apply(&otelCfg)
}
// Read environment variables to configure Jaeger
var ep jaegerotel.EndpointOption
// Create the jaeger exporter: address can be either agent address (host:port) or collector Endpoint.
if cfg.agentHostPort != "" {
ep = jaegerotel.WithAgentEndpoint(
jaegerotel.WithAgentHost(cfg.agentHost),
jaegerotel.WithAgentPort(cfg.agentPort))
} else {
ep = jaegerotel.WithCollectorEndpoint(
jaegerotel.WithEndpoint(cfg.jaegerEndpoint))
}
exp, err := jaegerotel.New(ep)
if err != nil {
return nil, err
}
// Configure sampling strategy
sampler := tracesdk.AlwaysSample()
if cfg.samplerType == "const" {
if cfg.samplerParam == 0 {
sampler = tracesdk.NeverSample()
}
} else if cfg.samplerType == "probabilistic" {
sampler = tracesdk.TraceIDRatioBased(cfg.samplerParam)
} else if cfg.samplerType == "remote" {
sampler = jaegerremote.New(serviceName, jaegerremote.WithSamplingServerURL(cfg.samplingServerURL),
jaegerremote.WithInitialSampler(tracesdk.TraceIDRatioBased(cfg.samplerParam)))
} else if cfg.samplerType != "" {
return nil, errors.Errorf("unknown sampler type %q", cfg.samplerType)
}
customAttrs := cfg.jaegerTags
customAttrs = append(customAttrs,
attribute.String("samplerType", cfg.samplerType),
attribute.Float64("samplerParam", cfg.samplerParam),
attribute.String("samplingServerURL", cfg.samplingServerURL),
)
customAttrs = append(customAttrs, otelCfg.resourceAttributes...)
if !cfg.samplerNotParentBased {
sampler = tracesdk.ParentBased(sampler)
} else {
customAttrs = append(customAttrs, attribute.Bool("samplerNotParentBased", true))
}
res, err := NewResource(serviceName, customAttrs)
if err != nil {
return nil, err
}
var batcherOptions []tracesdk.BatchSpanProcessorOption
if cfg.reporterMaxQueueSize > 0 {
batcherOptions = append(batcherOptions, tracesdk.WithMaxQueueSize(cfg.reporterMaxQueueSize))
}
tracerProviderOptions := []tracesdk.TracerProviderOption{
tracesdk.WithBatcher(exp, batcherOptions...),
tracesdk.WithResource(res),
tracesdk.WithSampler(sampler),
}
tracerProviderOptions = append(tracerProviderOptions, otelCfg.tracerProviderOptions...)
tpsdk := tracesdk.NewTracerProvider(tracerProviderOptions...)
tp := trace.TracerProvider(tpsdk)
if !otelCfg.pyroscopeDisabled {
tp = otelpyroscope.NewTracerProvider(tp)
}
level.Debug(logger).Log("msg", "OpenTelemetry tracer provider initialized from Jaeger env vars")
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(OTelPropagatorsFromEnv()...))
otel.SetErrorHandler(otelErrorHandlerFunc(func(err error) {
level.Error(logger).Log("msg", "OpenTelemetry.ErrorHandler", "err", err)
}))
return ioCloser(func() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := tpsdk.Shutdown(ctx); err != nil {
level.Error(logger).Log("msg", "OpenTelemetry trace provider failed to shutdown", "err", err)
return err
}
return nil
}), nil
}
type contextKey int
var jaegerDebugIdKey = contextKey(0)
type JaegerDebuggingPropagator struct{}
func (j JaegerDebuggingPropagator) Inject(ctx context.Context, carrier propagation.TextMapCarrier) {
// Nothing to do, we don't want to propagate the header.
}
func (j JaegerDebuggingPropagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context {
debugId := carrier.Get(jaegerDebugIdHeader)
if debugId == "" {
return ctx
}
return context.WithValue(ctx, jaegerDebugIdKey, debugId)
}
func (j JaegerDebuggingPropagator) Fields() []string {
return []string{jaegerDebugIdHeader}
}
type JaegerDebuggingSampler struct {
inner tracesdk.Sampler
}
func (j *JaegerDebuggingSampler) ShouldSample(parameters tracesdk.SamplingParameters) tracesdk.SamplingResult {
debugID, haveDebugID := parameters.ParentContext.Value(jaegerDebugIdKey).(string)
innerResult := j.inner.ShouldSample(parameters)
if !haveDebugID {
return innerResult
}
innerResult.Decision = tracesdk.RecordAndSample
attr := attribute.String("jaeger-debug-id", debugID)
innerResult.Attributes = append(innerResult.Attributes, attr)
return innerResult
}
func (j *JaegerDebuggingSampler) Description() string {
return "JaegerDebuggingSampler"
}