Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add access log filter for retry attempts #3042

Merged
merged 4 commits into from
Mar 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 11 additions & 2 deletions docs/configuration/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ logLevel = "INFO"

[accessLog.filters]
statusCodes = ["200", "300-302"]
retryAttempts = true

[accessLog.fields]
defaultMode = "keep"
Expand Down Expand Up @@ -44,6 +45,7 @@ For more information about the CLI, see the documentation about [Traefik command
--accessLog.filePath="/path/to/access.log"
--accessLog.format="json"
--accessLog.filters.statusCodes="200,300-302"
--accessLog.filters.retryAttempts="true"
--accessLog.fields.defaultMode="keep"
--accessLog.fields.names="Username=drop Hostname=drop"
--accessLog.fields.headers.defaultMode="keep"
Expand Down Expand Up @@ -122,20 +124,27 @@ filePath = "/path/to/access.log"
format = "json"
```

To filter logs by status code:
To filter logs you can specify a set of filters which are logically "OR-connected". Thus, specifying multiple filters will keep more access logs than specifying only one:
```toml
[accessLog]
filePath = "/path/to/access.log"
format = "json"

[accessLog.filters]

# statusCodes keep only access logs with status codes in the specified range
# statusCodes keep access logs with status codes in the specified range
#
# Optional
# Default: []
#
statusCodes = ["200", "300-302"]

# retryAttempts keep access logs when at least one retry happened
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a specify that this option will keep keep access logs when at least one retry happened even if the status code is not in the statusCodes ranges.
Thus users can understand that filters are OR-connected 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmengin I added now a sentence at the beginning of the segment, so that we don't have to repeat that sentence on every filter we have :D

Can you have a look and let me know whether it would have helped you?

#
# Optional
# Default: false
#
retryAttempts = true
```

To customize logs format:
Expand Down
52 changes: 28 additions & 24 deletions middlewares/accesslog/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@ const (

// LogHandler will write each request and its response to the access log.
type LogHandler struct {
config *types.AccessLog
logger *logrus.Logger
file *os.File
filePath string
mu sync.Mutex
httpCodeRanges types.HTTPCodeRanges
fields *types.AccessLogFields
}

// NewLogHandler creates a new LogHandler
Expand Down Expand Up @@ -71,17 +70,15 @@ func NewLogHandler(config *types.AccessLog) (*LogHandler, error) {
}

logHandler := &LogHandler{
logger: logger,
file: file,
filePath: config.FilePath,
fields: config.Fields,
config: config,
logger: logger,
file: file,
}

if config.Filters != nil {
httpCodeRanges, err := types.NewHTTPCodeRanges(config.Filters.StatusCodes)
if err != nil {
if httpCodeRanges, err := types.NewHTTPCodeRanges(config.Filters.StatusCodes); err != nil {
log.Errorf("Failed to create new HTTP code ranges: %s", err)
} else if httpCodeRanges != nil {
} else {
logHandler.httpCodeRanges = httpCodeRanges
}
}
Expand Down Expand Up @@ -178,7 +175,7 @@ func (l *LogHandler) Rotate() error {
}(l.file)
}

l.file, err = os.OpenFile(l.filePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664)
l.file, err = os.OpenFile(l.config.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664)
if err != nil {
return err
}
Expand Down Expand Up @@ -210,16 +207,19 @@ func usernameIfPresent(theURL *url.URL) string {
func (l *LogHandler) logTheRoundTrip(logDataTable *LogData, crr *captureRequestReader, crw *captureResponseWriter) {
core := logDataTable.Core

if core[RetryAttempts] == nil {
core[RetryAttempts] = 0
retryAttempts, ok := core[RetryAttempts].(int)
if !ok {
retryAttempts = 0
}
core[RetryAttempts] = retryAttempts

if crr != nil {
core[RequestContentSize] = crr.count
}

core[DownstreamStatus] = crw.Status()

if l.keepAccessLog(crw.Status()) {
if l.keepAccessLog(crw.Status(), retryAttempts) {
core[DownstreamStatusLine] = fmt.Sprintf("%03d %s", crw.Status(), http.StatusText(crw.Status()))
core[DownstreamContentSize] = crw.Size()
if original, ok := core[OriginContentSize]; ok {
Expand All @@ -240,7 +240,7 @@ func (l *LogHandler) logTheRoundTrip(logDataTable *LogData, crr *captureRequestR
fields := logrus.Fields{}

for k, v := range logDataTable.Core {
if l.fields.Keep(k) {
if l.config.Fields.Keep(k) {
fields[k] = v
}
}
Expand All @@ -257,7 +257,7 @@ func (l *LogHandler) logTheRoundTrip(logDataTable *LogData, crr *captureRequestR

func (l *LogHandler) redactHeaders(headers http.Header, fields logrus.Fields, prefix string) {
for k := range headers {
v := l.fields.KeepHeader(k)
v := l.config.Fields.KeepHeader(k)
if v == types.AccessLogKeep {
fields[prefix+k] = headers.Get(k)
} else if v == types.AccessLogRedact {
Expand All @@ -266,17 +266,21 @@ func (l *LogHandler) redactHeaders(headers http.Header, fields logrus.Fields, pr
}
}

func (l *LogHandler) keepAccessLog(status int) bool {
if l.httpCodeRanges == nil {
func (l *LogHandler) keepAccessLog(statusCode, retryAttempts int) bool {
switch {
case l.config.Filters == nil:
// no filters were specified
return true
case len(l.httpCodeRanges) == 0 && l.config.Filters.RetryAttempts == false:
// empty filters were specified, e.g. by passing --accessLog.filters only (without other filter options)
return true
case l.httpCodeRanges.Contains(statusCode):
return true
case l.config.Filters.RetryAttempts == true && retryAttempts > 0:
return true
default:
return false
}

for _, block := range l.httpCodeRanges {
if status >= block[0] && status <= block[1] {
return true
}
}
return false
}

//-------------------------------------------------------------------------------------------------
Expand Down
20 changes: 20 additions & 0 deletions middlewares/accesslog/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,15 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
},
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testFrontend" "http://127.0.0.1/testBackend" 1ms`,
},
{
desc: "default config with empty filters",
config: &types.AccessLog{
FilePath: "",
Format: CommonFormat,
Filters: &types.AccessLogFilters{},
},
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testFrontend" "http://127.0.0.1/testBackend" 1ms`,
},
{
desc: "Status code filter not matching",
config: &types.AccessLog{
Expand All @@ -357,6 +366,17 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
},
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testFrontend" "http://127.0.0.1/testBackend" 1ms`,
},
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I add this test case

{
	desc: "Default config",
	config: &types.AccessLog{
		FilePath: "",
		Format:   CommonFormat,
		Filters:  &types.AccessLogFilters{},
	},
	expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testFrontend" "http://127.0.0.1/testBackend" 1ms`,
},

The default response has changed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is right. Now when you specify Filters but nothing inside it will remove everything. Isn't that the kind of logical behavior? The filters are OR-connected and when you specify multiple (status codes and retry attempts) than the result is more than when you specify only one of those. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your response @marco-jantke
When we specify nothing in Filters, we need to keep all access logs. And when something is specified in Filters we need to keep only access logs matching at least one filter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As said before I think that's not logical. And "optimizing" for a use-case where someone would specify an empty filters config doesn't make too much sense. However, I implemented it now as you requested in 6ca142b. PTAL and let me know what you think. I didn't see a nicer option for detecting an empty configuration except validating each config value one by one to check whether it's the zero value. Note that with this approach we have to extend this method each time a new option gets added. I'm open to improvement ideas.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your correction @marco-jantke

I am pretty agree with you but I think that it is not the aim of this PR to change the default behavior. To me you have choose the best option to detect an empty configuration.

We should maybe do an other PR to improve this method to avoid to extend it every time we add a new option

desc: "Retry attempts filter matching",
config: &types.AccessLog{
FilePath: "",
Format: CommonFormat,
Filters: &types.AccessLogFilters{
RetryAttempts: true,
},
},
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testFrontend" "http://127.0.0.1/testBackend" 1ms`,
},
{
desc: "Default mode keep",
config: &types.AccessLog{
Expand Down
3 changes: 2 additions & 1 deletion types/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ type StatusCodes []string

// AccessLogFilters holds filters configuration
type AccessLogFilters struct {
StatusCodes StatusCodes `json:"statusCodes,omitempty" description:"Keep only specific ranges of HTTP Status codes" export:"true"`
StatusCodes StatusCodes `json:"statusCodes,omitempty" description:"Keep access logs with status codes in the specified range" export:"true"`
RetryAttempts bool `json:"retryAttempts,omitempty" description:"Keep access logs when at least one retry happened" export:"true"`
}

// FieldNames holds maps of fields with specific mode
Expand Down
13 changes: 12 additions & 1 deletion types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) {
// HTTPCodeRanges holds HTTP code ranges
type HTTPCodeRanges [][2]int

// NewHTTPCodeRanges create a new NewHTTPCodeRanges from a given []string].
// NewHTTPCodeRanges creates HTTPCodeRanges from a given []string.
// Break out the http status code ranges into a low int and high int
// for ease of use at runtime
func NewHTTPCodeRanges(strBlocks []string) (HTTPCodeRanges, error) {
Expand All @@ -563,3 +563,14 @@ func NewHTTPCodeRanges(strBlocks []string) (HTTPCodeRanges, error) {
}
return blocks, nil
}

// Contains tests whether the passed status code is within
// one of its HTTP code ranges.
func (h HTTPCodeRanges) Contains(statusCode int) bool {
for _, block := range h {
if statusCode >= block[0] && statusCode <= block[1] {
return true
}
}
return false
}
43 changes: 43 additions & 0 deletions types/types_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package types

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -93,3 +94,45 @@ func TestNewHTTPCodeRanges(t *testing.T) {
})
}
}

func TestHTTPCodeRanges_Contains(t *testing.T) {
testCases := []struct {
strBlocks []string
statusCode int
contains bool
}{
{
strBlocks: []string{"200-299"},
statusCode: 200,
contains: true,
},
{
strBlocks: []string{"200"},
statusCode: 200,
contains: true,
},
{
strBlocks: []string{"201"},
statusCode: 200,
contains: false,
},
{
strBlocks: []string{"200-299", "500-599"},
statusCode: 400,
contains: false,
},
}

for _, test := range testCases {
test := test
testName := fmt.Sprintf("%q contains %d", test.strBlocks, test.statusCode)
t.Run(testName, func(t *testing.T) {
t.Parallel()

httpCodeRanges, err := NewHTTPCodeRanges(test.strBlocks)
assert.NoError(t, err)

assert.Equal(t, test.contains, httpCodeRanges.Contains(test.statusCode))
})
}
}