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-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-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-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 | |
@@ -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.
!!! 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
```yaml tab="File (YAML)"
@@ -195,6 +198,7 @@ accessLog:
```sh tab="CLI"
--accesslog=true
--accesslog.dualoutput=true
--accesslog.format=json
--accesslog.filters.statuscodes=200,300-302
--accesslog.filters.retryattempts
@@ -213,6 +217,7 @@ The section below describes how to configure Traefik access logs using the stati
| 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-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-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 |
@@ -252,6 +257,8 @@ experimental:
otlpLogs: true
accesslog:
# Keep Stdio logs alongside OTEL logging
dualOutput: true
otlp:
http:
endpoint: https://collector:4318/v1/logs
@@ -263,6 +270,9 @@ accesslog:
[experimental]
otlpLogs = true
[accessLog]
dualOutput = true
[accesslog.otlp]
http.endpoint = "https://collector:4318/v1/logs"
http.headers.Authorization = "Bearer auth_asKXRhIMplM7El1JENjrotGouS1LYRdL"
+2
View File
@@ -128,8 +128,10 @@ func NewHandler(ctx context.Context, config *otypes.AccessLog) (*Handler, error)
}
logger.Hooks.Add(otellogrus.NewHook("traefik", otellogrus.WithLoggerProvider(otelLoggerProvider)))
if !config.DualOutput {
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.
+65 -1
View File
@@ -21,6 +21,7 @@ import (
"time"
"github.com/containous/alice"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
@@ -56,41 +57,99 @@ var (
testStart = time.Now()
)
func TestOTelAccessLogWithBody(t *testing.T) {
func TestOTelAccessLogWithBodyAndDualOutput(t *testing.T) {
testCases := []struct {
desc string
format string
filePath string
dualOutput bool
bodyCheckFn func(*testing.T, string)
outLoggerCheckFn func(*testing.T, *logrus.Logger)
}{
{
desc: "Common format with log body",
format: CommonFormat,
filePath: "",
dualOutput: false,
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.Equal(t, l.Out, io.Discard)
},
},
{
desc: "Generic CLF format with log body",
format: GenericCLFFormat,
filePath: "",
dualOutput: false,
bodyCheckFn: func(t *testing.T, log string) {
t.Helper()
// For generic CLF format, verify the body contains the CLF formatted string
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",
format: JSONFormat,
filePath: "",
dualOutput: false,
bodyCheckFn: func(t *testing.T, log string) {
t.Helper()
// For JSON format, verify the body contains the JSON formatted string
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)
},
},
}
@@ -119,6 +178,8 @@ func TestOTelAccessLogWithBody(t *testing.T) {
config := &otypes.AccessLog{
Format: test.format,
DualOutput: test.dualOutput,
FilePath: test.filePath,
OTLP: &otypes.OTelLog{
ServiceName: "test",
ResourceAttributes: map[string]string{"resource": "attribute"},
@@ -179,6 +240,9 @@ func TestOTelAccessLogWithBody(t *testing.T) {
// Run format-specific body checks
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"`
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"`
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"`
}