logger.go #1

  • //
  • p4lf/
  • dev/
  • internal/
  • logger/
  • logger.go
  • View
  • Commits
  • Open Download .zip Download (4 KB)
// Package logger provides p4lf's own rotating log writer.
//
// Logs are written to a fixed path ($LOGS/p4lf.log). When the file exceeds
// MaxLogSize, it is rotated: the current log is renamed to
// p4lf.<timestamp>.log and then gzipped. Log rotation is also triggered on
// SIGHUP (handled in main).
package logger

import (
	"compress/gzip"
	"fmt"
	"io"
	"os"
	"sync"
	"time"
)

// Logger writes structured log lines to a rotating file.
type Logger struct {
	mu      sync.Mutex
	path    string
	file    *os.File
	maxSize int64
	debug   int
	size    int64
}

// New opens (or creates) the log file at path.
func New(path string, maxSize int64, debug int) (*Logger, error) {
	f, sz, err := openLog(path)
	if err != nil {
		return nil, err
	}
	return &Logger{
		path:    path,
		file:    f,
		maxSize: maxSize,
		debug:   debug,
		size:    sz,
	}, nil
}

func openLog(path string) (*os.File, int64, error) {
	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		return nil, 0, fmt.Errorf("opening log %q: %w", path, err)
	}
	info, err := f.Stat()
	if err != nil {
		f.Close()
		return nil, 0, err
	}
	return f, info.Size(), nil
}

func (l *Logger) write(level, format string, args ...interface{}) {
	msg := fmt.Sprintf(format, args...)
	line := fmt.Sprintf("%s [%s] %s\n",
		time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
		level, msg)

	l.mu.Lock()
	defer l.mu.Unlock()

	n, _ := fmt.Fprint(l.file, line)
	l.size += int64(n)

	if l.maxSize > 0 && l.size >= l.maxSize {
		l.rotate()
	}
}

// Infof logs at INFO level.
func (l *Logger) Infof(format string, args ...interface{}) {
	l.write("INFO", format, args...)
}

// Warnf logs at WARN level.
func (l *Logger) Warnf(format string, args ...interface{}) {
	l.write("WARN", format, args...)
}

// Errorf logs at ERROR level.
func (l *Logger) Errorf(format string, args ...interface{}) {
	l.write("ERROR", format, args...)
}

// Debugf logs at DEBUG level (only when debug >= 1).
func (l *Logger) Debugf(format string, args ...interface{}) {
	if l.debug >= 1 {
		l.write("DEBUG", format, args...)
	}
}

// Debug2f logs at DEBUG level (only when debug >= 2).
func (l *Logger) Debug2f(format string, args ...interface{}) {
	if l.debug >= 2 {
		l.write("DEBUG2", format, args...)
	}
}

// Rotate forces an immediate log rotation. Called on SIGHUP or when MaxLogSize
// is reached. Must NOT be called with l.mu held externally.
func (l *Logger) Rotate() error {
	l.mu.Lock()
	defer l.mu.Unlock()
	return l.rotate()
}

// rotate performs the rotation. Must be called with l.mu held.
func (l *Logger) rotate() error {
	if l.file == nil {
		return nil
	}
	l.file.Close()
	l.file = nil

	ts := time.Now().UTC().Format("20060102T150405Z")
	rotatedName := l.path[:len(l.path)-len(".log")] + "." + ts + ".log"

	if err := os.Rename(l.path, rotatedName); err != nil {
		// Best effort; reopen original path anyway.
		_ = l.reopen()
		return fmt.Errorf("rotating log: %w", err)
	}

	// Gzip the rotated log in the background; don't block the service.
	go func() {
		if err := gzipFile(rotatedName); err != nil {
			// Can't log to our logger here (might cause deadlock); use stderr.
			fmt.Fprintf(os.Stderr, "p4lf: gzip rotated log %q: %v\n", rotatedName, err)
		}
	}()

	return l.reopen()
}

func (l *Logger) reopen() error {
	f, sz, err := openLog(l.path)
	if err != nil {
		return err
	}
	l.file = f
	l.size = sz
	return nil
}

// Close flushes and closes the log file.
func (l *Logger) Close() {
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.file != nil {
		l.file.Close()
		l.file = nil
	}
}

// gzipFile compresses src to src+".gz" then removes the original.
func gzipFile(src string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()

	out, err := os.Create(src + ".gz")
	if err != nil {
		return err
	}
	gz := gzip.NewWriter(out)
	if _, err := io.Copy(gz, in); err != nil {
		gz.Close()
		out.Close()
		return err
	}
	if err := gz.Close(); err != nil {
		out.Close()
		return err
	}
	if err := out.Close(); err != nil {
		return err
	}
	return os.Remove(src)
}
# Change User Description Committed
#1 32818 C. Thomas Tyler Initial implementation of p4lf in Go.

Adds:
- Go module (github.com/rcowham/p4lf) with fsnotify dependency
- internal/config: KEY=VALUE config parser with all settings
- internal/tailer: file reader with inode-based rotation detection and
  state file checkpoint/resume (inode + byte offset, JSON, atomic write)
- internal/chunker: gzip chunk writer with MaxLogChunks/MinLogSpace guards
- internal/logger: rotating log writer with gzip of rotated files
- cmd/p4lf/main.go: service main loop, SIGHUP/SIGTERM/SIGINT handling,
  config hot-reload on modtime change
- Makefile: build/test/install/release targets for Linux amd64/arm64,
  macOS arm64/amd64; version injected via ldflags
- p4lf.cfg.example: fully documented example config
- p4lf.service: systemd unit file (User=perforce, Restart=on-failure)
- ai/session_log_2026-06-25.md: design session log
- ai/session_log_2026-06-25-2.md: implementation session log
- .p4ignore: added bin/ and dist/