mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-20 12:42:34 +02:00
access logger support sharing the same file, tests added for concurrent logging
This commit is contained in:
parent
753e193d62
commit
6e30d39b78
5 changed files with 211 additions and 91 deletions
|
@ -18,10 +18,11 @@ type (
|
||||||
cfg *Config
|
cfg *Config
|
||||||
io AccessLogIO
|
io AccessLogIO
|
||||||
|
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer // buffer for non-flushed log
|
||||||
bufPool sync.Pool
|
bufMu sync.Mutex // protect buf
|
||||||
|
bufPool sync.Pool // buffer pool for formatting a single log line
|
||||||
|
|
||||||
flushThreshold int
|
flushThreshold int
|
||||||
flushMu sync.Mutex
|
|
||||||
|
|
||||||
Formatter
|
Formatter
|
||||||
}
|
}
|
||||||
|
@ -61,6 +62,8 @@ func NewAccessLogger(parent task.Parent, io AccessLogIO, cfg *Config) *AccessLog
|
||||||
l.Formatter = (*CombinedFormatter)(fmt)
|
l.Formatter = (*CombinedFormatter)(fmt)
|
||||||
case FormatJSON:
|
case FormatJSON:
|
||||||
l.Formatter = (*JSONFormatter)(fmt)
|
l.Formatter = (*JSONFormatter)(fmt)
|
||||||
|
default: // should not happen, validation has done by validate tags
|
||||||
|
panic("invalid access log format")
|
||||||
}
|
}
|
||||||
|
|
||||||
l.flushThreshold = int(cfg.BufferSize * 4 / 5) // 80%
|
l.flushThreshold = int(cfg.BufferSize * 4 / 5) // 80%
|
||||||
|
@ -91,11 +94,11 @@ func (l *AccessLogger) Log(req *http.Request, res *http.Response) {
|
||||||
l.Format(line, req, res)
|
l.Format(line, req, res)
|
||||||
line.WriteRune('\n')
|
line.WriteRune('\n')
|
||||||
|
|
||||||
l.flushMu.Lock()
|
l.bufMu.Lock()
|
||||||
l.buf.Write(line.Bytes())
|
l.buf.Write(line.Bytes())
|
||||||
line.Reset()
|
line.Reset()
|
||||||
l.bufPool.Put(line)
|
l.bufPool.Put(line)
|
||||||
l.flushMu.Unlock()
|
l.bufMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *AccessLogger) LogError(req *http.Request, err error) {
|
func (l *AccessLogger) LogError(req *http.Request, err error) {
|
||||||
|
@ -116,12 +119,12 @@ func (l *AccessLogger) Rotate() error {
|
||||||
return l.cfg.Retention.rotateLogFile(l.io)
|
return l.cfg.Retention.rotateLogFile(l.io)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *AccessLogger) Flush() {
|
func (l *AccessLogger) Flush(force bool) {
|
||||||
if l.buf.Len() >= l.flushThreshold {
|
if force || l.buf.Len() >= l.flushThreshold {
|
||||||
l.flushMu.Lock()
|
l.bufMu.Lock()
|
||||||
l.writeLine(l.buf.Bytes())
|
l.write(l.buf.Bytes())
|
||||||
l.buf.Reset()
|
l.buf.Reset()
|
||||||
l.flushMu.Unlock()
|
l.bufMu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,28 +135,28 @@ func (l *AccessLogger) handleErr(err error) {
|
||||||
func (l *AccessLogger) start() {
|
func (l *AccessLogger) start() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if l.buf.Len() > 0 { // flush last
|
if l.buf.Len() > 0 { // flush last
|
||||||
l.writeLine(l.buf.Bytes())
|
l.write(l.buf.Bytes())
|
||||||
}
|
}
|
||||||
l.io.Close()
|
l.io.Close()
|
||||||
l.task.Finish(nil)
|
l.task.Finish(nil)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// threshold flush with periodic check
|
// threshold flush with periodic check
|
||||||
flushTicker := time.NewTicker(3 * time.Second)
|
flushTicker := time.NewTicker(time.Second)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-l.task.Context().Done():
|
case <-l.task.Context().Done():
|
||||||
return
|
return
|
||||||
case <-flushTicker.C:
|
case <-flushTicker.C:
|
||||||
l.Flush()
|
l.Flush(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *AccessLogger) writeLine(line []byte) {
|
func (l *AccessLogger) write(data []byte) {
|
||||||
l.io.Lock() // prevent write on log rotation
|
l.io.Lock() // prevent concurrent write, i.e. log rotation, other access loggers
|
||||||
_, err := l.io.Write(line)
|
_, err := l.io.Write(data)
|
||||||
l.io.Unlock()
|
l.io.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.handleErr(err)
|
l.handleErr(err)
|
||||||
|
|
|
@ -13,10 +13,26 @@ type File struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
openedFiles = make(map[string]AccessLogIO)
|
||||||
|
openedFilesMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
func NewFileAccessLogger(parent task.Parent, cfg *Config) (*AccessLogger, error) {
|
func NewFileAccessLogger(parent task.Parent, cfg *Config) (*AccessLogger, error) {
|
||||||
|
openedFilesMu.Lock()
|
||||||
|
|
||||||
|
var io AccessLogIO
|
||||||
|
if opened, ok := openedFiles[cfg.Path]; ok {
|
||||||
|
io = opened
|
||||||
|
} else {
|
||||||
f, err := os.OpenFile(cfg.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
f, err := os.OpenFile(cfg.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("access log open error: %w", err)
|
return nil, fmt.Errorf("access log open error: %w", err)
|
||||||
}
|
}
|
||||||
return NewAccessLogger(parent, &File{File: f}, cfg), nil
|
io = &File{File: f}
|
||||||
|
openedFiles[cfg.Path] = io
|
||||||
|
}
|
||||||
|
|
||||||
|
openedFilesMu.Unlock()
|
||||||
|
return NewAccessLogger(parent, io, cfg), nil
|
||||||
}
|
}
|
||||||
|
|
95
internal/net/http/accesslog/file_logger_test.go
Normal file
95
internal/net/http/accesslog/file_logger_test.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package accesslog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConcurrentFileLoggersShareSameAccessLogIO(t *testing.T) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
cfg.Path = "test.log"
|
||||||
|
parent := task.RootTask("test", false)
|
||||||
|
|
||||||
|
loggerCount := 10
|
||||||
|
accessLogIOs := make([]AccessLogIO, loggerCount)
|
||||||
|
|
||||||
|
// make test log file
|
||||||
|
file, err := os.Create(cfg.Path)
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
file.Close()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ExpectNoError(t, os.Remove(cfg.Path))
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := range loggerCount {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(index int) {
|
||||||
|
defer wg.Done()
|
||||||
|
logger, err := NewFileAccessLogger(parent, cfg)
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
accessLogIOs[index] = logger.io
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
firstIO := accessLogIOs[0]
|
||||||
|
for _, io := range accessLogIOs {
|
||||||
|
ExpectEqual(t, io, firstIO)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrentAccessLoggerLogAndFlush(t *testing.T) {
|
||||||
|
var file MockFile
|
||||||
|
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
cfg.BufferSize = 1024
|
||||||
|
parent := task.RootTask("test", false)
|
||||||
|
|
||||||
|
loggerCount := 5
|
||||||
|
logCountPerLogger := 10
|
||||||
|
loggers := make([]*AccessLogger, loggerCount)
|
||||||
|
|
||||||
|
for i := range loggerCount {
|
||||||
|
loggers[i] = NewAccessLogger(parent, &file, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||||
|
resp := &http.Response{StatusCode: http.StatusOK}
|
||||||
|
|
||||||
|
for _, logger := range loggers {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(l *AccessLogger) {
|
||||||
|
defer wg.Done()
|
||||||
|
parallelLog(l, req, resp, logCountPerLogger)
|
||||||
|
l.Flush(true)
|
||||||
|
}(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
expected := loggerCount * logCountPerLogger
|
||||||
|
actual := file.Count()
|
||||||
|
ExpectEqual(t, actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parallelLog(logger *AccessLogger, req *http.Request, resp *http.Response, n int) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(n)
|
||||||
|
for range n {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
logger.Log(req, resp)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
74
internal/net/http/accesslog/mock_file.go
Normal file
74
internal/net/http/accesslog/mock_file.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package accesslog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockFile struct {
|
||||||
|
data []byte
|
||||||
|
position int64
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFile) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
switch whence {
|
||||||
|
case io.SeekStart:
|
||||||
|
m.position = offset
|
||||||
|
case io.SeekCurrent:
|
||||||
|
m.position += offset
|
||||||
|
case io.SeekEnd:
|
||||||
|
m.position = int64(len(m.data)) + offset
|
||||||
|
}
|
||||||
|
return m.position, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFile) Write(p []byte) (n int, err error) {
|
||||||
|
m.data = append(m.data, p...)
|
||||||
|
n = len(p)
|
||||||
|
m.position += int64(n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFile) Name() string {
|
||||||
|
return "mock"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFile) Read(p []byte) (n int, err error) {
|
||||||
|
if m.position >= int64(len(m.data)) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
n = copy(p, m.data[m.position:])
|
||||||
|
m.position += int64(n)
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFile) ReadAt(p []byte, off int64) (n int, err error) {
|
||||||
|
if off >= int64(len(m.data)) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
n = copy(p, m.data[off:])
|
||||||
|
m.position += int64(n)
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFile) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFile) Truncate(size int64) error {
|
||||||
|
m.data = m.data[:size]
|
||||||
|
m.position = size
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFile) Count() int {
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
return bytes.Count(m.data[:m.position], []byte("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFile) Len() int64 {
|
||||||
|
return m.position
|
||||||
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
package accesslog_test
|
package accesslog_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
. "github.com/yusing/go-proxy/internal/net/http/accesslog"
|
. "github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
)
|
)
|
||||||
|
@ -36,76 +35,9 @@ func TestParseRetention(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockFile struct {
|
|
||||||
data []byte
|
|
||||||
position int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) Seek(offset int64, whence int) (int64, error) {
|
|
||||||
switch whence {
|
|
||||||
case io.SeekStart:
|
|
||||||
m.position = offset
|
|
||||||
case io.SeekCurrent:
|
|
||||||
m.position += offset
|
|
||||||
case io.SeekEnd:
|
|
||||||
m.position = int64(len(m.data)) + offset
|
|
||||||
}
|
|
||||||
return m.position, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) Write(p []byte) (n int, err error) {
|
|
||||||
m.data = append(m.data, p...)
|
|
||||||
n = len(p)
|
|
||||||
m.position += int64(n)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) Name() string {
|
|
||||||
return "mock"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) Read(p []byte) (n int, err error) {
|
|
||||||
if m.position >= int64(len(m.data)) {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
n = copy(p, m.data[m.position:])
|
|
||||||
m.position += int64(n)
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) ReadAt(p []byte, off int64) (n int, err error) {
|
|
||||||
if off >= int64(len(m.data)) {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
n = copy(p, m.data[off:])
|
|
||||||
m.position += int64(n)
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) Truncate(size int64) error {
|
|
||||||
m.data = m.data[:size]
|
|
||||||
m.position = size
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) Lock() {}
|
|
||||||
func (m *mockFile) Unlock() {}
|
|
||||||
|
|
||||||
func (m *mockFile) Count() int {
|
|
||||||
return bytes.Count(m.data[:m.position], []byte("\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFile) Len() int64 {
|
|
||||||
return m.position
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRetentionCommonFormat(t *testing.T) {
|
func TestRetentionCommonFormat(t *testing.T) {
|
||||||
file := mockFile{}
|
var file MockFile
|
||||||
logger := NewAccessLogger(nil, &file, &Config{
|
logger := NewAccessLogger(task.RootTask("test", false), &file, &Config{
|
||||||
Format: FormatCommon,
|
Format: FormatCommon,
|
||||||
BufferSize: 1024,
|
BufferSize: 1024,
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Reference in a new issue