mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-14 01:04:14 +00:00
76af9ba53d
This implements a new debug/trace infrastructure based on a slightly hacked up logger. Instead of the traditional "if debug { ... }" I've rewritten the logger to have no-op Debugln and Debugf, unless debugging has been enabled for a given "facility". The "facility" is just a string, typically a package name. This will be slightly slower than before; but not that much as it's mostly a function call that returns immediately. For the cases where it matters (the Debugln takes a hex.Dump() of something for example, and it's not in a very occasional "if err != nil" branch) there is an l.ShouldDebug(facility) that is fast enough to be used like the old "if debug". The point of all this is that we can now toggle debugging for the various packages on and off at runtime. There's a new method /rest/system/debug that can be POSTed a set of facilities to enable and disable debug for, or GET from to get a list of facilities with descriptions and their current debug status. Similarly a /rest/system/log?since=... can grab the latest log entries, up to 250 of them (hardcoded constant in main.go) plus the initial few. Not implemented in this commit (but planned) is a simple debug GUI available on /debug that shows the current log in an easily pasteable format and has checkboxes to enable the various debug facilities. The debug instructions to a user then becomes "visit this URL, check these boxes, reproduce your problem, copy and paste the log". The actual log viewer on the hypothetical /debug URL can poll regularly for new log entries and this bypass the 250 line limit. The existing STTRACE=foo variable is still obeyed and just sets the start state of the system.
380 lines
9.0 KiB
Go
380 lines
9.0 KiB
Go
// Copyright (C) 2014 Jakob Borg. All rights reserved. Use of this source code
|
|
// is governed by an MIT-style license that can be found in the LICENSE file.
|
|
|
|
// Package logger implements a standardized logger with callback functionality
|
|
package logger
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// This package uses stdlib sync as it may be used to debug syncthing/lib/sync
|
|
// and that would cause an implosion of the universe.
|
|
|
|
type LogLevel int
|
|
|
|
const (
|
|
LevelDebug LogLevel = iota
|
|
LevelVerbose
|
|
LevelInfo
|
|
LevelOK
|
|
LevelWarn
|
|
LevelFatal
|
|
NumLevels
|
|
)
|
|
|
|
// A MessageHandler is called with the log level and message text.
|
|
type MessageHandler func(l LogLevel, msg string)
|
|
|
|
type Logger interface {
|
|
AddHandler(level LogLevel, h MessageHandler)
|
|
SetFlags(flag int)
|
|
SetPrefix(prefix string)
|
|
Debugln(vals ...interface{})
|
|
Debugf(format string, vals ...interface{})
|
|
Verboseln(vals ...interface{})
|
|
Verbosef(format string, vals ...interface{})
|
|
Infoln(vals ...interface{})
|
|
Infof(format string, vals ...interface{})
|
|
Okln(vals ...interface{})
|
|
Okf(format string, vals ...interface{})
|
|
Warnln(vals ...interface{})
|
|
Warnf(format string, vals ...interface{})
|
|
Fatalln(vals ...interface{})
|
|
Fatalf(format string, vals ...interface{})
|
|
ShouldDebug(facility string) bool
|
|
SetDebug(facility string, enabled bool)
|
|
Facilities() map[string]string
|
|
FacilityDebugging() []string
|
|
NewFacility(facility, description string) Logger
|
|
}
|
|
|
|
type logger struct {
|
|
logger *log.Logger
|
|
handlers [NumLevels][]MessageHandler
|
|
facilities map[string]string // facility name => description
|
|
debug map[string]bool // facility name => debugging enabled
|
|
mut sync.Mutex
|
|
}
|
|
|
|
// The default logger logs to standard output with a time prefix.
|
|
var DefaultLogger = New()
|
|
|
|
func New() Logger {
|
|
if os.Getenv("LOGGER_DISCARD") != "" {
|
|
// Hack to completely disable logging, for example when running benchmarks.
|
|
return &logger{
|
|
logger: log.New(ioutil.Discard, "", 0),
|
|
}
|
|
}
|
|
|
|
return &logger{
|
|
logger: log.New(os.Stdout, "", log.Ltime),
|
|
}
|
|
}
|
|
|
|
// AddHandler registers a new MessageHandler to receive messages with the
|
|
// specified log level or above.
|
|
func (l *logger) AddHandler(level LogLevel, h MessageHandler) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
l.handlers[level] = append(l.handlers[level], h)
|
|
}
|
|
|
|
// See log.SetFlags
|
|
func (l *logger) SetFlags(flag int) {
|
|
l.logger.SetFlags(flag)
|
|
}
|
|
|
|
// See log.SetPrefix
|
|
func (l *logger) SetPrefix(prefix string) {
|
|
l.logger.SetPrefix(prefix)
|
|
}
|
|
|
|
func (l *logger) callHandlers(level LogLevel, s string) {
|
|
for ll := LevelDebug; ll <= level; ll++ {
|
|
for _, h := range l.handlers[ll] {
|
|
h(level, strings.TrimSpace(s))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Debugln logs a line with a DEBUG prefix.
|
|
func (l *logger) Debugln(vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintln(vals...)
|
|
l.logger.Output(2, "DEBUG: "+s)
|
|
l.callHandlers(LevelDebug, s)
|
|
}
|
|
|
|
// Debugf logs a formatted line with a DEBUG prefix.
|
|
func (l *logger) Debugf(format string, vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintf(format, vals...)
|
|
l.logger.Output(2, "DEBUG: "+s)
|
|
l.callHandlers(LevelDebug, s)
|
|
}
|
|
|
|
// Infoln logs a line with a VERBOSE prefix.
|
|
func (l *logger) Verboseln(vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintln(vals...)
|
|
l.logger.Output(2, "VERBOSE: "+s)
|
|
l.callHandlers(LevelVerbose, s)
|
|
}
|
|
|
|
// Infof logs a formatted line with a VERBOSE prefix.
|
|
func (l *logger) Verbosef(format string, vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintf(format, vals...)
|
|
l.logger.Output(2, "VERBOSE: "+s)
|
|
l.callHandlers(LevelVerbose, s)
|
|
}
|
|
|
|
// Infoln logs a line with an INFO prefix.
|
|
func (l *logger) Infoln(vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintln(vals...)
|
|
l.logger.Output(2, "INFO: "+s)
|
|
l.callHandlers(LevelInfo, s)
|
|
}
|
|
|
|
// Infof logs a formatted line with an INFO prefix.
|
|
func (l *logger) Infof(format string, vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintf(format, vals...)
|
|
l.logger.Output(2, "INFO: "+s)
|
|
l.callHandlers(LevelInfo, s)
|
|
}
|
|
|
|
// Okln logs a line with an OK prefix.
|
|
func (l *logger) Okln(vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintln(vals...)
|
|
l.logger.Output(2, "OK: "+s)
|
|
l.callHandlers(LevelOK, s)
|
|
}
|
|
|
|
// Okf logs a formatted line with an OK prefix.
|
|
func (l *logger) Okf(format string, vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintf(format, vals...)
|
|
l.logger.Output(2, "OK: "+s)
|
|
l.callHandlers(LevelOK, s)
|
|
}
|
|
|
|
// Warnln logs a formatted line with a WARNING prefix.
|
|
func (l *logger) Warnln(vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintln(vals...)
|
|
l.logger.Output(2, "WARNING: "+s)
|
|
l.callHandlers(LevelWarn, s)
|
|
}
|
|
|
|
// Warnf logs a formatted line with a WARNING prefix.
|
|
func (l *logger) Warnf(format string, vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintf(format, vals...)
|
|
l.logger.Output(2, "WARNING: "+s)
|
|
l.callHandlers(LevelWarn, s)
|
|
}
|
|
|
|
// Fatalln logs a line with a FATAL prefix and exits the process with exit
|
|
// code 1.
|
|
func (l *logger) Fatalln(vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintln(vals...)
|
|
l.logger.Output(2, "FATAL: "+s)
|
|
l.callHandlers(LevelFatal, s)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Fatalf logs a formatted line with a FATAL prefix and exits the process with
|
|
// exit code 1.
|
|
func (l *logger) Fatalf(format string, vals ...interface{}) {
|
|
l.mut.Lock()
|
|
defer l.mut.Unlock()
|
|
s := fmt.Sprintf(format, vals...)
|
|
l.logger.Output(2, "FATAL: "+s)
|
|
l.callHandlers(LevelFatal, s)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// ShouldDebug returns true if the given facility has debugging enabled.
|
|
func (l *logger) ShouldDebug(facility string) bool {
|
|
l.mut.Lock()
|
|
res := l.debug[facility]
|
|
l.mut.Unlock()
|
|
return res
|
|
}
|
|
|
|
// SetDebug enabled or disables debugging for the given facility name.
|
|
func (l *logger) SetDebug(facility string, enabled bool) {
|
|
l.mut.Lock()
|
|
l.debug[facility] = enabled
|
|
l.mut.Unlock()
|
|
}
|
|
|
|
// FacilityDebugging returns the set of facilities that have debugging
|
|
// enabled.
|
|
func (l *logger) FacilityDebugging() []string {
|
|
var enabled []string
|
|
l.mut.Lock()
|
|
for facility, isEnabled := range l.debug {
|
|
if isEnabled {
|
|
enabled = append(enabled, facility)
|
|
}
|
|
}
|
|
l.mut.Unlock()
|
|
return enabled
|
|
}
|
|
|
|
// Facilities returns the currently known set of facilities and their
|
|
// descriptions.
|
|
func (l *logger) Facilities() map[string]string {
|
|
l.mut.Lock()
|
|
res := make(map[string]string, len(l.facilities))
|
|
for facility, descr := range l.facilities {
|
|
res[facility] = descr
|
|
}
|
|
l.mut.Unlock()
|
|
return res
|
|
}
|
|
|
|
// NewFacility returns a new logger bound to the named facility.
|
|
func (l *logger) NewFacility(facility, description string) Logger {
|
|
l.mut.Lock()
|
|
if l.facilities == nil {
|
|
l.facilities = make(map[string]string)
|
|
}
|
|
if description != "" {
|
|
l.facilities[facility] = description
|
|
}
|
|
|
|
if l.debug == nil {
|
|
l.debug = make(map[string]bool)
|
|
}
|
|
l.debug[facility] = false
|
|
l.mut.Unlock()
|
|
|
|
return &facilityLogger{
|
|
logger: l,
|
|
facility: facility,
|
|
}
|
|
}
|
|
|
|
// A facilityLogger is a regular logger but bound to a facility name. The
|
|
// Debugln and Debugf methods are no-ops unless debugging has been enabled for
|
|
// this facility on the parent logger.
|
|
type facilityLogger struct {
|
|
*logger
|
|
facility string
|
|
}
|
|
|
|
// Debugln logs a line with a DEBUG prefix.
|
|
func (l *facilityLogger) Debugln(vals ...interface{}) {
|
|
if !l.ShouldDebug(l.facility) {
|
|
return
|
|
}
|
|
l.logger.Debugln(vals...)
|
|
}
|
|
|
|
// Debugf logs a formatted line with a DEBUG prefix.
|
|
func (l *facilityLogger) Debugf(format string, vals ...interface{}) {
|
|
if !l.ShouldDebug(l.facility) {
|
|
return
|
|
}
|
|
l.logger.Debugf(format, vals...)
|
|
}
|
|
|
|
// A Recorder keeps a size limited record of log events.
|
|
type Recorder struct {
|
|
lines []Line
|
|
initial int
|
|
mut sync.Mutex
|
|
}
|
|
|
|
// A Line represents a single log entry.
|
|
type Line struct {
|
|
When time.Time
|
|
Message string
|
|
}
|
|
|
|
func NewRecorder(l Logger, level LogLevel, size, initial int) *Recorder {
|
|
r := &Recorder{
|
|
lines: make([]Line, 0, size),
|
|
initial: initial,
|
|
}
|
|
l.AddHandler(level, r.append)
|
|
return r
|
|
}
|
|
|
|
func (r *Recorder) Since(t time.Time) []Line {
|
|
r.mut.Lock()
|
|
defer r.mut.Unlock()
|
|
|
|
res := r.lines
|
|
for i := 0; i < len(res) && res[i].When.Before(t); i++ {
|
|
// nothing, just incrementing i
|
|
}
|
|
if len(res) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// We must copy the result as r.lines can be mutated as soon as the lock
|
|
// is released.
|
|
cp := make([]Line, len(res))
|
|
copy(cp, res)
|
|
return cp
|
|
}
|
|
|
|
func (r *Recorder) Clear() {
|
|
r.mut.Lock()
|
|
r.lines = r.lines[:0]
|
|
r.mut.Unlock()
|
|
}
|
|
|
|
func (r *Recorder) append(l LogLevel, msg string) {
|
|
line := Line{
|
|
When: time.Now(),
|
|
Message: msg,
|
|
}
|
|
|
|
r.mut.Lock()
|
|
defer r.mut.Unlock()
|
|
|
|
if len(r.lines) == cap(r.lines) {
|
|
if r.initial > 0 {
|
|
// Shift all lines one step to the left, keeping the "initial" first intact.
|
|
copy(r.lines[r.initial+1:], r.lines[r.initial+2:])
|
|
} else {
|
|
copy(r.lines, r.lines[1:])
|
|
}
|
|
// Add the new one at the end
|
|
r.lines[len(r.lines)-1] = line
|
|
return
|
|
}
|
|
|
|
r.lines = append(r.lines, line)
|
|
if len(r.lines) == r.initial {
|
|
r.lines = append(r.lines, Line{time.Now(), "..."})
|
|
}
|
|
}
|