1
0
Fork 0

Ultimate Access log filter

This commit is contained in:
Michael 2018-03-14 14:12:04 +01:00 committed by Traefiker Bot
parent f99363674b
commit 8d468925d3
24 changed files with 1722 additions and 683 deletions

185
types/logs.go Normal file
View file

@ -0,0 +1,185 @@
package types
import (
"fmt"
"strings"
)
const (
// AccessLogKeep is the keep string value
AccessLogKeep = "keep"
// AccessLogDrop is the drop string value
AccessLogDrop = "drop"
// AccessLogRedact is the redact string value
AccessLogRedact = "redact"
)
// TraefikLog holds the configuration settings for the traefik logger.
type TraefikLog struct {
FilePath string `json:"file,omitempty" description:"Traefik log file path. Stdout is used when omitted or empty"`
Format string `json:"format,omitempty" description:"Traefik log format: json | common"`
}
// AccessLog holds the configuration settings for the access logger (middlewares/accesslog).
type AccessLog struct {
FilePath string `json:"file,omitempty" description:"Access log file path. Stdout is used when omitted or empty" export:"true"`
Format string `json:"format,omitempty" description:"Access log format: json | common" export:"true"`
Filters *AccessLogFilters `json:"filters,omitempty" description:"Access log filters, used to keep only specific access logs" export:"true"`
Fields *AccessLogFields `json:"fields,omitempty" description:"AccessLogFields" export:"true"`
}
// StatusCodes holds status codes ranges to filter access log
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"`
}
// FieldNames holds maps of fields with specific mode
type FieldNames map[string]string
// AccessLogFields holds configuration for access log fields
type AccessLogFields struct {
DefaultMode string `json:"defaultMode,omitempty" description:"Default mode for fields: keep | drop" export:"true"`
Names FieldNames `json:"names,omitempty" description:"Override mode for fields" export:"true"`
Headers *FieldHeaders `json:"headers,omitempty" description:"Headers to keep, drop or redact" export:"true"`
}
// FieldHeaderNames holds maps of fields with specific mode
type FieldHeaderNames map[string]string
// FieldHeaders holds configuration for access log headers
type FieldHeaders struct {
DefaultMode string `json:"defaultMode,omitempty" description:"Default mode for fields: keep | drop | redact" export:"true"`
Names FieldHeaderNames `json:"names,omitempty" description:"Override mode for headers" export:"true"`
}
// Set adds strings elem into the the parser
// it splits str on , and ;
func (s *StatusCodes) Set(str string) error {
fargs := func(c rune) bool {
return c == ',' || c == ';'
}
// get function
slice := strings.FieldsFunc(str, fargs)
*s = append(*s, slice...)
return nil
}
// Get StatusCodes
func (s *StatusCodes) Get() interface{} { return *s }
// String return slice in a string
func (s *StatusCodes) String() string { return fmt.Sprintf("%v", *s) }
// SetValue sets StatusCodes into the parser
func (s *StatusCodes) SetValue(val interface{}) {
*s = val.(StatusCodes)
}
// String is the method to format the flag's value, part of the flag.Value interface.
// The String method's output will be used in diagnostics.
func (f *FieldNames) String() string {
return fmt.Sprintf("%+v", *f)
}
// Get return the FieldNames map
func (f *FieldNames) Get() interface{} {
return *f
}
// Set is the method to set the flag value, part of the flag.Value interface.
// Set's argument is a string to be parsed to set the flag.
// It's a space-separated list, so we split it.
func (f *FieldNames) Set(value string) error {
fields := strings.Fields(value)
for _, field := range fields {
n := strings.SplitN(field, "=", 2)
if len(n) == 2 {
(*f)[n[0]] = n[1]
}
}
return nil
}
// SetValue sets the FieldNames map with val
func (f *FieldNames) SetValue(val interface{}) {
*f = val.(FieldNames)
}
// String is the method to format the flag's value, part of the flag.Value interface.
// The String method's output will be used in diagnostics.
func (f *FieldHeaderNames) String() string {
return fmt.Sprintf("%+v", *f)
}
// Get return the FieldHeaderNames map
func (f *FieldHeaderNames) Get() interface{} {
return *f
}
// Set is the method to set the flag value, part of the flag.Value interface.
// Set's argument is a string to be parsed to set the flag.
// It's a space-separated list, so we split it.
func (f *FieldHeaderNames) Set(value string) error {
fields := strings.Fields(value)
for _, field := range fields {
n := strings.SplitN(field, "=", 2)
(*f)[n[0]] = n[1]
}
return nil
}
// SetValue sets the FieldHeaderNames map with val
func (f *FieldHeaderNames) SetValue(val interface{}) {
*f = val.(FieldHeaderNames)
}
// Keep check if the field need to be kept or dropped
func (f *AccessLogFields) Keep(field string) bool {
defaultKeep := true
if f != nil {
defaultKeep = checkFieldValue(f.DefaultMode, defaultKeep)
if v, ok := f.Names[field]; ok {
return checkFieldValue(v, defaultKeep)
}
}
return defaultKeep
}
func checkFieldValue(value string, defaultKeep bool) bool {
switch value {
case AccessLogKeep:
return true
case AccessLogDrop:
return false
default:
return defaultKeep
}
}
// KeepHeader checks if the headers need to be kept, dropped or redacted and returns the status
func (f *AccessLogFields) KeepHeader(header string) string {
defaultValue := AccessLogKeep
if f != nil && f.Headers != nil {
defaultValue = checkFieldHeaderValue(f.Headers.DefaultMode, defaultValue)
if v, ok := f.Headers.Names[header]; ok {
return checkFieldHeaderValue(v, defaultValue)
}
}
return defaultValue
}
func checkFieldHeaderValue(value string, defaultValue string) string {
if value == AccessLogKeep || value == AccessLogDrop || value == AccessLogRedact {
return value
}
return defaultValue
}

