package accesslog import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "regexp" "strings" "testing" "time" "github.com/containous/traefik/types" shellwords "github.com/mattn/go-shellwords" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( logFileNameSuffix = "/traefik/logger/test.log" testContent = "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" testRetryAttempts = 2 ) func TestLogRotation(t *testing.T) { tempDir, err := ioutil.TempDir("", "traefik_") if err != nil { t.Fatalf("Error setting up temporary directory: %s", err) } fileName := tempDir + "traefik.log" rotatedFileName := fileName + ".rotated" config := &types.AccessLog{FilePath: fileName, Format: CommonFormat} logHandler, err := NewLogHandler(config) if err != nil { t.Fatalf("Error creating new log handler: %s", err) } defer logHandler.Close() recorder := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "http://localhost", nil) next := func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) } iterations := 20 halfDone := make(chan bool) writeDone := make(chan bool) go func() { for i := 0; i < iterations; i++ { logHandler.ServeHTTP(recorder, req, next) if i == iterations/2 { halfDone <- true } } writeDone <- true }() <-halfDone err = os.Rename(fileName, rotatedFileName) if err != nil { t.Fatalf("Error renaming file: %s", err) } err = logHandler.Rotate() if err != nil { t.Fatalf("Error rotating file: %s", err) } select { case <-writeDone: gotLineCount := lineCount(t, fileName) + lineCount(t, rotatedFileName) if iterations != gotLineCount { t.Errorf("Wanted %d written log lines, got %d", iterations, gotLineCount) } case <-time.After(500 * time.Millisecond): t.Fatalf("test timed out") } close(halfDone) close(writeDone) } func lineCount(t *testing.T, fileName string) int { t.Helper() fileContents, err := ioutil.ReadFile(fileName) if err != nil { t.Fatalf("Error reading from file %s: %s", fileName, err) } count := 0 for _, line := range strings.Split(string(fileContents), "\n") { if strings.TrimSpace(line) == "" { continue } count++ } return count } func TestLoggerCLF(t *testing.T) { 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) assertValidLogData(t, logData) } func TestLoggerJSON(t *testing.T) { 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) jsonData := make(map[string]interface{}) err = json.Unmarshal(logData, &jsonData) require.NoError(t, err) expectedKeys := []string{ RequestHost, RequestAddr, RequestMethod, RequestPath, RequestProtocol, RequestPort, RequestLine, DownstreamStatus, DownstreamStatusLine, DownstreamContentSize, OriginContentSize, OriginStatus, "request_Referer", "request_User-Agent", FrontendName, BackendURL, ClientUsername, ClientHost, ClientPort, ClientAddr, "level", "msg", "downstream_Content-Type", RequestCount, Duration, Overhead, RetryAttempts, "time", "StartLocal", "StartUTC", } containsKeys(t, expectedKeys, jsonData) var assertCount int assert.Equal(t, testHostname, jsonData[RequestHost]) assertCount++ assert.Equal(t, testHostname, jsonData[RequestAddr]) assertCount++ assert.Equal(t, testMethod, jsonData[RequestMethod]) assertCount++ assert.Equal(t, testPath, jsonData[RequestPath]) assertCount++ assert.Equal(t, testProto, jsonData[RequestProtocol]) assertCount++ assert.Equal(t, "-", jsonData[RequestPort]) assertCount++ assert.Equal(t, fmt.Sprintf("%s %s %s", testMethod, testPath, testProto), jsonData[RequestLine]) assertCount++ assert.Equal(t, float64(testStatus), jsonData[DownstreamStatus]) assertCount++ assert.Equal(t, fmt.Sprintf("%d ", testStatus), jsonData[DownstreamStatusLine]) assertCount++ assert.Equal(t, float64(len(testContent)), jsonData[DownstreamContentSize]) assertCount++ assert.Equal(t, float64(len(testContent)), jsonData[OriginContentSize]) assertCount++ assert.Equal(t, float64(testStatus), jsonData[OriginStatus]) assertCount++ assert.Equal(t, testReferer, jsonData["request_Referer"]) assertCount++ assert.Equal(t, testUserAgent, jsonData["request_User-Agent"]) assertCount++ assert.Equal(t, testFrontendName, jsonData[FrontendName]) assertCount++ assert.Equal(t, testBackendName, jsonData[BackendURL]) assertCount++ assert.Equal(t, testUsername, jsonData[ClientUsername]) assertCount++ assert.Equal(t, testHostname, jsonData[ClientHost]) assertCount++ assert.Equal(t, fmt.Sprintf("%d", testPort), jsonData[ClientPort]) assertCount++ assert.Equal(t, fmt.Sprintf("%s:%d", testHostname, testPort), jsonData[ClientAddr]) assertCount++ assert.Equal(t, "info", jsonData["level"]) assertCount++ assert.Equal(t, "", jsonData["msg"]) assertCount++ assert.Equal(t, "text/plain; charset=utf-8", jsonData["downstream_Content-Type"].(string)) assertCount++ assert.NotZero(t, jsonData[RequestCount].(float64)) assertCount++ assert.NotZero(t, jsonData[Duration].(float64)) assertCount++ assert.NotZero(t, jsonData[Overhead].(float64)) assertCount++ assert.Equal(t, float64(testRetryAttempts), jsonData[RetryAttempts].(float64)) assertCount++ assert.NotEqual(t, "", jsonData["time"].(string)) assertCount++ assert.NotEqual(t, "", jsonData["StartLocal"].(string)) assertCount++ assert.NotEqual(t, "", jsonData["StartUTC"].(string)) assertCount++ 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) require.NoError(t, err) defer logger.Close() 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) { 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 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(testContent)) rw.WriteHeader(testStatus) logDataTable := GetLogDataTable(r) logDataTable.Core[FrontendName] = testFrontendName logDataTable.Core[BackendURL] = testBackendName logDataTable.Core[OriginStatus] = testStatus logDataTable.Core[OriginContentSize] = testContentSize logDataTable.Core[RetryAttempts] = testRetryAttempts }