GoDoxy/internal/net/gphttp/accesslog/rotate.go
Yuzerion 57292f0fe8
feat: proxmox idlewatcher (#88)
* feat: idle sleep for proxmox LXCs

* refactor: replace deprecated docker api types

* chore(api): remove debug task list endpoint

* refactor: move servemux to gphttp/servemux; favicon.go to v1/favicon

* refactor: introduce Pool interface, move agent_pool to agent module

* refactor: simplify api code

* feat: introduce debug api

* refactor: remove net.URL and net.CIDR types, improved unmarshal handling

* chore: update Makefile for debug build tag, update README

* chore: add gperr.Unwrap method

* feat: relative time and duration formatting

* chore: add ROOT_DIR environment variable, refactor

* migration: move homepage override and icon cache to $BASE_DIR/data, add migration code

* fix: nil dereference on marshalling service health

* fix: wait for route deletion

* chore: enhance tasks debuggability

* feat: stdout access logger and MultiWriter

* fix(agent): remove agent properly on verify error

* fix(metrics): disk exclusion logic and added corresponding tests

* chore: update schema and prettify, fix package.json and Makefile

* fix: I/O buffer not being shrunk before putting back to pool

* feat: enhanced error handling module

* chore: deps upgrade

* feat: better value formatting and handling

---------

Co-authored-by: yusing <yusing@6uo.me>
2025-04-16 14:52:33 +08:00

128 lines
2.5 KiB
Go

package accesslog
import (
"bytes"
ioPkg "io"
"time"
)
func (l *AccessLogger) rotate() (err error) {
io, ok := l.io.(supportRotate)
if !ok {
return nil
}
// Get retention configuration
config := l.Config().Retention
var shouldKeep func(t time.Time, lineCount int) bool
if config.Last > 0 {
shouldKeep = func(_ time.Time, lineCount int) bool {
return lineCount < int(config.Last)
}
} else if config.Days > 0 {
cutoff := time.Now().AddDate(0, 0, -int(config.Days))
shouldKeep = func(t time.Time, _ int) bool {
return !t.IsZero() && !t.Before(cutoff)
}
} else {
return nil // No retention policy set
}
s := NewBackScanner(io, defaultChunkSize)
nRead := 0
nLines := 0
for s.Scan() {
nRead += len(s.Bytes()) + 1
nLines++
t := ParseLogTime(s.Bytes())
if !shouldKeep(t, nLines) {
break
}
}
if s.Err() != nil {
return s.Err()
}
beg := int64(nRead)
if _, err := io.Seek(-beg, ioPkg.SeekEnd); err != nil {
return err
}
buf := make([]byte, nRead)
if _, err := io.Read(buf); err != nil {
return err
}
if err := l.writeTruncate(buf); err != nil {
return err
}
return nil
}
func (l *AccessLogger) writeTruncate(buf []byte) (err error) {
io, ok := l.io.(supportRotate)
if !ok {
return nil
}
// Seek to beginning and truncate
if _, err := io.Seek(0, 0); err != nil {
return err
}
// Write buffer back to file
nWritten, err := l.buffered.Write(buf)
if err != nil {
return err
}
if err = l.buffered.Flush(); err != nil {
return err
}
// Truncate file
if err = io.Truncate(int64(nWritten)); err != nil {
return err
}
// check bytes written == buffer size
if nWritten != len(buf) {
return ioPkg.ErrShortWrite
}
return
}
const timeLen = len(`"time":"`)
var timeJSON = []byte(`"time":"`)
func ParseLogTime(line []byte) (t time.Time) {
if len(line) == 0 {
return
}
if i := bytes.Index(line, timeJSON); i != -1 { // JSON format
var jsonStart = i + timeLen
var jsonEnd = i + timeLen + len(LogTimeFormat)
if len(line) < jsonEnd {
return
}
timeStr := line[jsonStart:jsonEnd]
t, _ = time.Parse(LogTimeFormat, string(timeStr))
return
}
// Common/Combined format
// Format: <virtual host> <host ip> - - [02/Jan/2006:15:04:05 -0700] ...
start := bytes.IndexByte(line, '[')
if start == -1 {
return
}
end := bytes.IndexByte(line[start:], ']')
if end == -1 {
return
}
end += start // adjust end position relative to full line
timeStr := line[start+1 : end]
t, _ = time.Parse(LogTimeFormat, string(timeStr)) // ignore error
return
}