411
types/logs_test.go Normal file
View file

@ -0,0 +1,411 @@
package types
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestStatusCodesSet(t *testing.T) {
testCases := []struct {
desc string
value string
expected StatusCodes
}{
{
desc: "One value should return StatusCodes of size 1",
value: "200",
expected: StatusCodes{"200"},
},
{
desc: "Two values separated by comma should return StatusCodes of size 2",
value: "200,400",
expected: StatusCodes{"200", "400"},
},
{
desc: "Two values separated by semicolon should return StatusCodes of size 2",
value: "200;400",
expected: StatusCodes{"200", "400"},
},
{
desc: "Three values separated by comma and semicolon should return StatusCodes of size 3",
value: "200,400;500",
expected: StatusCodes{"200", "400", "500"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var statusCodes StatusCodes
err := statusCodes.Set(test.value)
assert.Nil(t, err)
assert.Equal(t, test.expected, statusCodes)
})
}
}
func TestStatusCodesGet(t *testing.T) {
testCases := []struct {
desc string
values StatusCodes
expected StatusCodes
}{
{
desc: "Should return 1 value",
values: StatusCodes{"200"},
expected: StatusCodes{"200"},
},
{
desc: "Should return 2 values",
values: StatusCodes{"200", "400"},
expected: StatusCodes{"200", "400"},
},
{
desc: "Should return 3 values",
values: StatusCodes{"200", "400", "500"},
expected: StatusCodes{"200", "400", "500"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := test.values.Get()
assert.Equal(t, test.expected, actual)
})
}
}
func TestStatusCodesString(t *testing.T) {
testCases := []struct {
desc string
values StatusCodes
expected string
}{
{
desc: "Should return 1 value",
values: StatusCodes{"200"},
expected: "[200]",
},
{
desc: "Should return 2 values",
values: StatusCodes{"200", "400"},
expected: "[200 400]",
},
{
desc: "Should return 3 values",
values: StatusCodes{"200", "400", "500"},
expected: "[200 400 500]",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := test.values.String()
assert.Equal(t, test.expected, actual)
})
}
}
func TestStatusCodesSetValue(t *testing.T) {
testCases := []struct {
desc string
values StatusCodes
expected StatusCodes
}{
{
desc: "Should return 1 value",
values: StatusCodes{"200"},
expected: StatusCodes{"200"},
},
{
desc: "Should return 2 values",
values: StatusCodes{"200", "400"},
expected: StatusCodes{"200", "400"},
},
{
desc: "Should return 3 values",
values: StatusCodes{"200", "400", "500"},
expected: StatusCodes{"200", "400", "500"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var slice StatusCodes
slice.SetValue(test.values)
assert.Equal(t, test.expected, slice)
})
}
}
func TestFieldsNamesSet(t *testing.T) {
testCases := []struct {
desc string
value string
expected *FieldNames
}{
{
desc: "One value should return FieldNames of size 1",
value: "field-1=foo",
expected: &FieldNames{
"field-1": "foo",
},
},
{
desc: "Two values separated by space should return FieldNames of size 2",
value: "field-1=foo field-2=bar",
expected: &FieldNames{
"field-1": "foo",
"field-2": "bar",
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
fieldsNames := &FieldNames{}
err := fieldsNames.Set(test.value)
assert.NoError(t, err)
assert.Equal(t, test.expected, fieldsNames)
})
}
}
func TestFieldsNamesGet(t *testing.T) {
testCases := []struct {
desc string
values FieldNames
expected FieldNames
}{
{
desc: "Should return 1 value",
values: FieldNames{"field-1": "foo"},
expected: FieldNames{"field-1": "foo"},
},
{
desc: "Should return 2 values",
values: FieldNames{"field-1": "foo", "field-2": "bar"},
expected: FieldNames{"field-1": "foo", "field-2": "bar"},
},
{
desc: "Should return 3 values",
values: FieldNames{"field-1": "foo", "field-2": "bar", "field-3": "powpow"},
expected: FieldNames{"field-1": "foo", "field-2": "bar", "field-3": "powpow"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := test.values.Get()
assert.Equal(t, test.expected, actual)
})
}
}
func TestFieldsNamesString(t *testing.T) {
testCases := []struct {
desc string
values FieldNames
expected string
}{
{
desc: "Should return 1 value",
values: FieldNames{"field-1": "foo"},
expected: "map[field-1:foo]",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := test.values.String()
assert.Equal(t, test.expected, actual)
})
}
}
func TestFieldsNamesSetValue(t *testing.T) {
testCases := []struct {
desc string
values FieldNames
expected *FieldNames
}{
{
desc: "Should return 1 value",
values: FieldNames{"field-1": "foo"},
expected: &FieldNames{"field-1": "foo"},
},
{
desc: "Should return 2 values",
values: FieldNames{"field-1": "foo", "field-2": "bar"},
expected: &FieldNames{"field-1": "foo", "field-2": "bar"},
},
{
desc: "Should return 3 values",
values: FieldNames{"field-1": "foo", "field-2": "bar", "field-3": "powpow"},
expected: &FieldNames{"field-1": "foo", "field-2": "bar", "field-3": "powpow"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
fieldsNames := &FieldNames{}
fieldsNames.SetValue(test.values)
assert.Equal(t, test.expected, fieldsNames)
})
}
}
func TestFieldsHeadersNamesSet(t *testing.T) {
testCases := []struct {
desc string
value string
expected *FieldHeaderNames
}{
{
desc: "One value should return FieldNames of size 1",
value: "X-HEADER-1=foo",
expected: &FieldHeaderNames{
"X-HEADER-1": "foo",
},
},
{
desc: "Two values separated by space should return FieldNames of size 2",
value: "X-HEADER-1=foo X-HEADER-2=bar",
expected: &FieldHeaderNames{
"X-HEADER-1": "foo",
"X-HEADER-2": "bar",
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
headersNames := &FieldHeaderNames{}
err := headersNames.Set(test.value)
assert.NoError(t, err)
assert.Equal(t, test.expected, headersNames)
})
}
}
func TestFieldsHeadersNamesGet(t *testing.T) {
testCases := []struct {
desc string
values FieldHeaderNames
expected FieldHeaderNames
}{
{
desc: "Should return 1 value",
values: FieldHeaderNames{"X-HEADER-1": "foo"},
expected: FieldHeaderNames{"X-HEADER-1": "foo"},
},
{
desc: "Should return 2 values",
values: FieldHeaderNames{"X-HEADER-1": "foo", "X-HEADER-2": "bar"},
expected: FieldHeaderNames{"X-HEADER-1": "foo", "X-HEADER-2": "bar"},
},
{
desc: "Should return 3 values",
values: FieldHeaderNames{"X-HEADER-1": "foo", "X-HEADER-2": "bar", "X-HEADER-3": "powpow"},
expected: FieldHeaderNames{"X-HEADER-1": "foo", "X-HEADER-2": "bar", "X-HEADER-3": "powpow"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := test.values.Get()
assert.Equal(t, test.expected, actual)
})
}
}
func TestFieldsHeadersNamesString(t *testing.T) {
testCases := []struct {
desc string
values FieldHeaderNames
expected string
}{
{
desc: "Should return 1 value",
values: FieldHeaderNames{"X-HEADER-1": "foo"},
expected: "map[X-HEADER-1:foo]",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := test.values.String()
assert.Equal(t, test.expected, actual)
})
}
}
func TestFieldsHeadersNamesSetValue(t *testing.T) {
testCases := []struct {
desc string
values FieldHeaderNames
expected *FieldHeaderNames
}{
{
desc: "Should return 1 value",
values: FieldHeaderNames{"X-HEADER-1": "foo"},
expected: &FieldHeaderNames{"X-HEADER-1": "foo"},
},
{
desc: "Should return 2 values",
values: FieldHeaderNames{"X-HEADER-1": "foo", "X-HEADER-2": "bar"},
expected: &FieldHeaderNames{"X-HEADER-1": "foo", "X-HEADER-2": "bar"},
},
{
desc: "Should return 3 values",
values: FieldHeaderNames{"X-HEADER-1": "foo", "X-HEADER-2": "bar", "X-HEADER-3": "powpow"},
expected: &FieldHeaderNames{"X-HEADER-1": "foo", "X-HEADER-2": "bar", "X-HEADER-3": "powpow"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
headersNames := &FieldHeaderNames{}
headersNames.SetValue(test.values)
assert.Equal(t, test.expected, headersNames)
})
}
}

