Allow query parameters to be dropped from RequestPath in access log
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Build and Publish Documentation / Doc Process (push) Waiting to run
Build experimental image on branch / build-webui (push) Waiting to run
Build experimental image on branch / Build experimental image on branch (push) Waiting to run

This commit is contained in:
Cali Nelson 2026-05-22 09:40:06 -04:00 committed by GitHub
parent ee07d0f4d2
commit 1db7d439a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 196 additions and 36 deletions

View file

@ -175,6 +175,7 @@ When using the `json` format, you can customize which fields are included in you
- **Request Fields:** You can choose to `keep`, `drop`, or `redact` any of the standard request fields. A complete list of available fields like `ClientHost`, `RequestMethod`, and `Duration` can be found in the [reference documentation](../reference/install-configuration/observability/logs-and-accesslogs.md#json-format-fields).
- **Request Headers:** You can also specify which request headers should be included in the logs, and whether their values should be `kept`, `dropped`, or `redacted`.
- **Request Query Parameters:** You can choose to `keep` or `drop` the query parameters for a request.
!!! info
For detailed configuration options, refer to the [reference documentation](../reference/install-configuration/observability/logs-and-accesslogs.md).

View file

@ -14,6 +14,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <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-names-name" href="#opt-accesslog-fields-names-name" title="#opt-accesslog-fields-names-name">accesslog.fields.names._name_</a> | Override mode for fields | |
| <a id="opt-accesslog-fields-queryparameters-defaultmode" href="#opt-accesslog-fields-queryparameters-defaultmode" title="#opt-accesslog-fields-queryparameters-defaultmode">accesslog.fields.queryparameters.defaultmode</a> | Default mode for query parameters: keep | drop | keep |
| <a id="opt-accesslog-filepath" href="#opt-accesslog-filepath" title="#opt-accesslog-filepath">accesslog.filepath</a> | Access log file path. Stdout is used when omitted or empty. | |
| <a id="opt-accesslog-filters-minduration" href="#opt-accesslog-filters-minduration" title="#opt-accesslog-filters-minduration">accesslog.filters.minduration</a> | Keep access logs when request took longer than the specified duration. | 0 |
| <a id="opt-accesslog-filters-retryattempts" href="#opt-accesslog-filters-retryattempts" title="#opt-accesslog-filters-retryattempts">accesslog.filters.retryattempts</a> | Keep access logs when at least one retry happened. | false |

View file

@ -168,6 +168,9 @@ accessLog:
User-Agent: redact
# Drop the Authorization header value
Authorization: drop
queryParameters:
# Drop all query parameters
defaultMode: drop
```
```toml tab="File (TOML)"
@ -191,6 +194,9 @@ accessLog:
[accessLog.fields.headers.names]
User-Agent = "redact"
Authorization = "drop"
[accessLog.fields.queryParameters]
defaultMode = "drop"
```
```sh tab="CLI"
@ -204,6 +210,7 @@ accessLog:
--accesslog.fields.headers.defaultmode=keep
--accesslog.fields.headers.names.User-Agent=redact
--accesslog.fields.headers.names.Authorization=drop
--accesslog.fields.queryparameters.defaultmode=drop
```
### Configuration Options
@ -223,6 +230,7 @@ The section below describes how to configure Traefik access logs using the stati
| <a id="opt-accesslog-fields-names" href="#opt-accesslog-fields-names" title="#opt-accesslog-fields-names">`accesslog.fields.names`</a> | Set the fields list to display in the access logs (format `name:mode`).<br /> Available fields list [here](#json-format-fields). | [ ] | No |
| <a id="opt-accesslog-fields-headers-defaultMode" href="#opt-accesslog-fields-headers-defaultMode" title="#opt-accesslog-fields-headers-defaultMode">`accesslog.fields.headers.defaultMode`</a> | Mode to apply by default to the access logs headers (`keep`, `redact` or `drop`). | drop | No |
| <a id="opt-accesslog-fields-headers-names" href="#opt-accesslog-fields-headers-names" title="#opt-accesslog-fields-headers-names">`accesslog.fields.headers.names`</a> | Set the headers list to display in the access logs (format `name:mode`). | [ ] | No |
| <a id="opt-accesslog-fields-queryParameters-defaultMode" href="#opt-accesslog-fields-queryParameters-defaultMode" title="#opt-accesslog-fields-queryParameters-defaultMode">`accesslog.fields.queryParameters.defaultMode`</a> | Mode to apply by default to the access logs query parameters (`keep` or `drop`) | keep | No |
### OpenTelemetry

View file

@ -219,12 +219,18 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
core[RequestAddr] = req.Host
core[RequestHost], core[RequestPort] = silentSplitHostPort(req.Host)
}
queryParameters := ""
if h.config.Fields.KeepQueryParameters() {
queryParameters = req.URL.RawQuery
}
// copy the URL without the scheme, hostname etc
urlCopy := &url.URL{
Path: req.URL.Path,
RawPath: req.URL.RawPath,
RawQuery: req.URL.RawQuery,
ForceQuery: req.URL.ForceQuery,
RawQuery: queryParameters,
ForceQuery: req.URL.ForceQuery && h.config.Fields.KeepQueryParameters(),
Fragment: req.URL.Fragment,
}
urlCopyString := urlCopy.String()

View file

@ -37,23 +37,25 @@ import (
const delta float64 = 1e-10
var (
logFileNameSuffix = "/traefik/logger/test.log"
testContent = "Hello, World"
testServiceName = "http://127.0.0.1/testService"
testRouterName = "testRouter"
testStatus = 123
testContentSize int64 = 12
testHostname = "TestHost"
testUsername = "TestUser"
testPath = "testpath"
testPort = 8181
testProto = "HTTP/0.0"
testScheme = "http"
testMethod = http.MethodPost
testReferer = "testReferer"
testUserAgent = "testUserAgent"
testRetryAttempts = 2
testStart = time.Now()
logFileNameSuffix = "/traefik/logger/test.log"
testContent = "Hello, World"
testServiceName = "http://127.0.0.1/testService"
testRouterName = "testRouter"
testStatus = 123
testContentSize int64 = 12
testHostname = "TestHost"
testUsername = "TestUser"
testPath = "testpath"
testQueryParams = "param1=test1&param2=test2"
testPathWithQueryParams = testPath + "?" + testQueryParams
testPort = 8181
testProto = "HTTP/0.0"
testScheme = "http"
testMethod = http.MethodPost
testReferer = "testReferer"
testUserAgent = "testUserAgent"
testRetryAttempts = 2
testStart = time.Now()
)
func TestOTelAccessLogWithBody(t *testing.T) {
@ -393,7 +395,7 @@ func TestCommonLogger(t *testing.T) {
logData, err := os.ReadFile(logFilePath)
require.NoError(t, err)
expectedLog := ` TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 1 "testRouter" "http://127.0.0.1/testService" 1ms`
expectedLog := ` TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath?param1=test1&param2=test2 HTTP/0.0" 123 12 "testReferer" "testUserAgent" 1 "testRouter" "http://127.0.0.1/testService" 1ms`
assertValidCommonLogData(t, expectedLog, logData)
}
@ -408,6 +410,23 @@ func TestCommonLoggerWithBufferingSize(t *testing.T) {
logData, err := os.ReadFile(logFilePath)
require.NoError(t, err)
expectedLog := ` TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath?param1=test1&param2=test2 HTTP/0.0" 123 12 "testReferer" "testUserAgent" 1 "testRouter" "http://127.0.0.1/testService" 1ms`
assertValidCommonLogData(t, expectedLog, logData)
}
func TestCommonLoggerDropQueryParameters(t *testing.T) {
logFilePath := filepath.Join(t.TempDir(), logFileNameSuffix)
fieldConfig := &otypes.AccessLogFields{
QueryParameters: &otypes.FieldQueryParameters{
DefaultMode: "drop",
},
}
config := &otypes.AccessLog{FilePath: logFilePath, Format: CommonFormat, Fields: fieldConfig}
doLogging(t, config, false)
logData, err := os.ReadFile(logFilePath)
require.NoError(t, err)
expectedLog := ` TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 1 "testRouter" "http://127.0.0.1/testService" 1ms`
assertValidCommonLogData(t, expectedLog, logData)
}
@ -420,7 +439,7 @@ func TestLoggerGenericCLF(t *testing.T) {
logData, err := os.ReadFile(logFilePath)
require.NoError(t, err)
expectedLog := ` TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent"`
expectedLog := ` TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath?param1=test1&param2=test2 HTTP/0.0" 123 12 "testReferer" "testUserAgent"`
assertValidGenericCLFLogData(t, expectedLog, logData)
}
@ -435,6 +454,23 @@ func TestLoggerGenericCLFWithBufferingSize(t *testing.T) {
logData, err := os.ReadFile(logFilePath)
require.NoError(t, err)
expectedLog := ` TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath?param1=test1&param2=test2 HTTP/0.0" 123 12 "testReferer" "testUserAgent"`
assertValidGenericCLFLogData(t, expectedLog, logData)
}
func TestLoggerGenericCLFDropQueryParameters(t *testing.T) {
logFilePath := filepath.Join(t.TempDir(), logFileNameSuffix)
fieldConfig := &otypes.AccessLogFields{
QueryParameters: &otypes.FieldQueryParameters{
DefaultMode: "drop",
},
}
config := &otypes.AccessLog{FilePath: logFilePath, Format: GenericCLFFormat, Fields: fieldConfig}
doLogging(t, config, false)
logData, err := os.ReadFile(logFilePath)
require.NoError(t, err)
expectedLog := ` TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent"`
assertValidGenericCLFLogData(t, expectedLog, logData)
}
@ -490,7 +526,7 @@ func TestLoggerJSON(t *testing.T) {
RequestHost: assertString(testHostname),
RequestAddr: assertString(testHostname),
RequestMethod: assertString(testMethod),
RequestPath: assertString(testPath),
RequestPath: assertString(testPathWithQueryParams),
RequestProtocol: assertString(testProto),
RequestScheme: assertString(testScheme),
RequestPort: assertString("-"),
@ -530,7 +566,7 @@ func TestLoggerJSON(t *testing.T) {
RequestHost: assertString(testHostname),
RequestAddr: assertString(testHostname),
RequestMethod: assertString(testMethod),
RequestPath: assertString(testPath),
RequestPath: assertString(testPathWithQueryParams),
RequestProtocol: assertString(testProto),
RequestScheme: assertString(testScheme),
RequestPort: assertString("-"),
@ -574,7 +610,7 @@ func TestLoggerJSON(t *testing.T) {
RequestHost: assertString(testHostname),
RequestAddr: assertString(testHostname),
RequestMethod: assertString(testMethod),
RequestPath: assertString(testPath),
RequestPath: assertString(testPathWithQueryParams),
RequestProtocol: assertString(testProto),
RequestScheme: assertString("https"),
RequestPort: assertString("-"),
@ -714,6 +750,94 @@ func TestLoggerJSON(t *testing.T) {
RequestRefererHeader: assertString(testReferer),
},
},
{
desc: "default config, drop query parameters",
config: &otypes.AccessLog{
FilePath: "",
Format: JSONFormat,
Fields: &otypes.AccessLogFields{
QueryParameters: &otypes.FieldQueryParameters{
DefaultMode: "drop",
},
},
},
expected: map[string]func(t *testing.T, value any){
RequestContentSize: assertFloat64(0),
RequestHost: assertString(testHostname),
RequestAddr: assertString(testHostname),
RequestMethod: assertString(testMethod),
RequestPath: assertString(testPath),
RequestProtocol: assertString(testProto),
RequestScheme: assertString(testScheme),
RequestPort: assertString("-"),
DownstreamStatus: assertFloat64(float64(testStatus)),
DownstreamContentSize: assertFloat64(float64(len(testContent))),
OriginContentSize: assertFloat64(float64(len(testContent))),
OriginStatus: assertFloat64(float64(testStatus)),
RequestRefererHeader: assertString(testReferer),
RequestUserAgentHeader: assertString(testUserAgent),
RouterName: assertString(testRouterName),
ServiceURL: assertString(testServiceName),
ClientUsername: assertString(testUsername),
ClientHost: assertString(testHostname),
ClientPort: assertString(strconv.Itoa(testPort)),
ClientAddr: assertString(fmt.Sprintf("%s:%d", testHostname, testPort)),
"level": assertString("info"),
"msg": assertString(""),
"downstream_Content-Type": assertString("text/plain; charset=utf-8"),
RequestCount: assertFloat64NotZero(),
Duration: assertFloat64NotZero(),
Overhead: assertFloat64NotZero(),
RetryAttempts: assertFloat64(float64(testRetryAttempts)),
"time": assertNotEmpty(),
"StartLocal": assertNotEmpty(),
"StartUTC": assertNotEmpty(),
},
},
{
desc: "default config, keep query parameters",
config: &otypes.AccessLog{
FilePath: "",
Format: JSONFormat,
Fields: &otypes.AccessLogFields{
QueryParameters: &otypes.FieldQueryParameters{
DefaultMode: "keep",
},
},
},
expected: map[string]func(t *testing.T, value any){
RequestContentSize: assertFloat64(0),
RequestHost: assertString(testHostname),
RequestAddr: assertString(testHostname),
RequestMethod: assertString(testMethod),
RequestPath: assertString(testPathWithQueryParams),
RequestProtocol: assertString(testProto),
RequestScheme: assertString(testScheme),
RequestPort: assertString("-"),
DownstreamStatus: assertFloat64(float64(testStatus)),
DownstreamContentSize: assertFloat64(float64(len(testContent))),
OriginContentSize: assertFloat64(float64(len(testContent))),
OriginStatus: assertFloat64(float64(testStatus)),
RequestRefererHeader: assertString(testReferer),
RequestUserAgentHeader: assertString(testUserAgent),
RouterName: assertString(testRouterName),
ServiceURL: assertString(testServiceName),
ClientUsername: assertString(testUsername),
ClientHost: assertString(testHostname),
ClientPort: assertString(strconv.Itoa(testPort)),
ClientAddr: assertString(fmt.Sprintf("%s:%d", testHostname, testPort)),
"level": assertString("info"),
"msg": assertString(""),
"downstream_Content-Type": assertString("text/plain; charset=utf-8"),
RequestCount: assertFloat64NotZero(),
Duration: assertFloat64NotZero(),
Overhead: assertFloat64NotZero(),
RetryAttempts: assertFloat64(float64(testRetryAttempts)),
"time": assertNotEmpty(),
"StartLocal": assertNotEmpty(),
"StartUTC": assertNotEmpty(),
},
},
}
for _, test := range testCases {
@ -815,7 +939,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
FilePath: "",
Format: CommonFormat,
},
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath?param1=test1&param2=test2 HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
},
{
desc: "default config with empty filters",
@ -824,7 +948,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
Format: CommonFormat,
Filters: &otypes.AccessLogFilters{},
},
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath?param1=test1&param2=test2 HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
},
{
desc: "Status code filter not matching",
@ -846,7 +970,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
StatusCodes: []string{"123"},
},
},
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath?param1=test1&param2=test2 HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
},
{
desc: "Duration filter not matching",
@ -868,7 +992,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
MinDuration: ptypes.Duration(1 * time.Millisecond),
},
},
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath?param1=test1&param2=test2 HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
},
{
desc: "Retry attempts filter matching",
@ -879,7 +1003,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
RetryAttempts: true,
},
},
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath?param1=test1&param2=test2 HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
},
{
desc: "Default mode keep",
@ -890,7 +1014,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
DefaultMode: "keep",
},
},
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath?param1=test1&param2=test2 HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
},
{
desc: "Default mode keep with override",
@ -904,7 +1028,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
},
},
},
expectedLog: `- - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
expectedLog: `- - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath?param1=test1&param2=test2 HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
},
{
desc: "Default mode drop",
@ -1110,8 +1234,10 @@ func doLoggingTLSOpt(t *testing.T, config *otypes.AccessLog, enableTLS, tracing
Method: testMethod,
RemoteAddr: fmt.Sprintf("%s:%d", testHostname, testPort),
URL: &url.URL{
User: url.UserPassword(testUsername, ""),
Path: testPath,
User: url.UserPassword(testUsername, ""),
Path: testPath,
RawQuery: testQueryParams,
ForceQuery: true,
},
Body: io.NopCloser(bytes.NewReader([]byte("bar"))),
}

View file

@ -92,9 +92,15 @@ type FieldHeaders struct {
// AccessLogFields holds configuration for access log fields.
type AccessLogFields struct {
DefaultMode string `description:"Default mode for fields: keep | drop" json:"defaultMode,omitempty" toml:"defaultMode,omitempty" yaml:"defaultMode,omitempty" export:"true"`
Names map[string]string `description:"Override mode for fields" json:"names,omitempty" toml:"names,omitempty" yaml:"names,omitempty" export:"true"`
Headers *FieldHeaders `description:"Headers to keep, drop or redact" json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty" export:"true"`
DefaultMode string `description:"Default mode for fields: keep | drop" json:"defaultMode,omitempty" toml:"defaultMode,omitempty" yaml:"defaultMode,omitempty" export:"true"`
Names map[string]string `description:"Override mode for fields" json:"names,omitempty" toml:"names,omitempty" yaml:"names,omitempty" export:"true"`
Headers *FieldHeaders `description:"Headers to keep, drop or redact" json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty" export:"true"`
QueryParameters *FieldQueryParameters `description:"Keep or drop all query parameters" json:"queryParameters,omitempty" toml:"queryParameters,omitempty" yaml:"queryParameters,omitempty" export:"true"`
}
// FieldQueryParameters holds configuration for access log query parameters.
type FieldQueryParameters struct {
DefaultMode string `description:"Default mode for query parameters: keep | drop" json:"defaultMode,omitempty" toml:"defaultMode,omitempty" yaml:"defaultMode,omitempty" export:"true"`
}
// SetDefaults sets the default values.
@ -103,6 +109,9 @@ func (f *AccessLogFields) SetDefaults() {
f.Headers = &FieldHeaders{
DefaultMode: AccessLogDrop,
}
f.QueryParameters = &FieldQueryParameters{
DefaultMode: AccessLogKeep,
}
}
// Keep check if the field need to be kept or dropped.
@ -131,6 +140,15 @@ func (f *AccessLogFields) KeepHeader(header string) string {
return defaultValue
}
// KeepQueryParameters checks if the query parameters need to be kept or dropped.
func (f *AccessLogFields) KeepQueryParameters() bool {
defaultKeep := true
if f == nil || f.QueryParameters == nil {
return defaultKeep
}
return checkFieldValue(f.QueryParameters.DefaultMode, defaultKeep)
}
func checkFieldValue(value string, defaultKeep bool) bool {
switch value {
case AccessLogKeep: