1
0
Fork 0

OpenTelemetry Logs and Access Logs

Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
Romain 2024-12-06 14:50:04 +01:00 committed by GitHub
parent 33c1d700c0
commit 826a2b74aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 2297 additions and 475 deletions

View file

@ -502,14 +502,17 @@ func (e *experimental) deprecationNotice(logger zerolog.Logger) bool {
return false
}
//
type tracing struct {
SpanNameLimit *int `json:"spanNameLimit,omitempty" toml:"spanNameLimit,omitempty" yaml:"spanNameLimit,omitempty"`
Jaeger map[string]any `json:"jaeger,omitempty" toml:"jaeger,omitempty" yaml:"jaeger,omitempty" label:"allowEmpty" file:"allowEmpty"`
Zipkin map[string]any `json:"zipkin,omitempty" toml:"zipkin,omitempty" yaml:"zipkin,omitempty" label:"allowEmpty" file:"allowEmpty"`
Datadog map[string]any `json:"datadog,omitempty" toml:"datadog,omitempty" yaml:"datadog,omitempty" label:"allowEmpty" file:"allowEmpty"`
Instana map[string]any `json:"instana,omitempty" toml:"instana,omitempty" yaml:"instana,omitempty" label:"allowEmpty" file:"allowEmpty"`
Haystack map[string]any `json:"haystack,omitempty" toml:"haystack,omitempty" yaml:"haystack,omitempty" label:"allowEmpty" file:"allowEmpty"`
Elastic map[string]any `json:"elastic,omitempty" toml:"elastic,omitempty" yaml:"elastic,omitempty" label:"allowEmpty" file:"allowEmpty"`
SpanNameLimit *int `json:"spanNameLimit,omitempty" toml:"spanNameLimit,omitempty" yaml:"spanNameLimit,omitempty"`
GlobalAttributes map[string]string `json:"globalAttributes,omitempty" toml:"globalAttributes,omitempty" yaml:"globalAttributes,omitempty" export:"true"`
Jaeger map[string]any `json:"jaeger,omitempty" toml:"jaeger,omitempty" yaml:"jaeger,omitempty" label:"allowEmpty" file:"allowEmpty"`
Zipkin map[string]any `json:"zipkin,omitempty" toml:"zipkin,omitempty" yaml:"zipkin,omitempty" label:"allowEmpty" file:"allowEmpty"`
Datadog map[string]any `json:"datadog,omitempty" toml:"datadog,omitempty" yaml:"datadog,omitempty" label:"allowEmpty" file:"allowEmpty"`
Instana map[string]any `json:"instana,omitempty" toml:"instana,omitempty" yaml:"instana,omitempty" label:"allowEmpty" file:"allowEmpty"`
Haystack map[string]any `json:"haystack,omitempty" toml:"haystack,omitempty" yaml:"haystack,omitempty" label:"allowEmpty" file:"allowEmpty"`
Elastic map[string]any `json:"elastic,omitempty" toml:"elastic,omitempty" yaml:"elastic,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
func (t *tracing) deprecationNotice(logger zerolog.Logger) bool {
@ -523,6 +526,14 @@ func (t *tracing) deprecationNotice(logger zerolog.Logger) bool {
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.2/migration/v2-to-v3/#tracing")
}
if t.GlobalAttributes != nil {
log.Warn().Msgf("tracing.globalAttributes option is now deprecated, please use tracing.resourceAttributes instead.")
logger.Error().Msg("`tracing.globalAttributes` option has been deprecated in v3.3, and will be removed in the next major version." +
"Please use the `tracing.resourceAttributes` option instead." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.3/migration/v3/#tracing-global-attributes")
}
if t.Jaeger != nil {
incompatible = true
logger.Error().Msg("Jaeger Tracing backend has been removed in v3, please remove all Jaeger-related Tracing static configuration for Traefik to start." +

View file

@ -28,7 +28,6 @@ import (
"github.com/traefik/traefik/v3/pkg/provider/kv/zk"
"github.com/traefik/traefik/v3/pkg/provider/nomad"
"github.com/traefik/traefik/v3/pkg/provider/rest"
"github.com/traefik/traefik/v3/pkg/tracing/opentelemetry"
"github.com/traefik/traefik/v3/pkg/types"
)
@ -69,7 +68,7 @@ type Configuration struct {
Log *types.TraefikLog `description:"Traefik log settings." json:"log,omitempty" toml:"log,omitempty" yaml:"log,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
AccessLog *types.AccessLog `description:"Access log settings." json:"accessLog,omitempty" toml:"accessLog,omitempty" yaml:"accessLog,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
Tracing *Tracing `description:"OpenTracing configuration." json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
Tracing *Tracing `description:"Tracing configuration." json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
HostResolver *types.HostResolverConfig `description:"Enable CNAME Flattening." json:"hostResolver,omitempty" toml:"hostResolver,omitempty" yaml:"hostResolver,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
@ -200,15 +199,17 @@ func (a *LifeCycle) SetDefaults() {
// Tracing holds the tracing configuration.
type Tracing struct {
ServiceName string `description:"Set the name for this service." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"`
GlobalAttributes map[string]string `description:"Defines additional attributes (key:value) on all spans." json:"globalAttributes,omitempty" toml:"globalAttributes,omitempty" yaml:"globalAttributes,omitempty" export:"true"`
CapturedRequestHeaders []string `description:"Request headers to add as attributes for server and client spans." json:"capturedRequestHeaders,omitempty" toml:"capturedRequestHeaders,omitempty" yaml:"capturedRequestHeaders,omitempty" export:"true"`
CapturedResponseHeaders []string `description:"Response headers to add as attributes for server and client spans." json:"capturedResponseHeaders,omitempty" toml:"capturedResponseHeaders,omitempty" yaml:"capturedResponseHeaders,omitempty" export:"true"`
SafeQueryParams []string `description:"Query params to not redact." json:"safeQueryParams,omitempty" toml:"safeQueryParams,omitempty" yaml:"safeQueryParams,omitempty" export:"true"`
SampleRate float64 `description:"Sets the rate between 0.0 and 1.0 of requests to trace." json:"sampleRate,omitempty" toml:"sampleRate,omitempty" yaml:"sampleRate,omitempty" export:"true"`
AddInternals bool `description:"Enables tracing for internal services (ping, dashboard, etc...)." json:"addInternals,omitempty" toml:"addInternals,omitempty" yaml:"addInternals,omitempty" export:"true"`
ServiceName string `description:"Sets the name for this service." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"`
ResourceAttributes map[string]string `description:"Defines additional resource attributes (key:value)." json:"resourceAttributes,omitempty" toml:"resourceAttributes,omitempty" yaml:"resourceAttributes,omitempty" export:"true"`
CapturedRequestHeaders []string `description:"Request headers to add as attributes for server and client spans." json:"capturedRequestHeaders,omitempty" toml:"capturedRequestHeaders,omitempty" yaml:"capturedRequestHeaders,omitempty" export:"true"`
CapturedResponseHeaders []string `description:"Response headers to add as attributes for server and client spans." json:"capturedResponseHeaders,omitempty" toml:"capturedResponseHeaders,omitempty" yaml:"capturedResponseHeaders,omitempty" export:"true"`
SafeQueryParams []string `description:"Query params to not redact." json:"safeQueryParams,omitempty" toml:"safeQueryParams,omitempty" yaml:"safeQueryParams,omitempty" export:"true"`
SampleRate float64 `description:"Sets the rate between 0.0 and 1.0 of requests to trace." json:"sampleRate,omitempty" toml:"sampleRate,omitempty" yaml:"sampleRate,omitempty" export:"true"`
AddInternals bool `description:"Enables tracing for internal services (ping, dashboard, etc...)." json:"addInternals,omitempty" toml:"addInternals,omitempty" yaml:"addInternals,omitempty" export:"true"`
OTLP *types.OTelTracing `description:"Settings for OpenTelemetry." json:"otlp,omitempty" toml:"otlp,omitempty" yaml:"otlp,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
OTLP *opentelemetry.Config `description:"Settings for OpenTelemetry." json:"otlp,omitempty" toml:"otlp,omitempty" yaml:"otlp,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
// Deprecated: please use ResourceAttributes instead.
GlobalAttributes map[string]string `description:"(Deprecated) Defines additional resource attributes (key:value)." json:"globalAttributes,omitempty" toml:"globalAttributes,omitempty" yaml:"globalAttributes,omitempty" export:"true"`
}
// SetDefaults sets the default values.
@ -216,7 +217,7 @@ func (t *Tracing) SetDefaults() {
t.ServiceName = "traefik"
t.SampleRate = 1.0
t.OTLP = &opentelemetry.Config{}
t.OTLP = &types.OTelTracing{}
t.OTLP.SetDefaults()
}
@ -270,6 +271,10 @@ func (c *Configuration) SetEffectiveConfiguration() {
}
}
if c.Tracing != nil && c.Tracing.GlobalAttributes != nil && c.Tracing.ResourceAttributes == nil {
c.Tracing.ResourceAttributes = c.Tracing.GlobalAttributes
}
if c.Providers.Docker != nil {
if c.Providers.Docker.HTTPClientTimeout < 0 {
c.Providers.Docker.HTTPClientTimeout = 0
@ -381,6 +386,18 @@ func (c *Configuration) ValidateConfiguration() error {
}
}
if c.AccessLog != nil && c.AccessLog.OTLP != nil {
if c.AccessLog.OTLP.GRPC != nil && c.AccessLog.OTLP.GRPC.TLS != nil && c.AccessLog.OTLP.GRPC.Insecure {
return errors.New("access logs OTLP GRPC: TLS and Insecure options are mutually exclusive")
}
}
if c.Log != nil && c.Log.OTLP != nil {
if c.Log.OTLP.GRPC != nil && c.Log.OTLP.GRPC.TLS != nil && c.Log.OTLP.GRPC.Insecure {
return errors.New("logs OTLP GRPC: TLS and Insecure options are mutually exclusive")
}
}
if c.Tracing != nil && c.Tracing.OTLP != nil {
if c.Tracing.OTLP.GRPC != nil && c.Tracing.OTLP.GRPC.TLS != nil && c.Tracing.OTLP.GRPC.Insecure {
return errors.New("tracing OTLP GRPC: TLS and Insecure options are mutually exclusive")

120
pkg/logs/otel.go Normal file
View file

@ -0,0 +1,120 @@
package logs
import (
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/rs/zerolog"
"github.com/traefik/traefik/v3/pkg/types"
otellog "go.opentelemetry.io/otel/log"
)
// SetupOTelLogger sets up the OpenTelemetry logger.
func SetupOTelLogger(logger zerolog.Logger, config *types.OTelLog) (zerolog.Logger, error) {
if config == nil {
return logger, nil
}
provider, err := config.NewLoggerProvider()
if err != nil {
return zerolog.Logger{}, fmt.Errorf("setting up OpenTelemetry logger provider: %w", err)
}
return logger.Hook(&otelLoggerHook{logger: provider.Logger("traefik")}), nil
}
// otelLoggerHook is a zerolog hook that forwards logs to OpenTelemetry.
type otelLoggerHook struct {
logger otellog.Logger
}
// Run forwards the log message to OpenTelemetry.
func (h *otelLoggerHook) Run(e *zerolog.Event, level zerolog.Level, message string) {
if level == zerolog.Disabled {
return
}
// Discard the event to avoid double logging.
e.Discard()
var record otellog.Record
record.SetTimestamp(time.Now().UTC())
record.SetSeverity(otelLogSeverity(level))
record.SetBody(otellog.StringValue(message))
// See https://github.com/rs/zerolog/issues/493.
// This is a workaround to get the log fields from the event.
// At the moment there's no way to get the log fields from the event, so we use reflection to get the buffer and parse it.
logData := make(map[string]any)
eventBuffer := fmt.Sprintf("%s}", reflect.ValueOf(e).Elem().FieldByName("buf"))
if err := json.Unmarshal([]byte(eventBuffer), &logData); err != nil {
record.AddAttributes(otellog.String("parsing_error", fmt.Sprintf("parsing log fields: %s", err)))
h.logger.Emit(e.GetCtx(), record)
return
}
recordAttributes := make([]otellog.KeyValue, 0, len(logData))
for k, v := range logData {
if k == "level" {
continue
}
if k == "time" {
eventTimestamp, ok := v.(string)
if !ok {
continue
}
t, err := time.Parse(time.RFC3339, eventTimestamp)
if err == nil {
record.SetTimestamp(t)
continue
}
}
var attributeValue otellog.Value
switch v := v.(type) {
case string:
attributeValue = otellog.StringValue(v)
case int:
attributeValue = otellog.IntValue(v)
case int64:
attributeValue = otellog.Int64Value(v)
case float64:
attributeValue = otellog.Float64Value(v)
case bool:
attributeValue = otellog.BoolValue(v)
case []byte:
attributeValue = otellog.BytesValue(v)
default:
attributeValue = otellog.StringValue(fmt.Sprintf("%v", v))
}
recordAttributes = append(recordAttributes, otellog.KeyValue{
Key: k,
Value: attributeValue,
})
}
record.AddAttributes(recordAttributes...)
h.logger.Emit(e.GetCtx(), record)
}
func otelLogSeverity(level zerolog.Level) otellog.Severity {
switch level {
case zerolog.TraceLevel:
return otellog.SeverityTrace
case zerolog.DebugLevel:
return otellog.SeverityDebug
case zerolog.InfoLevel:
return otellog.SeverityInfo
case zerolog.WarnLevel:
return otellog.SeverityWarn
case zerolog.ErrorLevel:
return otellog.SeverityError
case zerolog.FatalLevel:
return otellog.SeverityFatal
case zerolog.PanicLevel:
return otellog.SeverityFatal4
default:
return otellog.SeverityUndefined
}
}

197
pkg/logs/otel_test.go Normal file
View file

@ -0,0 +1,197 @@
package logs
import (
"compress/gzip"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/collector/pdata/plog/plogotlp"
"go.opentelemetry.io/otel/trace"
)
func TestLog(t *testing.T) {
tests := []struct {
desc string
level zerolog.Level
assertFn func(*testing.T, string)
noLog bool
}{
{
desc: "no level log",
level: zerolog.NoLevel,
assertFn: func(t *testing.T, log string) {
t.Helper()
// SeverityUndefined Severity = 0 // UNDEFINED
assert.NotContains(t, log, `"severityNumber"`)
assert.Regexp(t, `{"key":"resource","value":{"stringValue":"attribute"}}`, log)
assert.Regexp(t, `{"key":"service.name","value":{"stringValue":"test"}}`, log)
assert.Regexp(t, `"body":{"stringValue":"test"}`, log)
assert.Regexp(t, `{"key":"foo","value":{"stringValue":"bar"}}`, log)
assert.Regexp(t, `"traceId":"01020304050607080000000000000000","spanId":"0102030405060708"`, log)
},
},
{
desc: "trace log",
level: zerolog.TraceLevel,
assertFn: func(t *testing.T, log string) {
t.Helper()
// SeverityTrace1 Severity = 1 // TRACE
assert.Contains(t, log, `"severityNumber":1`)
assert.Regexp(t, `{"key":"resource","value":{"stringValue":"attribute"}}`, log)
assert.Regexp(t, `{"key":"service.name","value":{"stringValue":"test"}}`, log)
assert.Regexp(t, `"body":{"stringValue":"test"}`, log)
assert.Regexp(t, `{"key":"foo","value":{"stringValue":"bar"}}`, log)
assert.Regexp(t, `"traceId":"01020304050607080000000000000000","spanId":"0102030405060708"`, log)
},
},
{
desc: "debug log",
level: zerolog.DebugLevel,
assertFn: func(t *testing.T, log string) {
t.Helper()
// SeverityDebug1 Severity = 5 // DEBUG
assert.Contains(t, log, `"severityNumber":5`)
assert.Regexp(t, `{"key":"resource","value":{"stringValue":"attribute"}}`, log)
assert.Regexp(t, `{"key":"service.name","value":{"stringValue":"test"}}`, log)
assert.Regexp(t, `"body":{"stringValue":"test"}`, log)
assert.Regexp(t, `{"key":"foo","value":{"stringValue":"bar"}}`, log)
assert.Regexp(t, `"traceId":"01020304050607080000000000000000","spanId":"0102030405060708"`, log)
},
},
{
desc: "info log",
level: zerolog.InfoLevel,
assertFn: func(t *testing.T, log string) {
t.Helper()
// SeverityInfo1 Severity = 9 // INFO
assert.Contains(t, log, `"severityNumber":9`)
assert.Regexp(t, `{"key":"resource","value":{"stringValue":"attribute"}}`, log)
assert.Regexp(t, `{"key":"service.name","value":{"stringValue":"test"}}`, log)
assert.Regexp(t, `"body":{"stringValue":"test"}`, log)
assert.Regexp(t, `{"key":"foo","value":{"stringValue":"bar"}}`, log)
assert.Regexp(t, `"traceId":"01020304050607080000000000000000","spanId":"0102030405060708"`, log)
},
},
{
desc: "warn log",
level: zerolog.WarnLevel,
assertFn: func(t *testing.T, log string) {
t.Helper()
// SeverityWarn1 Severity = 13 // WARN
assert.Contains(t, log, `"severityNumber":13`)
assert.Regexp(t, `{"key":"resource","value":{"stringValue":"attribute"}}`, log)
assert.Regexp(t, `{"key":"service.name","value":{"stringValue":"test"}}`, log)
assert.Regexp(t, `"body":{"stringValue":"test"}`, log)
assert.Regexp(t, `{"key":"foo","value":{"stringValue":"bar"}}`, log)
assert.Regexp(t, `"traceId":"01020304050607080000000000000000","spanId":"0102030405060708"`, log)
},
},
{
desc: "error log",
level: zerolog.ErrorLevel,
assertFn: func(t *testing.T, log string) {
t.Helper()
// SeverityError1 Severity = 17 // ERROR
assert.Contains(t, log, `"severityNumber":17`)
assert.Regexp(t, `{"key":"resource","value":{"stringValue":"attribute"}}`, log)
assert.Regexp(t, `{"key":"service.name","value":{"stringValue":"test"}}`, log)
assert.Regexp(t, `"body":{"stringValue":"test"}`, log)
assert.Regexp(t, `{"key":"foo","value":{"stringValue":"bar"}}`, log)
assert.Regexp(t, `"traceId":"01020304050607080000000000000000","spanId":"0102030405060708"`, log)
},
},
{
desc: "fatal log",
level: zerolog.FatalLevel,
assertFn: func(t *testing.T, log string) {
t.Helper()
// SeverityFatal Severity = 21 // FATAL
assert.Contains(t, log, `"severityNumber":21`)
assert.Regexp(t, `{"key":"resource","value":{"stringValue":"attribute"}}`, log)
assert.Regexp(t, `{"key":"service.name","value":{"stringValue":"test"}}`, log)
assert.Regexp(t, `"body":{"stringValue":"test"}`, log)
assert.Regexp(t, `{"key":"foo","value":{"stringValue":"bar"}}`, log)
assert.Regexp(t, `"traceId":"01020304050607080000000000000000","spanId":"0102030405060708"`, log)
},
},
{
desc: "panic log",
level: zerolog.PanicLevel,
assertFn: func(t *testing.T, log string) {
t.Helper()
// SeverityFatal4 Severity = 24 // FATAL
assert.Contains(t, log, `"severityNumber":24`)
assert.Regexp(t, `{"key":"resource","value":{"stringValue":"attribute"}}`, log)
assert.Regexp(t, `{"key":"service.name","value":{"stringValue":"test"}}`, log)
assert.Regexp(t, `"body":{"stringValue":"test"}`, log)
assert.Regexp(t, `{"key":"foo","value":{"stringValue":"bar"}}`, log)
assert.Regexp(t, `"traceId":"01020304050607080000000000000000","spanId":"0102030405060708"`, log)
},
},
}
logCh := make(chan string)
collector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gzr, err := gzip.NewReader(r.Body)
require.NoError(t, err)
body, err := io.ReadAll(gzr)
require.NoError(t, err)
req := plogotlp.NewExportRequest()
err = req.UnmarshalProto(body)
require.NoError(t, err)
marshalledReq, err := json.Marshal(req)
require.NoError(t, err)
logCh <- string(marshalledReq)
}))
t.Cleanup(collector.Close)
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
config := &types.OTelLog{
ServiceName: "test",
ResourceAttributes: map[string]string{"resource": "attribute"},
HTTP: &types.OTelHTTP{
Endpoint: collector.URL,
},
}
out := zerolog.MultiLevelWriter(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
logger := zerolog.New(out).With().Caller().Logger()
logger, err := SetupOTelLogger(logger, config)
require.NoError(t, err)
ctx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{
TraceID: trace.TraceID{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8},
SpanID: trace.SpanID{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8},
}))
logger = logger.With().Ctx(ctx).Logger()
logger.WithLevel(test.level).Str("foo", "bar").Msg("test")
select {
case <-time.After(5 * time.Second):
t.Error("Log not exported")
case log := <-logCh:
if test.assertFn != nil {
test.assertFn(t, log)
}
}
})
}
}

View file

@ -237,7 +237,7 @@ func newOpenTelemetryMeterProvider(ctx context.Context, config *types.OTLP) (*sd
return meterProvider, nil
}
func newHTTPExporter(ctx context.Context, config *types.OtelHTTP) (sdkmetric.Exporter, error) {
func newHTTPExporter(ctx context.Context, config *types.OTelHTTP) (sdkmetric.Exporter, error) {
endpoint, err := url.Parse(config.Endpoint)
if err != nil {
return nil, fmt.Errorf("invalid collector endpoint %q: %w", config.Endpoint, err)
@ -269,7 +269,7 @@ func newHTTPExporter(ctx context.Context, config *types.OtelHTTP) (sdkmetric.Exp
return otlpmetrichttp.New(ctx, opts...)
}
func newGRPCExporter(ctx context.Context, config *types.OtelGRPC) (sdkmetric.Exporter, error) {
func newGRPCExporter(ctx context.Context, config *types.OTelGRPC) (sdkmetric.Exporter, error) {
host, port, err := net.SplitHostPort(config.Endpoint)
if err != nil {
return nil, fmt.Errorf("invalid collector endpoint %q: %w", config.Endpoint, err)

View file

@ -327,7 +327,7 @@ func TestOpenTelemetry(t *testing.T) {
var cfg types.OTLP
(&cfg).SetDefaults()
cfg.AddRoutersLabels = true
cfg.HTTP = &types.OtelHTTP{
cfg.HTTP = &types.OTelHTTP{
Endpoint: ts.URL,
}
cfg.PushInterval = ptypes.Duration(10 * time.Millisecond)

View file

@ -23,6 +23,7 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/contrib/bridges/otellogrus"
)
type key string
@ -52,6 +53,7 @@ func (n noopCloser) Close() error {
}
type handlerParams struct {
ctx context.Context
logDataTable *LogData
}
@ -106,6 +108,16 @@ func NewHandler(config *types.AccessLog) (*Handler, error) {
Level: logrus.InfoLevel,
}
if config.OTLP != nil {
otelLoggerProvider, err := config.OTLP.NewLoggerProvider()
if err != nil {
return nil, fmt.Errorf("setting up OpenTelemetry logger provider: %w", err)
}
logger.Hooks.Add(otellogrus.NewHook("traefik", otellogrus.WithLoggerProvider(otelLoggerProvider)))
logger.Out = io.Discard
}
// Transform header names to a canonical form, to be used as is without further transformations,
// and transform field names to lower case, to enable case-insensitive lookup.
if config.Fields != nil {
@ -150,7 +162,7 @@ func NewHandler(config *types.AccessLog) (*Handler, error) {
go func() {
defer logHandler.wg.Done()
for handlerParams := range logHandler.logHandlerChan {
logHandler.logTheRoundTrip(handlerParams.logDataTable)
logHandler.logTheRoundTrip(handlerParams.ctx, handlerParams.logDataTable)
}
}()
}
@ -256,12 +268,13 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
if h.config.BufferingSize > 0 {
h.logHandlerChan <- handlerParams{
ctx: req.Context(),
logDataTable: logDataTable,
}
return
}
h.logTheRoundTrip(logDataTable)
h.logTheRoundTrip(req.Context(), logDataTable)
}()
next.ServeHTTP(rw, reqWithDataTable)
@ -313,7 +326,7 @@ func usernameIfPresent(theURL *url.URL) string {
}
// Logging handler to log frontend name, backend name, and elapsed time.
func (h *Handler) logTheRoundTrip(logDataTable *LogData) {
func (h *Handler) logTheRoundTrip(ctx context.Context, logDataTable *LogData) {
core := logDataTable.Core
retryAttempts, ok := core[RetryAttempts].(int)
@ -359,7 +372,7 @@ func (h *Handler) logTheRoundTrip(logDataTable *LogData) {
h.mu.Lock()
defer h.mu.Unlock()
h.logger.WithFields(fields).Println()
h.logger.WithContext(ctx).WithFields(fields).Println()
}
}

View file

@ -2,6 +2,7 @@ package accesslog
import (
"bytes"
"compress/gzip"
"context"
"crypto/tls"
"crypto/x509"
@ -25,6 +26,8 @@ import (
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/collector/pdata/plog/plogotlp"
"go.opentelemetry.io/otel/trace"
)
const delta float64 = 1e-10
@ -49,6 +52,75 @@ var (
testStart = time.Now()
)
func TestOTelAccessLog(t *testing.T) {
logCh := make(chan string)
collector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gzr, err := gzip.NewReader(r.Body)
require.NoError(t, err)
body, err := io.ReadAll(gzr)
require.NoError(t, err)
req := plogotlp.NewExportRequest()
err = req.UnmarshalProto(body)
require.NoError(t, err)
marshalledReq, err := json.Marshal(req)
require.NoError(t, err)
logCh <- string(marshalledReq)
}))
t.Cleanup(collector.Close)
config := &types.AccessLog{
OTLP: &types.OTelLog{
ServiceName: "test",
ResourceAttributes: map[string]string{"resource": "attribute"},
HTTP: &types.OTelHTTP{
Endpoint: collector.URL,
},
},
}
logHandler, err := NewHandler(config)
require.NoError(t, err)
t.Cleanup(func() {
err := logHandler.Close()
require.NoError(t, err)
})
req := &http.Request{
Header: map[string][]string{},
URL: &url.URL{
Path: testPath,
},
}
ctx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{
TraceID: trace.TraceID{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8},
SpanID: trace.SpanID{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8},
}))
req = req.WithContext(ctx)
chain := alice.New()
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logHandler))
handler, err := chain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
require.NoError(t, err)
handler.ServeHTTP(httptest.NewRecorder(), req)
select {
case <-time.After(5 * time.Second):
t.Error("AccessLog not exported")
case log := <-logCh:
assert.Regexp(t, `{"key":"resource","value":{"stringValue":"attribute"}}`, log)
assert.Regexp(t, `{"key":"service.name","value":{"stringValue":"test"}}`, log)
assert.Regexp(t, `{"key":"DownstreamStatus","value":{"intValue":"200"}}`, log)
assert.Regexp(t, `"traceId":"01020304050607080000000000000000","spanId":"0102030405060708"`, log)
}
}
func TestLogRotation(t *testing.T) {
fileName := filepath.Join(t.TempDir(), "traefik.log")
rotatedFileName := fileName + ".rotated"

View file

@ -9,6 +9,7 @@ import (
"time"
"github.com/containous/alice"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
@ -64,7 +65,11 @@ func (e *entryPointTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request)
start := time.Now()
tracingCtx, span := e.tracer.Start(tracingCtx, "EntryPoint", trace.WithSpanKind(trace.SpanKindServer), trace.WithTimestamp(start))
req = req.WithContext(tracingCtx)
// Associate the request context with the logger.
logger := log.Ctx(tracingCtx).With().Ctx(tracingCtx).Logger()
loggerCtx := logger.WithContext(tracingCtx)
req = req.WithContext(loggerCtx)
span.SetAttributes(attribute.String("entry_point", e.entryPoint))

View file

@ -30,7 +30,6 @@ import (
"github.com/traefik/traefik/v3/pkg/provider/kv/zk"
"github.com/traefik/traefik/v3/pkg/provider/rest"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/tracing/opentelemetry"
"github.com/traefik/traefik/v3/pkg/types"
)
@ -831,6 +830,25 @@ func TestDo_staticConfiguration(t *testing.T) {
MaxAge: 3,
MaxBackups: 4,
Compress: true,
OTLP: &types.OTelLog{
ServiceName: "foobar",
ResourceAttributes: map[string]string{
"foobar": "foobar",
},
GRPC: &types.OTelGRPC{
Endpoint: "foobar",
Insecure: true,
Headers: map[string]string{
"foobar": "foobar",
},
},
HTTP: &types.OTelHTTP{
Endpoint: "foobar",
Headers: map[string]string{
"foobar": "foobar",
},
},
},
}
config.AccessLog = &types.AccessLog{
@ -854,18 +872,46 @@ func TestDo_staticConfiguration(t *testing.T) {
},
},
BufferingSize: 42,
OTLP: &types.OTelLog{
ServiceName: "foobar",
ResourceAttributes: map[string]string{
"foobar": "foobar",
},
GRPC: &types.OTelGRPC{
Endpoint: "foobar",
Insecure: true,
Headers: map[string]string{
"foobar": "foobar",
},
},
HTTP: &types.OTelHTTP{
Endpoint: "foobar",
Headers: map[string]string{
"foobar": "foobar",
},
},
},
}
config.Tracing = &static.Tracing{
ServiceName: "myServiceName",
ResourceAttributes: map[string]string{
"foobar": "foobar",
},
GlobalAttributes: map[string]string{
"foobar": "foobar",
},
SampleRate: 42,
OTLP: &opentelemetry.Config{
HTTP: &types.OtelHTTP{
OTLP: &types.OTelTracing{
HTTP: &types.OTelHTTP{
Endpoint: "foobar",
TLS: nil,
Headers: map[string]string{
"foobar": "foobar",
},
},
GRPC: &types.OTelGRPC{
Endpoint: "foobar",
Insecure: true,
Headers: map[string]string{
"foobar": "foobar",
},

View file

@ -315,7 +315,17 @@
"maxSize": 5,
"maxAge": 3,
"maxBackups": 4,
"compress": true
"compress": true,
"otlp": {
"serviceName": "foobar",
"grpc": {
"endpoint": "xxxx",
"insecure": true
},
"http": {
"endpoint": "xxxx"
}
}
},
"accessLog": {
"filePath": "xxxx",
@ -340,18 +350,35 @@
}
}
},
"bufferingSize": 42
"bufferingSize": 42,
"otlp": {
"serviceName": "foobar",
"grpc": {
"endpoint": "xxxx",
"insecure": true
},
"http": {
"endpoint": "xxxx"
}
}
},
"tracing": {
"serviceName": "myServiceName",
"globalAttributes": {
"resourceAttributes": {
"foobar": "foobar"
},
"sampleRate": 42,
"otlp": {
"grpc": {
"endpoint": "xxxx",
"insecure": true
},
"http": {
"endpoint": "xxxx"
}
},
"globalAttributes": {
"foobar": "foobar"
}
},
"hostResolver": {
@ -370,11 +397,11 @@
"certificatesDuration": 42,
"dnsChallenge": {
"provider": "DNSProvider",
"delayBeforeCheck": "42ns",
"resolvers": [
"xxxx",
"xxxx"
],
"delayBeforeCheck": "42ns",
"disablePropagationCheck": true
},
"httpChallenge": {

View file

@ -54,6 +54,12 @@ func (o *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName stri
}
}
// 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)) || (o.metricsRegistry != nil && o.metricsRegistry.IsEpEnabled() && o.ShouldAddMetrics(resourceName)) {
chain = chain.Append(observability.WrapEntryPointHandler(ctx, o.tracer, o.semConvMetricRegistry, entryPointName))
}
if o.accessLoggerMiddleware != nil && o.ShouldAddAccessLogs(resourceName) {
chain = chain.Append(accesslog.WrapHandler(o.accessLoggerMiddleware))
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
@ -61,10 +67,6 @@ func (o *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName stri
})
}
if (o.tracer != nil && o.ShouldAddTracing(resourceName)) || (o.metricsRegistry != nil && o.metricsRegistry.IsEpEnabled() && o.ShouldAddMetrics(resourceName)) {
chain = chain.Append(observability.WrapEntryPointHandler(ctx, o.tracer, o.semConvMetricRegistry, entryPointName))
}
if o.metricsRegistry != nil && o.metricsRegistry.IsEpEnabled() && o.ShouldAddMetrics(resourceName) {
metricsHandler := metricsMiddle.WrapEntryPointHandler(ctx, o.metricsRegistry, entryPointName)

View file

@ -1,324 +0,0 @@
package opentelemetry_test
import (
"compress/gzip"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/containous/alice"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/static"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/traefik/traefik/v3/pkg/tracing/opentelemetry"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp"
)
func TestTracing(t *testing.T) {
tests := []struct {
desc string
propagators string
headers map[string]string
wantServiceHeadersFn func(t *testing.T, headers http.Header)
assertFn func(*testing.T, string)
}{
{
desc: "service name and version",
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `({"key":"service.name","value":{"stringValue":"traefik"}})`, trace)
assert.Regexp(t, `({"key":"service.version","value":{"stringValue":"dev"}})`, trace)
},
},
{
desc: "TraceContext propagation",
propagators: "tracecontext",
headers: map[string]string{
"traceparent": "00-00000000000000000000000000000001-0000000000000001-01",
"tracestate": "foo=bar",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(00-00000000000000000000000000000001-\w{16}-01)`, headers["Traceparent"][0])
assert.Equal(t, []string{"foo=bar"}, headers["Tracestate"])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"00000000000000000000000000000001")`, trace)
assert.Regexp(t, `("parentSpanId":"0000000000000001")`, trace)
assert.Regexp(t, `("traceState":"foo=bar")`, trace)
},
},
{
desc: "root span TraceContext propagation",
propagators: "tracecontext",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(00-\w{32}-\w{16}-01)`, headers["Traceparent"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "B3 propagation",
propagators: "b3",
headers: map[string]string{
"b3": "00000000000000000000000000000001-0000000000000002-1-0000000000000001",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(00000000000000000000000000000001-\w{16}-1)`, headers["B3"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"00000000000000000000000000000001")`, trace)
assert.Regexp(t, `("parentSpanId":"0000000000000002")`, trace)
},
},
{
desc: "root span B3 propagation",
propagators: "b3",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(\w{32}-\w{16}-1)`, headers["B3"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "B3 propagation Multiple Headers",
propagators: "b3multi",
headers: map[string]string{
"x-b3-traceid": "00000000000000000000000000000001",
"x-b3-parentspanid": "0000000000000001",
"x-b3-spanid": "0000000000000002",
"x-b3-sampled": "1",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Equal(t, "00000000000000000000000000000001", headers["X-B3-Traceid"][0])
assert.Equal(t, "0000000000000001", headers["X-B3-Parentspanid"][0])
assert.Equal(t, "1", headers["X-B3-Sampled"][0])
assert.Len(t, headers["X-B3-Spanid"][0], 16)
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"00000000000000000000000000000001")`, trace)
assert.Regexp(t, `("parentSpanId":"0000000000000002")`, trace)
},
},
{
desc: "root span B3 propagation Multiple Headers",
propagators: "b3multi",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(\w{32})`, headers["X-B3-Traceid"][0])
assert.Equal(t, "1", headers["X-B3-Sampled"][0])
assert.Regexp(t, `(\w{16})`, headers["X-B3-Spanid"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"")`, trace)
},
},
{
desc: "Baggage propagation",
propagators: "baggage",
headers: map[string]string{
"baggage": "userId=id",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Equal(t, []string{"userId=id"}, headers["Baggage"])
},
},
{
desc: "Jaeger propagation",
propagators: "jaeger",
headers: map[string]string{
"uber-trace-id": "00000000000000000000000000000001:0000000000000002:0000000000000001:1",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(00000000000000000000000000000001:\w{16}:0:1)`, headers["Uber-Trace-Id"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"00000000000000000000000000000001")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "root span Jaeger propagation",
propagators: "jaeger",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(\w{32}:\w{16}:0:1)`, headers["Uber-Trace-Id"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "XRay propagation",
propagators: "xray",
headers: map[string]string{
"X-Amzn-Trace-Id": "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(Root=1-5759e988-bd862e3fe1be46a994272793;Parent=\w{16};Sampled=1)`, headers["X-Amzn-Trace-Id"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"5759e988bd862e3fe1be46a994272793")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "root span XRay propagation",
propagators: "xray",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(Root=1-\w{8}-\w{24};Parent=\w{16};Sampled=1)`, headers["X-Amzn-Trace-Id"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "no propagation",
propagators: "none",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Empty(t, headers)
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
}
traceCh := make(chan string)
collector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gzr, err := gzip.NewReader(r.Body)
require.NoError(t, err)
body, err := io.ReadAll(gzr)
require.NoError(t, err)
req := ptraceotlp.NewExportRequest()
err = req.UnmarshalProto(body)
require.NoError(t, err)
marshalledReq, err := json.Marshal(req)
require.NoError(t, err)
traceCh <- string(marshalledReq)
}))
t.Cleanup(collector.Close)
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
t.Setenv("OTEL_PROPAGATORS", test.propagators)
service := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tracer := tracing.TracerFromContext(r.Context())
ctx, span := tracer.Start(r.Context(), "service")
defer span.End()
r = r.WithContext(ctx)
tracing.InjectContextIntoCarrier(r)
if test.wantServiceHeadersFn != nil {
test.wantServiceHeadersFn(t, r.Header)
}
})
tracingConfig := &static.Tracing{
ServiceName: "traefik",
SampleRate: 1.0,
OTLP: &opentelemetry.Config{
HTTP: &types.OtelHTTP{
Endpoint: collector.URL,
},
},
}
newTracing, closer, err := tracing.NewTracing(tracingConfig)
require.NoError(t, err)
t.Cleanup(func() {
_ = closer.Close()
})
chain := alice.New(observability.WrapEntryPointHandler(context.Background(), newTracing, nil, "test"))
epHandler, err := chain.Then(service)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "http://www.test.com", nil)
for k, v := range test.headers {
req.Header.Set(k, v)
}
rw := httptest.NewRecorder()
epHandler.ServeHTTP(rw, req)
select {
case <-time.After(10 * time.Second):
t.Error("Trace not exported")
case trace := <-traceCh:
assert.Equal(t, http.StatusOK, rw.Code)
if test.assertFn != nil {
test.assertFn(t, trace)
}
}
})
}
}

View file

@ -13,7 +13,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/static"
"github.com/traefik/traefik/v3/pkg/tracing/opentelemetry"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/contrib/propagators/autoprop"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
@ -38,7 +38,7 @@ func NewTracing(conf *static.Tracing) (*Tracer, io.Closer, error) {
if backend == nil {
log.Debug().Msg("Could not initialize tracing, using OpenTelemetry by default")
defaultBackend := &opentelemetry.Config{}
defaultBackend := &types.OTelTracing{}
backend = defaultBackend
}

View file

@ -1,10 +1,22 @@
package tracing
import (
"compress/gzip"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/containous/alice"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/static"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp"
"go.opentelemetry.io/otel/trace"
)
func Test_safeFullURL(t *testing.T) {
@ -55,3 +67,315 @@ func Test_safeFullURL(t *testing.T) {
})
}
}
func TestTracing(t *testing.T) {
tests := []struct {
desc string
propagators string
headers map[string]string
wantServiceHeadersFn func(t *testing.T, headers http.Header)
assertFn func(*testing.T, string)
}{
{
desc: "service name and version",
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `({"key":"service.name","value":{"stringValue":"traefik"}})`, trace)
assert.Regexp(t, `({"key":"service.version","value":{"stringValue":"dev"}})`, trace)
},
},
{
desc: "TraceContext propagation",
propagators: "tracecontext",
headers: map[string]string{
"traceparent": "00-00000000000000000000000000000001-0000000000000001-01",
"tracestate": "foo=bar",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(00-00000000000000000000000000000001-\w{16}-01)`, headers["Traceparent"][0])
assert.Equal(t, []string{"foo=bar"}, headers["Tracestate"])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"00000000000000000000000000000001")`, trace)
assert.Regexp(t, `("parentSpanId":"0000000000000001")`, trace)
assert.Regexp(t, `("traceState":"foo=bar")`, trace)
},
},
{
desc: "root span TraceContext propagation",
propagators: "tracecontext",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(00-\w{32}-\w{16}-01)`, headers["Traceparent"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "B3 propagation",
propagators: "b3",
headers: map[string]string{
"b3": "00000000000000000000000000000001-0000000000000002-1-0000000000000001",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(00000000000000000000000000000001-\w{16}-1)`, headers["B3"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"00000000000000000000000000000001")`, trace)
assert.Regexp(t, `("parentSpanId":"0000000000000002")`, trace)
},
},
{
desc: "root span B3 propagation",
propagators: "b3",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(\w{32}-\w{16}-1)`, headers["B3"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "B3 propagation Multiple Headers",
propagators: "b3multi",
headers: map[string]string{
"x-b3-traceid": "00000000000000000000000000000001",
"x-b3-parentspanid": "0000000000000001",
"x-b3-spanid": "0000000000000002",
"x-b3-sampled": "1",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Equal(t, "00000000000000000000000000000001", headers["X-B3-Traceid"][0])
assert.Equal(t, "0000000000000001", headers["X-B3-Parentspanid"][0])
assert.Equal(t, "1", headers["X-B3-Sampled"][0])
assert.Len(t, headers["X-B3-Spanid"][0], 16)
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"00000000000000000000000000000001")`, trace)
assert.Regexp(t, `("parentSpanId":"0000000000000002")`, trace)
},
},
{
desc: "root span B3 propagation Multiple Headers",
propagators: "b3multi",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(\w{32})`, headers["X-B3-Traceid"][0])
assert.Equal(t, "1", headers["X-B3-Sampled"][0])
assert.Regexp(t, `(\w{16})`, headers["X-B3-Spanid"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"")`, trace)
},
},
{
desc: "Baggage propagation",
propagators: "baggage",
headers: map[string]string{
"baggage": "userId=id",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Equal(t, []string{"userId=id"}, headers["Baggage"])
},
},
{
desc: "Jaeger propagation",
propagators: "jaeger",
headers: map[string]string{
"uber-trace-id": "00000000000000000000000000000001:0000000000000002:0000000000000001:1",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(00000000000000000000000000000001:\w{16}:0:1)`, headers["Uber-Trace-Id"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"00000000000000000000000000000001")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "root span Jaeger propagation",
propagators: "jaeger",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(\w{32}:\w{16}:0:1)`, headers["Uber-Trace-Id"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "XRay propagation",
propagators: "xray",
headers: map[string]string{
"X-Amzn-Trace-Id": "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1",
},
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(Root=1-5759e988-bd862e3fe1be46a994272793;Parent=\w{16};Sampled=1)`, headers["X-Amzn-Trace-Id"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"5759e988bd862e3fe1be46a994272793")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "root span XRay propagation",
propagators: "xray",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Regexp(t, `(Root=1-\w{8}-\w{24};Parent=\w{16};Sampled=1)`, headers["X-Amzn-Trace-Id"][0])
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
{
desc: "no propagation",
propagators: "none",
wantServiceHeadersFn: func(t *testing.T, headers http.Header) {
t.Helper()
assert.Empty(t, headers)
},
assertFn: func(t *testing.T, trace string) {
t.Helper()
assert.Regexp(t, `("traceId":"\w{32}")`, trace)
assert.Regexp(t, `("parentSpanId":"\w{16}")`, trace)
},
},
}
traceCh := make(chan string)
collector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gzr, err := gzip.NewReader(r.Body)
require.NoError(t, err)
body, err := io.ReadAll(gzr)
require.NoError(t, err)
req := ptraceotlp.NewExportRequest()
err = req.UnmarshalProto(body)
require.NoError(t, err)
marshalledReq, err := json.Marshal(req)
require.NoError(t, err)
traceCh <- string(marshalledReq)
}))
t.Cleanup(collector.Close)
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
t.Setenv("OTEL_PROPAGATORS", test.propagators)
service := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tracer := TracerFromContext(r.Context())
ctx, span := tracer.Start(r.Context(), "service")
defer span.End()
r = r.WithContext(ctx)
InjectContextIntoCarrier(r)
if test.wantServiceHeadersFn != nil {
test.wantServiceHeadersFn(t, r.Header)
}
})
tracingConfig := &static.Tracing{
ServiceName: "traefik",
SampleRate: 1.0,
OTLP: &types.OTelTracing{
HTTP: &types.OTelHTTP{
Endpoint: collector.URL,
},
},
}
tracer, closer, err := NewTracing(tracingConfig)
require.NoError(t, err)
t.Cleanup(func() {
_ = closer.Close()
})
chain := alice.New(func(next http.Handler) (http.Handler, error) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tracingCtx := ExtractCarrierIntoContext(r.Context(), r.Header)
start := time.Now()
tracingCtx, span := tracer.Start(tracingCtx, "test", trace.WithSpanKind(trace.SpanKindServer), trace.WithTimestamp(start))
end := time.Now()
span.End(trace.WithTimestamp(end))
next.ServeHTTP(w, r.WithContext(tracingCtx))
}), nil
})
epHandler, err := chain.Then(service)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "http://www.test.com", nil)
for k, v := range test.headers {
req.Header.Set(k, v)
}
rw := httptest.NewRecorder()
epHandler.ServeHTTP(rw, req)
select {
case <-time.After(10 * time.Second):
t.Error("Trace not exported")
case trace := <-traceCh:
assert.Equal(t, http.StatusOK, rw.Code)
if test.assertFn != nil {
test.assertFn(t, trace)
}
}
})
}
}

View file

@ -1,6 +1,22 @@
package types
import "github.com/traefik/paerser/types"
import (
"context"
"fmt"
"net"
"net/url"
"github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/version"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
otelsdk "go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/encoding/gzip"
)
const (
// AccessLogKeep is the keep string value.
@ -14,11 +30,10 @@ const (
const (
// CommonFormat is the common logging format (CLF).
CommonFormat string = "common"
// JSONFormat is the JSON logging format.
JSONFormat string = "json"
)
const OTelTraefikServiceName = "traefik"
// TraefikLog holds the configuration settings for the traefik logger.
type TraefikLog struct {
Level string `description:"Log level set to traefik logs." json:"level,omitempty" toml:"level,omitempty" yaml:"level,omitempty" export:"true"`
@ -30,6 +45,8 @@ type TraefikLog struct {
MaxAge int `description:"Maximum number of days to retain old log files based on the timestamp encoded in their filename." json:"maxAge,omitempty" toml:"maxAge,omitempty" yaml:"maxAge,omitempty" export:"true"`
MaxBackups int `description:"Maximum number of old log files to retain." json:"maxBackups,omitempty" toml:"maxBackups,omitempty" yaml:"maxBackups,omitempty" export:"true"`
Compress bool `description:"Determines if the rotated log files should be compressed using gzip." json:"compress,omitempty" toml:"compress,omitempty" yaml:"compress,omitempty" export:"true"`
OTLP *OTelLog `description:"Settings for OpenTelemetry." json:"otlp,omitempty" toml:"otlp,omitempty" yaml:"otlp,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// SetDefaults sets the default values.
@ -46,6 +63,8 @@ type AccessLog struct {
Fields *AccessLogFields `description:"AccessLogFields." json:"fields,omitempty" toml:"fields,omitempty" yaml:"fields,omitempty" export:"true"`
BufferingSize int64 `description:"Number of access log lines to process in a buffered way." json:"bufferingSize,omitempty" toml:"bufferingSize,omitempty" yaml:"bufferingSize,omitempty" export:"true"`
AddInternals bool `description:"Enables access log for internal services (ping, dashboard, etc...)." json:"addInternals,omitempty" toml:"addInternals,omitempty" yaml:"addInternals,omitempty" export:"true"`
OTLP *OTelLog `description:"Settings for OpenTelemetry." json:"otlp,omitempty" toml:"otlp,omitempty" yaml:"otlp,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// SetDefaults sets the default values.
@ -128,3 +147,123 @@ func checkFieldHeaderValue(value, defaultValue string) string {
}
return defaultValue
}
// OTelLog provides configuration settings for the open-telemetry logger.
type OTelLog struct {
ServiceName string `description:"Set the name for this service." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"`
ResourceAttributes map[string]string `description:"Defines additional resource attributes (key:value)." json:"resourceAttributes,omitempty" toml:"resourceAttributes,omitempty" yaml:"resourceAttributes,omitempty"`
GRPC *OTelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
HTTP *OTelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// SetDefaults sets the default values.
func (o *OTelLog) SetDefaults() {
o.ServiceName = OTelTraefikServiceName
o.HTTP = &OTelHTTP{}
o.HTTP.SetDefaults()
}
// NewLoggerProvider creates a new OpenTelemetry logger provider.
func (o *OTelLog) NewLoggerProvider() (*otelsdk.LoggerProvider, error) {
var (
err error
exporter otelsdk.Exporter
)
if o.GRPC != nil {
exporter, err = o.buildGRPCExporter()
} else {
exporter, err = o.buildHTTPExporter()
}
if err != nil {
return nil, fmt.Errorf("setting up exporter: %w", err)
}
attr := []attribute.KeyValue{
semconv.ServiceNameKey.String(o.ServiceName),
semconv.ServiceVersionKey.String(version.Version),
}
for k, v := range o.ResourceAttributes {
attr = append(attr, attribute.String(k, v))
}
res, err := resource.New(context.Background(),
resource.WithAttributes(attr...),
resource.WithFromEnv(),
resource.WithTelemetrySDK(),
resource.WithOSType(),
resource.WithProcessCommandArgs(),
)
if err != nil {
return nil, fmt.Errorf("building resource: %w", err)
}
// Register the trace provider to allow the global logger to access it.
bp := otelsdk.NewBatchProcessor(exporter)
loggerProvider := otelsdk.NewLoggerProvider(
otelsdk.WithResource(res),
otelsdk.WithProcessor(bp),
)
return loggerProvider, nil
}
func (o *OTelLog) buildHTTPExporter() (*otlploghttp.Exporter, error) {
endpoint, err := url.Parse(o.HTTP.Endpoint)
if err != nil {
return nil, fmt.Errorf("invalid collector endpoint %q: %w", o.HTTP.Endpoint, err)
}
opts := []otlploghttp.Option{
otlploghttp.WithEndpoint(endpoint.Host),
otlploghttp.WithHeaders(o.HTTP.Headers),
otlploghttp.WithCompression(otlploghttp.GzipCompression),
}
if endpoint.Scheme == "http" {
opts = append(opts, otlploghttp.WithInsecure())
}
if endpoint.Path != "" {
opts = append(opts, otlploghttp.WithURLPath(endpoint.Path))
}
if o.HTTP.TLS != nil {
tlsConfig, err := o.HTTP.TLS.CreateTLSConfig(context.Background())
if err != nil {
return nil, fmt.Errorf("creating TLS client config: %w", err)
}
opts = append(opts, otlploghttp.WithTLSClientConfig(tlsConfig))
}
return otlploghttp.New(context.Background(), opts...)
}
func (o *OTelLog) buildGRPCExporter() (*otlploggrpc.Exporter, error) {
host, port, err := net.SplitHostPort(o.GRPC.Endpoint)
if err != nil {
return nil, fmt.Errorf("invalid collector endpoint %q: %w", o.GRPC.Endpoint, err)
}
opts := []otlploggrpc.Option{
otlploggrpc.WithEndpoint(fmt.Sprintf("%s:%s", host, port)),
otlploggrpc.WithHeaders(o.GRPC.Headers),
otlploggrpc.WithCompressor(gzip.Name),
}
if o.GRPC.Insecure {
opts = append(opts, otlploggrpc.WithInsecure())
}
if o.GRPC.TLS != nil {
tlsConfig, err := o.GRPC.TLS.CreateTLSConfig(context.Background())
if err != nil {
return nil, fmt.Errorf("creating TLS client config: %w", err)
}
opts = append(opts, otlploggrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig)))
}
return otlploggrpc.New(context.Background(), opts...)
}

View file

@ -108,8 +108,8 @@ func (i *InfluxDB2) SetDefaults() {
// OTLP contains specific configuration used by the OpenTelemetry Metrics exporter.
type OTLP 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"`
HTTP *OtelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
GRPC *OTelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
HTTP *OTelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
AddEntryPointsLabels bool `description:"Enable metrics on entry points." json:"addEntryPointsLabels,omitempty" toml:"addEntryPointsLabels,omitempty" yaml:"addEntryPointsLabels,omitempty" export:"true"`
AddRoutersLabels bool `description:"Enable metrics on routers." json:"addRoutersLabels,omitempty" toml:"addRoutersLabels,omitempty" yaml:"addRoutersLabels,omitempty" export:"true"`
@ -121,14 +121,14 @@ type OTLP struct {
// SetDefaults sets the default values.
func (o *OTLP) SetDefaults() {
o.HTTP = &OtelHTTP{}
o.HTTP = &OTelHTTP{}
o.HTTP.SetDefaults()
o.AddEntryPointsLabels = true
o.AddServicesLabels = true
o.ExplicitBoundaries = []float64{.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10}
o.PushInterval = types.Duration(10 * time.Second)
o.ServiceName = "traefik"
o.ServiceName = OTelTraefikServiceName
}
// Statistics provides options for monitoring request and response stats.
@ -140,28 +140,3 @@ type Statistics struct {
func (s *Statistics) SetDefaults() {
s.RecentErrors = 10
}
// OtelGRPC provides configuration settings for the gRPC open-telemetry.
type OtelGRPC struct {
Endpoint string `description:"Sets the gRPC endpoint (host:port) of the collector." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Insecure bool `description:"Disables client transport security for the exporter." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
TLS *ClientTLS `description:"Defines client transport security parameters." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
Headers map[string]string `description:"Headers sent with payload." json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty"`
}
// SetDefaults sets the default values.
func (c *OtelGRPC) SetDefaults() {
c.Endpoint = "localhost:4317"
}
// OtelHTTP provides configuration settings for the HTTP open-telemetry.
type OtelHTTP struct {
Endpoint string `description:"Sets the HTTP endpoint (scheme://host:port/path) of the collector." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
TLS *ClientTLS `description:"Defines client transport security parameters." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
Headers map[string]string `description:"Headers sent with payload." json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty"`
}
// SetDefaults sets the default values.
func (c *OtelHTTP) SetDefaults() {
c.Endpoint = "https://localhost:4318"
}

26
pkg/types/otel.go Normal file
View file

@ -0,0 +1,26 @@
package types
// OTelGRPC provides configuration settings for the gRPC open-telemetry.
type OTelGRPC struct {
Endpoint string `description:"Sets the gRPC endpoint (host:port) of the collector." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Insecure bool `description:"Disables client transport security for the exporter." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
TLS *ClientTLS `description:"Defines client transport security parameters." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
Headers map[string]string `description:"Headers sent with payload." json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty"`
}
// SetDefaults sets the default values.
func (o *OTelGRPC) SetDefaults() {
o.Endpoint = "localhost:4317"
}
// OTelHTTP provides configuration settings for the HTTP open-telemetry.
type OTelHTTP struct {
Endpoint string `description:"Sets the HTTP endpoint (scheme://host:port/path) of the collector." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
TLS *ClientTLS `description:"Defines client transport security parameters." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
Headers map[string]string `description:"Headers sent with payload." json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty"`
}
// SetDefaults sets the default values.
func (o *OTelHTTP) SetDefaults() {
o.Endpoint = "https://localhost:4318"
}

View file

@ -1,4 +1,4 @@
package opentelemetry
package types
import (
"context"
@ -9,7 +9,6 @@ import (
"time"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/types"
"github.com/traefik/traefik/v3/pkg/version"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
@ -18,26 +17,26 @@ import (
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/encoding/gzip"
)
// Config provides configuration settings for the open-telemetry tracer.
type Config struct {
GRPC *types.OtelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
HTTP *types.OtelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
// 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"`
HTTP *OTelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// SetDefaults sets the default values.
func (c *Config) SetDefaults() {
c.HTTP = &types.OtelHTTP{}
func (c *OTelTracing) SetDefaults() {
c.HTTP = &OTelHTTP{}
c.HTTP.SetDefaults()
}
// Setup sets up the tracer.
func (c *Config) Setup(serviceName string, sampleRate float64, globalAttributes map[string]string) (trace.Tracer, io.Closer, error) {
func (c *OTelTracing) Setup(serviceName string, sampleRate float64, globalAttributes map[string]string) (trace.Tracer, io.Closer, error) {
var (
err error
exporter *otlptrace.Exporter
@ -87,7 +86,7 @@ func (c *Config) Setup(serviceName string, sampleRate float64, globalAttributes
return tracerProvider.Tracer("github.com/traefik/traefik"), &tpCloser{provider: tracerProvider}, err
}
func (c *Config) setupHTTPExporter() (*otlptrace.Exporter, error) {
func (c *OTelTracing) setupHTTPExporter() (*otlptrace.Exporter, error) {
endpoint, err := url.Parse(c.HTTP.Endpoint)
if err != nil {
return nil, fmt.Errorf("invalid collector endpoint %q: %w", c.HTTP.Endpoint, err)
@ -119,7 +118,7 @@ func (c *Config) setupHTTPExporter() (*otlptrace.Exporter, error) {
return otlptrace.New(context.Background(), otlptracehttp.NewClient(opts...))
}
func (c *Config) setupGRPCExporter() (*otlptrace.Exporter, error) {
func (c *OTelTracing) setupGRPCExporter() (*otlptrace.Exporter, error) {
host, port, err := net.SplitHostPort(c.GRPC.Endpoint)
if err != nil {
return nil, fmt.Errorf("invalid collector endpoint %q: %w", c.GRPC.Endpoint, err)