vibe-proxy/backend/internal/logging/request_logger_body_source.go
2026-08-24 00:10:41 +02:00

256 lines
5.6 KiB
Go

package logging
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"sync"
log "github.com/sirupsen/logrus"
)
// FileBodySource stores large log sections as ordered temp-file parts.
type FileBodySource struct {
mu sync.Mutex
dir string
paths []string
cleaned bool
}
// NewFileBodySourceInDir creates a temp-backed source under baseDir.
func NewFileBodySourceInDir(baseDir string, prefix string) (*FileBodySource, error) {
prefix = sanitizeTempPrefix(prefix)
baseDir = strings.TrimSpace(baseDir)
if baseDir == "" {
return nil, fmt.Errorf("base directory is required")
}
if errMkdir := os.MkdirAll(baseDir, 0755); errMkdir != nil {
return nil, errMkdir
}
dir, errCreate := os.MkdirTemp(baseDir, "request-log-parts-"+prefix+"-*")
if errCreate != nil {
return nil, errCreate
}
return &FileBodySource{dir: dir}, nil
}
func sanitizeTempPrefix(prefix string) string {
prefix = strings.TrimSpace(prefix)
if prefix == "" {
return "log"
}
var builder strings.Builder
for _, r := range prefix {
switch {
case r >= 'a' && r <= 'z':
builder.WriteRune(r)
case r >= 'A' && r <= 'Z':
builder.WriteRune(r)
case r >= '0' && r <= '9':
builder.WriteRune(r)
case r == '-' || r == '_':
builder.WriteRune(r)
default:
builder.WriteByte('-')
}
}
out := strings.Trim(builder.String(), "-_")
if out == "" {
return "log"
}
return out
}
// CreatePart creates one ordered detail log part.
func (s *FileBodySource) CreatePart(prefix string) (*os.File, error) {
if s == nil {
return nil, fmt.Errorf("file body source is nil")
}
s.mu.Lock()
defer s.mu.Unlock()
if s.cleaned {
return nil, fmt.Errorf("file body source has been cleaned")
}
prefix = sanitizeTempPrefix(prefix)
if errMkdir := os.MkdirAll(s.dir, 0755); errMkdir != nil {
return nil, errMkdir
}
file, errCreate := os.CreateTemp(s.dir, prefix+"-*.tmp")
if errCreate != nil {
return nil, errCreate
}
s.paths = append(s.paths, file.Name())
return file, nil
}
// AppendPart appends one complete ordered part to the source.
func (s *FileBodySource) AppendPart(data []byte) error {
data = bytes.TrimSpace(data)
if len(data) == 0 {
return nil
}
file, errCreate := s.CreatePart("part")
if errCreate != nil {
return errCreate
}
writeErr := writeLogPart(file, data, false)
if errClose := file.Close(); errClose != nil {
if writeErr == nil {
writeErr = errClose
}
}
return writeErr
}
// AppendBytes appends raw bytes to a single ordered part.
func (s *FileBodySource) AppendBytes(data []byte) error {
if s == nil {
return fmt.Errorf("file body source is nil")
}
if len(data) == 0 {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
if s.cleaned {
return fmt.Errorf("file body source has been cleaned")
}
if errMkdir := os.MkdirAll(s.dir, 0755); errMkdir != nil {
return errMkdir
}
var file *os.File
var errOpen error
if len(s.paths) == 0 {
file, errOpen = os.CreateTemp(s.dir, "part-*.tmp")
if errOpen == nil {
s.paths = append(s.paths, file.Name())
}
} else {
file, errOpen = os.OpenFile(s.paths[len(s.paths)-1], os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
}
if errOpen != nil {
return errOpen
}
_, writeErr := file.Write(data)
if errClose := file.Close(); errClose != nil {
if writeErr == nil {
writeErr = errClose
}
}
return writeErr
}
// HasPayload reports whether any detail parts were recorded.
func (s *FileBodySource) HasPayload() bool {
if s == nil {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
return len(s.paths) > 0 && !s.cleaned
}
// Paths returns a copy of the ordered part paths.
func (s *FileBodySource) Paths() []string {
if s == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
out := make([]string, len(s.paths))
copy(out, s.paths)
return out
}
// WriteTo merges all ordered parts into w.
func (s *FileBodySource) WriteTo(w io.Writer) error {
if s == nil || w == nil {
return nil
}
paths := s.Paths()
wrote := false
for _, path := range paths {
file, errOpen := os.Open(path)
if errOpen != nil {
if os.IsNotExist(errOpen) {
continue
}
return errOpen
}
if wrote {
if _, errWrite := io.WriteString(w, "\n"); errWrite != nil {
if errClose := file.Close(); errClose != nil {
log.WithError(errClose).Warn("failed to close log part file")
}
return errWrite
}
}
_, errCopy := io.Copy(w, file)
if errClose := file.Close(); errClose != nil {
log.WithError(errClose).Warn("failed to close log part file")
if errCopy == nil {
errCopy = errClose
}
}
if errCopy != nil {
return errCopy
}
wrote = true
}
return nil
}
// Bytes merges all ordered parts into memory.
func (s *FileBodySource) Bytes() ([]byte, error) {
var buf bytes.Buffer
if errWrite := s.WriteTo(&buf); errWrite != nil {
return nil, errWrite
}
return buf.Bytes(), nil
}
// Cleanup removes all temp detail parts and their directory.
func (s *FileBodySource) Cleanup() error {
if s == nil {
return nil
}
s.mu.Lock()
if s.cleaned {
s.mu.Unlock()
return nil
}
paths := make([]string, len(s.paths))
copy(paths, s.paths)
dir := s.dir
s.paths = nil
s.cleaned = true
s.mu.Unlock()
var firstErr error
for _, path := range paths {
if errRemove := os.Remove(path); errRemove != nil && !os.IsNotExist(errRemove) && firstErr == nil {
firstErr = errRemove
}
}
if dir != "" {
if errRemove := os.RemoveAll(dir); errRemove != nil && firstErr == nil {
firstErr = errRemove
}
}
return firstErr
}
func cleanupFileBodySources(sources ...*FileBodySource) {
for _, source := range sources {
if source == nil {
continue
}
if errCleanup := source.Cleanup(); errCleanup != nil {
log.WithError(errCleanup).Warn("failed to clean up log part files")
}
}
}