access logger improvements

This commit is contained in:
yusing 2025-01-01 06:09:35 +08:00
parent 1ab34ed46f
commit 4dda54c9e6
2 changed files with 108 additions and 70 deletions

View file

@ -2,13 +2,11 @@ package accesslog
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"net/http" "net/http"
"os" "sync"
"time" "time"
"github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
@ -16,13 +14,27 @@ import (
type ( type (
AccessLogger struct { AccessLogger struct {
parent *task.Task task *task.Task
buf chan []byte
cfg *Config cfg *Config
w io.WriteCloser io AccessLogIO
buf bytes.Buffer
bufPool sync.Pool
flushThreshold int
flushMu sync.Mutex
Formatter Formatter
} }
AccessLogIO interface {
io.ReadWriteCloser
io.ReadWriteSeeker
io.ReaderAt
sync.Locker
Name() string // file name or path
Truncate(size int64) error
}
Formatter interface { Formatter interface {
// Format writes a log line to line without a trailing newline // Format writes a log line to line without a trailing newline
Format(line *bytes.Buffer, req *http.Request, res *http.Response) Format(line *bytes.Buffer, req *http.Request, res *http.Response)
@ -31,48 +43,35 @@ type (
var logger = logging.With().Str("module", "accesslog").Logger() var logger = logging.With().Str("module", "accesslog").Logger()
var TestTimeNow = time.Now().Format(logTimeFormat) func NewAccessLogger(parent task.Parent, io AccessLogIO, cfg *Config) *AccessLogger {
const logTimeFormat = "02/Jan/2006:15:04:05 -0700"
func NewFileAccessLogger(parent *task.Task, cfg *Config) (*AccessLogger, error) {
f, err := os.OpenFile(cfg.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return nil, fmt.Errorf("access log open error: %w", err)
}
return NewAccessLogger(parent, f, cfg), nil
}
func NewAccessLogger(parent *task.Task, w io.WriteCloser, cfg *Config) *AccessLogger {
l := &AccessLogger{ l := &AccessLogger{
parent: parent, task: parent.Subtask("accesslog"),
cfg: cfg, cfg: cfg,
w: w, io: io,
} }
fmt := CommonFormatter{cfg: &l.cfg.Fields} if cfg.BufferSize < 1024 {
cfg.BufferSize = DefaultBufferSize
}
fmt := &CommonFormatter{cfg: &l.cfg.Fields, GetTimeNow: time.Now}
switch l.cfg.Format { switch l.cfg.Format {
case FormatCommon: case FormatCommon:
l.Formatter = fmt l.Formatter = fmt
case FormatCombined: case FormatCombined:
l.Formatter = CombinedFormatter{CommonFormatter: fmt} l.Formatter = (*CombinedFormatter)(fmt)
case FormatJSON: case FormatJSON:
l.Formatter = JSONFormatter{CommonFormatter: fmt} l.Formatter = (*JSONFormatter)(fmt)
} }
if cfg.BufferSize == 0 {
cfg.BufferSize = DefaultBufferSize l.flushThreshold = int(cfg.BufferSize * 4 / 5) // 80%
l.buf.Grow(int(cfg.BufferSize))
l.bufPool.New = func() any {
return new(bytes.Buffer)
} }
l.buf = make(chan []byte, cfg.BufferSize)
go l.start() go l.start()
return l return l
} }
func timeNow() string {
if !common.IsTest {
return time.Now().Format(logTimeFormat)
}
return TestTimeNow
}
func (l *AccessLogger) checkKeep(req *http.Request, res *http.Response) bool { func (l *AccessLogger) checkKeep(req *http.Request, res *http.Response) bool {
if !l.cfg.Filters.StatusCodes.CheckKeep(req, res) || if !l.cfg.Filters.StatusCodes.CheckKeep(req, res) ||
!l.cfg.Filters.Method.CheckKeep(req, res) || !l.cfg.Filters.Method.CheckKeep(req, res) ||
@ -88,25 +87,42 @@ func (l *AccessLogger) Log(req *http.Request, res *http.Response) {
return return
} }
var line bytes.Buffer line := l.bufPool.Get().(*bytes.Buffer)
l.Format(&line, req, res) l.Format(line, req, res)
line.WriteRune('\n') line.WriteRune('\n')
select { l.flushMu.Lock()
case <-l.parent.Context().Done(): l.buf.Write(line.Bytes())
return line.Reset()
default: l.bufPool.Put(line)
l.buf <- line.Bytes() l.flushMu.Unlock()
}
} }
func (l *AccessLogger) LogError(req *http.Request, err error) { func (l *AccessLogger) LogError(req *http.Request, err error) {
l.Log(req, &http.Response{StatusCode: http.StatusInternalServerError, Status: err.Error()}) l.Log(req, &http.Response{StatusCode: http.StatusInternalServerError, Status: err.Error()})
} }
func (l *AccessLogger) close() { func (l *AccessLogger) Config() *Config {
close(l.buf) return l.cfg
l.w.Close() }
// func (l *AccessLogger) Rotate() error {
// if l.cfg.Retention == nil {
// return nil
// }
// l.io.Lock()
// defer l.io.Unlock()
// return l.cfg.Retention.rotateLogFile(l.io)
// }
func (l *AccessLogger) Flush(force bool) {
l.flushMu.Lock()
if force || l.buf.Len() >= l.flushThreshold {
l.writeLine(l.buf.Bytes())
l.buf.Reset()
}
l.flushMu.Unlock()
} }
func (l *AccessLogger) handleErr(err error) { func (l *AccessLogger) handleErr(err error) {
@ -114,21 +130,34 @@ func (l *AccessLogger) handleErr(err error) {
} }
func (l *AccessLogger) start() { func (l *AccessLogger) start() {
task := l.parent.Subtask("access log flusher") defer func() {
defer task.Finish("done") if l.buf.Len() > 0 { // flush last
defer l.close() l.writeLine(l.buf.Bytes())
}
l.io.Close()
l.task.Finish(nil)
}()
// periodic + threshold flush
flushTicker := time.NewTicker(5 * time.Second)
for { for {
select { select {
case <-task.Context().Done(): case <-l.task.Context().Done():
return return
case <-flushTicker.C:
l.Flush(true)
default: default:
for line := range l.buf { l.Flush(false)
_, err := l.w.Write(line)
if err != nil {
l.handleErr(err)
}
}
} }
} }
} }
func (l *AccessLogger) writeLine(line []byte) {
l.io.Lock() // prevent write on log rotation
_, err := l.io.Write(line)
l.io.Unlock()
if err != nil {
l.handleErr(err)
}
}

View file

@ -7,10 +7,10 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"testing" "testing"
"time"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
. "github.com/yusing/go-proxy/internal/net/http/accesslog" . "github.com/yusing/go-proxy/internal/net/http/accesslog"
taskPkg "github.com/yusing/go-proxy/internal/task"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )
@ -51,19 +51,25 @@ var (
} }
) )
func fmtLog(cfg *Config) string { func fmtLog(cfg *Config) (ts string, line string) {
var line bytes.Buffer var buf bytes.Buffer
logger := NewAccessLogger(taskPkg.GlobalTask("test logger"), nil, cfg)
logger.Format(&line, req, resp) t := time.Now()
return line.String() logger := NewAccessLogger(nil, nil, cfg)
logger.Formatter.(*CommonFormatter).GetTimeNow = func() time.Time {
return t
}
logger.Format(&buf, req, resp)
return t.Format(LogTimeFormat), buf.String()
} }
func TestAccessLoggerCommon(t *testing.T) { func TestAccessLoggerCommon(t *testing.T) {
config := DefaultConfig() config := DefaultConfig()
config.Format = FormatCommon config.Format = FormatCommon
ExpectEqual(t, fmtLog(config), ts, log := fmtLog(config)
ExpectEqual(t, log,
fmt.Sprintf("%s %s - - [%s] \"%s %s %s\" %d %d", fmt.Sprintf("%s %s - - [%s] \"%s %s %s\" %d %d",
host, remote, TestTimeNow, method, uri, proto, status, contentLength, host, remote, ts, method, uri, proto, status, contentLength,
), ),
) )
} }
@ -71,9 +77,10 @@ func TestAccessLoggerCommon(t *testing.T) {
func TestAccessLoggerCombined(t *testing.T) { func TestAccessLoggerCombined(t *testing.T) {
config := DefaultConfig() config := DefaultConfig()
config.Format = FormatCombined config.Format = FormatCombined
ExpectEqual(t, fmtLog(config), ts, log := fmtLog(config)
ExpectEqual(t, log,
fmt.Sprintf("%s %s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\"", fmt.Sprintf("%s %s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\"",
host, remote, TestTimeNow, method, uri, proto, status, contentLength, referer, ua, host, remote, ts, method, uri, proto, status, contentLength, referer, ua,
), ),
) )
} }
@ -82,9 +89,10 @@ func TestAccessLoggerRedactQuery(t *testing.T) {
config := DefaultConfig() config := DefaultConfig()
config.Format = FormatCommon config.Format = FormatCommon
config.Fields.Query.Default = FieldModeRedact config.Fields.Query.Default = FieldModeRedact
ExpectEqual(t, fmtLog(config), ts, log := fmtLog(config)
ExpectEqual(t, log,
fmt.Sprintf("%s %s - - [%s] \"%s %s %s\" %d %d", fmt.Sprintf("%s %s - - [%s] \"%s %s %s\" %d %d",
host, remote, TestTimeNow, method, uriRedacted, proto, status, contentLength, host, remote, ts, method, uriRedacted, proto, status, contentLength,
), ),
) )
} }
@ -93,7 +101,8 @@ func getJSONEntry(t *testing.T, config *Config) JSONLogEntry {
t.Helper() t.Helper()
config.Format = FormatJSON config.Format = FormatJSON
var entry JSONLogEntry var entry JSONLogEntry
err := json.Unmarshal([]byte(fmtLog(config)), &entry) _, log := fmtLog(config)
err := json.Unmarshal([]byte(log), &entry)
ExpectNoError(t, err) ExpectNoError(t, err)
return entry return entry
} }