1
0
Fork 0

Introduce trace verbosity config and produce less spans by default

This commit is contained in:
Romain 2025-07-18 15:32:05 +02:00 committed by GitHub
parent 77ef7fe490
commit 8c23eb6833
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
93 changed files with 1005 additions and 524 deletions

View file

@ -88,9 +88,21 @@ type RouterTLSConfig struct {
// RouterObservabilityConfig holds the observability configuration for a router.
type RouterObservabilityConfig struct {
// AccessLogs enables access logs for this router.
AccessLogs *bool `json:"accessLogs,omitempty" toml:"accessLogs,omitempty" yaml:"accessLogs,omitempty" export:"true"`
Tracing *bool `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
Metrics *bool `json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
// Metrics enables metrics for this router.
Metrics *bool `json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
// Tracing enables tracing for this router.
Tracing *bool `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
// TraceVerbosity defines the verbosity level of the tracing for this router.
// +kubebuilder:validation:Enum=minimal;detailed
// +kubebuilder:default=minimal
TraceVerbosity types.TracingVerbosity `json:"traceVerbosity,omitempty" toml:"traceVerbosity,omitempty" yaml:"traceVerbosity,omitempty" export:"true"`
}
// SetDefaults Default values for a RouterObservabilityConfig.
func (r *RouterObservabilityConfig) SetDefaults() {
r.TraceVerbosity = types.MinimalVerbosity
}
// +k8s:deepcopy-gen=true

View file

@ -1335,13 +1335,13 @@ func (in *RouterObservabilityConfig) DeepCopyInto(out *RouterObservabilityConfig
*out = new(bool)
**out = **in
}
if in.Tracing != nil {
in, out := &in.Tracing, &out.Tracing
if in.Metrics != nil {
in, out := &in.Metrics, &out.Metrics
*out = new(bool)
**out = **in
}
if in.Metrics != nil {
in, out := &in.Metrics, &out.Metrics
if in.Tracing != nil {
in, out := &in.Tracing, &out.Tracing
*out = new(bool)
**out = **in
}

View file

@ -26,9 +26,10 @@ const (
type Configuration struct {
Routers map[string]*RouterInfo `json:"routers,omitempty"`
Middlewares map[string]*MiddlewareInfo `json:"middlewares,omitempty"`
TCPMiddlewares map[string]*TCPMiddlewareInfo `json:"tcpMiddlewares,omitempty"`
Services map[string]*ServiceInfo `json:"services,omitempty"`
Models map[string]*dynamic.Model `json:"-"`
TCPRouters map[string]*TCPRouterInfo `json:"tcpRouters,omitempty"`
TCPMiddlewares map[string]*TCPMiddlewareInfo `json:"tcpMiddlewares,omitempty"`
TCPServices map[string]*TCPServiceInfo `json:"tcpServices,omitempty"`
UDPRouters map[string]*UDPRouterInfo `json:"udpRouters,omitempty"`
UDPServices map[string]*UDPServiceInfo `json:"udpServices,omitempty"`
@ -66,6 +67,8 @@ func NewConfig(conf dynamic.Configuration) *Configuration {
runtimeConfig.Middlewares[k] = &MiddlewareInfo{Middleware: v, Status: StatusEnabled}
}
}
runtimeConfig.Models = conf.HTTP.Models
}
if conf.TCP != nil {

View file

@ -165,15 +165,17 @@ func (u *UDPConfig) SetDefaults() {
// ObservabilityConfig holds the observability configuration for an entry point.
type ObservabilityConfig struct {
AccessLogs *bool `json:"accessLogs,omitempty" toml:"accessLogs,omitempty" yaml:"accessLogs,omitempty" export:"true"`
Tracing *bool `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
Metrics *bool `json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
AccessLogs *bool `description:"Enables access-logs for this entryPoint." json:"accessLogs,omitempty" toml:"accessLogs,omitempty" yaml:"accessLogs,omitempty" export:"true"`
Metrics *bool `description:"Enables metrics for this entryPoint." json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
Tracing *bool `description:"Enables tracing for this entryPoint." json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
TraceVerbosity types.TracingVerbosity `description:"Defines the tracing verbosity level for this entryPoint." json:"traceVerbosity,omitempty" toml:"traceVerbosity,omitempty" yaml:"traceVerbosity,omitempty" export:"true"`
}
// SetDefaults sets the default values.
func (o *ObservabilityConfig) SetDefaults() {
defaultValue := true
o.AccessLogs = &defaultValue
o.Tracing = &defaultValue
o.Metrics = &defaultValue
o.Tracing = &defaultValue
o.TraceVerbosity = types.MinimalVerbosity
}

View file

@ -21,6 +21,7 @@ import (
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/contrib/bridges/otellogrus"
@ -69,11 +70,16 @@ type Handler struct {
wg sync.WaitGroup
}
// WrapHandler Wraps access log handler into an Alice Constructor.
func WrapHandler(handler *Handler) alice.Constructor {
// AliceConstructor returns an alice.Constructor that wraps the Handler (conditionally) in a middleware chain.
func (h *Handler) AliceConstructor() alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
handler.ServeHTTP(rw, req, next)
if h == nil {
next.ServeHTTP(rw, req)
return
}
h.ServeHTTP(rw, req, next)
}), nil
}
}
@ -196,6 +202,12 @@ func GetLogData(req *http.Request) *LogData {
}
func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http.Handler) {
if !observability.AccessLogsEnabled(req.Context()) {
next.ServeHTTP(rw, req)
return
}
now := time.Now().UTC()
core := CoreLogData{

View file

@ -7,6 +7,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCommonLogFormatter_Format(t *testing.T) {
@ -82,8 +83,9 @@ func TestCommonLogFormatter_Format(t *testing.T) {
},
}
// Set timezone to Etc/GMT+9 to have a constant behavior
t.Setenv("TZ", "Etc/GMT+9")
var err error
time.Local, err = time.LoadLocation("Etc/GMT+9")
require.NoError(t, err)
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {

View file

@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/collector/pdata/plog/plogotlp"
"go.opentelemetry.io/otel/attribute"
@ -105,7 +106,15 @@ func TestOTelAccessLog(t *testing.T) {
chain := alice.New()
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logHandler))
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: true,
}), nil
})
chain = chain.Append(logHandler.AliceConstructor())
handler, err := chain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
@ -138,7 +147,15 @@ func TestLogRotation(t *testing.T) {
chain := alice.New()
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logHandler))
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: true,
}), nil
})
chain = chain.Append(logHandler.AliceConstructor())
handler, err := chain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
@ -290,7 +307,15 @@ func TestLoggerHeaderFields(t *testing.T) {
chain := alice.New()
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logger))
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: true,
}), nil
})
chain = chain.Append(logger.AliceConstructor())
handler, err := chain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
@ -998,7 +1023,15 @@ func doLoggingTLSOpt(t *testing.T, config *types.AccessLog, enableTLS, tracing b
chain := alice.New()
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logger))
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: true,
}), nil
})
chain = chain.Append(logger.AliceConstructor())
handler, err := chain.Then(http.HandlerFunc(logWriterTestHandlerFunc))
require.NoError(t, err)
@ -1085,7 +1118,15 @@ func doLoggingWithAbortedStream(t *testing.T, config *types.AccessLog) {
}), nil
})
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logger))
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: true,
}), nil
})
chain = chain.Append(logger.AliceConstructor())
service := NewFieldHandler(http.HandlerFunc(streamBackend), ServiceURL, "http://stream", nil)
service = NewFieldHandler(service, ServiceAddr, "127.0.0.1", nil)

View file

