mirror of
https://github.com/rclone/rclone.git
synced 2025-06-25 19:03:37 +08:00

This removes logrus which is not developed any more and replaces it with the new log/slog from the Go standard library. It implements its own slog Handler which is backwards compatible with all of rclone's previous logging modes.
392 lines
9.8 KiB
Go
392 lines
9.8 KiB
Go
// Interfaces for the slog package
|
|
|
|
package log
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
)
|
|
|
|
// Handler is the standard handler for the logging.
|
|
var Handler = defaultHandler()
|
|
|
|
// Create the default OutputHandler
|
|
//
|
|
// This logs to stderr with standard go logger format at level INFO.
|
|
//
|
|
// This will be adjusted by InitLogging to be the configured levels
|
|
// but it is important we have a logger running regardless of whether
|
|
// InitLogging has been called yet or not.
|
|
func defaultHandler() *OutputHandler {
|
|
// Default options for default handler
|
|
var opts = &slog.HandlerOptions{
|
|
Level: fs.LogLevelToSlog(fs.InitialLogLevel()),
|
|
}
|
|
|
|
// Create our handler
|
|
h := NewOutputHandler(os.Stderr, opts, logFormatDate|logFormatTime)
|
|
|
|
// Set the slog default handler
|
|
slog.SetDefault(slog.New(h))
|
|
|
|
// Make log.Printf logs at level Notice
|
|
slog.SetLogLoggerLevel(fs.SlogLevelNotice)
|
|
|
|
return h
|
|
}
|
|
|
|
// Map slog level names to string
|
|
var slogNames = map[slog.Level]string{
|
|
slog.LevelDebug: "DEBUG",
|
|
slog.LevelInfo: "INFO",
|
|
fs.SlogLevelNotice: "NOTICE",
|
|
slog.LevelWarn: "WARNING",
|
|
slog.LevelError: "ERROR",
|
|
fs.SlogLevelCritical: "CRITICAL",
|
|
fs.SlogLevelAlert: "ALERT",
|
|
fs.SlogLevelEmergency: "EMERGENCY",
|
|
}
|
|
|
|
// Convert a slog level to string using rclone's extra levels
|
|
func slogLevelToString(level slog.Level) string {
|
|
levelStr := slogNames[level]
|
|
if levelStr == "" {
|
|
levelStr = level.String()
|
|
}
|
|
return levelStr
|
|
}
|
|
|
|
// ReplaceAttr function to customize the level key's string value in logs
|
|
func mapLogLevelNames(groups []string, a slog.Attr) slog.Attr {
|
|
if a.Key == slog.LevelKey {
|
|
level, ok := a.Value.Any().(slog.Level)
|
|
if !ok {
|
|
return a
|
|
}
|
|
levelStr := strings.ToLower(slogLevelToString(level))
|
|
a.Value = slog.StringValue(levelStr)
|
|
}
|
|
return a
|
|
}
|
|
|
|
// get the file and line number of the caller skipping skip levels
|
|
func getCaller(skip int) string {
|
|
var pc [64]uintptr
|
|
n := runtime.Callers(skip, pc[:])
|
|
if n == 0 {
|
|
return ""
|
|
}
|
|
frames := runtime.CallersFrames(pc[:n])
|
|
var more = true
|
|
var frame runtime.Frame
|
|
for more {
|
|
frame, more = frames.Next()
|
|
|
|
file := frame.File
|
|
if strings.Contains(file, "/log/") || strings.HasSuffix(file, "log.go") {
|
|
continue
|
|
}
|
|
line := frame.Line
|
|
|
|
// shorten file name
|
|
n := 0
|
|
for i := len(file) - 1; i > 0; i-- {
|
|
if file[i] == '/' {
|
|
n++
|
|
if n >= 2 {
|
|
file = file[i+1:]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return fmt.Sprintf("%s:%d", file, line)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// OutputHandler is a slog.Handler that writes log records in a format
|
|
// identical to the standard library's `log` package (e.g., "YYYY/MM/DD HH:MM:SS message").
|
|
//
|
|
// It can also write logs in JSON format identical to logrus.
|
|
type OutputHandler struct {
|
|
opts slog.HandlerOptions
|
|
levelVar slog.LevelVar
|
|
writer io.Writer
|
|
mu sync.Mutex
|
|
output []outputFn // log to writer if empty or the last item
|
|
outputExtra []outputExtra // log to all these additional places
|
|
format logFormat
|
|
jsonBuf bytes.Buffer
|
|
jsonHandler *slog.JSONHandler
|
|
}
|
|
|
|
// Records the type and function pointer for extra logging output.
|
|
type outputExtra struct {
|
|
json bool
|
|
output outputFn
|
|
}
|
|
|
|
// Define the type of the override logger
|
|
type outputFn func(level slog.Level, text string)
|
|
|
|
// NewOutputHandler creates a new OutputHandler with the specified flags.
|
|
//
|
|
// This is designed to use log/slog but produce output which is
|
|
// backwards compatible with previous rclone versions.
|
|
//
|
|
// If opts is nil, default options are used, with Level set to
|
|
// slog.LevelInfo.
|
|
func NewOutputHandler(out io.Writer, opts *slog.HandlerOptions, format logFormat) *OutputHandler {
|
|
h := &OutputHandler{
|
|
writer: out,
|
|
format: format,
|
|
}
|
|
if opts != nil {
|
|
h.opts = *opts
|
|
}
|
|
if h.opts.Level == nil {
|
|
h.opts.Level = slog.LevelInfo
|
|
}
|
|
// Set the level var with the configured level
|
|
h.levelVar.Set(h.opts.Level.Level())
|
|
// And use it from now on
|
|
h.opts.Level = &h.levelVar
|
|
|
|
// Create the JSON logger in case we need it
|
|
jsonOpts := slog.HandlerOptions{
|
|
Level: h.opts.Level,
|
|
ReplaceAttr: mapLogLevelNames,
|
|
}
|
|
h.jsonHandler = slog.NewJSONHandler(&h.jsonBuf, &jsonOpts)
|
|
return h
|
|
}
|
|
|
|
// SetOutput sets a new output handler for the log output.
|
|
//
|
|
// This is for temporarily overriding the output.
|
|
func (h *OutputHandler) SetOutput(fn outputFn) {
|
|
h.output = append(h.output, fn)
|
|
}
|
|
|
|
// ResetOutput resets the log output to what is was.
|
|
func (h *OutputHandler) ResetOutput() {
|
|
if len(h.output) > 0 {
|
|
h.output = h.output[:len(h.output)-1]
|
|
}
|
|
}
|
|
|
|
// AddOutput adds an additional logging destination of the type specified.
|
|
func (h *OutputHandler) AddOutput(json bool, fn outputFn) {
|
|
h.outputExtra = append(h.outputExtra, outputExtra{
|
|
json: json,
|
|
output: fn,
|
|
})
|
|
}
|
|
|
|
// SetLevel sets a new log level, returning the old one.
|
|
func (h *OutputHandler) SetLevel(level slog.Level) slog.Level {
|
|
oldLevel := h.levelVar.Level()
|
|
h.levelVar.Set(level)
|
|
return oldLevel
|
|
}
|
|
|
|
// Set the writer for the log to that passed.
|
|
func (h *OutputHandler) setWriter(writer io.Writer) {
|
|
h.writer = writer
|
|
}
|
|
|
|
// Set the format flags to that passed in.
|
|
func (h *OutputHandler) setFormat(format logFormat) {
|
|
h.format = format
|
|
}
|
|
|
|
// clear format flags that this output type doesn't want
|
|
func (h *OutputHandler) clearFormatFlags(bitMask logFormat) {
|
|
h.format &^= bitMask
|
|
}
|
|
|
|
// set format flags that this output type requires
|
|
func (h *OutputHandler) setFormatFlags(bitMask logFormat) {
|
|
h.format |= bitMask
|
|
}
|
|
|
|
// Enabled returns whether this logger is enabled for this level.
|
|
func (h *OutputHandler) Enabled(_ context.Context, level slog.Level) bool {
|
|
minLevel := slog.LevelInfo
|
|
if h.opts.Level != nil {
|
|
minLevel = h.opts.Level.Level()
|
|
}
|
|
return level >= minLevel
|
|
}
|
|
|
|
// Create a log header in Go standard log format.
|
|
func (h *OutputHandler) formatStdLogHeader(buf *bytes.Buffer, level slog.Level, t time.Time, object string, lineInfo string) {
|
|
// Add time in Go standard format if requested
|
|
if h.format&(logFormatDate|logFormatTime|logFormatMicroseconds) != 0 {
|
|
if h.format&logFormatUTC != 0 {
|
|
t = t.UTC()
|
|
}
|
|
if h.format&logFormatDate != 0 {
|
|
year, month, day := t.Date()
|
|
fmt.Fprintf(buf, "%04d/%02d/%02d ", year, month, day)
|
|
}
|
|
if h.format&(logFormatTime|logFormatMicroseconds) != 0 {
|
|
hour, min, sec := t.Clock()
|
|
fmt.Fprintf(buf, "%02d:%02d:%02d", hour, min, sec)
|
|
if h.format&logFormatMicroseconds != 0 {
|
|
fmt.Fprintf(buf, ".%06d", t.Nanosecond()/1e3)
|
|
}
|
|
buf.WriteByte(' ')
|
|
}
|
|
}
|
|
// Add source code filename:line if requested
|
|
if h.format&(logFormatShortFile|logFormatLongFile) != 0 && lineInfo != "" {
|
|
buf.WriteString(lineInfo)
|
|
buf.WriteByte(':')
|
|
buf.WriteByte(' ')
|
|
}
|
|
// Add PID if requested
|
|
if h.format&logFormatPid != 0 {
|
|
fmt.Fprintf(buf, "[%d] ", os.Getpid())
|
|
}
|
|
// Add log level if required
|
|
if h.format&logFormatNoLevel == 0 {
|
|
levelStr := slogLevelToString(level)
|
|
fmt.Fprintf(buf, "%-6s: ", levelStr)
|
|
}
|
|
// Add object if passed
|
|
if object != "" {
|
|
buf.WriteString(object)
|
|
buf.WriteByte(':')
|
|
buf.WriteByte(' ')
|
|
}
|
|
}
|
|
|
|
// Create a log in standard Go log format into buf.
|
|
func (h *OutputHandler) textLog(ctx context.Context, buf *bytes.Buffer, r slog.Record) error {
|
|
var lineInfo string
|
|
if h.format&(logFormatShortFile|logFormatLongFile) != 0 {
|
|
lineInfo = getCaller(2)
|
|
}
|
|
|
|
var object string
|
|
r.Attrs(func(attr slog.Attr) bool {
|
|
if attr.Key == "object" {
|
|
object = attr.Value.String()
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
h.formatStdLogHeader(buf, r.Level, r.Time, object, lineInfo)
|
|
buf.WriteString(r.Message)
|
|
if buf.Len() == 0 || buf.Bytes()[buf.Len()-1] != '\n' { // Ensure newline
|
|
buf.WriteByte('\n')
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Create a log in JSON format into buf.
|
|
func (h *OutputHandler) jsonLog(ctx context.Context, buf *bytes.Buffer, r slog.Record) (err error) {
|
|
// Call the JSON handler to create the JSON in buf
|
|
r.AddAttrs(
|
|
slog.String("source", getCaller(2)),
|
|
)
|
|
h.mu.Lock()
|
|
err = h.jsonHandler.Handle(ctx, r)
|
|
if err == nil {
|
|
_, err = h.jsonBuf.WriteTo(buf)
|
|
}
|
|
h.mu.Unlock()
|
|
return err
|
|
}
|
|
|
|
// Handle outputs a log in the current format
|
|
func (h *OutputHandler) Handle(ctx context.Context, r slog.Record) (err error) {
|
|
var (
|
|
bufJSON *bytes.Buffer
|
|
bufText *bytes.Buffer
|
|
buf *bytes.Buffer
|
|
)
|
|
|
|
// Check whether we need to build Text or JSON logs or both
|
|
needJSON := h.format&logFormatJSON != 0
|
|
needText := !needJSON
|
|
for _, out := range h.outputExtra {
|
|
if out.json {
|
|
needJSON = true
|
|
} else {
|
|
needText = true
|
|
}
|
|
}
|
|
|
|
if needJSON {
|
|
var bufJSONBack [256]byte
|
|
bufJSON = bytes.NewBuffer(bufJSONBack[:0])
|
|
err = h.jsonLog(ctx, bufJSON, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if needText {
|
|
var bufTextBack [256]byte
|
|
bufText = bytes.NewBuffer(bufTextBack[:0])
|
|
err = h.textLog(ctx, bufText, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Do the log, either to the default destination or to the alternate logging system
|
|
if h.format&logFormatJSON != 0 {
|
|
buf = bufJSON
|
|
} else {
|
|
buf = bufText
|
|
}
|
|
if len(h.output) > 0 {
|
|
h.output[len(h.output)-1](r.Level, buf.String())
|
|
err = nil
|
|
} else {
|
|
_, err = h.writer.Write(buf.Bytes())
|
|
}
|
|
|
|
// Log to any additional destinations required
|
|
for _, out := range h.outputExtra {
|
|
if out.json {
|
|
out.output(r.Level, bufJSON.String())
|
|
} else {
|
|
out.output(r.Level, bufText.String())
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// WithAttrs creates a new handler with the same writer, options, and flags.
|
|
// Attributes are ignored for the output format of this specific handler.
|
|
func (h *OutputHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
return NewOutputHandler(h.writer, &h.opts, h.format)
|
|
}
|
|
|
|
// WithGroup creates a new handler with the same writer, options, and flags.
|
|
// Groups are ignored for the output format of this specific handler.
|
|
func (h *OutputHandler) WithGroup(name string) slog.Handler {
|
|
return NewOutputHandler(h.writer, &h.opts, h.format)
|
|
}
|
|
|
|
// Check interface
|
|
var _ slog.Handler = (*OutputHandler)(nil)
|