Files
rclone/fs/log.go
Nick Craig-Wood 15510c66d4 log: add --windows-event-log-level to support Windows Event Log
This provides JSON logs in the Windows Event Log.
2025-05-23 11:27:49 +01:00

306 lines
9.3 KiB
Go

package fs
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"slices"
)
// LogLevel describes rclone's logs. These are a subset of the syslog log levels.
type LogLevel = Enum[logLevelChoices]
// Log levels. These are the syslog levels of which we only use a
// subset.
//
// LOG_EMERG system is unusable
// LOG_ALERT action must be taken immediately
// LOG_CRIT critical conditions
// LOG_ERR error conditions
// LOG_WARNING warning conditions
// LOG_NOTICE normal, but significant, condition
// LOG_INFO informational message
// LOG_DEBUG debug-level message
const (
LogLevelEmergency LogLevel = iota
LogLevelAlert
LogLevelCritical
LogLevelError // Error - can't be suppressed
LogLevelWarning
LogLevelNotice // Normal logging, -q suppresses
LogLevelInfo // Transfers, needs -v
LogLevelDebug // Debug level, needs -vv
LogLevelOff
)
type logLevelChoices struct{}
func (logLevelChoices) Choices() []string {
return []string{
LogLevelEmergency: "EMERGENCY",
LogLevelAlert: "ALERT",
LogLevelCritical: "CRITICAL",
LogLevelError: "ERROR",
LogLevelWarning: "WARNING",
LogLevelNotice: "NOTICE",
LogLevelInfo: "INFO",
LogLevelDebug: "DEBUG",
LogLevelOff: "OFF",
}
}
func (logLevelChoices) Type() string {
return "LogLevel"
}
// slogLevel definitions defined as slog.Level constants.
// The integer values determine severity for filtering.
// Lower values are less severe (e.g., Debug), higher values are more severe (e.g., Emergency).
// We fit our extra values into slog's scale.
const (
// slog.LevelDebug slog.Level = -4
// slog.LevelInfo slog.Level = 0
SlogLevelNotice = slog.Level(2) // Between Info (0) and Warn (4)
// slog.LevelWarn slog.Level = 4
// slog.LevelError slog.Level = 8
SlogLevelCritical = slog.Level(12) // More severe than Error
SlogLevelAlert = slog.Level(16) // More severe than Critical
SlogLevelEmergency = slog.Level(20) // Most severe
SlogLevelOff = slog.Level(24) // A very high value
)
// Map our level numbers to slog level numbers
var levelToSlog = []slog.Level{
LogLevelEmergency: SlogLevelEmergency,
LogLevelAlert: SlogLevelAlert,
LogLevelCritical: SlogLevelCritical,
LogLevelError: slog.LevelError,
LogLevelWarning: slog.LevelWarn,
LogLevelNotice: SlogLevelNotice,
LogLevelInfo: slog.LevelInfo,
LogLevelDebug: slog.LevelDebug,
LogLevelOff: SlogLevelOff,
}
// LogValueItem describes keyed item for a JSON log entry
type LogValueItem struct {
key string
value any
render bool
}
// LogValue should be used as an argument to any logging calls to
// augment the JSON output with more structured information.
//
// key is the dictionary parameter used to store value.
func LogValue(key string, value any) LogValueItem {
return LogValueItem{key: key, value: value, render: true}
}
// LogValueHide should be used as an argument to any logging calls to
// augment the JSON output with more structured information.
//
// key is the dictionary parameter used to store value.
//
// String() will return a blank string - this is useful to put items
// in which don't print into the log.
func LogValueHide(key string, value any) LogValueItem {
return LogValueItem{key: key, value: value, render: false}
}
// String returns the representation of value. If render is false this
// is an empty string so LogValueItem entries won't show in the
// textual representation of logs.
func (j LogValueItem) String() string {
if !j.render {
return ""
}
if do, ok := j.value.(fmt.Stringer); ok {
return do.String()
}
return fmt.Sprint(j.value)
}
// LogLevelToSlog converts an rclone log level to log/slog log level.
func LogLevelToSlog(level LogLevel) slog.Level {
slogLevel := slog.LevelError
// NB level is unsigned so we don't check < 0 here
if int(level) < len(levelToSlog) {
slogLevel = levelToSlog[level]
}
return slogLevel
}
func logSlog(level LogLevel, text string, attrs []any) {
slog.Log(context.Background(), LogLevelToSlog(level), text, attrs...)
}
func logSlogWithObject(level LogLevel, o any, text string, attrs []any) {
if o != nil {
attrs = slices.Concat(attrs, []any{
"object", fmt.Sprintf("%+v", o),
"objectType", fmt.Sprintf("%T", o),
})
}
logSlog(level, text, attrs)
}
// LogPrint produces a log string from the arguments passed in
func LogPrint(level LogLevel, o any, text string) {
logSlogWithObject(level, o, text, nil)
}
// LogPrintf produces a log string from the arguments passed in
func LogPrintf(level LogLevel, o any, text string, args ...any) {
text = fmt.Sprintf(text, args...)
var fields []any
for _, arg := range args {
if item, ok := arg.(LogValueItem); ok {
fields = append(fields, item.key, item.value)
}
}
logSlogWithObject(level, o, text, fields)
}
// LogLevelPrint writes logs at the given level
func LogLevelPrint(level LogLevel, o any, text string) {
if GetConfig(context.TODO()).LogLevel >= level {
LogPrint(level, o, text)
}
}
// LogLevelPrintf writes logs at the given level
func LogLevelPrintf(level LogLevel, o any, text string, args ...any) {
if GetConfig(context.TODO()).LogLevel >= level {
LogPrintf(level, o, text, args...)
}
}
// Panic writes alert log output for this Object or Fs and calls panic().
// It should always be seen by the user.
func Panic(o any, text string) {
if GetConfig(context.TODO()).LogLevel >= LogLevelAlert {
LogPrint(LogLevelAlert, o, text)
}
panic(text)
}
// Panicf writes alert log output for this Object or Fs and calls panic().
// It should always be seen by the user.
func Panicf(o any, text string, args ...any) {
if GetConfig(context.TODO()).LogLevel >= LogLevelAlert {
LogPrintf(LogLevelAlert, o, text, args...)
}
panic(fmt.Sprintf(text, args...))
}
// Fatal writes critical log output for this Object or Fs and calls os.Exit(1).
// It should always be seen by the user.
func Fatal(o any, text string) {
if GetConfig(context.TODO()).LogLevel >= LogLevelCritical {
LogPrint(LogLevelCritical, o, text)
}
os.Exit(1)
}
// Fatalf writes critical log output for this Object or Fs and calls os.Exit(1).
// It should always be seen by the user.
func Fatalf(o any, text string, args ...any) {
if GetConfig(context.TODO()).LogLevel >= LogLevelCritical {
LogPrintf(LogLevelCritical, o, text, args...)
}
os.Exit(1)
}
// Error writes error log output for this Object or Fs. It
// should always be seen by the user.
func Error(o any, text string) {
LogLevelPrint(LogLevelError, o, text)
}
// Errorf writes error log output for this Object or Fs. It
// should always be seen by the user.
func Errorf(o any, text string, args ...any) {
LogLevelPrintf(LogLevelError, o, text, args...)
}
// Print writes log output for this Object or Fs, same as Logf.
func Print(o any, text string) {
LogLevelPrint(LogLevelNotice, o, text)
}
// Printf writes log output for this Object or Fs, same as Logf.
func Printf(o any, text string, args ...any) {
LogLevelPrintf(LogLevelNotice, o, text, args...)
}
// Log writes log output for this Object or Fs. This should be
// considered to be Notice level logging. It is the default level.
// By default rclone should not log very much so only use this for
// important things the user should see. The user can filter these
// out with the -q flag.
func Log(o any, text string) {
LogLevelPrint(LogLevelNotice, o, text)
}
// Logf writes log output for this Object or Fs. This should be
// considered to be Notice level logging. It is the default level.
// By default rclone should not log very much so only use this for
// important things the user should see. The user can filter these
// out with the -q flag.
func Logf(o any, text string, args ...any) {
LogLevelPrintf(LogLevelNotice, o, text, args...)
}
// Infoc writes info on transfers for this Object or Fs. Use this
// level for logging transfers, deletions and things which should
// appear with the -v flag.
// There is name class on "Info", hence the name "Infoc", "c" for constant.
func Infoc(o any, text string) {
LogLevelPrint(LogLevelInfo, o, text)
}
// Infof writes info on transfers for this Object or Fs. Use this
// level for logging transfers, deletions and things which should
// appear with the -v flag.
func Infof(o any, text string, args ...any) {
LogLevelPrintf(LogLevelInfo, o, text, args...)
}
// Debug writes debugging output for this Object or Fs. Use this for
// debug only. The user must have to specify -vv to see this.
func Debug(o any, text string) {
LogLevelPrint(LogLevelDebug, o, text)
}
// Debugf writes debugging output for this Object or Fs. Use this for
// debug only. The user must have to specify -vv to see this.
func Debugf(o any, text string, args ...any) {
LogLevelPrintf(LogLevelDebug, o, text, args...)
}
// LogDirName returns an object for the logger, logging a root
// directory which would normally be "" as the Fs
func LogDirName(f Fs, dir string) any {
if dir != "" {
return dir
}
return f
}
// PrettyPrint formats JSON for improved readability in debug logs.
// If it can't Marshal JSON, it falls back to fmt.
func PrettyPrint(in any, label string, level LogLevel) {
if GetConfig(context.TODO()).LogLevel < level {
return
}
inBytes, err := json.MarshalIndent(in, "", "\t")
if err != nil || string(inBytes) == "{}" || string(inBytes) == "[]" {
LogPrintf(level, label, "\n%+v\n", in)
return
}
LogPrintf(level, label, "\n%s\n", string(inBytes))
}