Skip to content

Instantly share code, notes, and snippets.

@TheYkk
Created December 8, 2023 21:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TheYkk/c88312fe1266e7af7522b0f06cb59105 to your computer and use it in GitHub Desktop.
Save TheYkk/c88312fe1266e7af7522b0f06cb59105 to your computer and use it in GitHub Desktop.
Go echo otel
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