@ -7,7 +7,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const (
@ -39,8 +38,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.AddPrefix, name
return result, nil
}
func (a *addPrefix) GetTracingInformation() (string, string, trace.SpanKind) {
return a.name, typeName, trace.SpanKindInternal
func (a *addPrefix) GetTracingInformation() (string, string) {
return a.name, typeName
}
func (a *addPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -12,7 +12,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/singleflight"
)
@ -61,8 +60,8 @@ func NewBasic(ctx context.Context, next http.Handler, authConfig dynamic.BasicAu
return ba, nil
}
func (b *basicAuth) GetTracingInformation() (string, string, trace.SpanKind) {
return b.name, typeNameBasic, trace.SpanKindInternal
func (b *basicAuth) GetTracingInformation() (string, string) {
return b.name, typeNameBasic
}
func (b *basicAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -12,7 +12,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"go.opentelemetry.io/otel/trace"
)
const (
@ -54,8 +53,8 @@ func NewDigest(ctx context.Context, next http.Handler, authConfig dynamic.Digest
return da, nil
}
func (d *digestAuth) GetTracingInformation() (string, string, trace.SpanKind) {
return d.name, typeNameDigest, trace.SpanKindInternal
func (d *digestAuth) GetTracingInformation() (string, string) {
return d.name, typeNameDigest
}
func (d *digestAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -131,8 +131,8 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu
return fa, nil
}
func (fa *forwardAuth) GetTracingInformation() (string, string, trace.SpanKind) {
return fa.name, typeNameForward, trace.SpanKindInternal
func (fa *forwardAuth) GetTracingInformation() (string, string) {
return fa.name, typeNameForward
}
func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@ -180,7 +180,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
var forwardSpan trace.Span
var tracer *tracing.Tracer
if tracer = tracing.TracerFromContext(req.Context()); tracer != nil {
if tracer = tracing.TracerFromContext(req.Context()); tracer != nil && observability.TracingEnabled(req.Context()) {
var tracingCtx context.Context
tracingCtx, forwardSpan = tracer.Start(req.Context(), "AuthRequest", trace.WithSpanKind(trace.SpanKindClient))
defer forwardSpan.End()

View file

@ -16,6 +16,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/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"github.com/traefik/traefik/v3/pkg/tracing"
@ -756,6 +757,10 @@ func TestForwardAuthTracing(t *testing.T) {
next, err := NewForward(t.Context(), next, auth, "authTest")
require.NoError(t, err)
next = observability.WithObservabilityHandler(next, observability.Observability{
TracingEnabled: true,
})
req := httptest.NewRequest(http.MethodGet, "http://www.test.com/search?q=Opentelemetry", nil)
req.RemoteAddr = "10.0.0.1:1234"
req.Header.Set("User-Agent", "forward-test")

View file

@ -9,7 +9,6 @@ import (
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares"
oxybuffer "github.com/vulcand/oxy/v2/buffer"
"go.opentelemetry.io/otel/trace"
)
const (
@ -48,8 +47,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.Buffering, name
}, nil
}
func (b *buffer) GetTracingInformation() (string, string, trace.SpanKind) {
return b.name, typeName, trace.SpanKindInternal
func (b *buffer) GetTracingInformation() (string, string) {
return b.name, typeName
}
func (b *buffer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -12,7 +12,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/vulcand/oxy/v2/cbreaker"
"go.opentelemetry.io/otel/trace"
)
const typeName = "CircuitBreaker"
@ -68,8 +67,8 @@ func New(ctx context.Context, next http.Handler, confCircuitBreaker dynamic.Circ
}, nil
}
func (c *circuitBreaker) GetTracingInformation() (string, string, trace.SpanKind) {
return c.name, typeName, trace.SpanKindInternal
func (c *circuitBreaker) GetTracingInformation() (string, string) {
return c.name, typeName
}
func (c *circuitBreaker) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -13,7 +13,6 @@ import (
"github.com/klauspost/compress/zstd"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const typeName = "Compress"
@ -181,8 +180,8 @@ func (c *compress) chooseHandler(typ string, rw http.ResponseWriter, req *http.R
}
}
func (c *compress) GetTracingInformation() (string, string, trace.SpanKind) {
return c.name, typeName, trace.SpanKindInternal
func (c *compress) GetTracingInformation() (string, string) {
return c.name, typeName
}
func (c *compress) newGzipHandler() (http.Handler, error) {

View file

@ -15,7 +15,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"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.
@ -83,8 +82,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.ErrorPage, servi
}, nil
}
func (c *customErrors) GetTracingInformation() (string, string, trace.SpanKind) {
return c.name, typeName, trace.SpanKindInternal
func (c *customErrors) GetTracingInformation() (string, string) {
return c.name, typeName
}
func (c *customErrors) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -6,7 +6,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const requestHeaderModifierTypeName = "RequestHeaderModifier"
@ -35,8 +34,8 @@ func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dyn
}
}
func (r *requestHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, requestHeaderModifierTypeName, trace.SpanKindUnspecified
func (r *requestHeaderModifier) GetTracingInformation() (string, string) {
return r.name, requestHeaderModifierTypeName
}
func (r *requestHeaderModifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -6,7 +6,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const responseHeaderModifierTypeName = "ResponseHeaderModifier"
@ -35,8 +34,8 @@ func NewResponseHeaderModifier(ctx context.Context, next http.Handler, config dy
}
}
func (r *responseHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, responseHeaderModifierTypeName, trace.SpanKindUnspecified
func (r *responseHeaderModifier) GetTracingInformation() (string, string) {
return r.name, responseHeaderModifierTypeName
}
func (r *responseHeaderModifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -10,7 +10,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const typeName = "RequestRedirect"
@ -52,8 +51,8 @@ func NewRequestRedirect(ctx context.Context, next http.Handler, conf dynamic.Req
}, nil
}
func (r redirect) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, typeName, trace.SpanKindInternal
func (r redirect) GetTracingInformation() (string, string) {
return r.name, typeName
}
func (r redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -8,7 +8,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const (
@ -38,8 +37,8 @@ func NewURLRewrite(ctx context.Context, next http.Handler, conf dynamic.URLRewri
}
}
func (u urlRewrite) GetTracingInformation() (string, string, trace.SpanKind) {
return u.name, typeName, trace.SpanKindInternal
func (u urlRewrite) GetTracingInformation() (string, string) {
return u.name, typeName
}
func (u urlRewrite) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -8,7 +8,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const (
@ -58,8 +57,8 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Headers, name strin
}, nil
}
func (h *headers) GetTracingInformation() (string, string, trace.SpanKind) {
return h.name, typeName, trace.SpanKindInternal
func (h *headers) GetTracingInformation() (string, string) {
return h.name, typeName
}
func (h *headers) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -13,7 +13,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"go.opentelemetry.io/otel/trace"
)
func TestNew_withoutOptions(t *testing.T) {
@ -107,11 +106,10 @@ func Test_headers_getTracingInformation(t *testing.T) {
name: "testing",
}
name, typeName, spanKind := mid.GetTracingInformation()
name, typeName := mid.GetTracingInformation()
assert.Equal(t, "testing", name)
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

@ -10,7 +10,6 @@ import (
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/vulcand/oxy/v2/connlimit"
"go.opentelemetry.io/otel/trace"
)
const (
@ -53,8 +52,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, string, trace.SpanKind) {
return i.name, typeName, trace.SpanKindInternal
func (i *inFlightReq) GetTracingInformation() (string, string) {
return i.name, typeName
}
func (i *inFlightReq) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -11,7 +11,6 @@ import (
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"go.opentelemetry.io/otel/trace"
)
const (
@ -65,8 +64,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.IPAllowList, nam
}, nil
}
func (al *ipAllowLister) GetTracingInformation() (string, string, trace.SpanKind) {
return al.name, typeName, trace.SpanKindInternal
func (al *ipAllowLister) GetTracingInformation() (string, string) {
return al.name, typeName
}
func (al *ipAllowLister) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -11,7 +11,6 @@ import (
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"go.opentelemetry.io/otel/trace"
)
const (
@ -55,8 +54,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.IPWhiteList, nam
}, nil
}
func (wl *ipWhiteLister) GetTracingInformation() (string, string, trace.SpanKind) {
return wl.name, typeName, trace.SpanKindInternal
func (wl *ipWhiteLister) GetTracingInformation() (string, string) {
return wl.name, typeName
}
func (wl *ipWhiteLister) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -18,7 +18,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/middlewares/retry"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/codes"
)
@ -93,33 +92,45 @@ func NewServiceMiddleware(ctx context.Context, next http.Handler, registry metri
}
}
// WrapEntryPointHandler Wraps metrics entrypoint to alice.Constructor.
func WrapEntryPointHandler(ctx context.Context, registry metrics.Registry, entryPointName string) alice.Constructor {
// EntryPointMetricsHandler returns the metrics entrypoint handler.
func EntryPointMetricsHandler(ctx context.Context, registry metrics.Registry, entryPointName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
if registry == nil || !registry.IsEpEnabled() {
return next, nil
}
return NewEntryPointMiddleware(ctx, next, registry, entryPointName), nil
}
}
// WrapRouterHandler Wraps metrics router to alice.Constructor.
func WrapRouterHandler(ctx context.Context, registry metrics.Registry, routerName string, serviceName string) alice.Constructor {
// RouterMetricsHandler returns the metrics router handler.
func RouterMetricsHandler(ctx context.Context, registry metrics.Registry, routerName string, serviceName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
if registry == nil || !registry.IsRouterEnabled() {
return next, nil
}
return NewRouterMiddleware(ctx, next, registry, routerName, serviceName), nil
}
}
// WrapServiceHandler Wraps metrics service to alice.Constructor.
func WrapServiceHandler(ctx context.Context, registry metrics.Registry, serviceName string) alice.Constructor {
// ServiceMetricsHandler returns the metrics service handler.
func ServiceMetricsHandler(ctx context.Context, registry metrics.Registry, serviceName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
if registry == nil || !registry.IsSvcEnabled() {
return next, nil
}
return NewServiceMiddleware(ctx, next, registry, serviceName), nil
}
}
func (m *metricsMiddleware) GetTracingInformation() (string, string, trace.SpanKind) {
return m.name, typeName, trace.SpanKindInternal
func (m *metricsMiddleware) GetTracingInformation() (string, string) {
return m.name, typeName
}
func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if val := req.Context().Value(observability.DisableMetricsKey); val != nil {
if !observability.MetricsEnabled(req.Context()) {
m.next.ServeHTTP(rw, req)
return
}

View file

@ -48,11 +48,17 @@ func newEntryPoint(ctx context.Context, tracer *tracing.Tracer, entryPointName s
}
func (e *entryPointTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if e.tracer == nil || !TracingEnabled(req.Context()) {
e.next.ServeHTTP(rw, req)
return
}
tracingCtx := tracing.ExtractCarrierIntoContext(req.Context(), req.Header)
start := time.Now()
tracingCtx, span := e.tracer.Start(tracingCtx, "EntryPoint", trace.WithSpanKind(trace.SpanKindServer), trace.WithTimestamp(start))
// Associate the request context with the logger.
// This allows the logger to be aware of the tracing context and log accordingly (TraceID, SpanID, etc.).
logger := log.Ctx(tracingCtx).With().Ctx(tracingCtx).Logger()
loggerCtx := logger.WithContext(tracingCtx)

View file

@ -14,7 +14,7 @@ import (
// Traceable embeds tracing information.
type Traceable interface {
GetTracingInformation() (name string, typeName string, spanKind trace.SpanKind)
GetTracingInformation() (name string, typeName string)
}
// WrapMiddleware adds traceability to an alice.Constructor.
@ -29,21 +29,20 @@ func WrapMiddleware(ctx context.Context, constructor alice.Constructor) alice.Co
}
if traceableHandler, ok := handler.(Traceable); ok {
name, typeName, spanKind := traceableHandler.GetTracingInformation()
name, typeName := traceableHandler.GetTracingInformation()
log.Ctx(ctx).Debug().Str(logs.MiddlewareName, name).Msg("Adding tracing to middleware")
return NewMiddleware(handler, name, typeName, spanKind), nil
return NewMiddleware(handler, name, typeName), nil
}
return handler, nil
}
}
// NewMiddleware returns a http.Handler struct.
func NewMiddleware(next http.Handler, name string, typeName string, spanKind trace.SpanKind) http.Handler {
func NewMiddleware(next http.Handler, name string, typeName string) http.Handler {
return &middlewareTracing{
next: next,
name: name,
typeName: typeName,
spanKind: spanKind,
}
}
@ -52,12 +51,11 @@ 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))
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil && DetailedTracingEnabled(req.Context()) {
tracingCtx, span := tracer.Start(req.Context(), w.typeName, trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
req = req.WithContext(tracingCtx)

View file

@ -3,6 +3,7 @@ package observability
import (
"context"
"fmt"
"net/http"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
@ -10,8 +11,58 @@ import (
type contextKey int
// DisableMetricsKey is a context key used to disable the metrics.
const DisableMetricsKey contextKey = iota
const observabilityKey contextKey = iota
type Observability struct {
AccessLogsEnabled bool
MetricsEnabled bool
SemConvMetricsEnabled bool
TracingEnabled bool
DetailedTracingEnabled bool
}
// WithObservabilityHandler sets the observability state in the context for the next handler.
// This is also used for testing purposes to control whether access logs are enabled or not.
func WithObservabilityHandler(next http.Handler, obs Observability) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
next.ServeHTTP(rw, req.WithContext(WithObservability(req.Context(), obs)))
})
}
// WithObservability injects the observability state into the context.
func WithObservability(ctx context.Context, obs Observability) context.Context {
return context.WithValue(ctx, observabilityKey, obs)
}
// AccessLogsEnabled returns whether access-logs are enabled.
func AccessLogsEnabled(ctx context.Context) bool {
obs, ok := ctx.Value(observabilityKey).(Observability)
return ok && obs.AccessLogsEnabled
}
// MetricsEnabled returns whether metrics are enabled.
func MetricsEnabled(ctx context.Context) bool {
obs, ok := ctx.Value(observabilityKey).(Observability)
return ok && obs.MetricsEnabled
}
// SemConvMetricsEnabled returns whether metrics are enabled.
func SemConvMetricsEnabled(ctx context.Context) bool {
obs, ok := ctx.Value(observabilityKey).(Observability)
return ok && obs.SemConvMetricsEnabled
}
// TracingEnabled returns whether tracing is enabled.
func TracingEnabled(ctx context.Context) bool {
obs, ok := ctx.Value(observabilityKey).(Observability)
return ok && obs.TracingEnabled
}
// DetailedTracingEnabled returns whether detailed tracing is enabled.
func DetailedTracingEnabled(ctx context.Context) bool {
obs, ok := ctx.Value(observabilityKey).(Observability)
return ok && obs.DetailedTracingEnabled
}
// SetStatusErrorf flags the span as in error and log an event.
func SetStatusErrorf(ctx context.Context, format string, args ...interface{}) {

View file

@ -45,7 +45,7 @@ func newRouter(ctx context.Context, router, routerRule, service string, next htt
}
func (f *routerTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil && DetailedTracingEnabled(req.Context()) {
tracingCtx, span := tracer.Start(req.Context(), "Router", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()

View file

@ -46,7 +46,7 @@ func newServerMetricsSemConv(ctx context.Context, semConvMetricRegistry *metrics
}
func (e *semConvServerMetrics) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if e.semConvMetricRegistry == nil || e.semConvMetricRegistry.HTTPServerRequestDuration() == nil {
if e.semConvMetricRegistry == nil || e.semConvMetricRegistry.HTTPServerRequestDuration() == nil || !SemConvMetricsEnabled(req.Context()) {
e.next.ServeHTTP(rw, req)
return
}

View file

@ -83,6 +83,11 @@ func TestSemConvServerMetrics(t *testing.T) {
handler, err = capture.Wrap(handler)
require.NoError(t, err)
// Injection of the observability variables in the request context.
handler = WithObservabilityHandler(handler, Observability{
SemConvMetricsEnabled: true,
})
handler.ServeHTTP(rw, req)
got := metricdata.ResourceMetrics{}

View file

@ -32,7 +32,7 @@ func NewService(ctx context.Context, service string, next http.Handler) http.Han
}
func (t *serviceTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil && DetailedTracingEnabled(req.Context()) {
tracingCtx, span := tracer.Start(req.Context(), "Service", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()

View file

@ -14,7 +14,6 @@ import (
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const typeName = "PassClientTLSCert"
@ -139,8 +138,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.PassTLSClientCer
}, nil
}
func (p *passTLSClientCert) GetTracingInformation() (string, string, trace.SpanKind) {
return p.name, typeName, trace.SpanKindInternal
func (p *passTLSClientCert) GetTracingInformation() (string, string) {
return p.name, typeName
}
func (p *passTLSClientCert) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -14,7 +14,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/vulcand/oxy/v2/utils"
"go.opentelemetry.io/otel/trace"
"golang.org/x/time/rate"
)
@ -127,8 +126,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.RateLimit, name
}, nil
}
func (rl *rateLimiter) GetTracingInformation() (string, string, trace.SpanKind) {
return rl.name, typeName, trace.SpanKindInternal
func (rl *rateLimiter) GetTracingInformation() (string, string) {
return rl.name, typeName
}
func (rl *rateLimiter) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -6,7 +6,6 @@ import (
"regexp"
"github.com/vulcand/oxy/v2/utils"
"go.opentelemetry.io/otel/trace"
)
const (
@ -46,8 +45,8 @@ func newRedirect(next http.Handler, regex, replacement string, permanent bool, r
}, nil
}
func (r *redirect) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, typeName, trace.SpanKindInternal
func (r *redirect) GetTracingInformation() (string, string) {
return r.name, typeName
}
func (r *redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -8,7 +8,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"go.opentelemetry.io/otel/trace"
)
const (
@ -35,8 +34,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.ReplacePath, nam
}, nil
}
func (r *replacePath) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, typeName, trace.SpanKindInternal
func (r *replacePath) GetTracingInformation() (string, string) {
return r.name, typeName
}
func (r *replacePath) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -12,7 +12,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/middlewares/replacepath"
"go.opentelemetry.io/otel/trace"
)
const typeName = "ReplacePathRegex"
@ -42,8 +41,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.ReplacePathRegex
}, nil
}
func (rp *replacePathRegex) GetTracingInformation() (string, string, trace.SpanKind) {
return rp.name, typeName, trace.SpanKindInternal
func (rp *replacePathRegex) GetTracingInformation() (string, string) {
return rp.name, typeName
}
func (rp *replacePathRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

@ -14,6 +14,7 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
@ -124,7 +125,7 @@ func (r *retry) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
var currentSpan trace.Span
operation := func() error {
if tracer != nil {
if tracer != nil && observability.DetailedTracingEnabled(req.Context()) {
if currentSpan != nil {
currentSpan.End()
}

View file

@ -7,7 +7,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const (
@ -45,8 +44,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.StripPrefix, nam
}, nil
}
func (s *stripPrefix) GetTracingInformation() (string, string, trace.SpanKind) {
return s.name, typeName, trace.SpanKindUnspecified
func (s *stripPrefix) GetTracingInformation() (string, string) {
return s.name, typeName
}
func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View file

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

View file

@ -58,9 +58,10 @@ func Test_parseRouterConfig(t *testing.T) {
Options: "foobar",
},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},

View file

@ -124,9 +124,10 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
Options: "foobar",
},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},

View file

@ -242,9 +242,10 @@ func (i *Provider) entryPointModels(cfg *dynamic.Configuration) {
if ep.Observability != nil {
httpModel.Observability = dynamic.RouterObservabilityConfig{
AccessLogs: ep.Observability.AccessLogs,
Tracing: ep.Observability.Tracing,
Metrics: ep.Observability.Metrics,
AccessLogs: ep.Observability.AccessLogs,
Metrics: ep.Observability.Metrics,
Tracing: ep.Observability.Tracing,
TraceVerbosity: ep.Observability.TraceVerbosity,
}
}

View file

@ -38,17 +38,15 @@ func NewProxyBuilder(transportManager TransportManager, semConvMetricsRegistry *
func (r *ProxyBuilder) Update(_ map[string]*dynamic.ServersTransport) {}
// Build builds a new httputil.ReverseProxy with the given configuration.
func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, shouldObserve, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error) {
func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error) {
roundTripper, err := r.transportManager.GetRoundTripper(cfgName)
if err != nil {
return nil, fmt.Errorf("getting RoundTripper: %w", err)
}
if shouldObserve {
// Wrapping the roundTripper with the Tracing roundTripper,
// to handle the reverseProxy client span creation.
roundTripper = newObservabilityRoundTripper(r.semConvMetricsRegistry, roundTripper)
}
// Wrapping the roundTripper with the Tracing roundTripper,
// to create, if necessary, the reverseProxy client span and the semConv client metric.
roundTripper = newObservabilityRoundTripper(r.semConvMetricsRegistry, roundTripper)
return buildSingleHostProxy(targetURL, passHostHeader, preservePath, flushInterval, roundTripper, r.bufferPool), nil
}

View file

@ -23,7 +23,7 @@ func TestEscapedPath(t *testing.T) {
roundTrippers: map[string]http.RoundTripper{"default": &http.Transport{}},
}
p, err := NewProxyBuilder(transportManager, nil).Build("default", testhelpers.MustParseURL(srv.URL), false, true, false, 0)
p, err := NewProxyBuilder(transportManager, nil).Build("default", testhelpers.MustParseURL(srv.URL), true, false, 0)
require.NoError(t, err)
proxy := httptest.NewServer(http.HandlerFunc(p.ServeHTTP))

View file

@ -35,7 +35,7 @@ func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
var span trace.Span
var tracingCtx context.Context
var tracer *tracing.Tracer
if tracer = tracing.TracerFromContext(req.Context()); tracer != nil {
if tracer = tracing.TracerFromContext(req.Context()); tracer != nil && observability.TracingEnabled(req.Context()) {
tracingCtx, span = tracer.Start(req.Context(), "ReverseProxy", trace.WithSpanKind(trace.SpanKindClient))
defer span.End()
@ -68,38 +68,42 @@ func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
span.End(trace.WithTimestamp(end))
}
if req.Context().Value(observability.DisableMetricsKey) == nil && t.semConvMetricRegistry != nil && t.semConvMetricRegistry.HTTPClientRequestDuration() != nil {
var attrs []attribute.KeyValue
if statusCode < 100 || statusCode >= 600 {
attrs = append(attrs, attribute.Key("error.type").String(fmt.Sprintf("Invalid HTTP status code %d", statusCode)))
} else if statusCode >= 400 {
attrs = append(attrs, attribute.Key("error.type").String(strconv.Itoa(statusCode)))
}
attrs = append(attrs, semconv.HTTPRequestMethodKey.String(req.Method))
attrs = append(attrs, semconv.HTTPResponseStatusCode(statusCode))
attrs = append(attrs, semconv.NetworkProtocolName(strings.ToLower(req.Proto)))
attrs = append(attrs, semconv.NetworkProtocolVersion(observability.Proto(req.Proto)))
attrs = append(attrs, semconv.ServerAddress(req.URL.Host))
_, port, err := net.SplitHostPort(req.URL.Host)
if err != nil {
switch req.URL.Scheme {
case "http":
attrs = append(attrs, semconv.ServerPort(80))
case "https":
attrs = append(attrs, semconv.ServerPort(443))
}
} else {
intPort, _ := strconv.Atoi(port)
attrs = append(attrs, semconv.ServerPort(intPort))
}
attrs = append(attrs, semconv.URLScheme(req.Header.Get("X-Forwarded-Proto")))
t.semConvMetricRegistry.HTTPClientRequestDuration().Record(req.Context(), end.Sub(start).Seconds(), metric.WithAttributes(attrs...))
if !observability.SemConvMetricsEnabled(req.Context()) ||
t.semConvMetricRegistry == nil ||
t.semConvMetricRegistry.HTTPClientRequestDuration() == nil {
return response, err
}
var attrs []attribute.KeyValue
if statusCode < 100 || statusCode >= 600 {
attrs = append(attrs, attribute.Key("error.type").String(fmt.Sprintf("Invalid HTTP status code %d", statusCode)))
} else if statusCode >= 400 {
attrs = append(attrs, attribute.Key("error.type").String(strconv.Itoa(statusCode)))
}
attrs = append(attrs, semconv.HTTPRequestMethodKey.String(req.Method))
attrs = append(attrs, semconv.HTTPResponseStatusCode(statusCode))
attrs = append(attrs, semconv.NetworkProtocolName(strings.ToLower(req.Proto)))
attrs = append(attrs, semconv.NetworkProtocolVersion(observability.Proto(req.Proto)))
attrs = append(attrs, semconv.ServerAddress(req.URL.Host))
_, port, splitErr := net.SplitHostPort(req.URL.Host)
if splitErr != nil {
switch req.URL.Scheme {
case "http":
attrs = append(attrs, semconv.ServerPort(80))
case "https":
attrs = append(attrs, semconv.ServerPort(443))
}
} else {
intPort, _ := strconv.Atoi(port)
attrs = append(attrs, semconv.ServerPort(intPort))
}
attrs = append(attrs, semconv.URLScheme(req.Header.Get("X-Forwarded-Proto")))
t.semConvMetricRegistry.HTTPClientRequestDuration().Record(req.Context(), end.Sub(start).Seconds(), metric.WithAttributes(attrs...))
return response, err
}

View file

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/otel/attribute"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
@ -77,6 +78,11 @@ func TestObservabilityRoundTripper_metrics(t *testing.T) {
req.Header.Set("User-Agent", "rt-test")
req.Header.Set("X-Forwarded-Proto", "http")
// Injection of the observability variables in the request context.
req = req.WithContext(observability.WithObservability(req.Context(), observability.Observability{
SemConvMetricsEnabled: true,
}))
ort := newObservabilityRoundTripper(semConvMetricRegistry, mockRoundTripper{statusCode: test.statusCode})
_, err = ort.RoundTrip(req)
require.NoError(t, err)

View file

@ -301,7 +301,7 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
},
}
p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), false, true, false, 0)
p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), true, false, 0)
require.NoError(t, err)
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testhelpers.MustParseURL(srv.URL)
@ -357,7 +357,7 @@ func TestWebSocketUpgradeFailed(t *testing.T) {
},
}
p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), false, true, false, 0)
p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), true, false, 0)
require.NoError(t, err)
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path // keep the original path
@ -618,7 +618,7 @@ func createProxyWithForwarder(t *testing.T, uri string, transport http.RoundTrip
roundTrippers: map[string]http.RoundTripper{"fwd": transport},
}
p, err := NewProxyBuilder(transportManager, nil).Build("fwd", u, false, true, false, 0)
p, err := NewProxyBuilder(transportManager, nil).Build("fwd", u, true, false, 0)
require.NoError(t, err)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

View file

@ -45,7 +45,7 @@ func (b *SmartBuilder) Update(newConfigs map[string]*dynamic.ServersTransport) {
}
// Build builds an HTTP proxy for the given URL using the ServersTransport with the given name.
func (b *SmartBuilder) Build(configName string, targetURL *url.URL, shouldObserve, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error) {
func (b *SmartBuilder) Build(configName string, targetURL *url.URL, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error) {
serversTransport, err := b.transportManager.Get(configName)
if err != nil {
return nil, fmt.Errorf("getting ServersTransport: %w", err)
@ -55,7 +55,7 @@ func (b *SmartBuilder) Build(configName string, targetURL *url.URL, shouldObserv
// For the https scheme we cannot guess if the backend communication will use HTTP2,
// thus we check if HTTP/2 is disabled to use the fast proxy implementation when this is possible.
if targetURL.Scheme == "h2c" || (targetURL.Scheme == "https" && !serversTransport.DisableHTTP2) {
return b.proxyBuilder.Build(configName, targetURL, shouldObserve, passHostHeader, preservePath, flushInterval)
return b.proxyBuilder.Build(configName, targetURL, passHostHeader, preservePath, flushInterval)
}
return b.fastProxyBuilder.Build(configName, targetURL, passHostHeader, preservePath)
}

View file

@ -101,7 +101,7 @@ func TestSmartBuilder_Build(t *testing.T) {
httpProxyBuilder := httputil.NewProxyBuilder(transportManager, nil)
proxyBuilder := NewSmartBuilder(transportManager, httpProxyBuilder, test.fastProxyConfig)
proxyHandler, err := proxyBuilder.Build("test", targetURL, false, false, false, time.Second)
proxyHandler, err := proxyBuilder.Build("test", targetURL, false, false, time.Second)
require.NoError(t, err)
rw := httptest.NewRecorder()

View file

@ -10,6 +10,7 @@ import (
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/server/provider"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
)
func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoints []string) dynamic.Configuration {
@ -208,6 +209,10 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
cp.Observability.Tracing = m.Observability.Tracing
}
if cp.Observability.TraceVerbosity == "" {
cp.Observability.TraceVerbosity = m.Observability.TraceVerbosity
}
rtName := name
if len(eps) > 1 {
rtName = epName + "-" + name
@ -224,7 +229,7 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
cfg.HTTP.Routers = rts
}
// Apply default observability model to HTTP routers.
// Apply the default observability model to HTTP routers.
applyDefaultObservabilityModel(cfg)
if cfg.TCP == nil || len(cfg.TCP.Models) == 0 {
@ -256,14 +261,16 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
// and make sure it is serialized and available in the API.
// We could have introduced a "default" model, but it would have been more complex to manage for now.
// This could be generalized in the future.
// TODO: check if we can remove this and rely on the SetDefaults instead.
func applyDefaultObservabilityModel(cfg dynamic.Configuration) {
if cfg.HTTP != nil {
for _, router := range cfg.HTTP.Routers {
if router.Observability == nil {
router.Observability = &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
}
continue
@ -273,12 +280,16 @@ func applyDefaultObservabilityModel(cfg dynamic.Configuration) {
router.Observability.AccessLogs = pointer(true)
}
if router.Observability.Metrics == nil {
router.Observability.Metrics = pointer(true)
}
if router.Observability.Tracing == nil {
router.Observability.Tracing = pointer(true)
}
if router.Observability.Metrics == nil {
router.Observability.Metrics = pointer(true)
if router.Observability.TraceVerbosity == "" {
router.Observability.TraceVerbosity = types.MinimalVerbosity
}
}
}

View file

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
)
func Test_mergeConfiguration(t *testing.T) {
@ -521,9 +522,10 @@ func Test_applyModel(t *testing.T) {
Routers: map[string]*dynamic.Router{
"test": {
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -589,9 +591,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -622,9 +625,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -638,9 +642,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -651,9 +656,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -688,9 +694,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{CertResolver: "router"},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -730,9 +737,10 @@ func Test_applyModel(t *testing.T) {
"test": {
EntryPoints: []string{"web"},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
"websecure-test": {
@ -740,9 +748,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},

View file

@ -428,8 +428,5 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName)
}
// The tracing middleware is a NOOP if tracing is not setup on the middleware chain.
// Hence, regarding internal resources' observability deactivation,
// this would not enable tracing.
return observability.WrapMiddleware(ctx, middleware), nil
}

View file

@ -4,7 +4,6 @@ import (
"context"
"io"
"net/http"
"strings"
"github.com/containous/alice"
"github.com/rs/zerolog/log"
@ -17,6 +16,7 @@ import (
mmetrics "github.com/traefik/traefik/v3/pkg/middlewares/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/traefik/traefik/v3/pkg/types"
)
// ObservabilityMgr is a manager for observability (AccessLogs, Metrics and Tracing) enablement.
@ -42,111 +42,44 @@ func NewObservabilityMgr(config static.Configuration, metricsRegistry metrics.Re
}
// BuildEPChain an observability middleware chain by entry point.
func (o *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName string, resourceName string, observabilityConfig *dynamic.RouterObservabilityConfig) alice.Chain {
func (o *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName string, internal bool, config dynamic.RouterObservabilityConfig) alice.Chain {
chain := alice.New()
if o == nil {
return chain
}
if o.accessLoggerMiddleware != nil || o.metricsRegistry != nil && (o.metricsRegistry.IsEpEnabled() || o.metricsRegistry.IsRouterEnabled() || o.metricsRegistry.IsSvcEnabled()) {
if o.ShouldAddAccessLogs(resourceName, observabilityConfig) || o.ShouldAddMetrics(resourceName, observabilityConfig) {
chain = chain.Append(capture.Wrap)
}
// Injection of the observability variables in the request context.
// This injection must be the first step in order for other observability middlewares to rely on it.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return o.observabilityContextHandler(next, internal, config), nil
})
// Capture middleware for accessLogs or metrics.
if o.shouldAccessLog(internal, config) || o.shouldMeter(internal, config) || o.shouldMeterSemConv(internal, config) {
chain = chain.Append(capture.Wrap)
}
// As the Entry point observability middleware ensures that the tracing is added to the request and logger context,
// it needs to be added before the access log middleware to ensure that the trace ID is logged.
if o.tracer != nil && o.ShouldAddTracing(resourceName, observabilityConfig) {
chain = chain.Append(observability.EntryPointHandler(ctx, o.tracer, entryPointName))
}
chain = chain.Append(observability.EntryPointHandler(ctx, o.tracer, entryPointName))
if o.accessLoggerMiddleware != nil && o.ShouldAddAccessLogs(resourceName, observabilityConfig) {
chain = chain.Append(accesslog.WrapHandler(o.accessLoggerMiddleware))
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return accesslog.NewFieldHandler(next, logs.EntryPointName, entryPointName, accesslog.InitServiceFields), nil
})
}
// Access log handlers.
chain = chain.Append(o.accessLoggerMiddleware.AliceConstructor())
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return accesslog.NewFieldHandler(next, logs.EntryPointName, entryPointName, accesslog.InitServiceFields), nil
})
// Entrypoint metrics handler.
metricsHandler := mmetrics.EntryPointMetricsHandler(ctx, o.metricsRegistry, entryPointName)
chain = chain.Append(observability.WrapMiddleware(ctx, metricsHandler))
// Semantic convention server metrics handler.
if o.semConvMetricRegistry != nil && o.ShouldAddMetrics(resourceName, observabilityConfig) {
chain = chain.Append(observability.SemConvServerMetricsHandler(ctx, o.semConvMetricRegistry))
}
if o.metricsRegistry != nil && o.metricsRegistry.IsEpEnabled() && o.ShouldAddMetrics(resourceName, observabilityConfig) {
metricsHandler := mmetrics.WrapEntryPointHandler(ctx, o.metricsRegistry, entryPointName)
if o.tracer != nil && o.ShouldAddTracing(resourceName, observabilityConfig) {
chain = chain.Append(observability.WrapMiddleware(ctx, metricsHandler))
} else {
chain = chain.Append(metricsHandler)
}
}
// Inject context keys to control whether to produce metrics further downstream (services, round-tripper),
// because the router configuration cannot be evaluated during build time for services.
if observabilityConfig != nil && observabilityConfig.Metrics != nil && !*observabilityConfig.Metrics {
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
next.ServeHTTP(rw, req.WithContext(context.WithValue(req.Context(), observability.DisableMetricsKey, true)))
}), nil
})
}
chain = chain.Append(observability.SemConvServerMetricsHandler(ctx, o.semConvMetricRegistry))
return chain
}
// ShouldAddAccessLogs returns whether the access logs should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) ShouldAddAccessLogs(serviceName string, observabilityConfig *dynamic.RouterObservabilityConfig) bool {
if o == nil {
return false
}
if o.config.AccessLog == nil {
return false
}
if strings.HasSuffix(serviceName, "@internal") && !o.config.AccessLog.AddInternals {
return false
}
return observabilityConfig == nil || observabilityConfig.AccessLogs == nil || *observabilityConfig.AccessLogs
}
// ShouldAddMetrics returns whether the metrics should be enabled for the given resource and the observability config.
func (o *ObservabilityMgr) ShouldAddMetrics(serviceName string, observabilityConfig *dynamic.RouterObservabilityConfig) bool {
if o == nil {
return false
}
if o.config.Metrics == nil {
return false
}
if strings.HasSuffix(serviceName, "@internal") && !o.config.Metrics.AddInternals {
return false
}
return observabilityConfig == nil || observabilityConfig.Metrics == nil || *observabilityConfig.Metrics
}
// ShouldAddTracing returns whether the tracing should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) ShouldAddTracing(serviceName string, observabilityConfig *dynamic.RouterObservabilityConfig) bool {
if o == nil {
return false
}
if o.config.Tracing == nil {
return false
}
if strings.HasSuffix(serviceName, "@internal") && !o.config.Tracing.AddInternals {
return false
}
return observabilityConfig == nil || observabilityConfig.Tracing == nil || *observabilityConfig.Tracing
}
// MetricsRegistry is an accessor to the metrics registry.
func (o *ObservabilityMgr) MetricsRegistry() metrics.Registry {
if o == nil {
@ -191,3 +124,89 @@ func (o *ObservabilityMgr) RotateAccessLogs() error {
return o.accessLoggerMiddleware.Rotate()
}
func (o *ObservabilityMgr) observabilityContextHandler(next http.Handler, internal bool, config dynamic.RouterObservabilityConfig) http.Handler {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: o.shouldAccessLog(internal, config),
MetricsEnabled: o.shouldMeter(internal, config),
SemConvMetricsEnabled: o.shouldMeterSemConv(internal, config),
TracingEnabled: o.shouldTrace(internal, config, types.MinimalVerbosity),
DetailedTracingEnabled: o.shouldTrace(internal, config, types.DetailedVerbosity),
})
}
// shouldAccessLog returns whether the access logs should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldAccessLog(internal bool, observabilityConfig dynamic.RouterObservabilityConfig) bool {
if o == nil {
return false
}
if o.config.AccessLog == nil {
return false
}
if internal && !o.config.AccessLog.AddInternals {
return false
}
return observabilityConfig.AccessLogs == nil || *observabilityConfig.AccessLogs
}
// shouldMeter returns whether the metrics should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldMeter(internal bool, observabilityConfig dynamic.RouterObservabilityConfig) bool {
if o == nil || o.metricsRegistry == nil {
return false
}
if !o.metricsRegistry.IsEpEnabled() && !o.metricsRegistry.IsRouterEnabled() && !o.metricsRegistry.IsSvcEnabled() {
return false
}
if o.config.Metrics == nil {
return false
}
if internal && !o.config.Metrics.AddInternals {
return false
}
return observabilityConfig.Metrics == nil || *observabilityConfig.Metrics
}
// shouldMeterSemConv returns whether the OTel semantic convention metrics should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldMeterSemConv(internal bool, observabilityConfig dynamic.RouterObservabilityConfig) bool {
if o == nil || o.semConvMetricRegistry == nil {
return false
}
if o.config.Metrics == nil {
return false
}
if internal && !o.config.Metrics.AddInternals {
return false
}
return observabilityConfig.Metrics == nil || *observabilityConfig.Metrics
}
// shouldTrace returns whether the tracing should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldTrace(internal bool, observabilityConfig dynamic.RouterObservabilityConfig, verbosity types.TracingVerbosity) bool {
if o == nil {
return false
}
if o.config.Tracing == nil {
return false
}
if internal && !o.config.Tracing.AddInternals {
return false
}
if !observabilityConfig.TraceVerbosity.Allows(verbosity) {
return false
}
return observabilityConfig.Tracing == nil || *observabilityConfig.Tracing
}

View file

@ -7,7 +7,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/plugins"
"go.opentelemetry.io/otel/trace"
)
const typeName = "Plugin"
@ -55,6 +54,6 @@ func (s *traceablePlugin) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
s.h.ServeHTTP(rw, req)
}
func (s *traceablePlugin) GetTracingInformation() (string, string, trace.SpanKind) {
return s.name, typeName, trace.SpanKindInternal
func (s *traceablePlugin) GetTracingInformation() (string, string) {
return s.name, typeName
}

View file

@ -10,6 +10,7 @@ import (
"github.com/containous/alice"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/runtime"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
@ -70,11 +71,22 @@ func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls
func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, tls bool) map[string]http.Handler {
entryPointHandlers := make(map[string]http.Handler)
defaultObsConfig := dynamic.RouterObservabilityConfig{}
defaultObsConfig.SetDefaults()
for entryPointName, routers := range m.getHTTPRouters(rootCtx, entryPoints, tls) {
logger := log.Ctx(rootCtx).With().Str(logs.EntryPointName, entryPointName).Logger()
ctx := logger.WithContext(rootCtx)
handler, err := m.buildEntryPointHandler(ctx, entryPointName, routers)
// TODO: Improve this part. Relying on models is a shortcut to get the entrypoint observability configuration. Maybe we should pass down the static configuration.
// When the entry point has no observability configuration no model is produced,
// and we need to create the default configuration is this case.
epObsConfig := defaultObsConfig
if model, ok := m.conf.Models[entryPointName+"@internal"]; ok && model != nil {
epObsConfig = model.Observability
}
handler, err := m.buildEntryPointHandler(ctx, entryPointName, routers, epObsConfig)
if err != nil {
logger.Error().Err(err).Send()
continue
@ -93,7 +105,15 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t
continue
}
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, "", nil).Then(BuildDefaultHTTPRouter())
// TODO: Improve this part. Relying on models is a shortcut to get the entrypoint observability configuration. Maybe we should pass down the static configuration.
// When the entry point has no observability configuration no model is produced,
// and we need to create the default configuration is this case.
epObsConfig := defaultObsConfig
if model, ok := m.conf.Models[entryPointName+"@internal"]; ok && model != nil {
epObsConfig = model.Observability
}
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, false, epObsConfig).Then(http.NotFoundHandler())
if err != nil {
logger.Error().Err(err).Send()
continue
@ -104,10 +124,10 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t
return entryPointHandlers
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName string, configs map[string]*runtime.RouterInfo) (http.Handler, error) {
func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName string, configs map[string]*runtime.RouterInfo, config dynamic.RouterObservabilityConfig) (http.Handler, error) {
muxer := httpmuxer.NewMuxer(m.parser)
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, "", nil).Then(http.NotFoundHandler())
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, false, config).Then(http.NotFoundHandler())
if err != nil {
return nil, err
}
@ -136,7 +156,11 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str
continue
}
observabilityChain := m.observabilityMgr.BuildEPChain(ctx, entryPointName, routerConfig.Service, routerConfig.Observability)
if routerConfig.Observability != nil {
config = *routerConfig.Observability
}
observabilityChain := m.observabilityMgr.BuildEPChain(ctxRouter, entryPointName, strings.HasSuffix(routerConfig.Service, "@internal"), config)
handler, err = observabilityChain.Then(handler)
if err != nil {
routerConfig.AddError(err, true)
@ -180,22 +204,7 @@ func (m *Manager) buildRouterHandler(ctx context.Context, routerName string, rou
return nil, err
}
// Prevents from enabling observability for internal resources.
if !m.observabilityMgr.ShouldAddAccessLogs(provider.GetQualifiedName(ctx, routerConfig.Service), routerConfig.Observability) {
m.routerHandlers[routerName] = handler
return m.routerHandlers[routerName], nil
}
handlerWithAccessLog, err := alice.New(func(next http.Handler) (http.Handler, error) {
return accesslog.NewFieldHandler(next, accesslog.RouterName, routerName, nil), nil
}).Then(handler)
if err != nil {
log.Ctx(ctx).Error().Err(err).Send()
m.routerHandlers[routerName] = handler
} else {
m.routerHandlers[routerName] = handlerWithAccessLog
}
m.routerHandlers[routerName] = handler
return m.routerHandlers[routerName], nil
}
@ -210,40 +219,29 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn
return nil, errors.New("the service is missing on the router")
}
sHandler, err := m.serviceManager.BuildHTTP(ctx, router.Service)
if err != nil {
return nil, err
}
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
qualifiedService := provider.GetQualifiedName(ctx, router.Service)
chain := alice.New()
if m.observabilityMgr.MetricsRegistry() != nil && m.observabilityMgr.MetricsRegistry().IsRouterEnabled() &&
m.observabilityMgr.ShouldAddMetrics(provider.GetQualifiedName(ctx, router.Service), router.Observability) {
chain = chain.Append(metricsMiddle.WrapRouterHandler(ctx, m.observabilityMgr.MetricsRegistry(), routerName, provider.GetQualifiedName(ctx, router.Service)))
}
// Prevents from enabling tracing for internal resources.
if !m.observabilityMgr.ShouldAddTracing(provider.GetQualifiedName(ctx, router.Service), router.Observability) {
return chain.Extend(*mHandler).Then(sHandler)
}
chain = chain.Append(observability.WrapRouterHandler(ctx, routerName, router.Rule, provider.GetQualifiedName(ctx, router.Service)))
if m.observabilityMgr.MetricsRegistry() != nil && m.observabilityMgr.MetricsRegistry().IsRouterEnabled() {
metricsHandler := metricsMiddle.WrapRouterHandler(ctx, m.observabilityMgr.MetricsRegistry(), routerName, provider.GetQualifiedName(ctx, router.Service))
chain = chain.Append(observability.WrapMiddleware(ctx, metricsHandler))
}
if router.DefaultRule {
chain = chain.Append(denyrouterrecursion.WrapHandler(routerName))
}
// Access logs, metrics, and tracing middlewares are idempotent if the associated signal is disabled.
chain = chain.Append(observability.WrapRouterHandler(ctx, routerName, router.Rule, qualifiedService))
metricsHandler := metricsMiddle.RouterMetricsHandler(ctx, m.observabilityMgr.MetricsRegistry(), routerName, qualifiedService)
chain = chain.Append(observability.WrapMiddleware(ctx, metricsHandler))
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return accesslog.NewFieldHandler(next, accesslog.RouterName, routerName, nil), nil
})
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
sHandler, err := m.serviceManager.BuildHTTP(ctx, qualifiedService)
if err != nil {
return nil, err
}
return chain.Extend(*mHandler).Then(sHandler)
}
// BuildDefaultHTTPRouter creates a default HTTP router.
func BuildDefaultHTTPRouter() http.Handler {
return http.NotFoundHandler()
}

