enable logging to stdout for access logs

This commit is contained in:
Marco Jantke 2017-05-30 12:06:49 +02:00 committed by Ludovic Fernandez
parent f275e4ad3c
commit 885b9f371c
6 changed files with 138 additions and 93 deletions

View file

@ -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"

View file

@ -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 {

View file

@ -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)

View file

@ -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"`

View file

@ -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
# #

View file

@ -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"`
} }