mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* Update dskit and adjust memcached interface signature * Generate config manifest
364 lines
12 KiB
Go
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"
|
|
}
|