Introduce trace verbosity config and produce less spans by default
This commit is contained in:
parent
77ef7fe490
commit
8c23eb6833
93 changed files with 1005 additions and 524 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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{}) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
72
pkg/types/tracing_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue