log

package
v1.51.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

README

Stapler Squad Logging System

This package implements a configurable logging system for Stapler Squad with the following features:

Key Features

  • Configurable Log Location: Logs are stored in ~/.stapler-squad/logs/ by default, but this can be changed in the config.
  • Global and Session-Specific Logs: Separate log files are created for each session.
  • Log Rotation: Logs are automatically rotated based on size and age.
  • Configuration Options: Several options can be configured in ~/.stapler-squad/config.json

Configuration

The following logging options can be configured in config.json:

{
  "logs_enabled": true,
  "logs_dir": "",  // Empty for default location (~/.stapler-squad/logs/)
  "log_max_size": 10,  // Max log file size in MB before rotation
  "log_max_files": 5,  // Max number of rotated files to keep
  "log_max_age": 30,  // Max age in days for rotated files
  "log_compress": true,  // Whether to compress rotated files
  "use_session_logs": true  // Whether to create separate log files for each session
}

Usage

Global Logging

The global loggers (InfoLog, WarningLog, and ErrorLog) can be used directly:

log.InfoLog.Printf("This is an info message")
log.WarningLog.Printf("This is a warning message")
log.ErrorLog.Printf("This is an error message")
Session-Specific Logging

For session-specific logging, use the LogForSession function:

// Log to session-specific file and global log
log.LogForSession("session-id", "info", "This is an info message for session %s", "session-id")
log.LogForSession("session-id", "warning", "This is a warning message for session %s", "session-id")
log.LogForSession("session-id", "error", "This is an error message for session %s", "session-id")

Implementation Details

  • Log files are stored in ~/.stapler-squad/logs/ by default
  • Global log file is named claudesquad.log
  • Session log files are named session_<session-id>.log
  • Log rotation is implemented using the lumberjack package
  • Logs are rotated when they reach the configured size
  • Old log files are compressed if the log_compress option is enabled

Documentation

Index

Constants

View Source
const DropLogInterval = 100

DropLogInterval is how often a rate-limited "dropped" warning is logged: only every Nth drop, to avoid flooding logs under sustained backpressure.

Variables

View Source
var (
	// ErrSessionLogsDisabled is returned when session logs are disabled in config
	ErrSessionLogsDisabled = fmt.Errorf("session logs disabled in config")
)

Functions

func ClearAllPackageLevels added in v1.49.0

func ClearAllPackageLevels()

ClearAllPackageLevels removes every override.

func ClearPackageLevel added in v1.49.0

func ClearPackageLevel(pkg string)

ClearPackageLevel removes a package's override, falling back to the global runtime level (or a less-specific ancestor override) for that package.

func Close

func Close()

func Debug added in v1.35.0

func Debug(msg string, args ...any)

Debug logs a debug-level message through the default slog handler. The handler drops debug records when the runtime level is above DEBUG, so this is safe to call without an IsDebugEnabled() guard.

func DebugLog

func DebugLog() *log.Logger

DebugLog returns the current debug-level logger. Safe to call concurrently with SetDebugLogForTest or initializeWithConfig replacing it.

func DebugS

func DebugS(message string, fields ...map[string]interface{})

DebugS logs a structured debug message

func Error added in v1.35.0

func Error(msg string, args ...any)

Error logs an error-level message through the default slog handler.

func ErrorLog

func ErrorLog() *log.Logger

ErrorLog returns the current error-level logger. Safe to call concurrently with SetErrorLogForTest or initializeWithConfig replacing it.

func ErrorS

func ErrorS(message string, fields ...map[string]interface{})

ErrorS logs a structured error message

func FatalS

func FatalS(message string, fields ...map[string]interface{})

FatalS logs a structured fatal message

func ForSession added in v1.15.0

func ForSession(sessionID string) *slog.Logger

ForSession returns a *slog.Logger pre-populated with "session" = sessionID. All calls route through the async slog handler — no stdlib mutex serialization. Session-specific log files still receive the entry via LogForSession when needed.

func GetActiveSessionLogPaths

func GetActiveSessionLogPaths() map[string]string

GetActiveSessionLogPaths returns the paths to all active session log files

func GetConfigDir

func GetConfigDir() (string, error)

GetConfigDir returns the path to the application's configuration directory, honoring the same STAPLER_SQUAD_TEST_DIR / STAPLER_SQUAD_INSTANCE precedence as config.GetConfigDirForDir (Priorities 1-2 only; see that function's doc comment for the full 6-priority list — Priorities 3-6 are DB/session-state concerns with no log-directory analogue). Duplicated here rather than imported because config already imports log, and importing back would create a cycle.

func GetGlobalLogPath

func GetGlobalLogPath() string

GetGlobalLogPath returns the path to the global log file

func GetLogDir

func GetLogDir(cfg *LogConfig) (string, error)

GetLogDir returns the directory where logs should be stored

func GetLogFilePath

func GetLogFilePath(cfg *LogConfig) (string, error)

GetLogFilePath returns the full path to the log file

func GetPackageLevels added in v1.49.0

func GetPackageLevels() map[string]LogLevel

GetPackageLevels returns a snapshot of the current overrides, keyed by package path, for display (e.g. the debug API / web UI).

func GetSessionLogFilePath

func GetSessionLogFilePath(cfg *LogConfig, sessionID string) (string, error)

GetSessionLogFilePath returns the full path to a session-specific log file

func GetTestLogDir

func GetTestLogDir() (string, error)

GetTestLogDir returns the directory where test logs should be stored Test logs are isolated in a dedicated subdirectory for easy cleanup

func Info added in v1.35.0

func Info(msg string, args ...any)

Info logs an info-level message through the default slog handler (async, no mutex hold). args are alternating key-value pairs: log.Info("msg", "key", val, "key2", val2)

func InfoLog

func InfoLog() *log.Logger

InfoLog returns the current info-level logger. Safe to call concurrently with SetInfoLogForTest or initializeWithConfig replacing it.

func InfoS

func InfoS(message string, fields ...map[string]interface{})

InfoS logs a structured info message

func Initialize

func Initialize(daemon bool)

Initialize should be called once at the beginning of the program to set up logging. defer Close() after calling this function. It sets the go log output to the file in the configured log directory (default: ~/.stapler-squad/logs/).

Must run after config.LoadConfig() in any real entry point: GetConfigDir (used internally here) doesn't perform config.GetConfigDirForDir's legacy ~/.claude-squad migration, so calling this first would create ~/.stapler-squad ahead of migration and cause config's migration guard to skip it.

func InitializeForTests

func InitializeForTests(fileLevel LogLevel, consoleLevel LogLevel)

InitializeForTests sets up logging specifically for test environments with dual-stream configuration. This allows DEBUG logs to go to file while ERROR logs appear in console for immediate visibility.

Parameters:

  • fileLevel: Minimum level for file logging (typically DEBUG to capture everything)
  • consoleLevel: Minimum level for console logging (typically ERROR to avoid noise)

Example:

log.InitializeForTests(log.DEBUG, log.ERROR)  // DEBUG→file, ERROR→console

func InitializeWithConfig

func InitializeWithConfig(daemon bool, externalConfig interface{})

InitializeWithConfig sets up logging with the provided configuration.

func IsDebugEnabled added in v1.35.0

func IsDebugEnabled() bool

IsDebugEnabled returns true when the runtime level is DEBUG. Use this to gate expensive format-string construction before calling DebugLog.Printf.

func LoadPackageLevelsFromEnv added in v1.49.0

func LoadPackageLevelsFromEnv()

LoadPackageLevelsFromEnv parses STAPLER_SQUAD_LOG_LEVELS, a comma-separated list of "package=level" pairs (e.g. "session/tmux=debug,server/services=warn"), and installs them as package overrides. Call once at startup; safe to call again to re-parse. Malformed entries are skipped with a warning rather than failing startup.

func LogForSession

func LogForSession(sessionID, level, format string, v ...interface{})

LogForSession logs a message to the session-specific log file

func LogSessionPathsToStderr

func LogSessionPathsToStderr()

LogSessionPathsToStderr outputs session log file paths to stderr on exit

func PackageForPC added in v1.49.0

func PackageForPC(pc uintptr) string

PackageForPC resolves a program counter to its module-relative package path, e.g. "session/tmux" — the same resolution PackageLevelHandler uses internally. Exposed for tests and for debug tooling that wants to preview how a given call site would be classified for STAPLER_SQUAD_LOG_LEVELS.

func SetDebugLogForTest added in v1.47.0

func SetDebugLogForTest(l *log.Logger) *log.Logger

SetDebugLogForTest atomically replaces the debug logger and returns the previous value, so callers can restore it via t.Cleanup.

func SetErrorLogForTest added in v1.47.0

func SetErrorLogForTest(l *log.Logger) *log.Logger

SetErrorLogForTest atomically replaces the error logger and returns the previous value, so callers can restore it via t.Cleanup.

func SetInfoLogForTest added in v1.47.0

