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 }