gotosocial/internal/tracing/tracing.go
Daenney 5f10885dde
[feature] Don't trace non-existing routes (#2172)
c.FullPath() is the empty string if a request doesn't match any route on
our mux. In those cases, there's no value in emitting a trace. The trace
will be empty, containing no other information beyond the fact that we
didn't match a route. Since Gin breaks off the processing early we don't
need to trace this request as it won't do anything and consumes no
further resources.

The 404 will still be emitted by our logs and will be visible from a
reverse proxy too.
2023-09-04 17:15:14 +02:00

181 lines
5.4 KiB
Go

// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//go:build !notracing
package tracing
import (
"context"
"fmt"
"codeberg.org/gruf/go-kv"
"github.com/gin-gonic/gin"
"github.com/uptrace/bun"
"github.com/uptrace/bun/extra/bunotel"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
"go.opentelemetry.io/otel/semconv/v1.17.0/httpconv"
oteltrace "go.opentelemetry.io/otel/trace"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/log"
)
const (
tracerKey = "gotosocial-server-tracer"
tracerName = "github.com/superseriousbusiness/gotosocial/internal/tracing"
)
func Initialize() error {
if !config.GetTracingEnabled() {
return nil
}
insecure := config.GetTracingInsecureTransport()
var tpo trace.TracerProviderOption
switch config.GetTracingTransport() {
case "grpc":
opts := []otlptracegrpc.Option{
otlptracegrpc.WithEndpoint(config.GetTracingEndpoint()),
}
if insecure {
opts = append(opts, otlptracegrpc.WithInsecure())
}
exp, err := otlptracegrpc.New(context.Background(), opts...)
if err != nil {
return fmt.Errorf("building tracing exporter: %w", err)
}
tpo = trace.WithBatcher(exp)
case "jaeger":
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(config.GetTracingEndpoint())))
if err != nil {
return fmt.Errorf("building tracing exporter: %w", err)
}
tpo = trace.WithBatcher(exp)
default:
return fmt.Errorf("invalid tracing transport: %s", config.GetTracingTransport())
}
r, _ := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("GoToSocial"),
),
)
tp := trace.NewTracerProvider(
tpo,
trace.WithResource(r),
)
otel.SetTracerProvider(tp)
propagator := propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
otel.SetTextMapPropagator(propagator)
log.Hook(func(ctx context.Context, kvs []kv.Field) []kv.Field {
span := oteltrace.SpanFromContext(ctx)
if span != nil && span.SpanContext().HasTraceID() {
return append(kvs, kv.Field{K: "traceID", V: span.SpanContext().TraceID().String()})
}
return kvs
})
return nil
}
// InstrumentGin is a middleware injecting tracing information based on the
// otelgin implementation found at
// https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go
func InstrumentGin() gin.HandlerFunc {
provider := otel.GetTracerProvider()
tracer := provider.Tracer(
tracerName,
oteltrace.WithInstrumentationVersion(config.GetSoftwareVersion()),
)
propagator := otel.GetTextMapPropagator()
return func(c *gin.Context) {
spanName := c.FullPath()
// Do not trace a request if it didn't match a route. This doesn't omit
// all 404s as a request matching /:user for a user that doesn't exist
// still matches the route
if spanName == "" {
c.Next()
return
}
c.Set(tracerKey, tracer)
savedCtx := c.Request.Context()
defer func() {
c.Request = c.Request.WithContext(savedCtx)
}()
ctx := propagator.Extract(savedCtx, propagation.HeaderCarrier(c.Request.Header))
opts := []oteltrace.SpanStartOption{
oteltrace.WithAttributes(httpconv.ServerRequest(config.GetHost(), c.Request)...),
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
}
rAttr := semconv.HTTPRoute(spanName)
opts = append(opts, oteltrace.WithAttributes(rAttr))
id := gtscontext.RequestID(c.Request.Context())
if id != "" {
opts = append(opts, oteltrace.WithAttributes(attribute.String("requestID", id)))
}
ctx, span := tracer.Start(ctx, spanName, opts...)
defer span.End()
// pass the span through the request context
c.Request = c.Request.WithContext(ctx)
// serve the request to the next middleware
c.Next()
status := c.Writer.Status()
span.SetStatus(httpconv.ServerStatus(status))
if status > 0 {
span.SetAttributes(semconv.HTTPStatusCode(status))
}
if len(c.Errors) > 0 {
span.SetAttributes(attribute.String("gin.errors", c.Errors.String()))
}
}
}
func InjectRequestID() gin.HandlerFunc {
return func(c *gin.Context) {
id := gtscontext.RequestID(c.Request.Context())
if id != "" {
span := oteltrace.SpanFromContext(c.Request.Context())
span.SetAttributes(attribute.String("requestID", id))
}
}
}
func InstrumentBun() bun.QueryHook {
return bunotel.NewQueryHook(
bunotel.WithFormattedQueries(true),
)
}