Files
vaultik/internal/log/log.go
sneak efb0cea1c2
All checks were successful
check / check (pull_request) Successful in 3m11s
Suppress the revive package-name findings with no fix (closes #61)
Three findings remain that cannot be fixed without making a repo-wide
naming decision, so each carries a per-site //nolint directive with its
justification.

revive var-naming (internal/log, internal/crypto, internal/types):
fixing these means renaming packages across the whole codebase, which
is the repo owner's call, not a lint fix. Neither stdlib log nor stdlib
crypto is imported anywhere in the repo, so nothing is actually
shadowed today. The rename decision is tracked in issue #76. revive
reports a package-name failure only once per package directory, on
whichever file it happens to lint first, so every file of the affected
packages carries the directive and lists nolintlint alongside revive so
the ones that lose the race are not reported as unused.

No gosec directives are needed: the pinned golangci-lint v2.12.2 that
CI and the Dockerfile use reports nothing at the term.IsTerminal
conversions in internal/log and internal/ui or at the os.Remove calls
in internal/vaultik/verify.go, so suppressing there would itself fail
nolintlint as an unused directive.

Verification is script/cibuild, which builds the hash-pinned lint
image: it exits 0, with make lint reporting "0 issues" under the
canonical .golangci.yml (sha256
021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb,
unmodified), plus make fmt-check, make test and the release build. That
also unblocks issue #59. make check is not a valid gate here: script/lint
runs whatever golangci-lint is on PATH rather than the pinned version,
which is tracked in issue #78.

Also corrects the capacity comment in collectBatchFlushData: an empty
file contributes no chunk mappings, so the pending-file count is a
rough starting capacity, not a lower bound.

TODO.md: record this work, note that the previous next step (reconciling
uncommitted ARCHITECTURE.md edits) needed no work because the tree is
clean, and move the next step on to the stale-branch triage.
2026-08-09 02:13:57 +00:00

198 lines
4.8 KiB
Go

// Package log provides the application-wide structured logger: slog
// with a colorized TTY handler on terminals and JSON output otherwise.
package log //nolint:revive,nolintlint // stdlib log unused here; see #76
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"runtime"
"strings"
"golang.org/x/term"
)
// Level represents the logging level.
type Level int
const (
// LevelFatal represents a fatal error level that will exit the program.
LevelFatal Level = iota
// LevelError represents an error level.
LevelError
// LevelWarn represents a warning level.
LevelWarn
// LevelNotice represents a notice level (mapped to Info in slog).
LevelNotice
// LevelInfo represents an informational level.
LevelInfo
// LevelDebug represents a debug level.
LevelDebug
)
// Config holds logger configuration.
type Config struct {
Verbose bool
Debug bool
Cron bool
Quiet bool
}
//nolint:gochecknoglobals // package-level logger is the package's purpose
var logger *slog.Logger
// Initialize sets up the global logger based on the provided configuration.
func Initialize(cfg Config) {
// Determine log level based on configuration
var level slog.Level
switch {
case cfg.Cron || cfg.Quiet:
// In cron/quiet mode keep warnings and errors visible — the
// whole point of --cron is to stay silent only on total
// success, so that anything cron emails to root is genuinely
// "something went wrong, look at it." A backup with stuck
// permission errors or skipped files should NOT be silent.
level = slog.LevelWarn
case cfg.Debug || strings.Contains(os.Getenv("GODEBUG"), "vaultik"):
level = slog.LevelDebug
case cfg.Verbose:
level = slog.LevelInfo
default:
level = slog.LevelWarn
}
// Create handler with appropriate level
opts := &slog.HandlerOptions{
Level: level,
}
// Check if stdout is a TTY.
if term.IsTerminal(int(os.Stdout.Fd())) {
// Use colorized TTY handler
logger = slog.New(NewTTYHandler(os.Stdout, opts))
} else {
// Use JSON format for non-TTY output
logger = slog.New(slog.NewJSONHandler(os.Stdout, opts))
}
// Set as default logger
slog.SetDefault(logger)
}
// callerSkipFrames is the number of stack frames between runtime.Caller
// and the code that invoked the package-level logging function.
const callerSkipFrames = 2
// getCaller returns the caller information as a string
func getCaller() string {
_, file, line, ok := runtime.Caller(callerSkipFrames)
if !ok {
return "unknown"
}
return fmt.Sprintf("%s:%d", filepath.Base(file), line)
}
// Fatal logs a fatal error message and exits the program with code 1.
func Fatal(msg string, args ...any) {
if logger != nil {
// Add caller info to args
args = append(args, "caller", getCaller())
logger.Error(msg, args...)
}
os.Exit(1)
}
// Fatalf logs a formatted fatal error message and exits the program with code 1.
func Fatalf(format string, args ...any) {
Fatal(fmt.Sprintf(format, args...))
}
// Error logs an error message.
func Error(msg string, args ...any) {
if logger != nil {
args = append(args, "caller", getCaller())
logger.Error(msg, args...)
}
}
// Errorf logs a formatted error message.
func Errorf(format string, args ...any) {
Error(fmt.Sprintf(format, args...))
}
// Warn logs a warning message.
func Warn(msg string, args ...any) {
if logger != nil {
args = append(args, "caller", getCaller())
logger.Warn(msg, args...)
}
}
// Warnf logs a formatted warning message.
func Warnf(format string, args ...any) {
Warn(fmt.Sprintf(format, args...))
}
// Notice logs a notice message (mapped to Info level).
func Notice(msg string, args ...any) {
if logger != nil {
args = append(args, "caller", getCaller())
logger.Info(msg, args...)
}
}
// Noticef logs a formatted notice message.
func Noticef(format string, args ...any) {
Notice(fmt.Sprintf(format, args...))
}
// Info logs an informational message.
func Info(msg string, args ...any) {
if logger != nil {
args = append(args, "caller", getCaller())
logger.Info(msg, args...)
}
}
// Infof logs a formatted informational message.
func Infof(format string, args ...any) {
Info(fmt.Sprintf(format, args...))
}
// Debug logs a debug message.
func Debug(msg string, args ...any) {
if logger != nil {
args = append(args, "caller", getCaller())
logger.Debug(msg, args...)
}
}
// Debugf logs a formatted debug message.
func Debugf(format string, args ...any) {
Debug(fmt.Sprintf(format, args...))
}
// With returns a logger with additional context attributes.
func With(args ...any) *slog.Logger {
if logger != nil {
return logger.With(args...)
}
return slog.Default()
}
// WithContext returns a logger with the provided context.
func WithContext(_ context.Context) *slog.Logger {
return logger
}
// Logger returns the underlying slog.Logger instance.
func Logger() *slog.Logger {
return logger
}