func SetInfoLogForTest(l *log.Logger) *log.Logger

SetInfoLogForTest atomically replaces the info logger and returns the previous value, so callers can restore it via t.Cleanup.

func SetPackageLevel added in v1.49.0

func SetPackageLevel(pkg string, level LogLevel)

SetPackageLevel sets a minimum log level for one package (and, by prefix, its subpackages unless they have a more specific override — the same hierarchical-logger convention as Java's log4j/logback). Pass a path relative to the module root, e.g. "session/tmux" or "server/services".

func SetRuntimeLevel added in v1.35.0

func SetRuntimeLevel(level LogLevel)

SetRuntimeLevel changes the minimum log level for all output streams immediately. Safe to call from any goroutine. Takes effect on the next log call.

func SetSlogDefaultForTest added in v1.51.0

func SetSlogDefaultForTest(l *slog.Logger) *slog.Logger

SetSlogDefaultForTest atomically replaces the slog-backed default logger (read by logAt/ForSession) and returns the previous value, so tests can restore it via t.Cleanup instead of calling slog.SetDefault() — which would also rewire stdlib log.Print process-wide and is the root cause of the server/services capture-buffer race under -race this seam removes tests from touching at all.

func SetWarningLogForTest added in v1.47.0

func SetWarningLogForTest(l *log.Logger) *log.Logger

SetWarningLogForTest atomically replaces the warning logger and returns the previous value, so callers can restore it via t.Cleanup instead of racing a bare package-var assignment against concurrent t.Parallel() reads.

func Warn added in v1.35.0

func Warn(msg string, args ...any)

Warn logs a warning-level message through the default slog handler.

func WarningLog

func WarningLog() *log.Logger

WarningLog returns the current warning-level logger. Safe to call concurrently with SetWarningLogForTest or initializeWithConfig replacing it.

func WarningS

func WarningS(message string, fields ...map[string]interface{})

WarningS logs a structured warning message

Types

type AsyncHandler added in v1.35.0

type AsyncHandler struct {
	// contains filtered or unexported fields
}

AsyncHandler wraps a slog.Handler with a channel buffer. Log calls enqueue a cloned Record and return immediately; a background goroutine drains the channel. On full buffer the record is dropped and the drop counter increments. WithAttrs and WithGroup share the same underlying channel so a single goroutine drains all derived loggers.

func NewAsyncHandler added in v1.35.0

func NewAsyncHandler(next slog.Handler, bufSize int) *AsyncHandler

NewAsyncHandler wraps next with an async channel of bufSize capacity.

func (*AsyncHandler) Dropped added in v1.35.0

func (h *AsyncHandler) Dropped() int64

Dropped returns the number of records dropped due to a full buffer.

func (*AsyncHandler) Enabled added in v1.35.0

func (h *AsyncHandler) Enabled(ctx context.Context, level slog.Level) bool

Enabled delegates to the underlying handler.

func (*AsyncHandler) Flush added in v1.35.0

func (h *AsyncHandler) Flush(_ context.Context) error

Flush closes the channel and waits for all enqueued records to be written. After Flush the handler must not be used.

func (*AsyncHandler) Handle added in v1.35.0

func (h *AsyncHandler) Handle(ctx context.Context, r slog.Record) error

Handle enqueues the record for async writing. Drops and counts if buffer full. Safe to call concurrently with Flush — the RWMutex ensures close and send are mutually exclusive: Flush cannot close the channel while a send is in progress.

func (*AsyncHandler) StartDrain added in v1.35.0

func (h *AsyncHandler) StartDrain()

StartDrain launches the background drain goroutine. Must be called once before the handler is used. Call Flush to stop it and drain remaining work.

func (*AsyncHandler) WithAttrs added in v1.35.0

func (h *AsyncHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs returns a new AsyncHandler whose next handler has the given attrs, sharing the same channel so one drain goroutine serves all derived loggers.

func (*AsyncHandler) WithGroup added in v1.35.0

func (h *AsyncHandler) WithGroup(name string) slog.Handler

WithGroup returns a new AsyncHandler with a grouped next handler, sharing the channel.

type DropCounter added in v1.49.0

type DropCounter struct {
	// contains filtered or unexported fields
}

DropCounter rate-limits repeated "dropped due to backpressure" warnings: only every Nth drop is logged, with the running total attached. Shared by session/tokens and session/artifacts, whose worker-pool queues both need this exact pattern for queue-full drops.

func (*DropCounter) Hit added in v1.49.0

func (d *DropCounter) Hit() (total uint64, shouldLog bool)

Hit records one drop and reports the new running total plus whether this particular drop should be logged (every DropLogInterval-th one).

type Every

type Every struct {
	// contains filtered or unexported fields
}

Every is used to log at most once every timeout duration.

func NewEvery

func NewEvery(timeout time.Duration) *Every

func (*Every) ShouldLog

func (e *Every) ShouldLog() bool

ShouldLog returns true if the timeout has passed since the last log.

type LogConfig

type LogConfig struct {
	LogsEnabled    bool
	LogsDir        string
	LogMaxSize     int
	LogMaxFiles    int
	LogMaxAge      int
	LogCompress    bool
	UseSessionLogs bool
	LogLevel       LogLevel // Deprecated: Use FileLevel and ConsoleLevel instead
	StructuredLogs bool
	PrettyLogs     bool // For development - formats JSON logs for readability

	// Dual-stream logging configuration (file + console)
	ConsoleEnabled bool     // Enable/disable console output (default: true)
	ConsoleLevel   LogLevel // Minimum level for console (default: ERROR for tests, INFO for production)
	FileEnabled    bool     // Enable/disable file output (default: true)
	FileLevel      LogLevel // Minimum level for file (default: DEBUG)
}

LogConfig holds logging configuration

func ConfigToLogConfig

func ConfigToLogConfig(externalConfig interface{}) *LogConfig

ConfigToLogConfig converts an external config to our internal LogConfig

func DefaultLogConfig

func DefaultLogConfig() *LogConfig

DefaultLogConfig returns the default logging configuration

type LogLevel

type LogLevel int

LogLevel represents the severity of a log entry

const (
	DEBUG LogLevel = iota
	INFO
	WARNING
	ERROR
	FATAL
)

func GetRuntimeLevel added in v1.35.0

func GetRuntimeLevel() LogLevel

GetRuntimeLevel returns the current minimum log level.

func ParseLogLevel

func ParseLogLevel(level string) LogLevel

ParseLogLevel parses a string into a LogLevel

func (LogLevel) String

func (l LogLevel) String() string

String returns the string representation of a log level

type LogManager added in v1.35.0

type LogManager struct {
	// contains filtered or unexported fields
}

LogManager encapsulates all log state that was previously in package-level globals. Use NewLogManager to create one; use the package-level functions (InfoLog, etc.) via the defaultManager for zero-migration compatibility.

func (*LogManager) Close added in v1.35.0

func (m *LogManager) Close()

Close drains async writers, flushes the slog handler, and closes all log files. Drain order matters: async writers must be drained before the underlying file is closed, otherwise buffered entries are lost.

func (*LogManager) CloseSession added in v1.35.0

func (m *LogManager) CloseSession(id string)

CloseSession removes session-scoped loggers and closes their file handle.

func (*LogManager) ForSession added in v1.35.0

func (m *LogManager) ForSession(id string) (*SessionLoggers, error)

ForSession returns or creates session-scoped loggers.

type PackageLevelHandler added in v1.49.0

type PackageLevelHandler struct {
	// contains filtered or unexported fields
}

PackageLevelHandler wraps a slog.Handler and drops records that fall below the calling package's configured minimum level, resolved from the log call's program counter. Enabled fast-paths on the global level when no override is active; Handle does the precise per-package check otherwise.

func NewPackageLevelHandler added in v1.49.0

func NewPackageLevelHandler(next slog.Handler) *PackageLevelHandler

NewPackageLevelHandler wraps next with per-package level filtering.

func (*PackageLevelHandler) Enabled added in v1.49.0

func (h *PackageLevelHandler) Enabled(_ context.Context, level slog.Level) bool

Enabled restores slog's normal fast path (level >= global threshold) for the common no-override case, so logAt's own Enabled check can still skip runtime.Callers/NewRecord/Add for a disabled level. When a per-package override is active, it reports true unconditionally — Handle is the only place with enough information (the record's PC) to resolve the precise per-package threshold.

func (*PackageLevelHandler) Handle added in v1.49.0

func (h *PackageLevelHandler) Handle(ctx context.Context, r slog.Record) error

func (*PackageLevelHandler) WithAttrs added in v1.49.0

func (h *PackageLevelHandler) WithAttrs(attrs []slog.Attr) slog.Handler

func (*PackageLevelHandler) WithGroup added in v1.49.0

func (h *PackageLevelHandler) WithGroup(name string) slog.Handler

type SessionLogger added in v1.15.0

type SessionLogger struct {
	// contains filtered or unexported fields
}

SessionLogger is a session-scoped logger that automatically injects the session ID into every log call, eliminating the need to pass the session ID manually.

Usage:

logger := log.ForSession(i.Title)
logger.Error("Failed to setup git worktree: %v", err)

func ForSessionLegacy deprecated added in v1.35.0

func ForSessionLegacy(sessionID string) *SessionLogger

ForSessionLegacy returns the old SessionLogger for callers that write to per-session log files. New code should use ForSession instead.

Deprecated: use ForSession.

func (*SessionLogger) Debug added in v1.15.0

func (sl *SessionLogger) Debug(format string, v ...interface{})

func (*SessionLogger) Error added in v1.15.0

func (sl *SessionLogger) Error(format string, v ...interface{})

func (*SessionLogger) Info added in v1.15.0

func (sl *SessionLogger) Info(format string, v ...interface{})

func (*SessionLogger) Warning added in v1.15.0

func (sl *SessionLogger) Warning(format string, v ...interface{})

type SessionLoggers

type SessionLoggers struct {
	WarningLog *log.Logger
	InfoLog    *log.Logger
	ErrorLog   *log.Logger
	DebugLog   *log.Logger
	LogFile    io.Closer
}

SessionLoggers holds the loggers for a specific session

func GetSessionLoggers

func GetSessionLoggers(sessionID string) (*SessionLoggers, error)

GetSessionLoggers creates or retrieves loggers for a specific session

type StructuredLogEntry

type StructuredLogEntry struct {
	Timestamp time.Time              `json:"timestamp"`
	Level     string                 `json:"level"`
	Message   string                 `json:"message"`
	SessionID string                 `json:"session_id,omitempty"`
	Component string                 `json:"component,omitempty"`
	Function  string                 `json:"function,omitempty"`
	File      string                 `json:"file,omitempty"`
	Line      int                    `json:"line,omitempty"`
	Fields    map[string]interface{} `json:"fields,omitempty"`
	Error     string                 `json:"error,omitempty"`
}

StructuredLogEntry represents a structured log entry

type StructuredLogger

type StructuredLogger struct {
	// contains filtered or unexported fields
}

StructuredLogger provides structured logging functionality

func NewStructuredLogger

func NewStructuredLogger(writer io.Writer, level LogLevel, prettyLog bool) *StructuredLogger

NewStructuredLogger creates a new structured logger

func (*StructuredLogger) Debug

func (sl *StructuredLogger) Debug(message string, fields ...map[string]interface{})

Debug logs a debug message

func (*StructuredLogger) Error

func (sl *StructuredLogger) Error(message string, fields ...map[string]interface{})

Error logs an error message

func (*StructuredLogger) Fatal

func (sl *StructuredLogger) Fatal(message string, fields ...map[string]interface{})

Fatal logs a fatal message

func (*StructuredLogger) Info

func (sl *StructuredLogger) Info(message string, fields ...map[string]interface{})

Info logs an info message

func (*StructuredLogger) Log

func (sl *StructuredLogger) Log(level LogLevel, message string, fields map[string]interface{})

Log writes a structured log entry

func (*StructuredLogger) LogWithFields

func (sl *StructuredLogger) LogWithFields(level LogLevel, message string, fields map[string]interface{})

LogWithFields logs a message with additional fields

func (*StructuredLogger) Warning

func (sl *StructuredLogger) Warning(message string, fields ...map[string]interface{})

Warning logs a warning message

type TraceIDHandler added in v1.35.0

type TraceIDHandler struct {
	// contains filtered or unexported fields
}

TraceIDHandler is a slog.Handler middleware that injects OTel trace_id and span_id into every log record when a span is active in the context. It must be the outermost handler in the chain so trace IDs are extracted at call time, before the record enters the async buffer.

func NewTraceIDHandler added in v1.35.0

func NewTraceIDHandler(next slog.Handler) *TraceIDHandler

NewTraceIDHandler wraps next, injecting trace context into every Handle call.

func (*TraceIDHandler) Enabled added in v1.35.0

func (h *TraceIDHandler) Enabled(ctx context.Context, level slog.Level) bool

func (*TraceIDHandler) Handle added in v1.35.0

func (h *TraceIDHandler) Handle(ctx context.Context, r slog.Record) error

func (*TraceIDHandler) WithAttrs added in v1.35.0

func (h *TraceIDHandler) WithAttrs(attrs []slog.Attr) slog.Handler

func (*TraceIDHandler) WithGroup added in v1.35.0

func (h *TraceIDHandler) WithGroup(name string) slog.Handler

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL