Added access log to server and modified mock-fts to use it

This commit is contained in:
bwilliams committed 2026-10-16 08:38:41 +01:00
1 parent 2490519c2b
commit 02e3d910a2
10 files changed
+389 -29

No files matched your search

+30
View File
@@ -5,6 +5,8 @@ import (
"log"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -26,12 +28,20 @@ func (s *statusRecorder) WriteHeader(code int) {
s.ResponseWriter.WriteHeader(code)
}
// Unwrap returns the wrapped ResponseWriter, so that http.ResponseController
// can reach its Hijack and Flush: a WebSocket upgrade hijacks the connection.
func (s *statusRecorder) Unwrap() http.ResponseWriter {
return s.ResponseWriter
}
func (s *statusRecorder) Write(data []byte) (int, error) {
if s.status == 0 {
s.status = http.StatusOK
}
written, err := s.ResponseWriter.Write(data)
s.bytes += written
return written, err
}
@@ -108,3 +118,23 @@ func AccessLog(
logger.Println(line)
})
}
// OpenAccessLog opens the access log at path for appending, creating it if
// needed, or stdout for "-", for AccessLog. It returns a logger writing to it
// and a function that closes it (doing nothing for stdout).
func OpenAccessLog(path string) (*log.Logger, func() error, error) {
if path == "-" {
return log.New(os.Stdout, "", 0), func() error {
return nil
}, nil
}
file, err := os.OpenFile(
filepath.Clean(path), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600,
)
if err != nil {
return nil, nil, fmt.Errorf("opening access log: %w", err)
}
return log.New(file, "", 0), file.Close, nil
}
+97
View File
@@ -5,9 +5,13 @@ import (
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
"time"
)
func TestLogField(t *testing.T) {
@@ -152,3 +156,96 @@ func TestAccessLog(t *testing.T) {
})
}
}
// A handler behind AccessLog can hijack the connection through
// http.ResponseController, as a WebSocket upgrade does, and the request is
// still logged.
func TestAccessLogHijack(t *testing.T) {
var buffer lockedBuffer
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, readWriter, err := http.NewResponseController(w).Hijack()
if err != nil {
t.Errorf("hijack: %v", err)
return
}
defer conn.Close()
readWriter.WriteString("HTTP/1.1 204 No Content\r\n\r\n")
readWriter.Flush()
})
server := httptest.NewServer(AccessLog(log.New(&buffer, "", 0), true, handler))
defer server.Close()
res, err := http.Get(server.URL + "/hijacked")
if err != nil {
t.Fatal(err)
}
res.Body.Close()
if res.StatusCode != http.StatusNoContent {
t.Errorf("status = %d; want 204 from the hijacked connection", res.StatusCode)
}
// The line is written when the handler returns, after the client has its
// response.
deadline := time.Now().Add(time.Second)
for !strings.Contains(buffer.String(), `"GET /hijacked HTTP/1.1"`) {
if time.Now().After(deadline) {
t.Fatalf("request not logged: %q", buffer.String())
}
time.Sleep(5 * time.Millisecond)
}
}
// lockedBuffer is a bytes.Buffer safe to write from the server's goroutine
// while the test reads it.
type lockedBuffer struct {
mu sync.Mutex
buffer bytes.Buffer
}
func (b *lockedBuffer) Write(data []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.buffer.Write(data)
}
func (b *lockedBuffer) String() string {
b.mu.Lock()
defer b.mu.Unlock()
return b.buffer.String()
}
// An access log file is created if needed and appended to; "-" is stdout.
func TestOpenAccessLog(t *testing.T) {
path := filepath.Join(t.TempDir(), "access.log")
os.WriteFile(path, []byte("earlier line\n"), 0o600)
logger, closeLog, err := OpenAccessLog(path)
if err != nil {
t.Fatal(err)
}
logger.Print("new line")
if err := closeLog(); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(filepath.Clean(path))
if err != nil {
t.Fatal(err)
}
if got := string(data); got != "earlier line\nnew line\n" {
t.Errorf("access log = %q; want the new line appended", got)
}
logger, closeLog, err = OpenAccessLog("-")
if err != nil || logger.Writer() != os.Stdout {
t.Errorf("OpenAccessLog(-) = %v, %v; want stdout", logger, err)
}
if err := closeLog(); err != nil {
t.Errorf("closing stdout's access log: %v", err)
}
_, _, err = OpenAccessLog(filepath.Join(t.TempDir(), "missing", "a.log"))
if err == nil || !strings.Contains(err.Error(), "opening access log") {
t.Errorf("missing directory: %v; want an error", err)
}
}