View file

@ -929,7 +929,7 @@ func BenchmarkService(b *testing.B) {
type proxyBuilderMock struct{}
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _, _ bool, _ time.Duration) (http.Handler, error) {
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil
}

View file

@ -258,7 +258,7 @@ func TestInternalServices(t *testing.T) {
type proxyBuilderMock struct{}
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _, _ bool, _ time.Duration) (http.Handler, error) {
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil
}

View file

@ -29,7 +29,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/forwardedheaders"
"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v3/pkg/safe"
"github.com/traefik/traefik/v3/pkg/server/router"
tcprouter "github.com/traefik/traefik/v3/pkg/server/router/tcp"
"github.com/traefik/traefik/v3/pkg/server/service"
"github.com/traefik/traefik/v3/pkg/tcp"
@ -351,7 +350,7 @@ func (e *TCPEntryPoint) SwitchRouter(rt *tcprouter.Router) {
httpHandler := rt.GetHTTPHandler()
if httpHandler == nil {
httpHandler = router.BuildDefaultHTTPRouter()
httpHandler = http.NotFoundHandler()
}
e.httpServer.Switcher.UpdateHandler(httpHandler)
@ -360,7 +359,7 @@ func (e *TCPEntryPoint) SwitchRouter(rt *tcprouter.Router) {
httpsHandler := rt.GetHTTPSHandler()
if httpsHandler == nil {
httpsHandler = router.BuildDefaultHTTPRouter()
httpsHandler = http.NotFoundHandler()
}
e.httpsServer.Switcher.UpdateHandler(httpsHandler)
@ -591,7 +590,7 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
return nil, errors.New("max concurrent streams value must be greater than or equal to zero")
}
httpSwitcher := middlewares.NewHandlerSwitcher(router.BuildDefaultHTTPRouter())
httpSwitcher := middlewares.NewHandlerSwitcher(http.NotFoundHandler())
next, err := alice.New(requestdecorator.WrapHandler(reqDecorator)).Then(httpSwitcher)
if err != nil {

View file

@ -19,7 +19,6 @@ import (
"github.com/traefik/traefik/v3/pkg/healthcheck"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/middlewares/retry"
@ -37,7 +36,7 @@ import (
// ProxyBuilder builds reverse proxy handlers.
type ProxyBuilder interface {
Build(cfgName string, targetURL *url.URL, shouldObserve, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error)
Build(cfgName string, targetURL *url.URL, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error)
Update(configs map[string]*dynamic.ServersTransport)
}
@ -364,50 +363,32 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
qualifiedSvcName := provider.GetQualifiedName(ctx, serviceName)
shouldObserve := m.observabilityMgr.ShouldAddTracing(qualifiedSvcName, nil) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName, nil)
proxy, err := m.proxyBuilder.Build(service.ServersTransport, target, shouldObserve, passHostHeader, server.PreservePath, flushInterval)
proxy, err := m.proxyBuilder.Build(service.ServersTransport, target, passHostHeader, server.PreservePath, flushInterval)
if err != nil {
return nil, fmt.Errorf("error building proxy for server URL %s: %w", server.URL, err)
}
// The retry wrapping must be done just before the proxy handler,
// to make sure that the retry will not be triggered/disabled by
// middlewares in the chain.
proxy = retry.WrapHandler(proxy)
// Prevents from enabling observability for internal resources.
// Access logs, metrics, and tracing middlewares are idempotent if the associated signal is disabled.
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceURL, target.String(), nil)
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceAddr, target.Host, nil)
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceName, qualifiedSvcName, accesslog.AddServiceFields)
if m.observabilityMgr.ShouldAddAccessLogs(qualifiedSvcName, nil) {
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceURL, target.String(), nil)
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceAddr, target.Host, nil)
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceName, serviceName, accesslog.AddServiceFields)
metricsHandler := metricsMiddle.ServiceMetricsHandler(ctx, m.observabilityMgr.MetricsRegistry(), qualifiedSvcName)
metricsHandler = observability.WrapMiddleware(ctx, metricsHandler)
proxy, err = alice.New().
Append(metricsHandler).
Then(proxy)
if err != nil {
return nil, fmt.Errorf("error wrapping metrics handler: %w", err)
}
if m.observabilityMgr.MetricsRegistry() != nil && m.observabilityMgr.MetricsRegistry().IsSvcEnabled() &&
m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName, nil) {
metricsHandler := metricsMiddle.WrapServiceHandler(ctx, m.observabilityMgr.MetricsRegistry(), serviceName)
proxy, err = alice.New().
Append(observability.WrapMiddleware(ctx, metricsHandler)).
Then(proxy)
if err != nil {
return nil, fmt.Errorf("error wrapping metrics handler: %w", err)
}
}
if m.observabilityMgr.ShouldAddTracing(qualifiedSvcName, nil) {
proxy = observability.NewService(ctx, serviceName, proxy)
}
if m.observabilityMgr.ShouldAddAccessLogs(qualifiedSvcName, nil) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName, nil) {
// Some piece of middleware, like the ErrorPage, are relying on this serviceBuilder to get the handler for a given service,
// to re-target the request to it.
// Those pieces of middleware can be configured on routes that expose a Traefik internal service.
// In such a case, observability for internals being optional, the capture probe could be absent from context (no wrap via the entrypoint).
// But if the service targeted by this piece of middleware is not an internal one,
// and requires observability, we still want the capture probe to be present in the request context.
// Makes sure a capture probe is in the request context.
proxy, _ = capture.Wrap(proxy)
}
proxy = observability.NewService(ctx, qualifiedSvcName, proxy)
lb.AddServer(server.URL, proxy, server)