View file

@ -461,18 +461,6 @@ func (b *Buckets) SetValue(val interface{}) {
*b = val.(Buckets)
}
// TraefikLog holds the configuration settings for the traefik logger.
type TraefikLog struct {
FilePath string `json:"file,omitempty" description:"Traefik log file path. Stdout is used when omitted or empty"`
Format string `json:"format,omitempty" description:"Traefik log format: json | common"`
}
// AccessLog holds the configuration settings for the access logger (middlewares/accesslog).
type AccessLog struct {
FilePath string `json:"file,omitempty" description:"Access log file path. Stdout is used when omitted or empty" export:"true"`
Format string `json:"format,omitempty" description:"Access log format: json | common" export:"true"`
}
// ClientTLS holds TLS specific configurations as client
// CA, Cert and Key can be either path or file contents
type ClientTLS struct {
@ -497,7 +485,7 @@ func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) {
if _, errCA := os.Stat(clientTLS.CA); errCA == nil {
ca, err = ioutil.ReadFile(clientTLS.CA)
if err != nil {
return nil, fmt.Errorf("Failed to read CA. %s", err)
return nil, fmt.Errorf("failed to read CA. %s", err)
}
} else {
ca = []byte(clientTLS.CA)
@ -522,7 +510,7 @@ func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) {
if errKeyIsFile == nil {
cert, err = tls.LoadX509KeyPair(clientTLS.Cert, clientTLS.Key)
if err != nil {
return nil, fmt.Errorf("Failed to load TLS keypair: %v", err)
return nil, fmt.Errorf("failed to load TLS keypair: %v", err)
}
} else {
return nil, fmt.Errorf("tls cert is a file, but tls key is not")
@ -531,11 +519,11 @@ func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) {
if errKeyIsFile != nil {
cert, err = tls.X509KeyPair([]byte(clientTLS.Cert), []byte(clientTLS.Key))
if err != nil {
return nil, fmt.Errorf("Failed to load TLS keypair: %v", err)
return nil, fmt.Errorf("failed to load TLS keypair: %v", err)
}
} else {
return nil, fmt.Errorf("tls key is a file, but tls cert is not")
return nil, fmt.Errorf("TLS key is a file, but tls cert is not")
}
}
}
@ -548,3 +536,30 @@ func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) {
}
return TLSConfig, nil
}
// HTTPCodeRanges holds HTTP code ranges
type HTTPCodeRanges [][2]int
// NewHTTPCodeRanges create a new NewHTTPCodeRanges 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) {
var blocks HTTPCodeRanges
for _, block := range strBlocks {
codes := strings.Split(block, "-")
//if only a single HTTP code was configured, assume the best and create the correct configuration on the user's behalf
if len(codes) == 1 {
codes = append(codes, codes[0])
}
lowCode, err := strconv.Atoi(codes[0])
if err != nil {
return nil, err
}
highCode, err := strconv.Atoi(codes[1])
if err != nil {
return nil, err
}
blocks = append(blocks, [2]int{lowCode, highCode})
}
return blocks, nil
}

View file

@ -35,3 +35,61 @@ func TestHeaders_ShouldReturnTrueWhenHasSecureHeadersDefined(t *testing.T) {
assert.True(t, headers.HasSecureHeadersDefined())
}
func TestNewHTTPCodeRanges(t *testing.T) {
testCases := []struct {
desc string
strBlocks []string
expected HTTPCodeRanges
errExpected bool
}{
{
desc: "Should return 2 code range",
strBlocks: []string{
"200-500",
"502",
},
expected: HTTPCodeRanges{[2]int{200, 500}, [2]int{502, 502}},
errExpected: false,
},
{
desc: "Should return 2 code range",
strBlocks: []string{
"200-500",
"205",
},
expected: HTTPCodeRanges{[2]int{200, 500}, [2]int{205, 205}},
errExpected: false,
},
{
desc: "invalid code range",
strBlocks: []string{
"200-500",
"aaa",
},
expected: nil,
errExpected: true,
},
{
desc: "invalid code range nil",
strBlocks: nil,
expected: nil,
errExpected: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual, err := NewHTTPCodeRanges(test.strBlocks)
assert.Equal(t, test.expected, actual)
if test.errExpected {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}