From 6e0012cb0adf4d6ad965b46f8fc9ec2ebb1db412 Mon Sep 17 00:00:00 2001 From: Landry Benguigui Date: Fri, 17 Oct 2025 16:06:06 +0200 Subject: [PATCH] fix: enable stdio logging when otlp is enabled --- cmd/traefik/logger.go | 4 - .../observability/logs-and-accesslogs.md | 3 + integration/dual_logging_test.go | 134 ++++++++++++++++++ .../dual_logging/otlp_and_stdout.toml | 24 ++++ pkg/observability/logs/otel.go | 3 - 5 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 integration/dual_logging_test.go create mode 100644 integration/fixtures/dual_logging/otlp_and_stdout.toml diff --git a/cmd/traefik/logger.go b/cmd/traefik/logger.go index 2f1d1b8a0..e97665756 100644 --- a/cmd/traefik/logger.go +++ b/cmd/traefik/logger.go @@ -68,10 +68,6 @@ func setupLogger(ctx context.Context, staticConfiguration *static.Configuration) } func getLogWriter(staticConfiguration *static.Configuration) io.Writer { - if staticConfiguration.Log != nil && staticConfiguration.Log.OTLP != nil { - return io.Discard - } - var w io.Writer = os.Stdout if staticConfiguration.Log != nil && len(staticConfiguration.Log.FilePath) > 0 { _, _ = os.OpenFile(staticConfiguration.Log.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666) diff --git a/docs/content/reference/install-configuration/observability/logs-and-accesslogs.md b/docs/content/reference/install-configuration/observability/logs-and-accesslogs.md index 70165cca2..cbb84a227 100644 --- a/docs/content/reference/install-configuration/observability/logs-and-accesslogs.md +++ b/docs/content/reference/install-configuration/observability/logs-and-accesslogs.md @@ -65,6 +65,9 @@ experimental: !!! warning This is an experimental feature. +!!! note "Stdio logs remain available" + When OTLP logging is enabled, standard output (stdio) logs are still available and will continue to be written alongside OTLP exports. + #### Configuration Example ```yaml tab="File (YAML)" diff --git a/integration/dual_logging_test.go b/integration/dual_logging_test.go new file mode 100644 index 000000000..fef4cbd54 --- /dev/null +++ b/integration/dual_logging_test.go @@ -0,0 +1,134 @@ +package integration + +import ( + "compress/gzip" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "go.opentelemetry.io/collector/pdata/plog/plogotlp" +) + +const traefikTestOTLPLogFile = "traefik_otlp.log" + +// DualLoggingSuite tests that both OTLP and stdout logging can work together. +type DualLoggingSuite struct { + BaseSuite + otlpLogs []string + collector *httptest.Server +} + +func TestDualLoggingSuite(t *testing.T) { + suite.Run(t, new(DualLoggingSuite)) +} + +func (s *DualLoggingSuite) SetupSuite() { + s.BaseSuite.SetupSuite() + + // Clean up any existing log files + os.Remove(traefikTestLogFile) + os.Remove(traefikTestOTLPLogFile) +} + +func (s *DualLoggingSuite) TearDownSuite() { + s.BaseSuite.TearDownSuite() + + // Clean up log files + generatedFiles := []string{ + traefikTestLogFile, + traefikTestOTLPLogFile, + } + + for _, filename := range generatedFiles { + if err := os.Remove(filename); err != nil { + s.T().Logf("Failed to remove %s: %v", filename, err) + } + } +} + +func (s *DualLoggingSuite) SetupTest() { + s.otlpLogs = []string{} + + // Create mock OTLP collector + s.collector = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + gzr, err := gzip.NewReader(r.Body) + if err != nil { + s.T().Logf("Error creating gzip reader: %v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + defer gzr.Close() + + body, err := io.ReadAll(gzr) + if err != nil { + s.T().Logf("Error reading gzipped body: %v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + req := plogotlp.NewExportRequest() + err = req.UnmarshalProto(body) + if err != nil { + s.T().Logf("Error unmarshaling protobuf: %v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + marshalledReq, err := json.Marshal(req) + if err != nil { + s.T().Logf("Error marshaling to JSON: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + s.otlpLogs = append(s.otlpLogs, string(marshalledReq)) + + w.WriteHeader(http.StatusOK) + })) +} + +func (s *DualLoggingSuite) TearDownTest() { + if s.collector != nil { + s.collector.Close() + s.collector = nil + } +} + +func (s *DualLoggingSuite) TestOTLPAndStdoutLogging() { + tempObjects := struct { + CollectorURL string + }{ + CollectorURL: s.collector.URL + "/v1/logs", + } + + file := s.adaptFile("fixtures/dual_logging/otlp_and_stdout.toml", tempObjects) + + cmd, display := s.cmdTraefik(withConfigFile(file)) + defer s.displayTraefikLogFile(traefikTestLogFile) + + s.waitForTraefik("dashboard") + + time.Sleep(3 * time.Second) + + s.killCmd(cmd) + time.Sleep(1 * time.Second) + + assert.NotEmpty(s.T(), s.otlpLogs) + + output := display.String() + otlpOutput := strings.Join(s.otlpLogs, "\n") + + foundStdoutLog := strings.Contains(output, "Starting provider") + assert.True(s.T(), foundStdoutLog) + foundOTLPLog := strings.Contains(otlpOutput, "Starting provider") + assert.True(s.T(), foundOTLPLog) +} diff --git a/integration/fixtures/dual_logging/otlp_and_stdout.toml b/integration/fixtures/dual_logging/otlp_and_stdout.toml new file mode 100644 index 000000000..4e31afbf1 --- /dev/null +++ b/integration/fixtures/dual_logging/otlp_and_stdout.toml @@ -0,0 +1,24 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "INFO" + +[experimental] + otlpLogs = true + +[log.otlp] + serviceName = "traefik-test" + [log.otlp.http] + endpoint = "{{ .CollectorURL }}" + +[api] + insecure = true + +[entryPoints] + [entryPoints.web] + address = ":8000" + +[providers.docker] + exposedByDefault = false diff --git a/pkg/observability/logs/otel.go b/pkg/observability/logs/otel.go index 6e7734f78..f56f102f2 100644 --- a/pkg/observability/logs/otel.go +++ b/pkg/observability/logs/otel.go @@ -41,9 +41,6 @@ func (h *otelLoggerHook) Run(e *zerolog.Event, level zerolog.Level, message stri return } - // Discard the event to avoid double logging. - e.Discard() - var record otellog.Record record.SetTimestamp(time.Now().UTC()) record.SetSeverity(otelLogSeverity(level))