View file

@ -2,6 +2,7 @@ package testhelpers
import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/types"
)
// BuildConfiguration is a helper to create a configuration.
@ -57,9 +58,10 @@ func WithServiceName(serviceName string) func(*dynamic.Router) {
func WithObservability() func(*dynamic.Router) {
return func(r *dynamic.Router) {
r.Observability = &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
}
}
}

View file

@ -23,6 +23,22 @@ import (
"google.golang.org/grpc/encoding/gzip"
)
type TracingVerbosity string
const (
MinimalVerbosity TracingVerbosity = "minimal"
DetailedVerbosity TracingVerbosity = "detailed"
)
func (v TracingVerbosity) Allows(verbosity TracingVerbosity) bool {
switch v {
case DetailedVerbosity:
return verbosity == DetailedVerbosity || verbosity == MinimalVerbosity
default:
return verbosity == MinimalVerbosity
}
}
// OTelTracing provides configuration settings for the open-telemetry tracer.
type OTelTracing struct {
GRPC *OTelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`

72
pkg/types/tracing_test.go Normal file
View file

@ -0,0 +1,72 @@
package types
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestTracingVerbosity_Allows(t *testing.T) {
tests := []struct {
desc string
from TracingVerbosity
to TracingVerbosity
allows bool
}{
{
desc: "minimal vs minimal",
from: MinimalVerbosity,
to: MinimalVerbosity,
allows: true,
},
{
desc: "minimal vs detailed",
from: MinimalVerbosity,
to: DetailedVerbosity,
allows: false,
},
{
desc: "detailed vs minimal",
from: DetailedVerbosity,
to: MinimalVerbosity,
allows: true,
},
{
desc: "detailed vs detailed",
from: DetailedVerbosity,
to: DetailedVerbosity,
allows: true,
},
{
desc: "unknown vs minimal",
from: TracingVerbosity("unknown"),
to: MinimalVerbosity,
allows: true,
},
{
desc: "unknown vs detailed",
from: TracingVerbosity("unknown"),
to: DetailedVerbosity,
allows: false,
},
{
desc: "minimal vs unknown",
from: MinimalVerbosity,
to: TracingVerbosity("unknown"),
allows: false,
},
{
desc: "detailed vs unknown",
from: DetailedVerbosity,
to: TracingVerbosity("unknown"),
allows: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
require.Equal(t, test.allows, test.from.Allows(test.to))
})
}
}