tracing.go (5457B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 //go:build !notracing 19 20 package tracing 21 22 import ( 23 "context" 24 "fmt" 25 26 "codeberg.org/gruf/go-kv" 27 "github.com/gin-gonic/gin" 28 "github.com/uptrace/bun" 29 "github.com/uptrace/bun/extra/bunotel" 30 "go.opentelemetry.io/otel" 31 "go.opentelemetry.io/otel/attribute" 32 "go.opentelemetry.io/otel/exporters/jaeger" 33 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 34 "go.opentelemetry.io/otel/propagation" 35 "go.opentelemetry.io/otel/sdk/resource" 36 "go.opentelemetry.io/otel/sdk/trace" 37 semconv "go.opentelemetry.io/otel/semconv/v1.17.0" 38 "go.opentelemetry.io/otel/semconv/v1.17.0/httpconv" 39 oteltrace "go.opentelemetry.io/otel/trace" 40 41 "github.com/superseriousbusiness/gotosocial/internal/config" 42 "github.com/superseriousbusiness/gotosocial/internal/gtscontext" 43 "github.com/superseriousbusiness/gotosocial/internal/log" 44 ) 45 46 const ( 47 tracerKey = "gotosocial-server-tracer" 48 tracerName = "github.com/superseriousbusiness/gotosocial/internal/tracing" 49 ) 50 51 func Initialize() error { 52 if !config.GetTracingEnabled() { 53 return nil 54 } 55 56 insecure := config.GetTracingInsecureTransport() 57 58 var tpo trace.TracerProviderOption 59 switch config.GetTracingTransport() { 60 case "grpc": 61 opts := []otlptracegrpc.Option{ 62 otlptracegrpc.WithEndpoint(config.GetTracingEndpoint()), 63 } 64 if insecure { 65 opts = append(opts, otlptracegrpc.WithInsecure()) 66 } 67 exp, err := otlptracegrpc.New(context.Background(), opts...) 68 if err != nil { 69 return fmt.Errorf("building tracing exporter: %w", err) 70 } 71 tpo = trace.WithBatcher(exp) 72 case "jaeger": 73 exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(config.GetTracingEndpoint()))) 74 if err != nil { 75 return fmt.Errorf("building tracing exporter: %w", err) 76 } 77 tpo = trace.WithBatcher(exp) 78 default: 79 return fmt.Errorf("invalid tracing transport: %s", config.GetTracingTransport()) 80 } 81 r, _ := resource.Merge( 82 resource.Default(), 83 resource.NewWithAttributes( 84 semconv.SchemaURL, 85 semconv.ServiceName("GoToSocial"), 86 ), 87 ) 88 89 tp := trace.NewTracerProvider( 90 tpo, 91 trace.WithResource(r), 92 ) 93 otel.SetTracerProvider(tp) 94 propagator := propagation.NewCompositeTextMapPropagator( 95 propagation.TraceContext{}, 96 propagation.Baggage{}, 97 ) 98 otel.SetTextMapPropagator(propagator) 99 log.Hook(func(ctx context.Context, kvs []kv.Field) []kv.Field { 100 span := oteltrace.SpanFromContext(ctx) 101 if span != nil && span.SpanContext().HasTraceID() { 102 return append(kvs, kv.Field{K: "traceID", V: span.SpanContext().TraceID().String()}) 103 } 104 return kvs 105 }) 106 return nil 107 } 108 109 // InstrumentGin is a middleware injecting tracing information based on the 110 // otelgin implementation found at 111 // https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go 112 func InstrumentGin() gin.HandlerFunc { 113 provider := otel.GetTracerProvider() 114 tracer := provider.Tracer( 115 tracerName, 116 oteltrace.WithInstrumentationVersion(config.GetSoftwareVersion()), 117 ) 118 propagator := otel.GetTextMapPropagator() 119 return func(c *gin.Context) { 120 c.Set(tracerKey, tracer) 121 savedCtx := c.Request.Context() 122 defer func() { 123 c.Request = c.Request.WithContext(savedCtx) 124 }() 125 ctx := propagator.Extract(savedCtx, propagation.HeaderCarrier(c.Request.Header)) 126 opts := []oteltrace.SpanStartOption{ 127 oteltrace.WithAttributes(httpconv.ServerRequest(config.GetHost(), c.Request)...), 128 oteltrace.WithSpanKind(oteltrace.SpanKindServer), 129 } 130 spanName := c.FullPath() 131 if spanName == "" { 132 spanName = fmt.Sprintf("HTTP %s route not found", c.Request.Method) 133 } else { 134 rAttr := semconv.HTTPRoute(spanName) 135 opts = append(opts, oteltrace.WithAttributes(rAttr)) 136 } 137 id := gtscontext.RequestID(c.Request.Context()) 138 if id != "" { 139 opts = append(opts, oteltrace.WithAttributes(attribute.String("requestID", id))) 140 } 141 ctx, span := tracer.Start(ctx, spanName, opts...) 142 defer span.End() 143 144 // pass the span through the request context 145 c.Request = c.Request.WithContext(ctx) 146 147 // serve the request to the next middleware 148 c.Next() 149 150 status := c.Writer.Status() 151 span.SetStatus(httpconv.ServerStatus(status)) 152 if status > 0 { 153 span.SetAttributes(semconv.HTTPStatusCode(status)) 154 } 155 if len(c.Errors) > 0 { 156 span.SetAttributes(attribute.String("gin.errors", c.Errors.String())) 157 } 158 } 159 } 160 161 func InjectRequestID() gin.HandlerFunc { 162 return func(c *gin.Context) { 163 id := gtscontext.RequestID(c.Request.Context()) 164 if id != "" { 165 span := oteltrace.SpanFromContext(c.Request.Context()) 166 span.SetAttributes(attribute.String("requestID", id)) 167 } 168 } 169 } 170 171 func InstrumentBun() bun.QueryHook { 172 return bunotel.NewQueryHook( 173 bunotel.WithFormattedQueries(true), 174 ) 175 }