github.com/mailgun/holster/v4@v4.20.0/tracing/tracing.go (about)

     1  package tracing
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"runtime/debug"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"go.opentelemetry.io/otel"
    11  	"go.opentelemetry.io/otel/exporters/jaeger"
    12  	"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
    13  	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    14  	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
    15  	"go.opentelemetry.io/otel/propagation"
    16  	"go.opentelemetry.io/otel/sdk/resource"
    17  	sdktrace "go.opentelemetry.io/otel/sdk/trace"
    18  	semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
    19  	"go.opentelemetry.io/otel/trace"
    20  
    21  	"github.com/sirupsen/logrus"
    22  	"github.com/uptrace/opentelemetry-go-extra/otellogrus"
    23  
    24  	"github.com/mailgun/holster/v4/errors"
    25  )
    26  
    27  type initState struct {
    28  	opts  []sdktrace.TracerProviderOption
    29  	level Level
    30  }
    31  
    32  var (
    33  	logLevels = []logrus.Level{
    34  		logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel,
    35  		logrus.WarnLevel, logrus.InfoLevel, logrus.DebugLevel,
    36  		logrus.TraceLevel,
    37  	}
    38  	log               = logrus.WithField("category", "tracing")
    39  	globalLibraryName string
    40  	SemconvSchemaURL  = semconv.SchemaURL
    41  )
    42  
    43  // InitTracing initializes a global OpenTelemetry tracer provider singleton.
    44  // Call to initialize before using functions in this package.
    45  // Instruments logrus to mirror to active trace.  Must use `WithContext()`
    46  // method.
    47  // Call after initializing logrus.
    48  // libraryName is typically the application's module name.  Pass empty string to autodetect module name.
    49  // Prometheus metrics are accessible by registering the metrics at
    50  // `tracing.Metrics`.
    51  func InitTracing(ctx context.Context, libraryName string, opts ...TracingOption) error {
    52  	// Setup exporter.
    53  	var err error
    54  	state := &initState{
    55  		level: Level(logrus.GetLevel()),
    56  	}
    57  	exportersEnv := os.Getenv("OTEL_TRACES_EXPORTER")
    58  
    59  	for _, e := range strings.Split(exportersEnv, ",") {
    60  		var exporter sdktrace.SpanExporter
    61  
    62  		switch e {
    63  		case "none":
    64  			// No exporter.  Used with unit tests.
    65  			continue
    66  		case "jaeger":
    67  			exporter, err = makeJaegerExporter()
    68  			if err != nil {
    69  				return errors.Wrap(err, "error in makeJaegerExporter")
    70  			}
    71  		default:
    72  			// default assuming "otlp".
    73  			exporter, err = makeOtlpExporter(ctx)
    74  			if err != nil {
    75  				return errors.Wrap(err, "error in makeOtlpExporter")
    76  			}
    77  		}
    78  
    79  		exportProcessor := sdktrace.NewBatchSpanProcessor(exporter)
    80  
    81  		// Capture Prometheus metrics.
    82  		metricProcessor := NewMetricSpanProcessor(exportProcessor)
    83  		state.opts = append(state.opts, sdktrace.WithSpanProcessor(metricProcessor))
    84  	}
    85  
    86  	// Apply options.
    87  	for _, opt := range opts {
    88  		opt.apply(state)
    89  	}
    90  
    91  	tp := NewLevelTracerProvider(state.level, state.opts...)
    92  	otel.SetTracerProvider(tp)
    93  
    94  	if libraryName == "" {
    95  		libraryName = getMainModule()
    96  	}
    97  	globalLibraryName = libraryName
    98  
    99  	// Setup logrus instrumentation.
   100  	// Using logrus.WithContext() will mirror log to embedded span.
   101  	// Using WithFields() also converts to log attributes.
   102  	useLevels := []logrus.Level{}
   103  	for _, l := range logLevels {
   104  		if l <= logrus.Level(state.level) {
   105  			useLevels = append(useLevels, l)
   106  		}
   107  	}
   108  
   109  	logrus.AddHook(otellogrus.NewHook(
   110  		otellogrus.WithLevels(useLevels...),
   111  	))
   112  
   113  	// Required for trace propagation between services.
   114  	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
   115  
   116  	return err
   117  }
   118  
   119  func getMainModule() string {
   120  	info, ok := debug.ReadBuildInfo()
   121  	if !ok {
   122  		return ""
   123  	}
   124  
   125  	return info.Main.Path
   126  }
   127  
   128  // NewResource creates a resource with sensible defaults.
   129  // Replaces common use case of verbose usage.
   130  func NewResource(serviceName, version string, resources ...*resource.Resource) (*resource.Resource, error) {
   131  	res, err := resource.Merge(
   132  		resource.Default(),
   133  		resource.NewWithAttributes(
   134  			SemconvSchemaURL,
   135  			semconv.ServiceNameKey.String(serviceName),
   136  			semconv.ServiceVersionKey.String(version),
   137  		),
   138  	)
   139  	if err != nil {
   140  		return nil, errors.Wrap(err, "error in resource.Merge")
   141  	}
   142  
   143  	for i, res2 := range resources {
   144  		res, err = resource.Merge(res, res2)
   145  		if err != nil {
   146  			return nil, errors.Wrapf(err, "error in resource.Merge on resources index %d", i)
   147  		}
   148  	}
   149  
   150  	return res, nil
   151  }
   152  
   153  // CloseTracing closes the global OpenTelemetry tracer provider.
   154  // This allows queued up traces to be flushed.
   155  func CloseTracing(ctx context.Context) error {
   156  	tp, ok := otel.GetTracerProvider().(*LevelTracerProvider)
   157  	if !ok {
   158  		return errors.New("OpenTelemetry global tracer provider has not been initialized")
   159  	}
   160  
   161  	err := tp.Shutdown(ctx)
   162  	if err != nil {
   163  		return errors.Wrap(err, "error in tp.Shutdown")
   164  	}
   165  
   166  	return nil
   167  }
   168  
   169  // Tracer returns a tracer object.
   170  func Tracer(opts ...trace.TracerOption) trace.Tracer {
   171  	return otel.Tracer(globalLibraryName, opts...)
   172  }
   173  
   174  func getenvOrDefault(def string, names ...string) string {
   175  	for _, name := range names {
   176  		value := os.Getenv(name)
   177  		if value != "" {
   178  			return value
   179  		}
   180  	}
   181  
   182  	return def
   183  }
   184  
   185  func makeOtlpExporter(ctx context.Context) (*otlptrace.Exporter, error) {
   186  	protocol := getenvOrDefault("grpc", "OTEL_EXPORTER_OTLP_PROTOCOL", "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL")
   187  	var client otlptrace.Client
   188  
   189  	// OTel Jaeger client doesn't seem to implement the spec for
   190  	// OTEL_EXPORTER_OTLP_PROTOCOL selection.  So we must.
   191  	// NewClient will parse supported env var configuration.
   192  	switch protocol {
   193  	case "grpc":
   194  		client = otlptracegrpc.NewClient()
   195  	case "http/protobuf":
   196  		client = otlptracehttp.NewClient()
   197  	default:
   198  		log.WithField("OTEL_EXPORTER_OTLP_PROTOCOL", protocol).
   199  			Error("Unknown OTLP protocol configured")
   200  		protocol = "grpc"
   201  		client = otlptracegrpc.NewClient()
   202  	}
   203  
   204  	logFields := logrus.Fields{
   205  		"exporter": "otlp",
   206  		"protocol": protocol,
   207  		"endpoint": getenvOrDefault("", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"),
   208  	}
   209  
   210  	sampler := getenvOrDefault("", "OTEL_TRACES_SAMPLER")
   211  	logFields["sampler"] = sampler
   212  	if strings.HasSuffix(sampler, "traceidratio") {
   213  		logFields["sampler.ratio"], _ = strconv.ParseFloat(getenvOrDefault("", "OTEL_TRACES_SAMPLER_ARG"), 64)
   214  	}
   215  
   216  	log.WithFields(logFields).Info("Initializing OpenTelemetry")
   217  
   218  	return otlptrace.New(ctx, client)
   219  }
   220  
   221  func makeJaegerExporter() (*jaeger.Exporter, error) {
   222  	var endpointOption jaeger.EndpointOption
   223  	protocol := getenvOrDefault("udp/thrift.compact", "OTEL_EXPORTER_JAEGER_PROTOCOL")
   224  
   225  	logFields := logrus.Fields{
   226  		"exporter": "jaeger",
   227  		"protocol": protocol,
   228  	}
   229  
   230  	sampler := getenvOrDefault("", "OTEL_TRACES_SAMPLER")
   231  	logFields["sampler"] = sampler
   232  	if strings.HasSuffix(sampler, "traceidratio") {
   233  		logFields["sampler.ratio"], _ = strconv.ParseFloat(getenvOrDefault("", "OTEL_TRACES_SAMPLER_ARG"), 64)
   234  	}
   235  
   236  	// OTel Jaeger client doesn't seem to implement the spec for
   237  	// OTEL_EXPORTER_JAEGER_PROTOCOL selection.  So we must.
   238  	// Jaeger endpoint option will parse supported env var configuration.
   239  	switch protocol {
   240  	// TODO: Support for "grpc" protocol. https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#jaeger-exporter
   241  	case "http/thrift.binary":
   242  		logFields["endpoint"] = os.Getenv("OTEL_EXPORTER_JAEGER_ENDPOINT")
   243  		log.WithFields(logFields).Info("Initializing OpenTelemetry")
   244  		endpointOption = jaeger.WithCollectorEndpoint()
   245  
   246  	case "udp/thrift.binary":
   247  		logFields["agentHost"] = os.Getenv("OTEL_EXPORTER_JAEGER_AGENT_HOST")
   248  		logFields["agentPort"] = os.Getenv("OTEL_EXPORTER_JAEGER_AGENT_PORT")
   249  		log.WithFields(logFields).Info("Initializing OpenTelemetry")
   250  		endpointOption = jaeger.WithAgentEndpoint()
   251  
   252  	case "udp/thrift.compact":
   253  		logFields["agentHost"] = os.Getenv("OTEL_EXPORTER_JAEGER_AGENT_HOST")
   254  		logFields["agentPort"] = os.Getenv("OTEL_EXPORTER_JAEGER_AGENT_PORT")
   255  		log.WithFields(logFields).Info("Initializing OpenTelemetry")
   256  		endpointOption = jaeger.WithAgentEndpoint()
   257  
   258  	default:
   259  		log.WithField("OTEL_EXPORTER_JAEGER_PROTOCOL", protocol).
   260  			Error("Unknown Jaeger protocol configured")
   261  		endpointOption = jaeger.WithAgentEndpoint()
   262  	}
   263  
   264  	exp, err := jaeger.New(endpointOption)
   265  	if err != nil {
   266  		return nil, errors.Wrap(err, "error in jaeger.New")
   267  	}
   268  
   269  	return exp, nil
   270  }