diff --git a/docs/toml.md b/docs/toml.md index aa58f7713..0df944620 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -154,10 +154,15 @@ Supported filters: ## Access log definition -The standard access log uses the textual Common Log Format (CLF), extended with additional fields. -Alternatively logs can be written in JSON. -Using the default CLF option is simple, e.g. +Access logs are written when `[accessLog]` is defined. +By default it will write to stdout and produce logs in the textual Common Log Format (CLF), extended with additional fields. +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 [accessLog] filePath = "/path/to/access.log" diff --git a/middlewares/accesslog/logger.go b/middlewares/accesslog/logger.go index 2ba48a947..84f45300b 100644 --- a/middlewares/accesslog/logger.go +++ b/middlewares/accesslog/logger.go @@ -2,7 +2,6 @@ package accesslog import ( "context" - "errors" "fmt" "net" "net/http" @@ -38,19 +37,13 @@ type LogHandler struct { // NewLogHandler creates a new LogHandler func NewLogHandler(config *types.AccessLog) (*LogHandler, error) { - if len(config.FilePath) == 0 { - return nil, errors.New("Empty file path specified for accessLogsFile") - } - - 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 { - return nil, fmt.Errorf("error opening file: %s %s", dir, err) + file := os.Stdout + if len(config.FilePath) > 0 { + f, err := openAccessLogFile(config.FilePath) + if err != nil { + return nil, fmt.Errorf("error opening access log file: %s", err) + } + file = f } var formatter logrus.Formatter @@ -73,6 +66,21 @@ func NewLogHandler(config *types.AccessLog) (*LogHandler, error) { 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 // data as the request passes through the middleware chain. func GetLogDataTable(req *http.Request) *LogData { diff --git a/middlewares/accesslog/logger_test.go b/middlewares/accesslog/logger_test.go index e470f24b2..ff125583b 100644 --- a/middlewares/accesslog/logger_test.go +++ b/middlewares/accesslog/logger_test.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "path/filepath" + "regexp" "testing" "github.com/containous/traefik/types" @@ -18,9 +19,8 @@ import ( ) var ( - logger *LogHandler logFileNameSuffix = "/traefik/logger/test.log" - helloWorld = "Hello, World" + testContent = "Hello, World" testBackendName = "http://127.0.0.1/testBackend" testFrontendName = "testFrontend" testStatus = 123 @@ -36,32 +36,27 @@ var ( ) func TestLoggerCLF(t *testing.T) { - tmpDir, logFilePath := doLogging(t, CommonFormat) + tmpDir := createTempDir(t, CommonFormat) defer os.RemoveAll(tmpDir) + logFilePath := filepath.Join(tmpDir, logFileNameSuffix) + config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat} + doLogging(t, config) + logData, err := ioutil.ReadFile(logFilePath) require.NoError(t, err) - tokens, err := shellwords.Parse(string(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)) + assertValidLogData(t, logData) } func TestLoggerJSON(t *testing.T) { - tmpDir, logFilePath := doLogging(t, JSONFormat) + tmpDir := createTempDir(t, JSONFormat) defer os.RemoveAll(tmpDir) + logFilePath := filepath.Join(tmpDir, logFileNameSuffix) + config := &types.AccessLog{FilePath: logFilePath, Format: JSONFormat} + doLogging(t, config) + logData, err := ioutil.ReadFile(logFilePath) require.NoError(t, err) @@ -121,9 +116,9 @@ func TestLoggerJSON(t *testing.T) { assertCount++ assert.Equal(t, fmt.Sprintf("%d ", testStatus), jsonData[DownstreamStatusLine]) assertCount++ - assert.Equal(t, float64(len(helloWorld)), jsonData[DownstreamContentSize]) + assert.Equal(t, float64(len(testContent)), jsonData[DownstreamContentSize]) assertCount++ - assert.Equal(t, float64(len(helloWorld)), jsonData[OriginContentSize]) + assert.Equal(t, float64(len(testContent)), jsonData[OriginContentSize]) assertCount++ assert.Equal(t, float64(testStatus), jsonData[OriginStatus]) assertCount++ @@ -165,6 +160,90 @@ func TestLoggerJSON(t *testing.T) { 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{}) { for key, value := range data { if !contains(expectedKeys, key) { @@ -187,54 +266,8 @@ func contains(values []string, value string) bool { 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) { - rw.Write([]byte(helloWorld)) + rw.Write([]byte(testContent)) rw.WriteHeader(testStatus) logDataTable := GetLogDataTable(r) diff --git a/server/configuration.go b/server/configuration.go index e4926e3e4..a3cb33289 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -45,7 +45,7 @@ type GlobalConfiguration struct { CheckNewVersion bool `description:"Periodically check if a new version has been released"` AccessLogsFile string `description:"(Deprecated) Access logs file"` // Deprecated 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"` 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"` diff --git a/traefik.sample.toml b/traefik.sample.toml index 029bcf006..9dff28a06 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -220,21 +220,21 @@ # 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 # # [accessLog] -# Sets the file path for the access log. If none is given (the default) -# no access logs are produced. Intermediate directories are created if -# necessary. +# Sets the file path for the access log. If not specified, stdout will be used. +# Intermediate directories are created if necessary. # # Optional -# Default: "" +# Default: os.Stdout # # filePath = "/path/to/log/log.txt" -# # Format is either "json" or "common". # @@ -242,7 +242,6 @@ # Default: "common" # # format = "common" -# # Entrypoints definition # diff --git a/types/types.go b/types/types.go index bee7c89c1..fe2ba7c6e 100644 --- a/types/types.go +++ b/types/types.go @@ -360,6 +360,6 @@ func (b *Buckets) SetValue(val interface{}) { // AccessLog holds the configuration settings for the access logger (middlewares/accesslog). 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"` }