Add a new option to allow Stdio access logs alongsige OTLP logging

This commit is contained in:
Juri Duval
2026-01-13 15:36:05 +00:00
committed by GitHub
parent 5d3706468d
commit 5492079915
5 changed files with 90 additions and 12 deletions
@@ -10,6 +10,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-accesslog" href="#opt-accesslog" title="#opt-accesslog">accesslog</a> | Access log settings. | false | | <a id="opt-accesslog" href="#opt-accesslog" title="#opt-accesslog">accesslog</a> | Access log settings. | false |
| <a id="opt-accesslog-addinternals" href="#opt-accesslog-addinternals" title="#opt-accesslog-addinternals">accesslog.addinternals</a> | Enables access log for internal services (ping, dashboard, etc...). | false | | <a id="opt-accesslog-addinternals" href="#opt-accesslog-addinternals" title="#opt-accesslog-addinternals">accesslog.addinternals</a> | Enables access log for internal services (ping, dashboard, etc...). | false |
| <a id="opt-accesslog-bufferingsize" href="#opt-accesslog-bufferingsize" title="#opt-accesslog-bufferingsize">accesslog.bufferingsize</a> | Number of access log lines to process in a buffered way. | 0 | | <a id="opt-accesslog-bufferingsize" href="#opt-accesslog-bufferingsize" title="#opt-accesslog-bufferingsize">accesslog.bufferingsize</a> | Number of access log lines to process in a buffered way. | 0 |
| <a id="opt-accesslog-dualoutput" href="#opt-accesslog-dualoutput" title="#opt-accesslog-dualoutput">accesslog.dualoutput</a> | Enables access log output alongside OTLP. By default, this output is disabled when OTLP is configured. | false |
| <a id="opt-accesslog-fields-defaultmode" href="#opt-accesslog-fields-defaultmode" title="#opt-accesslog-fields-defaultmode">accesslog.fields.defaultmode</a> | Default mode for fields: keep | drop | keep | | <a id="opt-accesslog-fields-defaultmode" href="#opt-accesslog-fields-defaultmode" title="#opt-accesslog-fields-defaultmode">accesslog.fields.defaultmode</a> | Default mode for fields: keep | drop | keep |
| <a id="opt-accesslog-fields-headers-defaultmode" href="#opt-accesslog-fields-headers-defaultmode" title="#opt-accesslog-fields-headers-defaultmode">accesslog.fields.headers.defaultmode</a> | Default mode for fields: keep | drop | redact | drop | | <a id="opt-accesslog-fields-headers-defaultmode" href="#opt-accesslog-fields-headers-defaultmode" title="#opt-accesslog-fields-headers-defaultmode">accesslog.fields.headers.defaultmode</a> | Default mode for fields: keep | drop | redact | drop |
| <a id="opt-accesslog-fields-headers-names-name" href="#opt-accesslog-fields-headers-names-name" title="#opt-accesslog-fields-headers-names-name">accesslog.fields.headers.names._name_</a> | Override mode for headers | | | <a id="opt-accesslog-fields-headers-names-name" href="#opt-accesslog-fields-headers-names-name" title="#opt-accesslog-fields-headers-names-name">accesslog.fields.headers.names._name_</a> | Override mode for headers | |
@@ -141,6 +141,9 @@ Traefik also supports the `OTEL_RESOURCE_ATTRIBUTES` env variable to set up the
Access logs concern everything that happens to the requests handled by Traefik. Access logs concern everything that happens to the requests handled by Traefik.
!!! note "Stdio logs are not enabled by default alongside OTLP exports"
If you would like Stdio access logs to be available, use [accessLog.dualOutput](#opt-accesslog-dualOutput) option.
### Configuration Example ### Configuration Example
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
@@ -195,6 +198,7 @@ accessLog:
```sh tab="CLI" ```sh tab="CLI"
--accesslog=true --accesslog=true
--accesslog.dualoutput=true
--accesslog.format=json --accesslog.format=json
--accesslog.filters.statuscodes=200,300-302 --accesslog.filters.statuscodes=200,300-302
--accesslog.filters.retryattempts --accesslog.filters.retryattempts
@@ -213,6 +217,7 @@ The section below describes how to configure Traefik access logs using the stati
| Field | Description | Default | Required | | Field | Description | Default | Required |
|:-----------|:--------------------------|:--------|:---------| |:-----------|:--------------------------|:--------|:---------|
| <a id="opt-accesslog-filePath" href="#opt-accesslog-filePath" title="#opt-accesslog-filePath">`accesslog.filePath`</a> | By default, the access logs are written to the standard output.<br />You can configure a file path instead using the `filePath` option.| | No | | <a id="opt-accesslog-filePath" href="#opt-accesslog-filePath" title="#opt-accesslog-filePath">`accesslog.filePath`</a> | By default, the access logs are written to the standard output.<br />You can configure a file path instead using the `filePath` option.| | No |
| <a id="opt-accesslog-dualOutput" href="#opt-accesslog-dualOutput" title="#opt-accesslog-dualOutput">`accesslog.dualOutput`</a> | Force Stdio logging, even if OTLP is configured. By default, Stdio logging is disabled when OTLP is enabled for performance reasons. | false | No |
| <a id="opt-accesslog-format" href="#opt-accesslog-format" title="#opt-accesslog-format">`accesslog.format`</a> | By default, logs are written using the Traefik Common Log Format (CLF).<br />Available formats: [`common`](#traefik-clf-format-fields) (Traefik extended CLF), [`genericCLF`](#generic-clf-format-fields) (standard CLF compatible with analyzers), or [`json`](#json-format-fields).<br />If the given format is unsupported, the default (`common`) is used instead. | "common" | No | | <a id="opt-accesslog-format" href="#opt-accesslog-format" title="#opt-accesslog-format">`accesslog.format`</a> | By default, logs are written using the Traefik Common Log Format (CLF).<br />Available formats: [`common`](#traefik-clf-format-fields) (Traefik extended CLF), [`genericCLF`](#generic-clf-format-fields) (standard CLF compatible with analyzers), or [`json`](#json-format-fields).<br />If the given format is unsupported, the default (`common`) is used instead. | "common" | No |
| <a id="opt-accesslog-bufferingSize" href="#opt-accesslog-bufferingSize" title="#opt-accesslog-bufferingSize">`accesslog.bufferingSize`</a> | To write the logs in an asynchronous fashion, specify a `bufferingSize` option.<br />This option represents the number of log lines Traefik will keep in memory before writing them to the selected output.<br />In some cases, this option can greatly help performances.| 0 | No | | <a id="opt-accesslog-bufferingSize" href="#opt-accesslog-bufferingSize" title="#opt-accesslog-bufferingSize">`accesslog.bufferingSize`</a> | To write the logs in an asynchronous fashion, specify a `bufferingSize` option.<br />This option represents the number of log lines Traefik will keep in memory before writing them to the selected output.<br />In some cases, this option can greatly help performances.| 0 | No |
| <a id="opt-accesslog-addInternals" href="#opt-accesslog-addInternals" title="#opt-accesslog-addInternals">`accesslog.addInternals`</a> | Enables access logs for internal resources (e.g.: `ping@internal`). | false | No | | <a id="opt-accesslog-addInternals" href="#opt-accesslog-addInternals" title="#opt-accesslog-addInternals">`accesslog.addInternals`</a> | Enables access logs for internal resources (e.g.: `ping@internal`). | false | No |
@@ -252,6 +257,8 @@ experimental:
otlpLogs: true otlpLogs: true
accesslog: accesslog:
# Keep Stdio logs alongside OTEL logging
dualOutput: true
otlp: otlp:
http: http:
endpoint: https://collector:4318/v1/logs endpoint: https://collector:4318/v1/logs
@@ -263,6 +270,9 @@ accesslog:
[experimental] [experimental]
otlpLogs = true otlpLogs = true
[accessLog]
dualOutput = true
[accesslog.otlp] [accesslog.otlp]
http.endpoint = "https://collector:4318/v1/logs" http.endpoint = "https://collector:4318/v1/logs"
http.headers.Authorization = "Bearer auth_asKXRhIMplM7El1JENjrotGouS1LYRdL" http.headers.Authorization = "Bearer auth_asKXRhIMplM7El1JENjrotGouS1LYRdL"
+3 -1
View File
@@ -128,7 +128,9 @@ func NewHandler(ctx context.Context, config *otypes.AccessLog) (*Handler, error)
} }
logger.Hooks.Add(otellogrus.NewHook("traefik", otellogrus.WithLoggerProvider(otelLoggerProvider))) logger.Hooks.Add(otellogrus.NewHook("traefik", otellogrus.WithLoggerProvider(otelLoggerProvider)))
logger.Out = io.Discard if !config.DualOutput {
logger.Out = io.Discard
}
} }
// Transform header names to a canonical form, to be used as is without further transformations, // Transform header names to a canonical form, to be used as is without further transformations,
+75 -11
View File
@@ -21,6 +21,7 @@ import (
"time" "time"
"github.com/containous/alice" "github.com/containous/alice"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types" ptypes "github.com/traefik/paerser/types"
@@ -56,41 +57,99 @@ var (
testStart = time.Now() testStart = time.Now()
) )
func TestOTelAccessLogWithBody(t *testing.T) { func TestOTelAccessLogWithBodyAndDualOutput(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
format string format string
bodyCheckFn func(*testing.T, string) filePath string
dualOutput bool
bodyCheckFn func(*testing.T, string)
outLoggerCheckFn func(*testing.T, *logrus.Logger)
}{ }{
{ {
desc: "Common format with log body", desc: "Common format with log body",
format: CommonFormat, format: CommonFormat,
filePath: "",
dualOutput: false,
bodyCheckFn: func(t *testing.T, log string) { bodyCheckFn: func(t *testing.T, log string) {
t.Helper() t.Helper()
// For common format, verify the body contains the Traefik common log formatted string // For common format, verify the body contains the Traefik common log formatted string
assert.Regexp(t, `"body":{"stringValue":".*- /health -.*200.*[0-9]+ms.*"}`, log) assert.Regexp(t, `"body":{"stringValue":".*- /health -.*200.*[0-9]+ms.*"}`, log)
}, },
outLoggerCheckFn: func(t *testing.T, l *logrus.Logger) {
t.Helper()
assert.Equal(t, l.Out, io.Discard)
},
}, },
{ {
desc: "Generic CLF format with log body", desc: "Generic CLF format with log body",
format: GenericCLFFormat, format: GenericCLFFormat,
filePath: "",
dualOutput: false,
bodyCheckFn: func(t *testing.T, log string) { bodyCheckFn: func(t *testing.T, log string) {
t.Helper() t.Helper()
// For generic CLF format, verify the body contains the CLF formatted string // For generic CLF format, verify the body contains the CLF formatted string
assert.Regexp(t, `"body":{"stringValue":".*- /health -.*200.*"}`, log) assert.Regexp(t, `"body":{"stringValue":".*- /health -.*200.*"}`, log)
}, },
outLoggerCheckFn: func(t *testing.T, l *logrus.Logger) {
t.Helper()
assert.Equal(t, l.Out, io.Discard)
},
}, },
{ {
desc: "JSON format with log body", desc: "JSON format with log body",
format: JSONFormat, format: JSONFormat,
filePath: "",
dualOutput: false,
bodyCheckFn: func(t *testing.T, log string) { bodyCheckFn: func(t *testing.T, log string) {
t.Helper() t.Helper()
// For JSON format, verify the body contains the JSON formatted string // For JSON format, verify the body contains the JSON formatted string
assert.Regexp(t, `"body":{"stringValue":".*DownstreamStatus.*:200.*"}`, log) assert.Regexp(t, `"body":{"stringValue":".*DownstreamStatus.*:200.*"}`, log)
}, },
outLoggerCheckFn: func(t *testing.T, l *logrus.Logger) {
t.Helper()
assert.Equal(t, l.Out, io.Discard)
},
},
{
desc: "Common format with log body and Dual Output (STDOUT + OTEL)",
format: CommonFormat,
filePath: "",
dualOutput: true,
bodyCheckFn: func(t *testing.T, log string) {
t.Helper()
// For common format, verify the body contains the Traefik common log formatted string
assert.Regexp(t, `"body":{"stringValue":".*- /health -.*200.*[0-9]+ms.*"}`, log)
},
outLoggerCheckFn: func(t *testing.T, l *logrus.Logger) {
t.Helper()
assert.NotEqual(t, l.Out, io.Discard)
},
},
{
desc: "Common format with log body and Dual Output (File logging + OTEL)",
format: CommonFormat,
filePath: filepath.Join(t.TempDir(), "traefik.log"),
dualOutput: true,
bodyCheckFn: func(t *testing.T, log string) {
t.Helper()
// For common format, verify the body contains the Traefik common log formatted string
assert.Regexp(t, `"body":{"stringValue":".*- /health -.*200.*[0-9]+ms.*"}`, log)
},
outLoggerCheckFn: func(t *testing.T, l *logrus.Logger) {
t.Helper()
assert.NotEqual(t, l.Out, io.Discard)
},
}, },
} }
@@ -118,7 +177,9 @@ func TestOTelAccessLogWithBody(t *testing.T) {
t.Cleanup(collector.Close) t.Cleanup(collector.Close)
config := &otypes.AccessLog{ config := &otypes.AccessLog{
Format: test.format, Format: test.format,
DualOutput: test.dualOutput,
FilePath: test.filePath,
OTLP: &otypes.OTelLog{ OTLP: &otypes.OTelLog{
ServiceName: "test", ServiceName: "test",
ResourceAttributes: map[string]string{"resource": "attribute"}, ResourceAttributes: map[string]string{"resource": "attribute"},
@@ -179,6 +240,9 @@ func TestOTelAccessLogWithBody(t *testing.T) {
// Run format-specific body checks // Run format-specific body checks
test.bodyCheckFn(t, log) test.bodyCheckFn(t, log)
// Run OUT logger checks
test.outLoggerCheckFn(t, logHandler.logger)
} }
}) })
} }
+1
View File
@@ -64,6 +64,7 @@ type AccessLog struct {
Fields *AccessLogFields `description:"AccessLogFields." json:"fields,omitempty" toml:"fields,omitempty" yaml:"fields,omitempty" export:"true"` 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"` 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"` AddInternals bool `description:"Enables access log for internal services (ping, dashboard, etc...)." json:"addInternals,omitempty" toml:"addInternals,omitempty" yaml:"addInternals,omitempty" export:"true"`
DualOutput bool `description:"Enables access log output alongside OTLP. By default, this output is disabled when OTLP is configured." json:"dualOutput,omitempty" toml:"dualOutput,omitempty" yaml:"dualOutput,omitempty" export:"true"`
OTLP *OTelLog `description:"Settings for OpenTelemetry." json:"otlp,omitempty" toml:"otlp,omitempty" yaml:"otlp,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` OTLP *OTelLog `description:"Settings for OpenTelemetry." json:"otlp,omitempty" toml:"otlp,omitempty" yaml:"otlp,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
} }