diff --git a/lib/logger/LICENSE b/lib/logger/LICENSE new file mode 100644 index 000000000..fa5b4e205 --- /dev/null +++ b/lib/logger/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2013 Jakob Borg + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +- The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/logger/logger.go b/lib/logger/logger.go new file mode 100644 index 000000000..476fab782 --- /dev/null +++ b/lib/logger/logger.go @@ -0,0 +1,281 @@ +// 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" +) + +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() (enabled, disabled []string) + NewFacility(facility string) Logger +} + +type logger struct { + logger *log.Logger + handlers [NumLevels][]MessageHandler + debug map[string]bool + 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() +} + +// Facilities returns the currently known set of facilities, both those for +// which debug is enabled and those for which it is disabled. +func (l *logger) Facilities() (enabled, disabled []string) { + l.mut.Lock() + for facility, isEnabled := range l.debug { + if isEnabled { + enabled = append(enabled, facility) + } else { + disabled = append(disabled, facility) + } + } + l.mut.Unlock() + return +} + +// NewFacility returns a new logger bound to the named facility. +func (l *logger) NewFacility(facility string) Logger { + l.mut.Lock() + 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...) +} diff --git a/lib/logger/logger_test.go b/lib/logger/logger_test.go new file mode 100644 index 000000000..e9025e1c2 --- /dev/null +++ b/lib/logger/logger_test.go @@ -0,0 +1,84 @@ +// 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 + +import ( + "strings" + "testing" +) + +func TestAPI(t *testing.T) { + l := New() + l.SetFlags(0) + l.SetPrefix("testing") + + debug := 0 + l.AddHandler(LevelDebug, checkFunc(t, LevelDebug, "test 0", &debug)) + info := 0 + l.AddHandler(LevelInfo, checkFunc(t, LevelInfo, "test 1", &info)) + warn := 0 + l.AddHandler(LevelWarn, checkFunc(t, LevelWarn, "test 2", &warn)) + ok := 0 + l.AddHandler(LevelOK, checkFunc(t, LevelOK, "test 3", &ok)) + + l.Debugf("test %d", 0) + l.Debugln("test", 0) + l.Infof("test %d", 1) + l.Infoln("test", 1) + l.Warnf("test %d", 2) + l.Warnln("test", 2) + l.Okf("test %d", 3) + l.Okln("test", 3) + + if debug != 2 { + t.Errorf("Debug handler called %d != 2 times", debug) + } + if info != 2 { + t.Errorf("Info handler called %d != 2 times", info) + } + if warn != 2 { + t.Errorf("Warn handler called %d != 2 times", warn) + } + if ok != 2 { + t.Errorf("Ok handler called %d != 2 times", ok) + } +} + +func checkFunc(t *testing.T, expectl LogLevel, expectmsg string, counter *int) func(LogLevel, string) { + return func(l LogLevel, msg string) { + *counter++ + if l != expectl { + t.Errorf("Incorrect message level %d != %d", l, expectl) + } + if !strings.HasSuffix(msg, expectmsg) { + t.Errorf("%q does not end with %q", msg, expectmsg) + } + } +} + +func TestFacilityDebugging(t *testing.T) { + l := New() + l.SetFlags(0) + + msgs := 0 + l.AddHandler(LevelDebug, func(l LogLevel, msg string) { + msgs++ + if strings.Contains(msg, "f1") { + t.Fatal("Should not get message for facility f1") + } + }) + + l.SetDebug("f0", true) + l.SetDebug("f1", false) + + f0 := l.NewFacility("f0") + f1 := l.NewFacility("f1") + + f0.Debugln("Debug line from f0") + f1.Debugln("Debug line from f1") + + if msgs != 1 { + t.Fatalf("Incorrent number of messages, %d != 1", msgs) + } +}