Some checks failed
check / check (pull_request) Failing after 59s
Seven findings remain that cannot be fixed without either lying about the code or making a repo-wide naming decision, so each carries a per-site //nolint directive with its justification. gosec G115 (internal/log, internal/ui): term.IsTerminal takes an int and os.File.Fd() returns a uintptr, so the conversion is forced by the API. A file descriptor always fits in an int on every platform Go supports, and a closed file yields -1, which IsTerminal reports as not a terminal. gosec G703 (internal/vaultik/verify.go): the removed path comes from os.CreateTemp a few lines above and never from user input. G703's taint analysis treats every path derived from an *os.File as tainted, so there is no code shape that clears it. 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. With this, make check exits 0 under the canonical .golangci.yml (sha256 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb, unmodified), which also unblocks issue #59. TODO.md: record this work, correct the earlier entry that claimed make check was green when lint was still red, and move the next step on to the stale-branch triage.
200 lines
5.0 KiB
Go
200 lines
5.0 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. term.IsTerminal takes an int, and a file
|
|
// descriptor always fits in one on every platform Go supports; a
|
|
// closed file yields -1, which IsTerminal reports as not a terminal.
|
|
if term.IsTerminal(int(os.Stdout.Fd())) { //nolint:gosec // G115: fd fits in int
|
|
// 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
|
|
}
|