1
0
Fork 0

Switch access logging to logrus

This commit is contained in:
Richard Shepherd 2017-05-22 20:39:29 +01:00 committed by Emile Vauge
parent 2643271053
commit 64e8b31d49
7 changed files with 175 additions and 263 deletions

View file

@ -2,12 +2,17 @@ package accesslog
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"sync/atomic"
"time"
"github.com/Sirupsen/logrus"
)
type key string
@ -19,15 +24,35 @@ const (
)
// LogHandler will write each request and its response to the access log.
// It gets some information from the logInfoResponseWriter set up by previous middleware.
// Note: Current implementation collects log data but does not have the facility to
// write anywhere.
type LogHandler struct {
logger *logrus.Logger
file *os.File
}
// NewLogHandler creates a new LogHandler
func NewLogHandler() *LogHandler {
return &LogHandler{}
func NewLogHandler(filePath string) (*LogHandler, error) {
if len(filePath) == 0 {
return nil, errors.New("Empty file name specified for accessLogsFile")
}
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", dir, err)
}
logger := &logrus.Logger{
Out: file,
Formatter: new(CommonLogFormatter),
Hooks: make(logrus.LevelHooks),
Level: logrus.InfoLevel,
}
return &LogHandler{logger: logger, file: file}, nil
}
// GetLogDataTable gets the request context object that contains logging data. This accretes
@ -85,7 +110,7 @@ func (l *LogHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next h
// Close closes the Logger (i.e. the file etc).
func (l *LogHandler) Close() error {
return nil
return l.file.Close()
}
func silentSplitHostPort(value string) (host string, port string) {
@ -133,6 +158,26 @@ func (l *LogHandler) logTheRoundTrip(logDataTable *LogData, crr *captureRequestR
} else {
core[Overhead] = total
}
fields := logrus.Fields{}
for k, v := range logDataTable.Core {
fields[k] = v
}
for k := range logDataTable.Request {
fields["request_"+k] = logDataTable.Request.Get(k)
}
for k := range logDataTable.OriginResponse {
fields["origin_"+k] = logDataTable.OriginResponse.Get(k)
}
for k := range logDataTable.DownstreamResponse {
fields["downstream_"+k] = logDataTable.DownstreamResponse.Get(k)
}
l.logger.WithFields(fields).Println()
}
//-------------------------------------------------------------------------------------------------

View file

@ -0,0 +1,68 @@
package accesslog
import (
"bytes"
"fmt"
"time"
"github.com/Sirupsen/logrus"
)
// default format for time presentation
const commonLogTimeFormat = "02/Jan/2006:15:04:05 -0700"
// CommonLogFormatter provides formatting in the Traefik common log format
type CommonLogFormatter struct {
}
//Format formats the log entry in the Traefik common log format
func (f *CommonLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
b := &bytes.Buffer{}
timestamp := entry.Data[StartUTC].(time.Time).Format(commonLogTimeFormat)
elapsedMillis := entry.Data[Duration].(time.Duration).Nanoseconds() / 1000000
_, err := fmt.Fprintf(b, "%s - %s [%s] \"%s %s %s\" %d %d %s %s %d %s %s %dms\n",
entry.Data[ClientHost],
entry.Data[ClientUsername],
timestamp,
entry.Data[RequestMethod],
entry.Data[RequestPath],
entry.Data[RequestProtocol],
entry.Data[OriginStatus],
entry.Data[OriginContentSize],
toLogString(entry.Data["request_Referer"]),
toLogString(entry.Data["request_User-Agent"]),
entry.Data[RequestCount],
toLogString(entry.Data[FrontendName]),
toLogString(entry.Data[BackendURL]),
elapsedMillis)
return b.Bytes(), err
}
func toLogString(v interface{}) string {
defaultValue := "-"
if v == nil {
return defaultValue
}
switch s := v.(type) {
case string:
return quoted(s, defaultValue)
case fmt.Stringer:
return quoted(s.String(), defaultValue)
default:
return defaultValue
}
}
func quoted(s string, defaultValue string) string {
if len(s) == 0 {
return defaultValue
}
return `"` + s + `"`
}

View file

@ -0,0 +1,119 @@
package accesslog
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"testing"
shellwords "github.com/mattn/go-shellwords"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type logtestResponseWriter struct{}
var (
logger *LogHandler
logfileNameSuffix = "/traefik/logger/test.log"
helloWorld = "Hello, World"
testBackendName = "http://127.0.0.1/testBackend"
testFrontendName = "testFrontend"
testStatus = 123
testContentSize int64 = 12
testHostname = "TestHost"
testUsername = "TestUser"
testPath = "testpath"
testPort = 8181
testProto = "HTTP/0.0"
testMethod = "POST"
testReferer = "testReferer"
testUserAgent = "testUserAgent"
)
func TestLogger(t *testing.T) {
tmp, err := ioutil.TempDir("", "testlogger")
if err != nil {
t.Fatalf("failed to create temp dir: %s", err)
}
defer os.RemoveAll(tmp)
logfilePath := filepath.Join(tmp, logfileNameSuffix)
logger, err = NewLogHandler(logfilePath)
require.NoError(t, err)
defer logger.Close()
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,
},
}
logger.ServeHTTP(&logtestResponseWriter{}, req, LogWriterTestHandlerFunc)
if logdata, err := ioutil.ReadFile(logfilePath); err != nil {
fmt.Printf("%s\n%s\n", string(logdata), err.Error())
assert.Nil(t, err)
} else if tokens, err := shellwords.Parse(string(logdata)); err != nil {
fmt.Printf("%s\n", err.Error())
assert.Nil(t, err)
} else if 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 printLogdata(logdata []byte) string {
return fmt.Sprintf(
"\nExpected: %s\n"+
"Actual: %s",
"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",
string(logdata))
}
func LogWriterTestHandlerFunc(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte(helloWorld))
rw.WriteHeader(testStatus)
logDataTable := GetLogDataTable(r)
logDataTable.Core[FrontendName] = testFrontendName
logDataTable.Core[BackendURL] = testBackendName
logDataTable.Core[OriginStatus] = testStatus
logDataTable.Core[OriginContentSize] = testContentSize
}
func (lrw *logtestResponseWriter) Header() http.Header {
return map[string][]string{}
}
func (lrw *logtestResponseWriter) Write(b []byte) (int, error) {
return len(b), nil
}
func (lrw *logtestResponseWriter) WriteHeader(s int) {
}