Documentation
¶
Overview ¶
Package logging provides self-managed log file rotation, context-propagated structured logging, and a queryable SQLite index for Thane.
The Rotator implements io.WriteCloser and handles daily log rotation with optional gzip compression of previous days' files. Log files are organized by date:
logs/ thane.log # current, active thane-2026-03-09.log.gz # rotated daily, compressed thane-2026-03-08.log.gz ...never deleted...
The WithLogger / Logger helpers thread a *slog.Logger through context.Context so that every log line in a request chain automatically carries trace fields (request_id, session, conversation, subsystem, iteration index).
ShortenSource strips the module prefix from source file paths when slog's AddSource option is enabled, keeping log lines compact.
The IndexHandler wraps any slog.Handler and simultaneously indexes every log record into a SQLite database. Promoted fields (request_id, session_id, conversation_id, subsystem, tool, model) are extracted into indexed columns for fast queries; remaining attributes go into a JSON catch-all. Use Prune to manage index retention while preserving the raw log files as the canonical record.
Index ¶
- Constants
- func ChainReplaceAttr(fns ...func([]string, slog.Attr) slog.Attr) func([]string, slog.Attr) slog.Attr
- func Logger(ctx context.Context) *slog.Logger
- func Migrate(db *sql.DB) error
- func Prune(db *sql.DB, maxAge time.Duration, minKeepLevel slog.Level) (int64, error)
- func ShortenSource(_ []string, a slog.Attr) slog.Attr
- func WithLogger(ctx context.Context, logger *slog.Logger) context.Context
- type Archiver
- type ContentWriter
- type IndexHandler
- func (h *IndexHandler) Close()
- func (h *IndexHandler) Enabled(ctx context.Context, level slog.Level) bool
- func (h *IndexHandler) Handle(ctx context.Context, r slog.Record) error
- func (h *IndexHandler) WithAttrs(attrs []slog.Attr) slog.Handler
- func (h *IndexHandler) WithGroup(name string) slog.Handler
- type LiveRequestStore
- type LogEntry
- type QueryParams
- type RequestContent
- type RequestDetail
- type RequestRecordFunc
- type Rotator
- type ToolDetail
Constants ¶
const ( SubsystemAgent = "agent" SubsystemDelegate = "delegate" SubsystemSignal = "signal" SubsystemScheduler = "scheduler" SubsystemMetacog = "metacog" SubsystemLoop = "loop" SubsystemAPI = "api" )
Standard subsystem names for structured log filtering.
const DefaultLiveRequestStoreSize = 512
DefaultLiveRequestStoreSize bounds the number of recent request detail records kept in memory for live inspection when archival storage is disabled.
Variables ¶
This section is empty.
Functions ¶
func ChainReplaceAttr ¶
func ChainReplaceAttr(fns ...func([]string, slog.Attr) slog.Attr) func([]string, slog.Attr) slog.Attr
ChainReplaceAttr composes multiple slog.HandlerOptions.ReplaceAttr functions into one. Each function is applied in order, so earlier functions can transform the attribute before later ones see it.
func Logger ¶
Logger extracts the *slog.Logger stored by WithLogger. If no logger is present (or nil was stored), it returns slog.Default as a safe fallback so callers never need nil checks.
func Migrate ¶
Migrate creates or upgrades the log_entries table and indexes. Call this once after opening the database and before logging begins.
func Prune ¶
Prune deletes log index entries older than maxAge whose level is strictly below minKeepLevel. For example, passing slog.LevelInfo prunes DEBUG and TRACE entries while keeping INFO, WARN, and ERROR. Returns the number of rows deleted.
func ShortenSource ¶
ShortenSource is a slog.HandlerOptions.ReplaceAttr function that strips the module prefix from source file and function paths when AddSource is enabled. This yields compact output like "internal/agent/loop.go:730" and "internal/agent.(*Loop).Run" instead of fully qualified module paths. Requires -trimpath in the build flags so Go embeds module-relative paths rather than absolute filesystem paths.
func WithLogger ¶
WithLogger returns a copy of ctx carrying logger. Retrieve it with Logger. Typically called at request entry points to inject a logger pre-enriched with trace fields (request_id, subsystem, etc.), then again at iteration boundaries to add the iteration index.
Types ¶
type Archiver ¶ added in v0.9.1
type Archiver struct {
// contains filtered or unexported fields
}
Archiver exports old log_request_content rows to monthly JSONL files and removes them from the database. Each archived line is a self-contained JSON object (RequestDetail with nested ToolCalls and the resolved system prompt) so the flat files need no external index to be useful.
Archive files are written to {dir}/archive/YYYY-MM.jsonl and are appended to on each run, so the operation is safe to repeat. If the process is interrupted after writing but before the database delete, some rows will be re-archived on the next run (harmless duplicate lines in the JSONL).
log_prompts rows are never deleted — they are content-addressed and bounded by system prompt variation, not request volume.
func NewArchiver ¶ added in v0.9.1
NewArchiver creates an Archiver that writes JSONL files directly into dir. The caller is responsible for resolving the directory path (see LoggingConfig.ContentArchiveDirPath for the default).
type ContentWriter ¶ added in v0.9.1
type ContentWriter struct {
// contains filtered or unexported fields
}
ContentWriter writes request-level content (system prompts, tool call details, message bodies) to the log index database. It is safe for concurrent use.
func NewContentWriter ¶ added in v0.9.1
NewContentWriter creates a writer for the given logs.db connection. maxLen controls the maximum character count for retained content fields (tool results, message bodies). Pass 0 for unlimited.
func (*ContentWriter) Close ¶ added in v0.9.1
func (w *ContentWriter) Close() error
Close releases prepared statements.
func (*ContentWriter) WriteRequest ¶ added in v0.9.1
func (w *ContentWriter) WriteRequest(ctx context.Context, rc RequestContent)
WriteRequest persists a completed request's content. The system prompt is stored content-addressed (deduplicated by SHA-256 hash). Tool call arguments and results are extracted from the message history. Errors are logged but not returned — content retention is best-effort and must never block request processing.
type IndexHandler ¶
type IndexHandler struct {
// contains filtered or unexported fields
}
IndexHandler is an slog.Handler that wraps another handler and simultaneously indexes every log record into a SQLite database. The wrapped handler produces the canonical raw log output (file and/or stdout); the SQLite index enables fast queries by time, level, request ID, subsystem, etc.
Writes to SQLite happen asynchronously via a buffered channel so that logging never blocks on database I/O. The channel is drained by a single background goroutine started by NewIndexHandler. Call IndexHandler.Close to flush pending entries and stop the background goroutine.
func NewIndexHandler ¶
NewIndexHandler wraps inner with a SQLite indexing handler. The db must be an open SQLite connection (typically from [database.Open]). If rotator is non-nil, each entry records the raw log filename and line number for back-linking.
The caller must call IndexHandler.Close on shutdown to flush pending entries and release the background goroutine.
func (*IndexHandler) Close ¶
func (h *IndexHandler) Close()
Close flushes pending index entries and stops the background goroutine. It is safe to call multiple times.
func (*IndexHandler) Enabled ¶
Enabled reports whether the handler handles records at the given level.
func (*IndexHandler) Handle ¶
Handle delegates to the wrapped handler and then asynchronously indexes the record into SQLite.
type LiveRequestStore ¶ added in v0.9.1
type LiveRequestStore struct {
// contains filtered or unexported fields
}
LiveRequestStore keeps a bounded in-memory buffer of recent request details for live forensics independently of any persistent log index.
func NewLiveRequestStore ¶ added in v0.9.1
func NewLiveRequestStore(maxEntries, maxLen int) *LiveRequestStore
NewLiveRequestStore creates a bounded in-memory request detail store. maxEntries defaults to DefaultLiveRequestStoreSize when non-positive. maxLen follows the same semantics as logging max content length: non-positive means unlimited.
func (*LiveRequestStore) QueryRequestDetail ¶ added in v0.9.1
func (s *LiveRequestStore) QueryRequestDetail(requestID string) (*RequestDetail, error)
QueryRequestDetail returns a copy of the stored request detail, or nil when the request is no longer present in the live buffer.
func (*LiveRequestStore) WriteRequest ¶ added in v0.9.1
func (s *LiveRequestStore) WriteRequest(_ context.Context, rc RequestContent)
WriteRequest stores the latest request detail snapshot in memory.
type LogEntry ¶
type LogEntry struct {
ID int64
Timestamp time.Time
Level string
Msg string
RequestID string
SessionID string
ConversationID string
Subsystem string
Tool string
Model string
LoopID string
LoopName string
Attrs string
SourceFile string
SourceLine int
}
LogEntry is an exported representation of a log index row suitable for display in the web dashboard and tool queries.
func Query ¶ added in v0.8.3
func Query(db *sql.DB, params QueryParams) ([]LogEntry, error)
Query returns log entries matching the given filter parameters, ordered by timestamp ascending (chronological). The limit defaults to 50 and is capped at 200. Level filtering is "minimum level" — e.g., WARN returns WARN and ERROR entries.
func QueryBySession ¶
QueryBySession returns log entries matching the given session ID, ordered by timestamp ascending (chronological). When limit is positive, only the most recent limit entries are returned — the query selects newest-first and then reverses in Go so callers always receive chronological order. Optional filters narrow by level and/or subsystem.
type QueryParams ¶ added in v0.8.3
type QueryParams struct {
SessionID string
ConversationID string
RequestID string
Subsystem string
Tool string
Model string
LoopID string
LoopName string
Level string // minimum level: ERROR > WARN > INFO > DEBUG
Since time.Time // zero = no lower bound
Until time.Time // zero = defaults to now
Pattern string // substring match on msg
SourceFilePrefix string // prefix match on source_file (e.g., "cmd/thane/")
ExcludeSourcePrefixes []string // exclude entries whose source_file starts with any of these
Limit int // default 50, max 200
}
QueryParams holds filter criteria for querying the log index. All fields are optional — zero values are ignored. Level is treated as a minimum severity: WARN returns WARN and ERROR entries, DEBUG returns everything including TRACE.
type RequestContent ¶ added in v0.9.1
type RequestContent struct {
RequestID string
SystemPrompt string // full assembled system prompt
UserContent string // inbound user message
Model string
// From iterate.Result:
AssistantContent string
IterationCount int
InputTokens int
OutputTokens int
ToolsUsed map[string]int
Exhausted bool
ExhaustReason string
// Full message history for tool call extraction.
Messages []llm.Message
}
RequestContent holds the data to persist for a completed request.
type RequestDetail ¶ added in v0.9.1
type RequestDetail struct {
RequestID string `json:"request_id"`
PromptHash string `json:"prompt_hash,omitempty"`
SystemPrompt string `json:"system_prompt,omitempty"`
UserContent string `json:"user_content,omitempty"`
AssistantContent string `json:"assistant_content,omitempty"`
Model string `json:"model,omitempty"`
IterationCount int `json:"iteration_count"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
ToolsUsed map[string]int `json:"tools_used,omitempty"`
Exhausted bool `json:"exhausted"`
ExhaustReason string `json:"exhaust_reason,omitempty"`
CreatedAt string `json:"created_at"`
ToolCalls []ToolDetail `json:"tool_calls"`
}
RequestDetail holds the full content retained for a single request, ready for JSON serialization by the web API.
func QueryRequestDetail ¶ added in v0.9.1
func QueryRequestDetail(db *sql.DB, requestID string) (*RequestDetail, error)
QueryRequestDetail fetches the full retained content for a request by its request ID. Returns nil, nil if the request is not found or if content retention was not active when the request was processed.
type RequestRecordFunc ¶ added in v0.9.1
type RequestRecordFunc func(ctx context.Context, rc RequestContent)
RequestRecordFunc captures completed request content for later inspection.
func CombineRequestRecorders ¶ added in v0.9.1
func CombineRequestRecorders(recorders ...RequestRecordFunc) RequestRecordFunc
CombineRequestRecorders fan-outs request content to every non-nil recorder. It returns nil when no recorders are provided.
type Rotator ¶
type Rotator struct {
// contains filtered or unexported fields
}
Rotator is a file-backed io.WriteCloser that rotates log files at date boundaries. Each day's log is written to thane.log; on rotation the previous day's file is renamed to thane-YYYY-MM-DD.log (and optionally gzip-compressed to thane-YYYY-MM-DD.log.gz).
Rotator is safe for concurrent use.
func Open ¶
Open creates a Rotator that writes to dir/thane.log. The directory is created if it does not exist. If compress is true, rotated files are gzip-compressed.
On open, if thane.log already exists and belongs to a previous date (determined by the file's last modification time), it is rotated immediately.
func (*Rotator) ActiveFile ¶
ActiveFile returns the filename (not full path) of the current active log file. This is always [activeLogName] ("thane.log").
type ToolDetail ¶ added in v0.9.1
type ToolDetail struct {
ToolCallID string `json:"tool_call_id,omitempty"`
ToolName string `json:"tool_name"`
Arguments string `json:"arguments,omitempty"`
Result string `json:"result,omitempty"`
IterationIndex int `json:"iteration_index"`
}
ToolDetail holds the retained content for a single tool invocation.