enable logging to stdout for access logs
This commit is contained in:
parent
f275e4ad3c
commit
885b9f371c
6 changed files with 138 additions and 93 deletions
11
docs/toml.md
11
docs/toml.md
|
@ -154,10 +154,15 @@ Supported filters:
|
||||||
|
|
||||||
## Access log definition
|
## Access log definition
|
||||||
|
|
||||||
The standard access log uses the textual Common Log Format (CLF), extended with additional fields.
|
Access logs are written when `[accessLog]` is defined.
|
||||||
Alternatively logs can be written in JSON.
|
By default it will write to stdout and produce logs in the textual Common Log Format (CLF), extended with additional fields.
|
||||||
Using the default CLF option is simple, e.g.
|
|
||||||
|
|
||||||
|
To enable access logs using the default settings just add the `[accessLog]` entry.
|
||||||
|
```toml
|
||||||
|
[accessLog]
|
||||||
|
```
|
||||||
|
|
||||||
|
To write the logs into a logfile specify the `filePath`.
|
||||||
```toml
|
```toml
|
||||||
[accessLog]
|
[accessLog]
|
||||||
filePath = "/path/to/access.log"
|
filePath = "/path/to/access.log"
|
||||||
|
|
|
@ -2,7 +2,6 @@ package accesslog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -38,19 +37,13 @@ type LogHandler struct {
|
||||||
|
|
||||||
// NewLogHandler creates a new LogHandler
|
// NewLogHandler creates a new LogHandler
|
||||||
func NewLogHandler(config *types.AccessLog) (*LogHandler, error) {
|
func NewLogHandler(config *types.AccessLog) (*LogHandler, error) {
|
||||||
if len(config.FilePath) == 0 {
|
file := os.Stdout
|
||||||
return nil, errors.New("Empty file path specified for accessLogsFile")
|
if len(config.FilePath) > 0 {
|
||||||
}
|
f, err := openAccessLogFile(config.FilePath)
|
||||||
|
|
||||||
dir := filepath.Dir(config.FilePath)
|
|
||||||
|
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create log path %s: %s", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.OpenFile(config.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error opening file: %s %s", dir, err)
|
return nil, fmt.Errorf("error opening access log file: %s", err)
|
||||||
|
}
|
||||||
|
file = f
|
||||||
}
|
}
|
||||||
|
|
||||||
var formatter logrus.Formatter
|
var formatter logrus.Formatter
|
||||||
|
@ -73,6 +66,21 @@ func NewLogHandler(config *types.AccessLog) (*LogHandler, error) {
|
||||||
return &LogHandler{logger: logger, file: file}, nil
|
return &LogHandler{logger: logger, file: file}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openAccessLogFile(filePath string) (*os.File, error) {
|
||||||
|
dir := filepath.Dir(filePath)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create log path %s: %s", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error opening file %s: %s", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetLogDataTable gets the request context object that contains logging data. This accretes
|
// GetLogDataTable gets the request context object that contains logging data. This accretes
|
||||||
// data as the request passes through the middleware chain.
|
// data as the request passes through the middleware chain.
|
||||||
func GetLogDataTable(req *http.Request) *LogData {
|
func GetLogDataTable(req *http.Request) *LogData {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
|
@ -18,9 +19,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger *LogHandler
|
|
||||||
logFileNameSuffix = "/traefik/logger/test.log"
|
logFileNameSuffix = "/traefik/logger/test.log"
|
||||||
helloWorld = "Hello, World"
|
testContent = "Hello, World"
|
||||||
testBackendName = "http://127.0.0.1/testBackend"
|
testBackendName = "http://127.0.0.1/testBackend"
|
||||||
testFrontendName = "testFrontend"
|
testFrontendName = "testFrontend"
|
||||||
testStatus = 123
|
testStatus = 123
|
||||||
|
@ -36,32 +36,27 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoggerCLF(t *testing.T) {
|
func TestLoggerCLF(t *testing.T) {
|
||||||
tmpDir, logFilePath := doLogging(t, CommonFormat)
|
tmpDir := createTempDir(t, CommonFormat)
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
|
||||||
|
config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat}
|
||||||
|
doLogging(t, config)
|
||||||
|
|
||||||
logData, err := ioutil.ReadFile(logFilePath)
|
logData, err := ioutil.ReadFile(logFilePath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
tokens, err := shellwords.Parse(string(logData))
|
assertValidLogData(t, logData)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, 14, len(tokens), printLogData(logData))
|
|
||||||
assert.Equal(t, testHostname, tokens[0], printLogData(logData))
|
|
||||||
assert.Equal(t, testUsername, tokens[2], printLogData(logData))
|
|
||||||
assert.Equal(t, fmt.Sprintf("%s %s %s", testMethod, testPath, testProto), tokens[5], printLogData(logData))
|
|
||||||
assert.Equal(t, fmt.Sprintf("%d", testStatus), tokens[6], printLogData(logData))
|
|
||||||
assert.Equal(t, fmt.Sprintf("%d", len(helloWorld)), tokens[7], printLogData(logData))
|
|
||||||
assert.Equal(t, testReferer, tokens[8], printLogData(logData))
|
|
||||||
assert.Equal(t, testUserAgent, tokens[9], printLogData(logData))
|
|
||||||
assert.Equal(t, "1", tokens[10], printLogData(logData))
|
|
||||||
assert.Equal(t, testFrontendName, tokens[11], printLogData(logData))
|
|
||||||
assert.Equal(t, testBackendName, tokens[12], printLogData(logData))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerJSON(t *testing.T) {
|
func TestLoggerJSON(t *testing.T) {
|
||||||
tmpDir, logFilePath := doLogging(t, JSONFormat)
|
tmpDir := createTempDir(t, JSONFormat)
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
|
||||||
|
config := &types.AccessLog{FilePath: logFilePath, Format: JSONFormat}
|
||||||
|
doLogging(t, config)
|
||||||
|
|
||||||
logData, err := ioutil.ReadFile(logFilePath)
|
logData, err := ioutil.ReadFile(logFilePath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -121,9 +116,9 @@ func TestLoggerJSON(t *testing.T) {
|
||||||
assertCount++
|
assertCount++
|
||||||
assert.Equal(t, fmt.Sprintf("%d ", testStatus), jsonData[DownstreamStatusLine])
|
assert.Equal(t, fmt.Sprintf("%d ", testStatus), jsonData[DownstreamStatusLine])
|
||||||
assertCount++
|
assertCount++
|
||||||
assert.Equal(t, float64(len(helloWorld)), jsonData[DownstreamContentSize])
|
assert.Equal(t, float64(len(testContent)), jsonData[DownstreamContentSize])
|
||||||
assertCount++
|
assertCount++
|
||||||
assert.Equal(t, float64(len(helloWorld)), jsonData[OriginContentSize])
|
assert.Equal(t, float64(len(testContent)), jsonData[OriginContentSize])
|
||||||
assertCount++
|
assertCount++
|
||||||
assert.Equal(t, float64(testStatus), jsonData[OriginStatus])
|
assert.Equal(t, float64(testStatus), jsonData[OriginStatus])
|
||||||
assertCount++
|
assertCount++
|
||||||
|
@ -165,6 +160,90 @@ func TestLoggerJSON(t *testing.T) {
|
||||||
assert.Equal(t, len(jsonData), assertCount, string(logData))
|
assert.Equal(t, len(jsonData), assertCount, string(logData))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewLogHandlerOutputStdout(t *testing.T) {
|
||||||
|
file, restoreStdout := captureStdout(t)
|
||||||
|
defer restoreStdout()
|
||||||
|
|
||||||
|
config := &types.AccessLog{FilePath: "", Format: CommonFormat}
|
||||||
|
doLogging(t, config)
|
||||||
|
|
||||||
|
written, err := ioutil.ReadFile(file.Name())
|
||||||
|
require.NoError(t, err, "unable to read captured stdout from file")
|
||||||
|
require.NotZero(t, len(written), "expected access log message on stdout")
|
||||||
|
assertValidLogData(t, written)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertValidLogData(t *testing.T, logData []byte) {
|
||||||
|
tokens, err := shellwords.Parse(string(logData))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
formatErrMessage := fmt.Sprintf(`
|
||||||
|
Expected: TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 1 "testFrontend" "http://127.0.0.1/testBackend" 1ms
|
||||||
|
Actual: %s
|
||||||
|
`,
|
||||||
|
string(logData))
|
||||||
|
require.Equal(t, 14, len(tokens), formatErrMessage)
|
||||||
|
assert.Equal(t, testHostname, tokens[0], formatErrMessage)
|
||||||
|
assert.Equal(t, testUsername, tokens[2], formatErrMessage)
|
||||||
|
assert.Equal(t, fmt.Sprintf("%s %s %s", testMethod, testPath, testProto), tokens[5], formatErrMessage)
|
||||||
|
assert.Equal(t, fmt.Sprintf("%d", testStatus), tokens[6], formatErrMessage)
|
||||||
|
assert.Equal(t, fmt.Sprintf("%d", len(testContent)), tokens[7], formatErrMessage)
|
||||||
|
assert.Equal(t, testReferer, tokens[8], formatErrMessage)
|
||||||
|
assert.Equal(t, testUserAgent, tokens[9], formatErrMessage)
|
||||||
|
assert.Regexp(t, regexp.MustCompile("[0-9]*"), tokens[10], formatErrMessage)
|
||||||
|
assert.Equal(t, testFrontendName, tokens[11], formatErrMessage)
|
||||||
|
assert.Equal(t, testBackendName, tokens[12], formatErrMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func captureStdout(t *testing.T) (out *os.File, restoreStdout func()) {
|
||||||
|
file, err := ioutil.TempFile("", "testlogger")
|
||||||
|
require.NoError(t, err, "failed to create temp file")
|
||||||
|
|
||||||
|
original := os.Stdout
|
||||||
|
os.Stdout = file
|
||||||
|
|
||||||
|
restoreStdout = func() {
|
||||||
|
os.Stdout = original
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, restoreStdout
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTempDir(t *testing.T, prefix string) string {
|
||||||
|
tmpDir, err := ioutil.TempDir("", prefix)
|
||||||
|
require.NoError(t, err, "failed to create temp dir")
|
||||||
|
|
||||||
|
return tmpDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func doLogging(t *testing.T, config *types.AccessLog) {
|
||||||
|
logger, err := NewLogHandler(config)
|
||||||
|
defer logger.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if config.FilePath != "" {
|
||||||
|
_, err = os.Stat(config.FilePath)
|
||||||
|
require.NoError(t, err, fmt.Sprintf("logger should create %s", config.FilePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &http.Request{
|
||||||
|
Header: map[string][]string{
|
||||||
|
"User-Agent": {testUserAgent},
|
||||||
|
"Referer": {testReferer},
|
||||||
|
},
|
||||||
|
Proto: testProto,
|
||||||
|
Host: testHostname,
|
||||||
|
Method: testMethod,
|
||||||
|
RemoteAddr: fmt.Sprintf("%s:%d", testHostname, testPort),
|
||||||
|
URL: &url.URL{
|
||||||
|
User: url.UserPassword(testUsername, ""),
|
||||||
|
Path: testPath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.ServeHTTP(httptest.NewRecorder(), req, logWriterTestHandlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
func containsKeys(t *testing.T, expectedKeys []string, data map[string]interface{}) {
|
func containsKeys(t *testing.T, expectedKeys []string, data map[string]interface{}) {
|
||||||
for key, value := range data {
|
for key, value := range data {
|
||||||
if !contains(expectedKeys, key) {
|
if !contains(expectedKeys, key) {
|
||||||
|
@ -187,54 +266,8 @@ func contains(values []string, value string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func doLogging(t *testing.T, format string) (string, string) {
|
|
||||||
tmp, err := ioutil.TempDir("", format)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create temp dir: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logFilePath := filepath.Join(tmp, logFileNameSuffix)
|
|
||||||
|
|
||||||
config := types.AccessLog{FilePath: logFilePath, Format: format}
|
|
||||||
|
|
||||||
logger, err = NewLogHandler(&config)
|
|
||||||
defer logger.Close()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
|
|
||||||
t.Fatalf("logger should create %s", logFilePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &http.Request{
|
|
||||||
Header: map[string][]string{
|
|
||||||
"User-Agent": {testUserAgent},
|
|
||||||
"Referer": {testReferer},
|
|
||||||
},
|
|
||||||
Proto: testProto,
|
|
||||||
Host: testHostname,
|
|
||||||
Method: testMethod,
|
|
||||||
RemoteAddr: fmt.Sprintf("%s:%d", testHostname, testPort),
|
|
||||||
URL: &url.URL{
|
|
||||||
User: url.UserPassword(testUsername, ""),
|
|
||||||
Path: testPath,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
|
||||||
logger.ServeHTTP(rw, req, logWriterTestHandlerFunc)
|
|
||||||
return tmp, logFilePath
|
|
||||||
}
|
|
||||||
|
|
||||||
func printLogData(logdata []byte) string {
|
|
||||||
return fmt.Sprintf(`
|
|
||||||
Expected: TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 1 "testFrontend" "http://127.0.0.1/testBackend" 1ms
|
|
||||||
Actual: %s
|
|
||||||
`,
|
|
||||||
string(logdata))
|
|
||||||
}
|
|
||||||
|
|
||||||
func logWriterTestHandlerFunc(rw http.ResponseWriter, r *http.Request) {
|
func logWriterTestHandlerFunc(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.Write([]byte(helloWorld))
|
rw.Write([]byte(testContent))
|
||||||
rw.WriteHeader(testStatus)
|
rw.WriteHeader(testStatus)
|
||||||
|
|
||||||
logDataTable := GetLogDataTable(r)
|
logDataTable := GetLogDataTable(r)
|
||||||
|
|
|
@ -45,7 +45,7 @@ type GlobalConfiguration struct {
|
||||||
CheckNewVersion bool `description:"Periodically check if a new version has been released"`
|
CheckNewVersion bool `description:"Periodically check if a new version has been released"`
|
||||||
AccessLogsFile string `description:"(Deprecated) Access logs file"` // Deprecated
|
AccessLogsFile string `description:"(Deprecated) Access logs file"` // Deprecated
|
||||||
AccessLog *types.AccessLog `description:"Access log settings"`
|
AccessLog *types.AccessLog `description:"Access log settings"`
|
||||||
TraefikLogsFile string `description:"Traefik logs file"`
|
TraefikLogsFile string `description:"Traefik logs file. Stdout is used when omitted or empty"`
|
||||||
LogLevel string `short:"l" description:"Log level"`
|
LogLevel string `short:"l" description:"Log level"`
|
||||||
EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key;prod/traefik.crt,prod/traefik.key'"`
|
EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key;prod/traefik.crt,prod/traefik.key'"`
|
||||||
Cluster *types.Cluster `description:"Enable clustering"`
|
Cluster *types.Cluster `description:"Enable clustering"`
|
||||||
|
|
|
@ -220,21 +220,21 @@
|
||||||
# main = "local4.com"
|
# main = "local4.com"
|
||||||
|
|
||||||
|
|
||||||
# Set access log options
|
# Enable access logs
|
||||||
|
# By default it will write to stdout and produce logs in the textual
|
||||||
|
# Common Log Format (CLF), extended with additional fields.
|
||||||
#
|
#
|
||||||
# Optional
|
# Optional
|
||||||
#
|
#
|
||||||
# [accessLog]
|
# [accessLog]
|
||||||
|
|
||||||
# Sets the file path for the access log. If none is given (the default)
|
# Sets the file path for the access log. If not specified, stdout will be used.
|
||||||
# no access logs are produced. Intermediate directories are created if
|
# Intermediate directories are created if necessary.
|
||||||
# necessary.
|
|
||||||
#
|
#
|
||||||
# Optional
|
# Optional
|
||||||
# Default: ""
|
# Default: os.Stdout
|
||||||
#
|
#
|
||||||
# filePath = "/path/to/log/log.txt"
|
# filePath = "/path/to/log/log.txt"
|
||||||
#
|
|
||||||
|
|
||||||
# Format is either "json" or "common".
|
# Format is either "json" or "common".
|
||||||
#
|
#
|
||||||
|
@ -242,7 +242,6 @@
|
||||||
# Default: "common"
|
# Default: "common"
|
||||||
#
|
#
|
||||||
# format = "common"
|
# format = "common"
|
||||||
#
|
|
||||||
|
|
||||||
# Entrypoints definition
|
# Entrypoints definition
|
||||||
#
|
#
|
||||||
|
|
|
@ -360,6 +360,6 @@ func (b *Buckets) SetValue(val interface{}) {
|
||||||
|
|
||||||
// AccessLog holds the configuration settings for the access logger (middlewares/accesslog).
|
// AccessLog holds the configuration settings for the access logger (middlewares/accesslog).
|
||||||
type AccessLog struct {
|
type AccessLog struct {
|
||||||
FilePath string `json:"file,omitempty" description:"Access log file path"`
|
FilePath string `json:"file,omitempty" description:"Access log file path. Stdout is used when omitted or empty"`
|
||||||
Format string `json:"format,omitempty" description:"Access log format: json | common"`
|
Format string `json:"format,omitempty" description:"Access log format: json | common"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue