Skip to content

Commit

Permalink
enable logging to stdout for access logs
Browse files Browse the repository at this point in the history
  • Loading branch information
m3co-code authored and ldez committed Jun 13, 2017
1 parent f275e4a commit 885b9f3
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 83 deletions.
11 changes: 8 additions & 3 deletions docs/toml.md
Expand Up @@ -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"
Expand Down
36 changes: 22 additions & 14 deletions middlewares/accesslog/logger.go
Expand Up @@ -2,7 +2,6 @@ package accesslog

import (
"context"
"errors"
"fmt"
"net"
"net/http"
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
147 changes: 90 additions & 57 deletions middlewares/accesslog/logger_test.go
Expand Up @@ -9,6 +9,7 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"testing"

"github.com/containous/traefik/types"
Expand All @@ -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
Expand All @@ -36,32 +36,27 @@ var (
)

func TestLoggerCLF(t *testing.T) {
tmpDir, logFilePath := doLogging(t, CommonFormat)
tmpDir := createTempDir(t, CommonFormat)
defer os.RemoveAll(tmpDir)

logData, err := ioutil.ReadFile(logFilePath)
require.NoError(t, err)
logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat}
doLogging(t, config)

tokens, err := shellwords.Parse(string(logData))
logData, err := ioutil.ReadFile(logFilePath)
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)

Expand Down Expand Up @@ -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++
Expand Down Expand Up @@ -165,44 +160,70 @@ func TestLoggerJSON(t *testing.T) {
assert.Equal(t, len(jsonData), assertCount, string(logData))
}

func containsKeys(t *testing.T, expectedKeys []string, data map[string]interface{}) {
for key, value := range data {
if !contains(expectedKeys, key) {
t.Errorf("Unexpected log key: %s [value: %s]", key, value)
}
}
for _, k := range expectedKeys {
if _, ok := data[k]; !ok {
t.Errorf("the expected key '%s' is not present in the map. %+v", k, data)
}
}
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 contains(values []string, value string) bool {
for _, v := range values {
if value == v {
return true
}
}
return false
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 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)
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
}

logFilePath := filepath.Join(tmp, logFileNameSuffix)
return file, restoreStdout
}

config := types.AccessLog{FilePath: logFilePath, Format: format}
func createTempDir(t *testing.T, prefix string) string {
tmpDir, err := ioutil.TempDir("", prefix)
require.NoError(t, err, "failed to create temp dir")

logger, err = NewLogHandler(&config)
return tmpDir
}

func doLogging(t *testing.T, config *types.AccessLog) {
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)
if config.FilePath != "" {
_, err = os.Stat(config.FilePath)
require.NoError(t, err, fmt.Sprintf("logger should create %s", config.FilePath))
}

req := &http.Request{
Expand All @@ -220,21 +241,33 @@ func doLogging(t *testing.T, format string) (string, string) {
},
}

rw := httptest.NewRecorder()
logger.ServeHTTP(rw, req, logWriterTestHandlerFunc)
return tmp, logFilePath
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) {
t.Errorf("Unexpected log key: %s [value: %s]", key, value)
}
}
for _, k := range expectedKeys {
if _, ok := data[k]; !ok {
t.Errorf("the expected key '%s' is not present in the map. %+v", k, data)
}
}
}

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 contains(values []string, value string) bool {
for _, v := range values {
if value == v {
return true
}
}
return false
}

func logWriterTestHandlerFunc(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte(helloWorld))
rw.Write([]byte(testContent))
rw.WriteHeader(testStatus)

logDataTable := GetLogDataTable(r)
Expand Down
2 changes: 1 addition & 1 deletion server/configuration.go
Expand Up @@ -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"`
Expand Down
13 changes: 6 additions & 7 deletions traefik.sample.toml
Expand Up @@ -220,29 +220,28 @@
# 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".
#
# Optional
# Default: "common"
#
# format = "common"
#

# Entrypoints definition
#
Expand Down
2 changes: 1 addition & 1 deletion types/types.go
Expand Up @@ -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"`
}

0 comments on commit 885b9f3

Please sign in to comment.