mirror of
https://github.com/rclone/rclone.git
synced 2025-11-23 21:44:49 +02:00
400 lines
10 KiB
Go
400 lines
10 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
|
|
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])
|
|
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.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.output = append(h.output, fn)
|
|
}
|
|
|
|
// ResetOutput resets the log output to what is was.
|
|
func (h *OutputHandler) ResetOutput() {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
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.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
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 {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
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)
|