1
0
Fork 0

Migrate to opentelemetry

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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