Created
December 8, 2023 21:24
-
-
Save TheYkk/c88312fe1266e7af7522b0f06cb59105 to your computer and use it in GitHub Desktop.
Go echo otel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package tracing | |
import ( | |
"context" | |
"fmt" | |
"os" | |
"sync" | |
"github.com/labstack/echo/v4" | |
"github.com/labstack/echo/v4/middleware" | |
"github.com/rs/zerolog/log" | |
"go.opentelemetry.io/otel" | |
"go.opentelemetry.io/otel/attribute" | |
"go.opentelemetry.io/otel/exporters/otlp/otlptrace" | |
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" | |
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" | |
"go.opentelemetry.io/otel/propagation" | |
"go.opentelemetry.io/otel/sdk/resource" | |
"go.opentelemetry.io/otel/sdk/trace" | |
"go.opentelemetry.io/otel/semconv/v1.20.0/httpconv" | |
semconv "go.opentelemetry.io/otel/semconv/v1.21.0" | |
otelTrace "go.opentelemetry.io/otel/trace" | |
"theykk.net/apikey/backend/pkg/config" | |
) | |
// EchoWrapper provides Honeycomb instrumentation for the Echo router via middleware | |
type ( | |
EchoWrapper struct { | |
handlerNames map[string]string | |
once sync.Once | |
otelTrace.Tracer | |
middleware.Skipper | |
} | |
) | |
// New returns a new EchoWrapper struct | |
func New(t otelTrace.Tracer) *EchoWrapper { | |
return &EchoWrapper{Tracer: t} | |
} | |
// Middleware returns an echo.MiddlewareFunc to be used with Echo.Use() | |
func (e *EchoWrapper) Middleware() echo.MiddlewareFunc { | |
return func(next echo.HandlerFunc) echo.HandlerFunc { | |
return func(c echo.Context) error { | |
if e.Skipper == nil { | |
e.Skipper = middleware.DefaultSkipper | |
} | |
if e.Skipper(c) { | |
return next(c) | |
} | |
r := c.Request() | |
// get a new context with our trace from the request | |
wireContext := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)) | |
opts := []otelTrace.SpanStartOption{ | |
otelTrace.WithAttributes(httpconv.ServerRequest("", r)...), | |
otelTrace.WithAttributes(httpconv.RequestHeader(r.Header)...), | |
otelTrace.WithSpanKind(otelTrace.SpanKindServer), | |
otelTrace.WithLinks(otelTrace.LinkFromContext(wireContext)), | |
} | |
if path := c.Path(); path != "" { | |
rAttr := semconv.HTTPRoute(path) | |
opts = append(opts, otelTrace.WithAttributes(rAttr)) | |
} | |
spanName := fmt.Sprintf("HTTP %s %s", r.Method, c.Path()) | |
ctx, span := e.Tracer.Start(wireContext, spanName, opts...) | |
defer span.End() | |
// push the context with our trace and span on to the request | |
c.SetRequest(r.WithContext(ctx)) | |
// get name of handler | |
handlerName := e.handlerName(c) | |
if handlerName == "" { | |
handlerName = "handler" | |
} | |
span.SetAttributes(attribute.String("handler.name", handlerName)) | |
span.SetAttributes(attribute.String("http.request.id", r.Header.Get(echo.HeaderXRequestID))) | |
// add route related fields | |
for _, name := range c.ParamNames() { | |
// add field for each path param | |
span.SetAttributes(attribute.String("route.params."+name, c.Param(name))) | |
} | |
// invoke next middleware in chain | |
err := next(c) | |
if err != nil { | |
span.SetAttributes(attribute.String("echo.error", err.Error())) | |
// invokes the registered HTTP error handler | |
c.Error(err) | |
} | |
// Send trace id as header | |
c.Response().Header().Add("X-Argonix-trace-id", span.SpanContext().TraceID().String()) | |
status := c.Response().Status | |
span.SetStatus(httpconv.ServerStatus(status)) | |
if status > 0 { | |
span.SetAttributes(semconv.HTTPStatusCode(status)) | |
} | |
// add fields for http response code and size | |
span.SetAttributes(attribute.String("http.response.id", c.Response().Header().Get(echo.HeaderXRequestID))) | |
span.SetAttributes(semconv.HTTPResponseBodySize(int(c.Response().Size))) | |
span.SetAttributes(httpconv.ResponseHeader(c.Response().Header())...) | |
return nil | |
} | |
} | |
} | |
// Unfortunately the name of c.Handler() is an anonymous function | |
// (https://github.com/labstack/echo/blob/master/echo.go#L487-L494). | |
// This function will return the correct handler name by building a | |
// map of request paths to actual handler names (only during the first | |
// request thus providing quick lookup for every request thereafter). | |
func (e *EchoWrapper) handlerName(c echo.Context) string { | |
// only perform once | |
e.once.Do(func() { | |
// build map of request paths to handler names | |
routes := c.Echo().Routes() | |
e.handlerNames = make(map[string]string, len(routes)) | |
for _, r := range c.Echo().Routes() { | |
e.handlerNames[r.Method+r.Path] = r.Name | |
} | |
}) | |
// lookup handler name for this request | |
return e.handlerNames[c.Request().Method+c.Path()] | |
} | |
func InitTracing(ctx context.Context, e *echo.Echo, version string) *trace.TracerProvider { | |
var client otlptrace.Client | |
proto := os.Getenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL") | |
if proto == "" { | |
proto = os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") | |
if proto == "" { | |
proto = "grpc" | |
} | |
} | |
if proto == "http/protobuf" { | |
client = otlptracehttp.NewClient() | |
} else { | |
client = otlptracegrpc.NewClient() | |
} | |
exporter, err := otlptrace.New(ctx, client) | |
if err != nil { | |
log.Error().Err(err).Msg("failed to initialize exporter") | |
} | |
attrs := []attribute.KeyValue{ | |
attribute.String("app.version", version), | |
} | |
res, err := resource.New(ctx, | |
resource.WithFromEnv(), | |
resource.WithProcessPID(), | |
resource.WithProcessExecutableName(), | |
resource.WithProcessExecutablePath(), | |
resource.WithProcessRuntimeName(), | |
resource.WithProcessRuntimeVersion(), | |
resource.WithProcessRuntimeDescription(), | |
resource.WithTelemetrySDK(), | |
resource.WithHost(), | |
resource.WithHostID(), | |
resource.WithAttributes(attrs...), | |
) | |
if err != nil { | |
log.Error().Err(err).Msg("resource creation for otel") | |
} | |
// Create a new tracer provider with a batch span processor and the otlp exporter. | |
tp := trace.NewTracerProvider( | |
trace.WithBatcher(exporter), | |
trace.WithResource(res), | |
trace.WithSampler(trace.TraceIDRatioBased(config.Cfg.App.TraceRatio)), | |
) | |
// Handle shutdown errors in a sensible manner where possible | |
// Set the Tracer Provider global | |
otel.SetTracerProvider(tp) | |
// Register the trace context and baggage propagators so data is propagated across services/processes. | |
otel.SetTextMapPropagator( | |
propagation.NewCompositeTextMapPropagator( | |
propagation.TraceContext{}, | |
propagation.Baggage{}, | |
), | |
) | |
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { | |
log.Error().Err(err).Msg("opentelemetry") | |
})) | |
otelTracer := tp.Tracer("api-backend") | |
e.Use(New(otelTracer).Middleware()) | |
return tp | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment