- Pull from sync-branch before rename if configured - Import all issues from JSONL before rename to prevent data loss - Export directly to JSONL after rename (don't rely on flushManager) - Apply same pattern to --repair mode - Add newSilentLogger() for production use (not test-only) - Add comprehensive tests for JSONL update scenarios 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
170 lines
4.6 KiB
Go
170 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
|
|
"gopkg.in/natefinch/lumberjack.v2"
|
|
)
|
|
|
|
// daemonLogger wraps slog for daemon logging.
|
|
// Provides level-specific methods and backward-compatible log() for migration.
|
|
type daemonLogger struct {
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// log is the backward-compatible logging method (maps to Info level).
|
|
// Use Info(), Warn(), Error(), Debug() for explicit levels.
|
|
func (d *daemonLogger) log(format string, args ...interface{}) {
|
|
d.logger.Info(format, toSlogArgs(args)...)
|
|
}
|
|
|
|
// Info logs at INFO level.
|
|
func (d *daemonLogger) Info(msg string, args ...interface{}) {
|
|
d.logger.Info(msg, toSlogArgs(args)...)
|
|
}
|
|
|
|
// Warn logs at WARN level.
|
|
func (d *daemonLogger) Warn(msg string, args ...interface{}) {
|
|
d.logger.Warn(msg, toSlogArgs(args)...)
|
|
}
|
|
|
|
// Error logs at ERROR level.
|
|
func (d *daemonLogger) Error(msg string, args ...interface{}) {
|
|
d.logger.Error(msg, toSlogArgs(args)...)
|
|
}
|
|
|
|
// Debug logs at DEBUG level.
|
|
func (d *daemonLogger) Debug(msg string, args ...interface{}) {
|
|
d.logger.Debug(msg, toSlogArgs(args)...)
|
|
}
|
|
|
|
// toSlogArgs converts variadic args to slog-compatible key-value pairs.
|
|
// If args are already in key-value format (string, value, string, value...),
|
|
// they're passed through. Otherwise, they're wrapped as "args" for sprintf-style logs.
|
|
func toSlogArgs(args []interface{}) []any {
|
|
if len(args) == 0 {
|
|
return nil
|
|
}
|
|
// Check if args look like slog key-value pairs (string key followed by value)
|
|
// If first arg is a string and we have pairs, treat as slog format
|
|
if len(args) >= 2 {
|
|
if _, ok := args[0].(string); ok {
|
|
// Likely slog-style: "key", value, "key2", value2
|
|
result := make([]any, len(args))
|
|
for i, a := range args {
|
|
result[i] = a
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
// For sprintf-style args, wrap them (caller should use fmt.Sprintf)
|
|
result := make([]any, len(args))
|
|
for i, a := range args {
|
|
result[i] = a
|
|
}
|
|
return result
|
|
}
|
|
|
|
// parseLogLevel converts a log level string to slog.Level.
|
|
func parseLogLevel(level string) slog.Level {
|
|
switch strings.ToLower(level) {
|
|
case "debug":
|
|
return slog.LevelDebug
|
|
case "info":
|
|
return slog.LevelInfo
|
|
case "warn", "warning":
|
|
return slog.LevelWarn
|
|
case "error":
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|
|
|
|
// setupDaemonLogger creates a structured logger for the daemon.
|
|
// Returns the lumberjack logger (for cleanup) and the daemon logger.
|
|
//
|
|
// Parameters:
|
|
// - logPath: path to log file (uses lumberjack for rotation)
|
|
// - jsonFormat: if true, output JSON; otherwise text format
|
|
// - level: log level (debug, info, warn, error)
|
|
func setupDaemonLogger(logPath string, jsonFormat bool, level slog.Level) (*lumberjack.Logger, daemonLogger) {
|
|
maxSizeMB := getEnvInt("BEADS_DAEMON_LOG_MAX_SIZE", 50)
|
|
maxBackups := getEnvInt("BEADS_DAEMON_LOG_MAX_BACKUPS", 7)
|
|
maxAgeDays := getEnvInt("BEADS_DAEMON_LOG_MAX_AGE", 30)
|
|
compress := getEnvBool("BEADS_DAEMON_LOG_COMPRESS", true)
|
|
|
|
logF := &lumberjack.Logger{
|
|
Filename: logPath,
|
|
MaxSize: maxSizeMB,
|
|
MaxBackups: maxBackups,
|
|
MaxAge: maxAgeDays,
|
|
Compress: compress,
|
|
}
|
|
|
|
// Create multi-writer to log to both file and stderr (for foreground mode visibility)
|
|
var w io.Writer = logF
|
|
|
|
// Configure slog handler
|
|
opts := &slog.HandlerOptions{
|
|
Level: level,
|
|
}
|
|
|
|
var handler slog.Handler
|
|
if jsonFormat {
|
|
handler = slog.NewJSONHandler(w, opts)
|
|
} else {
|
|
handler = slog.NewTextHandler(w, opts)
|
|
}
|
|
|
|
logger := daemonLogger{
|
|
logger: slog.New(handler),
|
|
}
|
|
|
|
return logF, logger
|
|
}
|
|
|
|
// SetupStderrLogger creates a logger that writes to stderr only (no file).
|
|
// Useful for foreground mode or testing.
|
|
func SetupStderrLogger(jsonFormat bool, level slog.Level) daemonLogger {
|
|
opts := &slog.HandlerOptions{
|
|
Level: level,
|
|
}
|
|
|
|
var handler slog.Handler
|
|
if jsonFormat {
|
|
handler = slog.NewJSONHandler(os.Stderr, opts)
|
|
} else {
|
|
handler = slog.NewTextHandler(os.Stderr, opts)
|
|
}
|
|
|
|
return daemonLogger{
|
|
logger: slog.New(handler),
|
|
}
|
|
}
|
|
|
|
// newTestLogger creates a no-op logger for testing.
|
|
// Logs are discarded - use this when you don't need to verify log output.
|
|
func newTestLogger() daemonLogger {
|
|
return newSilentLogger()
|
|
}
|
|
|
|
// newSilentLogger creates a logger that discards all output.
|
|
// Use this for operations that need a logger but shouldn't produce output.
|
|
func newSilentLogger() daemonLogger {
|
|
return daemonLogger{
|
|
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
|
}
|
|
}
|
|
|
|
// newTestLoggerWithWriter creates a logger that writes to the given writer.
|
|
// Use this when you need to capture and verify log output in tests.
|
|
func newTestLoggerWithWriter(w io.Writer) daemonLogger {
|
|
return daemonLogger{
|
|
logger: slog.New(slog.NewTextHandler(w, nil)),
|
|
}
|
|
}
|