1
0
Fork 0

Migrate to opentelemetry

This commit is contained in:
Jesse Haka 2024-01-08 10:10:06 +02:00 committed by GitHub
parent 45bb00be04
commit 4ddef9830b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
89 changed files with 2113 additions and 3898 deletions

View file

@ -5,10 +5,9 @@ import (
"fmt"
"net/http"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
const (
@ -40,8 +39,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.AddPrefix, name
return result, nil
}
func (a *addPrefix) GetTracingInformation() (string, ext.SpanKindEnum) {
return a.name, tracing.SpanKindNoneEnum
func (a *addPrefix) GetTracingInformation() (string, string, trace.SpanKind) {
return a.name, typeName, trace.SpanKindInternal
}
func (a *addPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -8,15 +8,15 @@ import (
"strings"
goauth "github.com/abbot/go-http-auth"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
const (
basicTypeName = "BasicAuth"
typeNameBasic = "BasicAuth"
)
type basicAuth struct {
@ -30,7 +30,7 @@ type basicAuth struct {
// NewBasic creates a basicAuth middleware.
func NewBasic(ctx context.Context, next http.Handler, authConfig dynamic.BasicAuth, name string) (http.Handler, error) {
middlewares.GetLogger(ctx, name, basicTypeName).Debug().Msg("Creating middleware")
middlewares.GetLogger(ctx, name, typeNameBasic).Debug().Msg("Creating middleware")
users, err := getUsers(authConfig.UsersFile, authConfig.Users, basicUserParser)
if err != nil {
@ -55,12 +55,12 @@ func NewBasic(ctx context.Context, next http.Handler, authConfig dynamic.BasicAu
return ba, nil
}
func (b *basicAuth) GetTracingInformation() (string, ext.SpanKindEnum) {
return b.name, tracing.SpanKindNoneEnum
func (b *basicAuth) GetTracingInformation() (string, string, trace.SpanKind) {
return b.name, typeNameBasic, trace.SpanKindInternal
}
func (b *basicAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
logger := middlewares.GetLogger(req.Context(), b.name, basicTypeName)
logger := middlewares.GetLogger(req.Context(), b.name, typeNameBasic)
user, password, ok := req.BasicAuth()
if ok {
@ -77,7 +77,7 @@ func (b *basicAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if !ok {
logger.Debug().Msg("Authentication failed")
tracing.SetErrorWithEvent(req, "Authentication failed")
tracing.SetStatusErrorf(req.Context(), "Authentication failed")
b.auth.RequireAuth(rw, req)
return

View file

@ -8,15 +8,15 @@ import (
"strings"
goauth "github.com/abbot/go-http-auth"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
const (
digestTypeName = "digestAuth"
typeNameDigest = "digestAuth"
)
type digestAuth struct {
@ -30,7 +30,7 @@ type digestAuth struct {
// NewDigest creates a digest auth middleware.
func NewDigest(ctx context.Context, next http.Handler, authConfig dynamic.DigestAuth, name string) (http.Handler, error) {
middlewares.GetLogger(ctx, name, digestTypeName).Debug().Msg("Creating middleware")
middlewares.GetLogger(ctx, name, typeNameDigest).Debug().Msg("Creating middleware")
users, err := getUsers(authConfig.UsersFile, authConfig.Users, digestUserParser)
if err != nil {
@ -54,12 +54,12 @@ func NewDigest(ctx context.Context, next http.Handler, authConfig dynamic.Digest
return da, nil
}
func (d *digestAuth) GetTracingInformation() (string, ext.SpanKindEnum) {
return d.name, tracing.SpanKindNoneEnum
func (d *digestAuth) GetTracingInformation() (string, string, trace.SpanKind) {
return d.name, typeNameDigest, trace.SpanKindInternal
}
func (d *digestAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
logger := middlewares.GetLogger(req.Context(), d.name, digestTypeName)
logger := middlewares.GetLogger(req.Context(), d.name, typeNameDigest)
username, authinfo := d.auth.CheckAuth(req)
if username == "" {
@ -78,13 +78,13 @@ func (d *digestAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if authinfo != nil && *authinfo == "stale" {
logger.Debug().Msg("Digest authentication failed, possibly because out of order requests")
tracing.SetErrorWithEvent(req, "Digest authentication failed, possibly because out of order requests")
tracing.SetStatusErrorf(req.Context(), "Digest authentication failed, possibly because out of order requests")
d.auth.RequireAuthStale(rw, req)
return
}
logger.Debug().Msg("Digest authentication failed")
tracing.SetErrorWithEvent(req, "Digest authentication failed")
tracing.SetStatusErrorf(req.Context(), "Digest authentication failed")
d.auth.RequireAuth(rw, req)
return
}

View file

@ -11,21 +11,22 @@ import (
"strings"
"time"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/connectionheader"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/vulcand/oxy/v2/forward"
"github.com/vulcand/oxy/v2/utils"
"go.opentelemetry.io/otel/trace"
)
const (
xForwardedURI = "X-Forwarded-Uri"
xForwardedMethod = "X-Forwarded-Method"
forwardedTypeName = "ForwardedAuthType"
xForwardedURI = "X-Forwarded-Uri"
xForwardedMethod = "X-Forwarded-Method"
)
const typeNameForward = "ForwardAuth"
// hopHeaders Hop-by-hop headers to be removed in the authentication request.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
// Proxy-Authorization header is forwarded to the authentication server (see https://tools.ietf.org/html/rfc7235#section-4.4).
@ -51,7 +52,7 @@ type forwardAuth struct {
// NewForward creates a forward auth middleware.
func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAuth, name string) (http.Handler, error) {
middlewares.GetLogger(ctx, name, forwardedTypeName).Debug().Msg("Creating middleware")
middlewares.GetLogger(ctx, name, typeNameForward).Debug().Msg("Creating middleware")
fa := &forwardAuth{
address: config.Address,
@ -89,30 +90,38 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu
fa.authResponseHeadersRegex = re
}
return connectionheader.Remover(fa), nil
return fa, nil
}
func (fa *forwardAuth) GetTracingInformation() (string, ext.SpanKindEnum) {
return fa.name, ext.SpanKindRPCClientEnum
func (fa *forwardAuth) GetTracingInformation() (string, string, trace.SpanKind) {
return fa.name, typeNameForward, trace.SpanKindInternal
}
func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
logger := middlewares.GetLogger(req.Context(), fa.name, forwardedTypeName)
logger := middlewares.GetLogger(req.Context(), fa.name, typeNameForward)
forwardReq, err := http.NewRequest(http.MethodGet, fa.address, nil)
tracing.LogRequest(tracing.GetSpan(req), forwardReq)
req = connectionheader.Remove(req)
forwardReq, err := http.NewRequestWithContext(req.Context(), http.MethodGet, fa.address, nil)
if err != nil {
logMessage := fmt.Sprintf("Error calling %s. Cause %s", fa.address, err)
logger.Debug().Msg(logMessage)
tracing.SetErrorWithEvent(req, logMessage)
tracing.SetStatusErrorf(req.Context(), logMessage)
rw.WriteHeader(http.StatusInternalServerError)
return
}
// Ensure tracing headers are in the request before we copy the headers to the
// forwardReq.
tracing.InjectRequestHeaders(req)
var forwardSpan trace.Span
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil {
var tracingCtx context.Context
tracingCtx, forwardSpan = tracer.Start(req.Context(), "AuthRequest", trace.WithSpanKind(trace.SpanKindClient))
defer forwardSpan.End()
forwardReq = forwardReq.WithContext(tracingCtx)
tracing.InjectContextIntoCarrier(forwardReq)
tracing.LogClientRequest(forwardSpan, forwardReq)
}
writeHeader(req, forwardReq, fa.trustForwardHeader, fa.authRequestHeaders)
@ -120,7 +129,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if forwardErr != nil {
logMessage := fmt.Sprintf("Error calling %s. Cause: %s", fa.address, forwardErr)
logger.Debug().Msg(logMessage)
tracing.SetErrorWithEvent(req, logMessage)
tracing.SetStatusErrorf(forwardReq.Context(), logMessage)
rw.WriteHeader(http.StatusInternalServerError)
return
@ -131,12 +140,18 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if readError != nil {
logMessage := fmt.Sprintf("Error reading body %s. Cause: %s", fa.address, readError)
logger.Debug().Msg(logMessage)
tracing.SetErrorWithEvent(req, logMessage)
tracing.SetStatusErrorf(forwardReq.Context(), logMessage)
rw.WriteHeader(http.StatusInternalServerError)
return
}
// Ending the forward request span as soon as the response is handled.
// If any errors happen earlier, this span will be close by the defer instruction.
if forwardSpan != nil {
forwardSpan.End()
}
// Pass the forward response's body and selected headers if it
// didn't return a response within the range of [200, 300).
if forwardResponse.StatusCode < http.StatusOK || forwardResponse.StatusCode >= http.StatusMultipleChoices {
@ -152,7 +167,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if !errors.Is(err, http.ErrNoLocation) {
logMessage := fmt.Sprintf("Error reading response location header %s. Cause: %s", fa.address, err)
logger.Debug().Msg(logMessage)
tracing.SetErrorWithEvent(req, logMessage)
tracing.SetStatusErrorf(forwardReq.Context(), logMessage)
rw.WriteHeader(http.StatusInternalServerError)
return
@ -162,7 +177,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Location", redirectURL.String())
}
tracing.LogResponseCode(tracing.GetSpan(req), forwardResponse.StatusCode)
tracing.LogResponseCode(forwardSpan, forwardResponse.StatusCode, trace.SpanKindClient)
rw.WriteHeader(forwardResponse.StatusCode)
if _, err = rw.Write(body); err != nil {
@ -193,6 +208,8 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
}
tracing.LogResponseCode(forwardSpan, forwardResponse.StatusCode, trace.SpanKindClient)
req.RequestURI = req.URL.RequestURI()
fa.next.ServeHTTP(rw, req)
}

View file

@ -8,15 +8,22 @@ import (
"net/http/httptest"
"testing"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/containous/alice"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/static"
tracingMiddleware "github.com/traefik/traefik/v3/pkg/middlewares/tracing"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/traefik/traefik/v3/pkg/tracing/opentelemetry"
"github.com/traefik/traefik/v3/pkg/version"
"github.com/vulcand/oxy/v2/forward"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)
func TestForwardAuthFail(t *testing.T) {
@ -452,8 +459,8 @@ func Test_writeHeader(t *testing.T) {
func TestForwardAuthUsesTracing(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Mockpfx-Ids-Traceid") == "" {
t.Errorf("expected Mockpfx-Ids-Traceid header to be present in request")
if r.Header.Get("Traceparent") == "" {
t.Errorf("expected Traceparent header to be present in request")
}
}))
t.Cleanup(server.Close)
@ -464,15 +471,44 @@ func TestForwardAuthUsesTracing(t *testing.T) {
Address: server.URL,
}
tracer := mocktracer.New()
opentracing.SetGlobalTracer(tracer)
exporter := tracetest.NewInMemoryExporter()
tr, _ := tracing.NewTracing("testApp", 100, &mockBackend{tracer})
next, err := NewForward(context.Background(), next, auth, "authTest")
tres, err := resource.New(context.Background(),
resource.WithAttributes(semconv.ServiceNameKey.String("traefik")),
resource.WithAttributes(semconv.ServiceVersionKey.String(version.Version)),
resource.WithFromEnv(),
resource.WithTelemetrySDK(),
)
require.NoError(t, err)
next = tracingMiddleware.NewEntryPoint(context.Background(), tr, "tracingTest", next)
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithResource(tres),
sdktrace.WithBatcher(exporter),
)
otel.SetTracerProvider(tracerProvider)
config := &static.Tracing{
ServiceName: "testApp",
SampleRate: 1,
OTLP: &opentelemetry.Config{
HTTP: &opentelemetry.HTTP{
Endpoint: "http://127.0.0.1:8080",
},
},
}
tr, closer, err := tracing.NewTracing(config)
require.NoError(t, err)
t.Cleanup(func() {
_ = closer.Close()
})
next, err = NewForward(context.Background(), next, auth, "authTest")
require.NoError(t, err)
chain := alice.New(tracingMiddleware.WrapEntryPointHandler(context.Background(), tr, "tracingTest"))
next, err = chain.Then(next)
require.NoError(t, err)
ts := httptest.NewServer(next)
t.Cleanup(ts.Close)
@ -482,11 +518,3 @@ func TestForwardAuthUsesTracing(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
}
type mockBackend struct {
opentracing.Tracer
}
func (b *mockBackend) Setup(componentName string) (opentracing.Tracer, io.Closer, error) {
return b.Tracer, io.NopCloser(nil), nil
}

View file

@ -4,13 +4,12 @@ import (
"context"
"net/http"
"github.com/opentracing/opentracing-go/ext"
"github.com/rs/zerolog"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
oxybuffer "github.com/vulcand/oxy/v2/buffer"
"go.opentelemetry.io/otel/trace"
)
const (
@ -49,8 +48,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.Buffering, name
}, nil
}
func (b *buffer) GetTracingInformation() (string, ext.SpanKindEnum) {
return b.name, tracing.SpanKindNoneEnum
func (b *buffer) GetTracingInformation() (string, string, trace.SpanKind) {
return b.name, typeName, trace.SpanKindInternal
}
func (b *buffer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -5,7 +5,6 @@ import (
"net/http"
"time"
"github.com/opentracing/opentracing-go/ext"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
@ -13,6 +12,7 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/vulcand/oxy/v2/cbreaker"
"go.opentelemetry.io/otel/trace"
)
const typeName = "CircuitBreaker"
@ -32,7 +32,7 @@ func New(ctx context.Context, next http.Handler, confCircuitBreaker dynamic.Circ
cbOpts := []cbreaker.Option{
cbreaker.Fallback(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
tracing.SetErrorWithEvent(req, "blocked by circuit-breaker (%q)", expression)
tracing.SetStatusErrorf(req.Context(), "blocked by circuit-breaker (%q)", expression)
rw.WriteHeader(http.StatusServiceUnavailable)
if _, err := rw.Write([]byte(http.StatusText(http.StatusServiceUnavailable))); err != nil {
@ -66,8 +66,8 @@ func New(ctx context.Context, next http.Handler, confCircuitBreaker dynamic.Circ
}, nil
}
func (c *circuitBreaker) GetTracingInformation() (string, ext.SpanKindEnum) {
return c.name, tracing.SpanKindNoneEnum
func (c *circuitBreaker) GetTracingInformation() (string, string, trace.SpanKind) {
return c.name, typeName, trace.SpanKindInternal
}
func (c *circuitBreaker) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -9,11 +9,10 @@ import (
"strings"
"github.com/klauspost/compress/gzhttp"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/compress/brotli"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
const typeName = "Compress"
@ -114,8 +113,8 @@ func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
c.next.ServeHTTP(rw, req)
}
func (c *compress) GetTracingInformation() (string, ext.SpanKindEnum) {
return c.name, tracing.SpanKindNoneEnum
func (c *compress) GetTracingInformation() (string, string, trace.SpanKind) {
return c.name, typeName, trace.SpanKindInternal
}
func (c *compress) newGzipHandler() (http.Handler, error) {

View file

@ -17,24 +17,29 @@ const (
// See RFC 7230, section 6.1.
func Remover(next http.Handler) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
var reqUpType string
if httpguts.HeaderValuesContainsToken(req.Header[connectionHeader], upgradeHeader) {
reqUpType = req.Header.Get(upgradeHeader)
}
removeConnectionHeaders(req.Header)
if reqUpType != "" {
req.Header.Set(connectionHeader, upgradeHeader)
req.Header.Set(upgradeHeader, reqUpType)
} else {
req.Header.Del(connectionHeader)
}
next.ServeHTTP(rw, req)
next.ServeHTTP(rw, Remove(req))
}
}
// Remove removes hop-by-hop header on the request.
func Remove(req *http.Request) *http.Request {
var reqUpType string
if httpguts.HeaderValuesContainsToken(req.Header[connectionHeader], upgradeHeader) {
reqUpType = req.Header.Get(upgradeHeader)
}
removeConnectionHeaders(req.Header)
if reqUpType != "" {
req.Header.Set(connectionHeader, upgradeHeader)
req.Header.Set(upgradeHeader, reqUpType)
} else {
req.Header.Del(connectionHeader)
}
return req
}
func removeConnectionHeaders(h http.Header) {
for _, f := range h[connectionHeader] {
for _, sf := range strings.Split(f, ",") {

View file

@ -10,12 +10,12 @@ import (
"strconv"
"strings"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/traefik/traefik/v3/pkg/types"
"github.com/vulcand/oxy/v2/utils"
"go.opentelemetry.io/otel/trace"
)
// Compile time validation that the response recorder implements http interfaces correctly.
@ -24,7 +24,7 @@ var (
_ middlewares.Stateful = &codeCatcher{}
)
const typeName = "customError"
const typeName = "CustomError"
type serviceBuilder interface {
BuildHTTP(ctx context.Context, serviceName string) (http.Handler, error)
@ -62,8 +62,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.ErrorPage, servi
}, nil
}
func (c *customErrors) GetTracingInformation() (string, ext.SpanKindEnum) {
return c.name, tracing.SpanKindNoneEnum
func (c *customErrors) GetTracingInformation() (string, string, trace.SpanKind) {
return c.name, typeName, trace.SpanKindInternal
}
func (c *customErrors) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@ -71,7 +71,7 @@ func (c *customErrors) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if c.backendHandler == nil {
logger.Error().Msg("Error pages: no backend handler.")
tracing.SetErrorWithEvent(req, "Error pages: no backend handler.")
tracing.SetStatusErrorf(req.Context(), "Error pages: no backend handler.")
c.next.ServeHTTP(rw, req)
return
}
@ -96,12 +96,12 @@ func (c *customErrors) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
pageReq, err := newRequest("http://" + req.Host + query)
if err != nil {
logger.Error().Err(err).Send()
tracing.SetStatusErrorf(req.Context(), err.Error())
http.Error(rw, http.StatusText(code), code)
return
}
utils.CopyHeaders(pageReq.Header, req.Header)
c.backendHandler.ServeHTTP(newCodeModifier(rw, code),
pageReq.WithContext(req.Context()))
}

View file

@ -9,7 +9,7 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
)
const typeName = "grpc-web"
const typeName = "GRPCWeb"
// New builds a new gRPC web request converter.
func New(ctx context.Context, next http.Handler, config dynamic.GrpcWeb, name string) http.Handler {

View file

@ -6,11 +6,10 @@ import (
"errors"
"net/http"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/connectionheader"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
const (
@ -61,8 +60,8 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Headers, name strin
}, nil
}
func (h *headers) GetTracingInformation() (string, ext.SpanKindEnum) {
return h.name, tracing.SpanKindNoneEnum
func (h *headers) GetTracingInformation() (string, string, trace.SpanKind) {
return h.name, typeName, trace.SpanKindInternal
}
func (h *headers) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
func TestNew_withoutOptions(t *testing.T) {
@ -109,10 +109,11 @@ func Test_headers_getTracingInformation(t *testing.T) {
name: "testing",
}
name, trace := mid.GetTracingInformation()
name, typeName, spanKind := mid.GetTracingInformation()
assert.Equal(t, "testing", name)
assert.Equal(t, tracing.SpanKindNoneEnum, trace)
assert.Equal(t, "Headers", typeName)
assert.Equal(t, trace.SpanKindInternal, spanKind)
}
// This test is an adapted version of net/http/httputil.Test1xxResponses test.

View file

@ -5,13 +5,12 @@ import (
"fmt"
"net/http"
"github.com/opentracing/opentracing-go/ext"
"github.com/rs/zerolog"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/vulcand/oxy/v2/connlimit"
"go.opentelemetry.io/otel/trace"
)
const (
@ -54,8 +53,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.InFlightReq, nam
return &inFlightReq{handler: handler, name: name}, nil
}
func (i *inFlightReq) GetTracingInformation() (string, ext.SpanKindEnum) {
return i.name, tracing.SpanKindNoneEnum
func (i *inFlightReq) GetTracingInformation() (string, string, trace.SpanKind) {
return i.name, typeName, trace.SpanKindInternal
}
func (i *inFlightReq) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -6,12 +6,12 @@ import (
"fmt"
"net/http"
"github.com/opentracing/opentracing-go/ext"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
const (
@ -55,8 +55,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.IPAllowList, nam
}, nil
}
func (al *ipAllowLister) GetTracingInformation() (string, ext.SpanKindEnum) {
return al.name, tracing.SpanKindNoneEnum
func (al *ipAllowLister) GetTracingInformation() (string, string, trace.SpanKind) {
return al.name, typeName, trace.SpanKindInternal
}
func (al *ipAllowLister) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@ -68,7 +68,7 @@ func (al *ipAllowLister) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if err != nil {
msg := fmt.Sprintf("Rejecting IP %s: %v", clientIP, err)
logger.Debug().Msg(msg)
tracing.SetErrorWithEvent(req, msg)
tracing.SetStatusErrorf(req.Context(), msg)
reject(ctx, rw)
return
}

View file

@ -16,6 +16,8 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
"github.com/traefik/traefik/v3/pkg/middlewares/retry"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/codes"
)
@ -39,6 +41,7 @@ type metricsMiddleware struct {
reqsBytesCounter gokitmetrics.Counter
respsBytesCounter gokitmetrics.Counter
baseLabels []string
name string
}
// NewEntryPointMiddleware creates a new metrics middleware for an Entrypoint.
@ -53,6 +56,7 @@ func NewEntryPointMiddleware(ctx context.Context, next http.Handler, registry me
reqsBytesCounter: registry.EntryPointReqsBytesCounter(),
respsBytesCounter: registry.EntryPointRespsBytesCounter(),
baseLabels: []string{"entrypoint", entryPointName},
name: nameEntrypoint,
}
}
@ -68,6 +72,7 @@ func NewRouterMiddleware(ctx context.Context, next http.Handler, registry metric
reqsBytesCounter: registry.RouterReqsBytesCounter(),
respsBytesCounter: registry.RouterRespsBytesCounter(),
baseLabels: []string{"router", routerName, "service", serviceName},
name: nameRouter,
}
}
@ -83,6 +88,7 @@ func NewServiceMiddleware(ctx context.Context, next http.Handler, registry metri
reqsBytesCounter: registry.ServiceReqsBytesCounter(),
respsBytesCounter: registry.ServiceRespsBytesCounter(),
baseLabels: []string{"service", serviceName},
name: nameService,
}
}
@ -100,6 +106,17 @@ func WrapRouterHandler(ctx context.Context, registry metrics.Registry, routerNam
}
}
// WrapServiceHandler Wraps metrics service to alice.Constructor.
func WrapServiceHandler(ctx context.Context, registry metrics.Registry, serviceName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
return NewServiceMiddleware(ctx, next, registry, serviceName), nil
}
}
func (m *metricsMiddleware) GetTracingInformation() (string, string, trace.SpanKind) {
return m.name, typeName, trace.SpanKindInternal
}
func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
proto := getRequestProtocol(req)
@ -127,6 +144,7 @@ func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request)
}
logger := with.Logger()
logger.Error().Err(err).Msg("Could not get Capture")
tracing.SetStatusErrorf(req.Context(), "Could not get Capture")
return
}

View file

@ -11,11 +11,10 @@ import (
"net/url"
"strings"
"github.com/opentracing/opentracing-go/ext"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
const typeName = "PassClientTLSCert"
@ -140,8 +139,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.PassTLSClientCer
}, nil
}
func (p *passTLSClientCert) GetTracingInformation() (string, ext.SpanKindEnum) {
return p.name, tracing.SpanKindNoneEnum
func (p *passTLSClientCert) GetTracingInformation() (string, string, trace.SpanKind) {
return p.name, typeName, trace.SpanKindInternal
}
func (p *passTLSClientCert) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -9,17 +9,17 @@ import (
"time"
"github.com/mailgun/ttlmap"
"github.com/opentracing/opentracing-go/ext"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/vulcand/oxy/v2/utils"
"go.opentelemetry.io/otel/trace"
"golang.org/x/time/rate"
)
const (
typeName = "RateLimiterType"
typeName = "RateLimiter"
maxSources = 65536
)
@ -122,8 +122,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.RateLimit, name
}, nil
}
func (rl *rateLimiter) GetTracingInformation() (string, ext.SpanKindEnum) {
return rl.name, tracing.SpanKindNoneEnum
func (rl *rateLimiter) GetTracingInformation() (string, string, trace.SpanKind) {
return rl.name, typeName, trace.SpanKindInternal
}
func (rl *rateLimiter) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@ -153,12 +153,14 @@ func (rl *rateLimiter) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// as the expiryTime is supposed to reflect the activity (or lack thereof) on that source.
if err := rl.buckets.Set(source, bucket, rl.ttl); err != nil {
logger.Error().Err(err).Msg("Could not insert/update bucket")
tracing.SetStatusErrorf(req.Context(), "Could not insert/update bucket")
http.Error(rw, "could not insert/update bucket", http.StatusInternalServerError)
return
}
res := bucket.Reserve()
if !res.OK() {
tracing.SetStatusErrorf(req.Context(), "No bursty traffic allowed")
http.Error(rw, "No bursty traffic allowed", http.StatusTooManyRequests)
return
}

View file

@ -5,9 +5,8 @@ import (
"net/url"
"regexp"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/vulcand/oxy/v2/utils"
"go.opentelemetry.io/otel/trace"
)
const (
@ -15,6 +14,8 @@ const (
schemeHTTPS = "https"
)
const typeName = "Redirect"
var uriRegexp = regexp.MustCompile(`^(https?):\/\/(\[[\w:.]+\]|[\w\._-]+)?(:\d+)?(.*)$`)
type redirect struct {
@ -45,8 +46,8 @@ func newRedirect(next http.Handler, regex, replacement string, permanent bool, r
}, nil
}
func (r *redirect) GetTracingInformation() (string, ext.SpanKindEnum) {
return r.name, tracing.SpanKindNoneEnum
func (r *redirect) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, typeName, trace.SpanKindInternal
}
func (r *redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -5,10 +5,10 @@ import (
"net/http"
"net/url"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
const (
@ -35,8 +35,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.ReplacePath, nam
}, nil
}
func (r *replacePath) GetTracingInformation() (string, ext.SpanKindEnum) {
return r.name, tracing.SpanKindNoneEnum
func (r *replacePath) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, typeName, trace.SpanKindInternal
}
func (r *replacePath) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@ -52,6 +52,7 @@ func (r *replacePath) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
req.URL.Path, err = url.PathUnescape(req.URL.RawPath)
if err != nil {
middlewares.GetLogger(context.Background(), r.name, typeName).Error().Err(err).Send()
tracing.SetStatusErrorf(req.Context(), err.Error())
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}

View file

@ -8,11 +8,11 @@ import (
"regexp"
"strings"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/replacepath"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
const typeName = "ReplacePathRegex"
@ -42,8 +42,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.ReplacePathRegex
}, nil
}
func (rp *replacePathRegex) GetTracingInformation() (string, ext.SpanKindEnum) {
return rp.name, tracing.SpanKindNoneEnum
func (rp *replacePathRegex) GetTracingInformation() (string, string, trace.SpanKind) {
return rp.name, typeName, trace.SpanKindInternal
}
func (rp *replacePathRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@ -63,6 +63,7 @@ func (rp *replacePathRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request)
req.URL.Path, err = url.PathUnescape(req.URL.RawPath)
if err != nil {
middlewares.GetLogger(context.Background(), rp.name, typeName).Error().Err(err).Send()
tracing.SetStatusErrorf(req.Context(), err.Error())
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}

View file

@ -12,10 +12,12 @@ import (
"time"
"github.com/cenkalti/backoff/v4"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"go.opentelemetry.io/otel/trace"
)
// Compile time validation that the response writer implements http interfaces correctly.
@ -60,10 +62,6 @@ func New(ctx context.Context, next http.Handler, config dynamic.Retry, listener
}, nil
}
func (r *retry) GetTracingInformation() (string, ext.SpanKindEnum) {
return r.name, tracing.SpanKindNoneEnum
}
func (r *retry) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if r.attempts == 1 {
r.next.ServeHTTP(rw, req)
@ -79,12 +77,35 @@ func (r *retry) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
attempts := 1
initialCtx := req.Context()
tracer := tracing.TracerFromContext(initialCtx)
var currentSpan trace.Span
operation := func() error {
if tracer != nil {
if currentSpan != nil {
currentSpan.End()
}
// Because multiple tracing spans may need to be created,
// the Retry middleware does not implement trace.Traceable,
// and creates directly a new span for each retry operation.
var tracingCtx context.Context
tracingCtx, currentSpan = tracer.Start(initialCtx, typeName, trace.WithSpanKind(trace.SpanKindInternal))
currentSpan.SetAttributes(attribute.String("traefik.middleware.name", r.name))
// Only add the attribute "http.resend_count" defined by semantic conventions starting from second attempt.
if attempts > 1 {
currentSpan.SetAttributes(semconv.HTTPResendCount(attempts - 1))
}
req = req.WithContext(tracingCtx)
}
shouldRetry := attempts < r.attempts
retryResponseWriter := newResponseWriter(rw, shouldRetry)
// Disable retries when the backend already received request data
trace := &httptrace.ClientTrace{
clientTrace := &httptrace.ClientTrace{
WroteHeaders: func() {
retryResponseWriter.DisableRetries()
},
@ -92,7 +113,7 @@ func (r *retry) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
retryResponseWriter.DisableRetries()
},
}
newCtx := httptrace.WithClientTrace(req.Context(), trace)
newCtx := httptrace.WithClientTrace(req.Context(), clientTrace)
r.next.ServeHTTP(retryResponseWriter, req.Clone(newCtx))
@ -119,6 +140,10 @@ func (r *retry) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if err != nil {
logger.Debug().Err(err).Msg("Final retry attempt failed")
}
if currentSpan != nil {
currentSpan.End()
}
}
func (r *retry) newBackOff() backoff.BackOff {

View file

@ -5,10 +5,9 @@ import (
"net/http"
"strings"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
const (
@ -34,8 +33,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.StripPrefix, nam
}, nil
}
func (s *stripPrefix) GetTracingInformation() (string, ext.SpanKindEnum) {
return s.name, tracing.SpanKindNoneEnum
func (s *stripPrefix) GetTracingInformation() (string, string, trace.SpanKind) {
return s.name, typeName, trace.SpanKindUnspecified
}
func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -6,11 +6,10 @@ import (
"regexp"
"strings"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/stripprefix"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
const (
@ -44,8 +43,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.StripPrefixRegex
return &stripPrefix, nil
}
func (s *stripPrefixRegex) GetTracingInformation() (string, ext.SpanKindEnum) {
return s.name, tracing.SpanKindNoneEnum
func (s *stripPrefixRegex) GetTracingInformation() (string, string, trace.SpanKind) {
return s.name, typeName, trace.SpanKindInternal
}
func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -5,57 +5,53 @@ import (
"net/http"
"github.com/containous/alice"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
const (
entryPointTypeName = "TracingEntryPoint"
)
// NewEntryPoint creates a new middleware that the incoming request.
func NewEntryPoint(ctx context.Context, t *tracing.Tracing, entryPointName string, next http.Handler) http.Handler {
middlewares.GetLogger(ctx, "tracing", entryPointTypeName).Debug().Msg("Creating middleware")
return &entryPointMiddleware{
entryPoint: entryPointName,
Tracing: t,
next: next,
}
}
type entryPointMiddleware struct {
*tracing.Tracing
type entryPointTracing struct {
tracer trace.Tracer
entryPoint string
next http.Handler
}
func (e *entryPointMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
spanCtx, err := e.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header))
if err != nil {
middlewares.GetLogger(req.Context(), "tracing", entryPointTypeName).
Debug().Err(err).Msg("Failed to extract the context")
// WrapEntryPointHandler Wraps tracing to alice.Constructor.
func WrapEntryPointHandler(ctx context.Context, tracer trace.Tracer, entryPointName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
return newEntryPoint(ctx, tracer, entryPointName, next), nil
}
}
span, req, finish := e.StartSpanf(req, ext.SpanKindRPCServerEnum, "EntryPoint", []string{e.entryPoint, req.Host}, " ", ext.RPCServerOption(spanCtx))
defer finish()
// newEntryPoint creates a new tracing middleware for incoming requests.
func newEntryPoint(ctx context.Context, tracer trace.Tracer, entryPointName string, next http.Handler) http.Handler {
middlewares.GetLogger(ctx, "tracing", entryPointTypeName).Debug().Msg("Creating middleware")
ext.Component.Set(span, e.ServiceName)
tracing.LogRequest(span, req)
return &entryPointTracing{
entryPoint: entryPointName,
tracer: tracer,
next: next,
}
}
req = req.WithContext(tracing.WithTracing(req.Context(), e.Tracing))
func (e *entryPointTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
tracingCtx := tracing.ExtractCarrierIntoContext(req.Context(), req.Header)
tracingCtx, span := e.tracer.Start(tracingCtx, "EntryPoint", trace.WithSpanKind(trace.SpanKindServer))
defer span.End()
req = req.WithContext(tracingCtx)
span.SetAttributes(attribute.String("entry_point", e.entryPoint))
tracing.LogServerRequest(span, req)
recorder := newStatusCodeRecorder(rw, http.StatusOK)
e.next.ServeHTTP(recorder, req)
tracing.LogResponseCode(span, recorder.Status())
}
// WrapEntryPointHandler Wraps tracing to alice.Constructor.
func WrapEntryPointHandler(ctx context.Context, tracer *tracing.Tracing, entryPointName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
return NewEntryPoint(ctx, tracer, entryPointName, next), nil
}
tracing.LogResponseCode(span, recorder.Status(), trace.SpanKindServer)
}

View file

@ -6,81 +6,69 @@ import (
"net/http/httptest"
"testing"
"github.com/opentracing/opentracing-go/ext"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
)
func TestEntryPointMiddleware(t *testing.T) {
type expected struct {
Tags map[string]interface{}
OperationName string
name string
attributes []attribute.KeyValue
}
testCases := []struct {
desc string
entryPoint string
spanNameLimit int
tracing *trackingBackenMock
expected expected
desc string
entryPoint string
expected expected
}{
{
desc: "no truncation test",
entryPoint: "test",
spanNameLimit: 0,
tracing: &trackingBackenMock{
tracer: &MockTracer{Span: &MockSpan{Tags: make(map[string]interface{})}},
},
desc: "basic test",
entryPoint: "test",
expected: expected{
Tags: map[string]interface{}{
"span.kind": ext.SpanKindRPCServerEnum,
"http.method": http.MethodGet,
"component": "",
"http.url": "http://www.test.com",
"http.host": "www.test.com",
name: "EntryPoint",
attributes: []attribute.KeyValue{
attribute.String("span.kind", "server"),
attribute.String("entry_point", "test"),
attribute.String("http.request.method", "GET"),
attribute.String("network.protocol.version", "1.1"),
attribute.Int64("http.request.body.size", int64(0)),
attribute.String("url.path", "/search"),
attribute.String("url.query", "q=Opentelemetry"),
attribute.String("url.scheme", "http"),
attribute.String("user_agent.original", "entrypoint-test"),
attribute.String("server.address", "www.test.com"),
attribute.String("network.peer.address", "10.0.0.1"),
attribute.String("network.peer.port", "1234"),
attribute.String("client.address", "10.0.0.1"),
attribute.Int64("client.port", int64(1234)),
attribute.String("client.socket.address", ""),
attribute.Int64("http.response.status_code", int64(404)),
},
OperationName: "EntryPoint test www.test.com",
},
},
{
desc: "basic test",
entryPoint: "test",
spanNameLimit: 25,
tracing: &trackingBackenMock{
tracer: &MockTracer{Span: &MockSpan{Tags: make(map[string]interface{})}},
},
expected: expected{
Tags: map[string]interface{}{
"span.kind": ext.SpanKindRPCServerEnum,
"http.method": http.MethodGet,
"component": "",
"http.url": "http://www.test.com",
"http.host": "www.test.com",
},
OperationName: "EntryPoint te... ww... 0c15301b",
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
newTracing, err := tracing.NewTracing("", test.spanNameLimit, test.tracing)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "http://www.test.com", nil)
req := httptest.NewRequest(http.MethodGet, "http://www.test.com/search?q=Opentelemetry", nil)
rw := httptest.NewRecorder()
req.RemoteAddr = "10.0.0.1:1234"
req.Header.Set("User-Agent", "entrypoint-test")
req.Header.Set("X-Forwarded-Proto", "http")
next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
span := test.tracing.tracer.(*MockTracer).Span
tags := span.Tags
assert.Equal(t, test.expected.Tags, tags)
assert.Equal(t, test.expected.OperationName, span.OpName)
next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusNotFound)
})
handler := NewEntryPoint(context.Background(), newTracing, test.entryPoint, next)
tracer := &mockTracer{}
handler := newEntryPoint(context.Background(), tracer, test.entryPoint, next)
handler.ServeHTTP(rw, req)
for _, span := range tracer.spans {
assert.Equal(t, test.expected.name, span.name)
assert.Equal(t, test.expected.attributes, span.attributes)
}
})
}
}

View file

@ -1,59 +0,0 @@
package tracing
import (
"context"
"net/http"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
)
const (
forwarderTypeName = "TracingForwarder"
)
type forwarderMiddleware struct {
router string
service string
next http.Handler
}
// NewForwarder creates a new forwarder middleware that traces the outgoing request.
func NewForwarder(ctx context.Context, router, service string, next http.Handler) http.Handler {
middlewares.GetLogger(ctx, "tracing", forwarderTypeName).
Debug().Str(logs.ServiceName, service).Msg("Added outgoing tracing middleware")
return &forwarderMiddleware{
router: router,
service: service,
next: next,
}
}
func (f *forwarderMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
tr, err := tracing.FromContext(req.Context())
if err != nil {
f.next.ServeHTTP(rw, req)
return
}
opParts := []string{f.service, f.router}
span, req, finish := tr.StartSpanf(req, ext.SpanKindRPCClientEnum, "forward", opParts, "/")
defer finish()
span.SetTag("traefik.service.name", f.service)
span.SetTag("traefik.router.name", f.router)
ext.HTTPMethod.Set(span, req.Method)
ext.HTTPUrl.Set(span, req.URL.String())
span.SetTag("http.host", req.Host)
tracing.InjectRequestHeaders(req)
recorder := newStatusCodeRecorder(rw, 200)
f.next.ServeHTTP(recorder, req)
tracing.LogResponseCode(span, recorder.Status())
}

View file

@ -1,136 +0,0 @@
package tracing
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/opentracing/opentracing-go/ext"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/tracing"
)
func TestNewForwarder(t *testing.T) {
type expected struct {
Tags map[string]interface{}
OperationName string
}
testCases := []struct {
desc string
spanNameLimit int
tracing *trackingBackenMock
service string
router string
expected expected
}{
{
desc: "Simple Forward Tracer without truncation and hashing",
spanNameLimit: 101,
tracing: &trackingBackenMock{
tracer: &MockTracer{Span: &MockSpan{Tags: make(map[string]interface{})}},
},
service: "some-service.domain.tld",
router: "some-service.domain.tld",
expected: expected{
Tags: map[string]interface{}{
"http.host": "www.test.com",
"http.method": "GET",
"http.url": "http://www.test.com/toto",
"traefik.service.name": "some-service.domain.tld",
"traefik.router.name": "some-service.domain.tld",
"span.kind": ext.SpanKindRPCClientEnum,
},
OperationName: "forward some-service.domain.tld/some-service.domain.tld",
},
},
{
desc: "Simple Forward Tracer with truncation and hashing",
spanNameLimit: 101,
tracing: &trackingBackenMock{
tracer: &MockTracer{Span: &MockSpan{Tags: make(map[string]interface{})}},
},
service: "some-service-100.slug.namespace.environment.domain.tld",
router: "some-service-100.slug.namespace.environment.domain.tld",
expected: expected{
Tags: map[string]interface{}{
"http.host": "www.test.com",
"http.method": "GET",
"http.url": "http://www.test.com/toto",
"traefik.service.name": "some-service-100.slug.namespace.environment.domain.tld",
"traefik.router.name": "some-service-100.slug.namespace.environment.domain.tld",
"span.kind": ext.SpanKindRPCClientEnum,
},
OperationName: "forward some-service-100.slug.namespace.enviro.../some-service-100.slug.namespace.enviro.../bc4a0d48",
},
},
{
desc: "Exactly 101 chars",
spanNameLimit: 101,
tracing: &trackingBackenMock{
tracer: &MockTracer{Span: &MockSpan{Tags: make(map[string]interface{})}},
},
service: "some-service1.namespace.environment.domain.tld",
router: "some-service1.namespace.environment.domain.tld",
expected: expected{
Tags: map[string]interface{}{
"http.host": "www.test.com",
"http.method": "GET",
"http.url": "http://www.test.com/toto",
"traefik.service.name": "some-service1.namespace.environment.domain.tld",
"traefik.router.name": "some-service1.namespace.environment.domain.tld",
"span.kind": ext.SpanKindRPCClientEnum,
},
OperationName: "forward some-service1.namespace.environment.domain.tld/some-service1.namespace.environment.domain.tld",
},
},
{
desc: "More than 101 chars",
spanNameLimit: 101,
tracing: &trackingBackenMock{
tracer: &MockTracer{Span: &MockSpan{Tags: make(map[string]interface{})}},
},
service: "some-service1.frontend.namespace.environment.domain.tld",
router: "some-service1.backend.namespace.environment.domain.tld",
expected: expected{
Tags: map[string]interface{}{
"http.host": "www.test.com",
"http.method": "GET",
"http.url": "http://www.test.com/toto",
"traefik.service.name": "some-service1.frontend.namespace.environment.domain.tld",
"traefik.router.name": "some-service1.backend.namespace.environment.domain.tld",
"span.kind": ext.SpanKindRPCClientEnum,
},
OperationName: "forward some-service1.frontend.namespace.envir.../some-service1.backend.namespace.enviro.../fa49dd23",
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
newTracing, err := tracing.NewTracing("", test.spanNameLimit, test.tracing)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "http://www.test.com/toto", nil)
req = req.WithContext(tracing.WithTracing(req.Context(), newTracing))
rw := httptest.NewRecorder()
next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
span := test.tracing.tracer.(*MockTracer).Span
tags := span.Tags
assert.Equal(t, test.expected.Tags, tags)
assert.LessOrEqual(t, len(test.expected.OperationName), test.spanNameLimit,
"the len of the operation name %q [len: %d] doesn't respect limit %d",
test.expected.OperationName, len(test.expected.OperationName), test.spanNameLimit)
assert.Equal(t, test.expected.OperationName, span.OpName)
})
handler := NewForwarder(context.Background(), test.router, test.service, next)
handler.ServeHTTP(rw, req)
})
}
}

View file

@ -0,0 +1,71 @@
package tracing
import (
"context"
"net/http"
"github.com/containous/alice"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// Traceable embeds tracing information.
type Traceable interface {
GetTracingInformation() (name string, typeName string, spanKind trace.SpanKind)
}
// WrapMiddleware adds traceability to an alice.Constructor.
func WrapMiddleware(ctx context.Context, constructor alice.Constructor) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
if constructor == nil {
return nil, nil
}
handler, err := constructor(next)
if err != nil {
return nil, err
}
if traceableHandler, ok := handler.(Traceable); ok {
name, typeName, spanKind := traceableHandler.GetTracingInformation()
log.Ctx(ctx).Debug().Str(logs.MiddlewareName, name).Msg("Adding tracing to middleware")
return NewMiddleware(handler, name, typeName, spanKind), nil
}
return handler, nil
}
}
// NewMiddleware returns a http.Handler struct.
func NewMiddleware(next http.Handler, name string, typeName string, spanKind trace.SpanKind) http.Handler {
return &middlewareTracing{
next: next,
name: name,
typeName: typeName,
spanKind: spanKind,
}
}
// middlewareTracing is used to wrap http handler middleware.
type middlewareTracing struct {
next http.Handler
name string
typeName string
spanKind trace.SpanKind
}
func (w *middlewareTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil {
tracingCtx, span := tracer.Start(req.Context(), w.typeName, trace.WithSpanKind(w.spanKind))
defer span.End()
req = req.WithContext(tracingCtx)
span.SetAttributes(attribute.String("traefik.middleware.name", w.name))
}
if w.next != nil {
w.next.ServeHTTP(rw, req)
}
}

View file

@ -1,70 +1,62 @@
package tracing
import (
"io"
"context"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/log"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/embedded"
)
type MockTracer struct {
Span *MockSpan
type mockTracerProvider struct {
embedded.TracerProvider
}
// StartSpan belongs to the Tracer interface.
func (n MockTracer) StartSpan(operationName string, opts ...opentracing.StartSpanOption) opentracing.Span {
n.Span.OpName = operationName
return n.Span
var _ trace.TracerProvider = mockTracerProvider{}
func (p mockTracerProvider) Tracer(string, ...trace.TracerOption) trace.Tracer {
return &mockTracer{}
}
// Inject belongs to the Tracer interface.
func (n MockTracer) Inject(sp opentracing.SpanContext, format, carrier interface{}) error {
return nil
type mockTracer struct {
embedded.Tracer
spans []*mockSpan
}
// Extract belongs to the Tracer interface.
func (n MockTracer) Extract(format, carrier interface{}) (opentracing.SpanContext, error) {
return nil, opentracing.ErrSpanContextNotFound
var _ trace.Tracer = &mockTracer{}
func (t *mockTracer) Start(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
config := trace.NewSpanStartConfig(opts...)
span := &mockSpan{}
span.SetName(name)
span.SetAttributes(attribute.String("span.kind", config.SpanKind().String()))
span.SetAttributes(config.Attributes()...)
t.spans = append(t.spans, span)
return trace.ContextWithSpan(ctx, span), span
}
// MockSpanContext a span context mock.
type MockSpanContext struct{}
// mockSpan is an implementation of Span that preforms no operations.
type mockSpan struct {
embedded.Span
func (n MockSpanContext) ForeachBaggageItem(handler func(k, v string) bool) {}
// MockSpan a span mock.
type MockSpan struct {
OpName string
Tags map[string]interface{}
name string
attributes []attribute.KeyValue
}
func (n MockSpan) Context() opentracing.SpanContext { return MockSpanContext{} }
func (n MockSpan) SetBaggageItem(key, val string) opentracing.Span {
return MockSpan{Tags: make(map[string]interface{})}
}
func (n MockSpan) BaggageItem(key string) string { return "" }
func (n MockSpan) SetTag(key string, value interface{}) opentracing.Span {
n.Tags[key] = value
return n
}
func (n MockSpan) LogFields(fields ...log.Field) {}
func (n MockSpan) LogKV(keyVals ...interface{}) {}
func (n MockSpan) Finish() {}
func (n MockSpan) FinishWithOptions(opts opentracing.FinishOptions) {}
func (n MockSpan) SetOperationName(operationName string) opentracing.Span { return n }
func (n MockSpan) Tracer() opentracing.Tracer { return MockTracer{} }
func (n MockSpan) LogEvent(event string) {}
func (n MockSpan) LogEventWithPayload(event string, payload interface{}) {}
func (n MockSpan) Log(data opentracing.LogData) {}
func (n *MockSpan) Reset() {
n.Tags = make(map[string]interface{})
}
var _ trace.Span = &mockSpan{}
type trackingBackenMock struct {
tracer opentracing.Tracer
func (*mockSpan) SpanContext() trace.SpanContext { return trace.SpanContext{} }
func (*mockSpan) IsRecording() bool { return false }
func (s *mockSpan) SetStatus(_ codes.Code, _ string) {}
func (s *mockSpan) SetAttributes(kv ...attribute.KeyValue) {
s.attributes = append(s.attributes, kv...)
}
func (s *mockSpan) End(...trace.SpanEndOption) {}
func (s *mockSpan) RecordError(_ error, _ ...trace.EventOption) {}
func (s *mockSpan) AddEvent(_ string, _ ...trace.EventOption) {}
func (t *trackingBackenMock) Setup(componentName string) (opentracing.Tracer, io.Closer, error) {
opentracing.SetGlobalTracer(t.tracer)
return t.tracer, nil, nil
}
func (s *mockSpan) SetName(name string) { s.name = name }
func (*mockSpan) TracerProvider() trace.TracerProvider { return mockTracerProvider{} }

View file

@ -0,0 +1,60 @@
package tracing
import (
"context"
"net/http"
"github.com/containous/alice"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"go.opentelemetry.io/otel/trace"
)
const (
routerTypeName = "TracingRouter"
)
type routerTracing struct {
router string
routerRule string
service string
next http.Handler
}
// WrapRouterHandler Wraps tracing to alice.Constructor.
func WrapRouterHandler(ctx context.Context, router, routerRule, service string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
return newRouter(ctx, router, routerRule, service, next), nil
}
}
// newRouter creates a new tracing middleware that traces the internal requests.
func newRouter(ctx context.Context, router, routerRule, service string, next http.Handler) http.Handler {
middlewares.GetLogger(ctx, "tracing", routerTypeName).
Debug().Str(logs.RouterName, router).Str(logs.ServiceName, service).Msg("Added outgoing tracing middleware")
return &routerTracing{
router: router,
routerRule: routerRule,
service: service,
next: next,
}
}
func (f *routerTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil {
tracingCtx, span := tracer.Start(req.Context(), "Router", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
req = req.WithContext(tracingCtx)
span.SetAttributes(attribute.String("traefik.service.name", f.service))
span.SetAttributes(attribute.String("traefik.router.name", f.router))
span.SetAttributes(semconv.HTTPRoute(f.routerRule))
}
f.next.ServeHTTP(rw, req)
}

View file

@ -0,0 +1,86 @@
package tracing
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
func TestNewRouter(t *testing.T) {
type expected struct {
attributes []attribute.KeyValue
name string
}
testCases := []struct {
desc string
service string
router string
routerRule string
expected []expected
}{
{
desc: "base",
service: "myService",
router: "myRouter",
routerRule: "Path(`/`)",
expected: []expected{
{
name: "EntryPoint",
attributes: []attribute.KeyValue{
attribute.String("span.kind", "server"),
},
},
{
name: "Router",
attributes: []attribute.KeyValue{
attribute.String("span.kind", "internal"),
attribute.String("http.request.method", "GET"),
attribute.Int64("http.response.status_code", int64(404)),
attribute.String("network.protocol.version", "1.1"),
attribute.String("server.address", "www.test.com"),
attribute.Int64("server.port", int64(80)),
attribute.String("url.full", "http://www.test.com/traces?p=OpenTelemetry"),
attribute.String("url.scheme", "http"),
attribute.String("traefik.service.name", "myService"),
attribute.String("traefik.router.name", "myRouter"),
attribute.String("http.route", "Path(`/`)"),
attribute.String("user_agent.original", "router-test"),
},
},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "http://www.test.com/traces?p=OpenTelemetry", nil)
req.RemoteAddr = "10.0.0.1:1234"
req.Header.Set("User-Agent", "router-test")
tracer := &mockTracer{}
tracingCtx, entryPointSpan := tracer.Start(req.Context(), "EntryPoint", trace.WithSpanKind(trace.SpanKindServer))
defer entryPointSpan.End()
req = req.WithContext(tracingCtx)
rw := httptest.NewRecorder()
next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusNotFound)
})
handler := newRouter(context.Background(), test.router, test.routerRule, test.service, next)
handler.ServeHTTP(rw, req)
for i, span := range tracer.spans {
assert.Equal(t, test.expected[i].name, span.name)
assert.Equal(t, test.expected[i].attributes, span.attributes)
}
})
}
}

View file

@ -0,0 +1,45 @@
package tracing
import (
"context"
"net/http"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
const (
serviceTypeName = "TracingService"
)
type serviceTracing struct {
service string
next http.Handler
}
// NewService creates a new tracing middleware that traces the outgoing requests.
func NewService(ctx context.Context, service string, next http.Handler) http.Handler {
middlewares.GetLogger(ctx, "tracing", serviceTypeName).
Debug().Str(logs.ServiceName, service).Msg("Added outgoing tracing middleware")
return &serviceTracing{
service: service,
next: next,
}
}
func (t *serviceTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil {
tracingCtx, span := tracer.Start(req.Context(), "Service", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
req = req.WithContext(tracingCtx)
span.SetAttributes(attribute.String("traefik.service.name", t.service))
}
t.next.ServeHTTP(rw, req)
}

View file

@ -0,0 +1,80 @@
package tracing
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
func TestNewService(t *testing.T) {
type expected struct {
attributes []attribute.KeyValue
name string
}
testCases := []struct {
desc string
service string
expected []expected
}{
{
desc: "base",
service: "myService",
expected: []expected{
{
name: "EntryPoint",
attributes: []attribute.KeyValue{
attribute.String("span.kind", "server"),
},
},
{
name: "Service",
attributes: []attribute.KeyValue{
attribute.String("span.kind", "internal"),
attribute.String("http.request.method", "GET"),
attribute.Int64("http.response.status_code", int64(404)),
attribute.String("network.protocol.version", "1.1"),
attribute.String("server.address", "www.test.com"),
attribute.Int64("server.port", int64(80)),
attribute.String("url.full", "http://www.test.com/traces?p=OpenTelemetry"),
attribute.String("url.scheme", "http"),
attribute.String("traefik.service.name", "myService"),
attribute.String("user_agent.original", "service-test"),
},
},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "http://www.test.com/traces?p=OpenTelemetry", nil)
req.RemoteAddr = "10.0.0.1:1234"
req.Header.Set("User-Agent", "service-test")
tracer := &mockTracer{}
tracingCtx, entryPointSpan := tracer.Start(req.Context(), "EntryPoint", trace.WithSpanKind(trace.SpanKindServer))
defer entryPointSpan.End()
req = req.WithContext(tracingCtx)
rw := httptest.NewRecorder()
next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusNotFound)
})
handler := NewService(context.Background(), test.service, next)
handler.ServeHTTP(rw, req)
for i, span := range tracer.spans {
assert.Equal(t, test.expected[i].name, span.name)
assert.Equal(t, test.expected[i].attributes, span.attributes)
}
})
}
}

View file

@ -1,69 +0,0 @@
package tracing
import (
"context"
"net/http"
"github.com/containous/alice"
"github.com/opentracing/opentracing-go/ext"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/tracing"
)
// Traceable embeds tracing information.
type Traceable interface {
GetTracingInformation() (name string, spanKind ext.SpanKindEnum)
}
// Wrap adds traceability to an alice.Constructor.
func Wrap(ctx context.Context, constructor alice.Constructor) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
if constructor == nil {
return nil, nil
}
handler, err := constructor(next)
if err != nil {
return nil, err
}
if traceableHandler, ok := handler.(Traceable); ok {
name, spanKind := traceableHandler.GetTracingInformation()
log.Ctx(ctx).Debug().Str(logs.MiddlewareName, name).Msg("Adding tracing to middleware")
return NewWrapper(handler, name, spanKind), nil
}
return handler, nil
}
}
// NewWrapper returns a http.Handler struct.
func NewWrapper(next http.Handler, name string, spanKind ext.SpanKindEnum) http.Handler {
return &Wrapper{
next: next,
name: name,
spanKind: spanKind,
}
}
// Wrapper is used to wrap http handler middleware.
type Wrapper struct {
next http.Handler
name string
spanKind ext.SpanKindEnum
}
func (w *Wrapper) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
_, err := tracing.FromContext(req.Context())
if err != nil {
w.next.ServeHTTP(rw, req)
return
}
var finish func()
_, req, finish = tracing.StartSpan(req, w.name, w.spanKind)
defer finish()
if w.next != nil {
w.next.ServeHTTP(rw, req)
}
}