1
0
Fork 0

Add captured headers options for tracing

Co-authored-by: Baptiste Mayelle <baptiste.mayelle@traefik.io>
This commit is contained in:
Romain 2024-03-11 11:50:04 +01:00 committed by GitHub
parent 86be0a4e6f
commit 709ff6fb09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 520 additions and 119 deletions

View file

@ -15,7 +15,7 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics"
tracingMiddle "github.com/traefik/traefik/v3/pkg/middlewares/tracing"
"go.opentelemetry.io/otel/trace"
"github.com/traefik/traefik/v3/pkg/tracing"
)
// ObservabilityMgr is a manager for observability (AccessLogs, Metrics and Tracing) enablement.
@ -23,12 +23,12 @@ type ObservabilityMgr struct {
config static.Configuration
accessLoggerMiddleware *accesslog.Handler
metricsRegistry metrics.Registry
tracer trace.Tracer
tracer *tracing.Tracer
tracerCloser io.Closer
}
// NewObservabilityMgr creates a new ObservabilityMgr.
func NewObservabilityMgr(config static.Configuration, metricsRegistry metrics.Registry, accessLoggerMiddleware *accesslog.Handler, tracer trace.Tracer, tracerCloser io.Closer) *ObservabilityMgr {
func NewObservabilityMgr(config static.Configuration, metricsRegistry metrics.Registry, accessLoggerMiddleware *accesslog.Handler, tracer *tracing.Tracer, tracerCloser io.Closer) *ObservabilityMgr {
return &ObservabilityMgr{
config: config,
metricsRegistry: metricsRegistry,

View file

@ -14,25 +14,27 @@ type wrapper struct {
func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
var span trace.Span
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil {
var tracer *tracing.Tracer
if tracer = tracing.TracerFromContext(req.Context()); tracer != nil {
var tracingCtx context.Context
tracingCtx, span = tracer.Start(req.Context(), "ReverseProxy", trace.WithSpanKind(trace.SpanKindClient))
defer span.End()
req = req.WithContext(tracingCtx)
tracing.LogClientRequest(span, req)
tracer.CaptureClientRequest(span, req)
tracing.InjectContextIntoCarrier(req)
}
response, err := t.rt.RoundTrip(req)
if err != nil {
statusCode := computeStatusCode(err)
tracing.LogResponseCode(span, statusCode, trace.SpanKindClient)
tracer.CaptureResponse(span, nil, statusCode, trace.SpanKindClient)
return response, err
}
tracing.LogResponseCode(span, response.StatusCode, trace.SpanKindClient)
tracer.CaptureResponse(span, response.Header, response.StatusCode, trace.SpanKindClient)
return response, nil
}

View file

@ -0,0 +1,138 @@
package service
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/embedded"
)
func TestTracingRoundTripper(t *testing.T) {
type expected struct {
name string
attributes []attribute.KeyValue
}
testCases := []struct {
desc string
expected []expected
}{
{
desc: "basic test",
expected: []expected{
{
name: "initial",
attributes: []attribute.KeyValue{
attribute.String("span.kind", "unspecified"),
},
},
{
name: "ReverseProxy",
attributes: []attribute.KeyValue{
attribute.String("span.kind", "client"),
attribute.String("http.request.method", "GET"),
attribute.String("network.protocol.version", "1.1"),
attribute.String("url.full", "http://www.test.com/search?q=Opentelemetry"),
attribute.String("url.scheme", "http"),
attribute.String("user_agent.original", "reverse-test"),
attribute.String("network.peer.address", ""),
attribute.String("server.address", "www.test.com"),
attribute.String("network.peer.port", "80"),
attribute.Int64("server.port", int64(80)),
attribute.StringSlice("http.request.header.x-foo", []string{"foo", "bar"}),
attribute.Int64("http.response.status_code", int64(404)),
attribute.StringSlice("http.response.header.x-bar", []string{"foo", "bar"}),
},
},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
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", "reverse-test")
req.Header.Set("X-Forwarded-Proto", "http")
req.Header.Set("X-Foo", "foo")
req.Header.Add("X-Foo", "bar")
mockTracer := &mockTracer{}
tracer := tracing.NewTracer(mockTracer, []string{"X-Foo"}, []string{"X-Bar"})
initialCtx, initialSpan := tracer.Start(req.Context(), "initial")
defer initialSpan.End()
req = req.WithContext(initialCtx)
tracingRoundTripper := newTracingRoundTripper(roundTripperFn(func(req *http.Request) (*http.Response, error) {
return &http.Response{
Header: map[string][]string{
"X-Bar": {"foo", "bar"},
},
StatusCode: http.StatusNotFound,
}, nil
}))
_, err := tracingRoundTripper.RoundTrip(req)
require.NoError(t, err)
for i, span := range mockTracer.spans {
assert.Equal(t, test.expected[i].name, span.name)
assert.Equal(t, test.expected[i].attributes, span.attributes)
}
})
}
}
type mockTracer struct {
embedded.Tracer
spans []*mockSpan
}
var _ trace.Tracer = &mockTracer{}
func (t *mockTracer) Start(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
config := trace.NewSpanStartConfig(opts...)
span := &mockSpan{}
span.SetName(name)
span.SetAttributes(attribute.String("span.kind", config.SpanKind().String()))
span.SetAttributes(config.Attributes()...)
t.spans = append(t.spans, span)
return trace.ContextWithSpan(ctx, span), span
}
// mockSpan is an implementation of Span that preforms no operations.
type mockSpan struct {
embedded.Span
name string
attributes []attribute.KeyValue
}
var _ trace.Span = &mockSpan{}
func (*mockSpan) SpanContext() trace.SpanContext {
return trace.NewSpanContext(trace.SpanContextConfig{TraceID: trace.TraceID{1}, SpanID: trace.SpanID{1}})
}
func (*mockSpan) IsRecording() bool { return false }
func (s *mockSpan) SetStatus(_ codes.Code, _ string) {}
func (s *mockSpan) SetAttributes(kv ...attribute.KeyValue) {
s.attributes = append(s.attributes, kv...)
}
func (s *mockSpan) End(...trace.SpanEndOption) {}
func (s *mockSpan) RecordError(_ error, _ ...trace.EventOption) {}
func (s *mockSpan) AddEvent(_ string, _ ...trace.EventOption) {}
func (s *mockSpan) SetName(name string) { s.name = name }
func (s *mockSpan) TracerProvider() trace.TracerProvider {
return nil
}