diff --git a/logging/console_logger.go b/logging/console_logger.go new file mode 100644 index 0000000..b200bea --- /dev/null +++ b/logging/console_logger.go @@ -0,0 +1,14 @@ +package logging + +import "os" + +// Console is a Logger that writes to the console. It must be +// constructed with a call to NewConsole. +type Console struct { + *LogWriter +} + +// NewConsole returns a new console logger. +func NewConsole() *Console { + return &Console{LogWriter: NewLogWriter(os.Stdout, os.Stderr)} +} diff --git a/logging/doc.go b/logging/doc.go new file mode 100644 index 0000000..3237df8 --- /dev/null +++ b/logging/doc.go @@ -0,0 +1,15 @@ +// Package logging implements attribute-based logging. Log entries +// consist of timestamps, an actor and event string, and a mapping of +// string key-value attribute pairs. For example, +// +// log.Error("serialiser", "failed to open file", +// map[string]string{ +// "error": err.Error(), +// "path": "data.bin", +// }) +// +// This produces the output message +// +// [2016-04-01T15:04:30-0700] [ERROR] [actor:serialiser event:failed to open file] error=is a directory path=data.bin +// +package logging diff --git a/logging/example/example.go b/logging/example/example.go index ef53ab2..d8dc02e 100644 --- a/logging/example/example.go +++ b/logging/example/example.go @@ -1,75 +1,43 @@ package main import ( - "fmt" + "os" + "time" "github.com/kisom/goutils/logging" - "github.com/kisom/testio" ) -var log = logging.Init() -var olog, _ = logging.New("subsystem #42", logging.LevelNotice) +var log = logging.NewConsole() +var olog = logging.NewConsole() func main() { - exampleNewWriters() - log.Notice("Hello, world.") - log.Warning("this program is about to end") + log.Info("example", "Hello, world.", nil) + log.Warn("example", "this program is about to end", nil) - log.SetLevel(logging.LevelDebug) - log.Debug("hello world") - log.SetLevel(logging.LevelNotice) + log.Critical("example", "screaming into the void", nil) + olog.Critical("other", "can anyone hear me?", nil) - olog.Print("now online") - logging.Suppress("olog") - olog.Print("extraneous information") + log.Warn("example", "but not for long", nil) - logging.Enable("olog") - olog.Print("relevant now") + log.Info("example", "fare thee well", nil) + olog.Info("other", "all good journeys must come to an end", + map[string]string{"when": time.Now().String()}) - logging.SuppressAll() - log.Alert("screaming into the void") - olog.Critical("can anyone hear me?") - - log.Enable() - log.Notice("i'm baaack") - log.Suppress() - log.Warning("but not for long") - - logging.EnableAll() - log.Notice("fare thee well") - olog.Print("all good journeys must come to an end") + log.Info("example", "filelog test", nil) exampleNewFromFile() -} - -func exampleNewWriters() { - o := testio.NewBufCloser(nil) - e := testio.NewBufCloser(nil) - - wlog, _ := logging.NewFromWriters("writers", logging.DefaultLevel, o, e) - wlog.Notice("hello, world") - wlog.Notice("some more things happening") - wlog.Warning("something suspicious has happened") - wlog.Alert("pick up that can, Citizen!") - - fmt.Println("--- BEGIN OUT ---") - fmt.Printf("%s", o.Bytes()) - fmt.Println("--- END OUT ---") - - fmt.Println("--- BEGIN ERR ---") - fmt.Printf("%s", e.Bytes()) - fmt.Println("--- END ERR ---") + os.Remove("example.log") + os.Remove("example.err") } func exampleNewFromFile() { - flog, err := logging.NewFromFile("file logger", logging.LevelNotice, - "example.log", "example.err", true) + flog, err := logging.NewSplitFile("example.log", "example.err", true) if err != nil { - log.Fatalf("failed to open logger: %v", err) + log.Fatal("filelog", "failed to open logger", + map[string]string{"error": err.Error()}) } - defer flog.Close() - flog.Notice("hello, world") - flog.Notice("some more things happening") - flog.Warning("something suspicious has happened") - flog.Alert("pick up that can, Citizen!") + flog.Info("filelog", "hello, world", nil) + flog.Info("filelog", "some more things happening", nil) + flog.Warn("filelog", "something suspicious has happened", nil) + flog.Critical("filelog", "pick up that can, Citizen!", nil) } diff --git a/logging/example_test.go b/logging/example_test.go index eb3aa73..22949ae 100644 --- a/logging/example_test.go +++ b/logging/example_test.go @@ -1,44 +1,37 @@ package logging_test -import "github.com/kisom/goutils/logging" +import ( + "time" -var log = logging.Init() -var olog, _ = logging.New("subsystem #42", logging.LevelNotice) + "github.com/kisom/goutils/logging" +) + +var log = logging.NewConsole() +var olog = logging.NewConsole() func Example() { - log.Notice("Hello, world.") - log.Warning("this program is about to end") + log.Info("example", "Hello, world.", nil) + log.Warn("example", "this program is about to end", nil) - olog.Print("now online") - logging.Suppress("olog") - olog.Print("extraneous information") + log.Critical("example", "screaming into the void", nil) + olog.Critical("other", "can anyone hear me?", nil) - logging.Enable("olog") - olog.Print("relevant now") + log.Warn("example", "but not for long", nil) - logging.SuppressAll() - log.Alert("screaming into the void") - olog.Critical("can anyone hear me?") - - log.Enable() - log.Notice("i'm baaack") - log.Suppress() - log.Warning("but not for long") - - logging.EnableAll() - log.Notice("fare thee well") - olog.Print("all good journeys must come to an end") + log.Info("example", "fare thee well", nil) + olog.Info("example", "all good journeys must come to an end", + map[string]string{"when": time.Now().String()}) } func ExampleNewFromFile() { - log, err := logging.NewFromFile("file logger", logging.LevelNotice, - "example.log", "example.err", true) + flog, err := logging.NewSplitFile("example.log", "example.err", true) if err != nil { - log.Fatalf("failed to open logger: %v", err) + log.Fatal("filelog", "failed to open logger", + map[string]string{"error": err.Error()}) } - log.Notice("hello, world") - log.Notice("some more things happening") - log.Warning("something suspicious has happened") - log.Alert("pick up that can, Citizen!") + flog.Info("filelog", "hello, world", nil) + flog.Info("filelog", "some more things happening", nil) + flog.Warn("filelog", "something suspicious has happened", nil) + flog.Critical("filelog", "pick up that can, Citizen!", nil) } diff --git a/logging/file.go b/logging/file.go new file mode 100644 index 0000000..48d3b1b --- /dev/null +++ b/logging/file.go @@ -0,0 +1,73 @@ +package logging + +import "os" + +// File writes its logs to file. +type File struct { + fo, fe *os.File + *LogWriter +} + +func (fl *File) Close() { + fl.fo.Close() + if fl.fe != nil { + fl.fe.Close() + } +} + +// NewFile creates a new Logger that writes all logs to the file +// specified by path. If overwrite is specified, the log file will be +// truncated before writing. Otherwise, the log file will be appended +// to. +func NewFile(path string, overwrite bool) (*File, error) { + fl := new(File) + + var err error + + if overwrite { + fl.fo, err = os.Create(path) + } else { + fl.fo, err = os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0644) + } + + if err != nil { + return nil, err + } + + fl.LogWriter = NewLogWriter(fl.fo, fl.fo) + return fl, nil +} + +// NewSplitFile creates a new Logger that writes debug and information +// messages to the output file, and warning and higher messages to the +// error file. If overwrite is specified, the log files will be +// truncated before writing. +func NewSplitFile(outpath, errpath string, overwrite bool) (*File, error) { + fl := new(File) + + var err error + + if overwrite { + fl.fo, err = os.Create(outpath) + } else { + fl.fo, err = os.OpenFile(outpath, os.O_WRONLY|os.O_APPEND, 0644) + } + + if err != nil { + return nil, err + } + + if overwrite { + fl.fe, err = os.Create(errpath) + } else { + fl.fe, err = os.OpenFile(errpath, os.O_WRONLY|os.O_APPEND, 0644) + } + + if err != nil { + fl.Close() + return nil, err + } + + fl.LogWriter = NewLogWriter(fl.fo, fl.fe) + return fl, nil +} diff --git a/logging/levels.go b/logging/levels.go index 65a0ee0..2b81c6b 100644 --- a/logging/levels.go +++ b/logging/levels.go @@ -1,12 +1,5 @@ package logging -import ( - "fmt" - "os" - "runtime" - "time" -) - // A Level represents a logging level. type Level uint8 @@ -14,15 +7,11 @@ type Level uint8 const ( // LevelDebug are debug output useful during program testing // and debugging. - LevelDebug = iota + LevelDebug = 1 << iota // LevelInfo is used for informational messages. LevelInfo - // LevelNotice is for messages that are normal but - // significant. - LevelNotice - // LevelWarning is for messages that are warning conditions: // they're not indicative of a failure, but of a situation // that may lead to a failure later. @@ -35,15 +24,13 @@ const ( // LevelCritical are messages for critical conditions. LevelCritical - // LevelAlert are for messages indicating that action - // must be taken immediately. - LevelAlert - // LevelFatal messages are akin to syslog's LOG_EMERG: the // system is unusable and cannot continue execution. LevelFatal ) +const DefaultLevel = LevelInfo + // Cheap integer to fixed-width decimal ASCII. Give a negative width // to avoid zero-padding. (From log/log.go in the standard library). func itoa(i int, wid int) string { @@ -70,168 +57,13 @@ func writeToOut(level Level) bool { } var levelPrefix = [...]string{ - LevelDebug: "[DEBUG] ", - LevelInfo: "[INFO] ", - LevelNotice: "[NOTICE] ", - LevelWarning: "[WARNING] ", - LevelError: "[ERROR] ", - LevelCritical: "[CRITICAL] ", - LevelAlert: "[ALERT] ", - LevelFatal: "[FATAL] ", + LevelDebug: "DEBUG", + LevelInfo: "INFO", + LevelWarning: "WARNING", + LevelError: "ERROR", + LevelCritical: "CRITICAL", + LevelFatal: "FATAL", } // DateFormat contains the default date format string used by the logger. -var DateFormat = "2006-01-02T15:03:04-0700" - -func (l *Logger) outputf(level Level, format string, v []interface{}) { - if !l.Enabled() { - return - } - - if level >= l.level { - domain := l.domain - if level == LevelDebug { - _, file, line, ok := runtime.Caller(2) - if ok { - domain += " " + file + ":" + itoa(line, -1) - } - } - - format = fmt.Sprintf("%s %s: %s%s\n", - time.Now().Format(DateFormat), - domain, levelPrefix[level], format) - if writeToOut(level) { - fmt.Fprintf(l.out, format, v...) - } else { - fmt.Fprintf(l.err, format, v...) - } - } -} - -func (l *Logger) output(level Level, v []interface{}) { - if !l.Enabled() { - return - } - - if level >= l.level { - domain := l.domain - if level == LevelDebug { - _, file, line, ok := runtime.Caller(2) - if ok { - domain += " " + file + ":" + itoa(line, -1) - } - } - - format := fmt.Sprintf("%s %s: %s", - time.Now().Format(DateFormat), - domain, levelPrefix[level]) - if writeToOut(level) { - fmt.Fprintf(l.out, format) - fmt.Fprintln(l.out, v...) - } else { - fmt.Fprintf(l.err, format) - fmt.Fprintln(l.err, v...) - } - } -} - -// Fatalf logs a formatted message at the "fatal" level and then exits. The -// arguments are handled in the same manner as fmt.Printf. -func (l *Logger) Fatalf(format string, v ...interface{}) { - l.outputf(LevelFatal, format, v) - os.Exit(1) -} - -// Fatal logs its arguments at the "fatal" level and then exits. -func (l *Logger) Fatal(v ...interface{}) { - l.output(LevelFatal, v) - os.Exit(1) -} - -// Alertf logs a formatted message at the "alert" level. The -// arguments are handled in the same manner as fmt.Printf. -func (l *Logger) Alertf(format string, v ...interface{}) { - l.outputf(LevelAlert, format, v) -} - -// Alert logs its arguments at the "alert" level. -func (l *Logger) Alert(v ...interface{}) { - l.output(LevelAlert, v) -} - -// Criticalf logs a formatted message at the "critical" level. The -// arguments are handled in the same manner as fmt.Printf. -func (l *Logger) Criticalf(format string, v ...interface{}) { - l.outputf(LevelCritical, format, v) -} - -// Critical logs its arguments at the "critical" level. -func (l *Logger) Critical(v ...interface{}) { - l.output(LevelCritical, v) -} - -// Errorf logs a formatted message at the "error" level. The arguments -// are handled in the same manner as fmt.Printf. -func (l *Logger) Errorf(format string, v ...interface{}) { - l.outputf(LevelError, format, v) -} - -// Error logs its arguments at the "error" level. -func (l *Logger) Error(v ...interface{}) { - l.output(LevelError, v) -} - -// Warningf logs a formatted message at the "warning" level. The -// arguments are handled in the same manner as fmt.Printf. -func (l *Logger) Warningf(format string, v ...interface{}) { - l.outputf(LevelWarning, format, v) -} - -// Warning logs its arguments at the "warning" level. -func (l *Logger) Warning(v ...interface{}) { - l.output(LevelWarning, v) -} - -// Noticef logs a formatted message at the "notice" level. The arguments -// are handled in the same manner as fmt.Printf. -func (l *Logger) Noticef(format string, v ...interface{}) { - l.outputf(LevelNotice, format, v) -} - -// Notice logs its arguments at the "notice" level. -func (l *Logger) Notice(v ...interface{}) { - l.output(LevelNotice, v) -} - -// Infof logs a formatted message at the "info" level. The arguments -// are handled in the same manner as fmt.Printf. -func (l *Logger) Infof(format string, v ...interface{}) { - l.outputf(LevelInfo, format, v) -} - -// Info logs its arguments at the "info" level. -func (l *Logger) Info(v ...interface{}) { - l.output(LevelInfo, v) -} - -// Debugf logs a formatted message at the "debug" level. The arguments -// are handled in the same manner as fmt.Printf. Note that debug -// logging will print the current -func (l *Logger) Debugf(format string, v ...interface{}) { - l.outputf(LevelDebug, format, v) -} - -// Debug logs its arguments at the "debug" level. -func (l *Logger) Debug(v ...interface{}) { - l.output(LevelDebug, v) -} - -// Printf prints a formatted message at the default level. -func (l *Logger) Printf(format string, v ...interface{}) { - l.outputf(DefaultLevel, format, v) -} - -// Print prints its arguments at the default level. -func (l *Logger) Print(v ...interface{}) { - l.output(DefaultLevel, v) -} +const DateFormat = "2006-01-02T15:03:04-0700" diff --git a/logging/log.go b/logging/log.go index 8bbbc61..b16dd68 100644 --- a/logging/log.go +++ b/logging/log.go @@ -1,274 +1,279 @@ -// Package logging provides domain-based logging in the same style as -// sylog. Domains are some name for which logging can be selectively -// enabled or disabled. Logging also differentiates between normal -// messages (which are sent to standard output) and errors, which are -// sent to standard error; debug messages will also include the file -// and line number. -// -// Domains are intended for identifying logging subystems. A domain -// can be suppressed with Suppress, and re-enabled with Enable. There -// are prefixed versions of these as well. -// -// Packages (e.g. those meant to be imported by programs) using the -// loggers here should observe a few etiquette guides: -// -// 1. A package should never suppress or enable other loggers except -// via an exported function that should be called by the end user. -// -// 2. A package should never call the global `SetLevel`; this is -// reserved for the end user. -// -// 3. Packages should use consistent, sane domains: preferably, -// related packages should use an unsurprising common prefix in their -// domains. -// -// This package was adapted from the CFSSL logging code. package logging import ( + "fmt" "io" "os" - "path/filepath" - "strings" - "sync" - - "github.com/kisom/goutils/mwc" + "time" ) -var logConfig = struct { - registered map[string]*Logger - lock *sync.Mutex -}{ - registered: map[string]*Logger{}, - lock: new(sync.Mutex), +// Logger provides a standardised logging interface. +type Logger interface { + // SetLevel sets the minimum log level. + SetLevel(Level) + + // Good returns true if the Logger is healthy. + Good() bool + + // Status returns an error corresponding to the logger's state; + // if it's healthy (e.g. Good() returns true), Error will + // return nil. + Status() error + + // Close gives the Logger the opportunity to perform any cleanup. + Close() + + // Log messages consist of four components: + // + // 1. The **level** attaches a notion of priority to the log message. + // Several log levels are available: + // + // + FATAL (32): the system is in an unsuable state, and cannot + // continue to run. Most of the logging for this will cause the + // program to exit with an error code. + // + CRITICAL (16): critical conditions. The error, if uncorrected, is + // likely to cause a fatal condition shortly. An example is running + // out of disk space. This is something that the ops team should get + // paged for. + // + ERROR (8): error conditions. A single error doesn't require an + // ops team to be paged, but repeated errors should often trigger a + // page based on threshold triggers. An example is a network + // failure: it might be a transient failure (these do happen), but + // most of the time it's self-correcting. + // + WARNING (4): warning conditions. An example of this is a bad + // request sent to a server. This isn't an error on the part of the + // program, but it may be indicative of other things. Like errors, + // the ops team shouldn't be paged for errors, but a page might be + // triggered if a certain threshold of warnings is reached (which is + // typically much higher than errors). For example, repeated + // warnings might be a sign that the system is under attack. + // + INFO (2): informational message. This is a normal log message + // that is used to deliver information, such as recording + // requests. Ops teams are never paged for informational + // messages. This is the default log level. + // + DEBUG (1): debug-level message. These are only used during + // development or if a deployed system repeatedly sees abnormal + // errors. + // + // The numeric values indicate the priority of a given level. + // + // 2. The **actor** is used to specify which component is generating + // the log message. This could be the program name, or it could be + // a specific component inside the system. + // + // 3. The **event** is a short message indicating what happened. This is + // most like the traditional log message. + // + // 4. The **attributes** are an optional set of key-value string pairs that + // provide additional information. + // + // Additionally, each log message has an associated timestamp. For the + // text-based logs, this is "%FT%T%z"; for the binary logs, this is a + // 64-bit Unix timestamp. An example text-based timestamp might look like :: + // + // [2016-03-27T20:59:27-0700] [INFO] [actor:server event:request received] client=192.168.2.5 request-size=839 + // + // Note that this is organised in a manner that facilitates parsing:: + // + // /\[(\d{4}-\d{3}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{4})\] \[(\w+\)]\) \[actor:(.+?) event:(.+?)\]/ + // + // will cover the header: + // + // + ``$1`` contains the timestamp + // + ``$2`` contains the level + // + ``$3`` contains the actor + // + ``$4`` contains the event + Debug(actor, event string, attrs map[string]string) + Info(actor, event string, attrs map[string]string) + Warn(actor, event string, attrs map[string]string) + Error(actor, event string, attrs map[string]string) + Critical(actor, event string, attrs map[string]string) + Fatal(actor, event string, attrs map[string]string) + FatalCode(exitcode int, actor, event string, attrs map[string]string) + FatalNoDie(actor, event string, attrs map[string]string) } -// SetLevel sets the logging level for all loggers. -func SetLevel(level Level) { - logConfig.lock.Lock() - defer logConfig.lock.Unlock() +// A LogWriter is a Logger that operates on an io.Writer. +type LogWriter struct { + wo, we io.Writer + lvl Level + state error + snl bool // suppress newline +} - for _, l := range logConfig.registered { - l.SetLevel(level) +// NewLogWriter takes an output writer (wo) and an error writer (we), +// and produces a new Logger. If the error writer is nil, error logs +// will be multiplexed onto the output writer. +func NewLogWriter(wo, we io.Writer) *LogWriter { + if we == nil { + we = wo + } + + return &LogWriter{ + wo: wo, + we: we, + lvl: DefaultLevel, + state: nil, } } -// DefaultLevel defaults to the notice level of logging. -const DefaultLevel = LevelNotice - -// Init returns a new default logger. The domain is set to the -// program's name, and the default logging level is used. -func Init() *Logger { - l, _ := New(filepath.Base(os.Args[0]), DefaultLevel) - return l -} - -// A Logger writes logs on behalf of a particular domain at a certain -// level. -type Logger struct { - enabled bool - lock *sync.Mutex - domain string - level Level - out io.WriteCloser - err io.WriteCloser -} - -// Close closes the log's writers and suppresses the logger. -func (l *Logger) Close() error { - Suppress(l.domain) - err := l.out.Close() - if err != nil { - return nil +func (lw *LogWriter) output(w io.Writer, lvl Level, actor, event string, attrs map[string]string) { + t := time.Now().Format(DateFormat) + fmt.Fprintf(w, "[%s] [%s] [actor:%s event:%s]", t, levelPrefix[lvl], actor, event) + for k, v := range attrs { + fmt.Fprintf(w, " %s=%s", k, v) } - return l.err.Close() -} - -// Suppress ignores logs from a specific domain. -func Suppress(domain string) { - logConfig.lock.Lock() - defer logConfig.lock.Unlock() - l, ok := logConfig.registered[domain] - if ok { - l.Suppress() + if !lw.snl { + fmt.Fprintf(w, "\n") } } -// SuppressPrefix suppress logs whose domain is prefixed with the -// prefix. -func SuppressPrefix(prefix string) { - logConfig.lock.Lock() - defer logConfig.lock.Unlock() - for domain, l := range logConfig.registered { - if strings.HasPrefix(domain, prefix) { - l.Suppress() - } +// Debug emits a debug-level message. These are only used during +// development or if a deployed system repeatedly sees abnormal +// errors. +// +// Actor specifies the component emitting the message; event indicates +// the event that caused the log message to be emitted. attrs is a map +// of key-value string pairs that can be used to provide additional +// information. +func (lw *LogWriter) Debug(actor, event string, attrs map[string]string) { + if lw.lvl > LevelDebug { + return } + lw.output(lw.wo, LevelDebug, actor, event, attrs) } -// SuppressAll suppresses all logging output. -func SuppressAll() { - logConfig.lock.Lock() - defer logConfig.lock.Unlock() - for _, l := range logConfig.registered { - l.Suppress() +// Info emits an informational message. This is a normal log message +// that is used to deliver information, such as recording +// requests. Ops teams are never paged for informational +// messages. This is the default log level. +// +// Actor specifies the component emitting the message; event indicates +// the event that caused the log message to be emitted. attrs is a map +// of key-value string pairs that can be used to provide additional +// information. +func (lw *LogWriter) Info(actor, event string, attrs map[string]string) { + if lw.lvl > LevelInfo { + return } + lw.output(lw.wo, LevelInfo, actor, event, attrs) } -// Enable enables logs from a specific domain. -func Enable(domain string) { - logConfig.lock.Lock() - defer logConfig.lock.Unlock() - l, ok := logConfig.registered[domain] - if ok { - l.Enable() +// Warn emits a warning message. An example of this is a bad request +// sent to a server. This isn't an error on the part of the program, +// but it may be indicative of other things. Like errors, the ops team +// shouldn't be paged for errors, but a page might be triggered if a +// certain threshold of warnings is reached (which is typically much +// higher than errors). For example, repeated warnings might be a sign +// that the system is under attack. +// +// Actor specifies the component emitting the message; event indicates +// the event that caused the log message to be emitted. attrs is a map +// of key-value string pairs that can be used to provide additional +// information. +func (lw *LogWriter) Warn(actor, event string, attrs map[string]string) { + if lw.lvl > LevelWarning { + return } + lw.output(lw.we, LevelWarning, actor, event, attrs) } -// EnablePrefix enables logs whose domain is prefixed with prefix. -func EnablePrefix(prefix string) { - logConfig.lock.Lock() - defer logConfig.lock.Unlock() - for domain, l := range logConfig.registered { - if strings.HasPrefix(domain, prefix) { - l.Enable() - } +// Error emits an error message. A single error doesn't require an ops +// team to be paged, but repeated errors should often trigger a page +// based on threshold triggers. An example is a network failure: it +// might be a transient failure (these do happen), but most of the +// time it's self-correcting. +// +// Actor specifies the component emitting the message; event indicates +// the event that caused the log message to be emitted. attrs is a map +// of key-value string pairs that can be used to provide additional +// information. +func (lw *LogWriter) Error(actor, event string, attrs map[string]string) { + if lw.lvl > LevelError { + return } + lw.output(lw.we, LevelError, actor, event, attrs) } -// EnableAll enables all domains. -func EnableAll() { - logConfig.lock.Lock() - defer logConfig.lock.Unlock() - for _, l := range logConfig.registered { - l.Enable() +// Critical emits a message indicating a critical condition. The +// error, if uncorrected, is likely to cause a fatal condition +// shortly. An example is running out of disk space. This is +// something that the ops team should get paged for. +// +// Actor specifies the component emitting the message; event indicates +// the event that caused the log message to be emitted. attrs is a map +// of key-value string pairs that can be used to provide additional +// information. +func (lw *LogWriter) Critical(actor, event string, attrs map[string]string) { + if lw.lvl > LevelCritical { + return } + lw.output(lw.we, LevelCritical, actor, event, attrs) } -// New returns a new logger that writes to standard output for Notice -// and below and standard error for levels above Notice. If a logger -// with the same domain exists, the logger will set its level to level -// and return the logger; in this case, the registered return value -// will be true. -func New(domain string, level Level) (l *Logger, registered bool) { - logConfig.lock.Lock() - defer logConfig.lock.Unlock() - - l = logConfig.registered[domain] - if l != nil { - l.SetLevel(level) - return l, true +// Fatal emits a message indicating that the system is in an unsuable +// state, and cannot continue to run. The program will exit with exit +// code 1. +// +// Actor specifies the component emitting the message; event indicates +// the event that caused the log message to be emitted. attrs is a map +// of key-value string pairs that can be used to provide additional +// information. +func (lw *LogWriter) Fatal(actor, event string, attrs map[string]string) { + if lw.lvl > LevelFatal { + return } - - l = &Logger{ - domain: domain, - level: level, - out: os.Stdout, - err: os.Stderr, - lock: new(sync.Mutex), - } - - l.Enable() - logConfig.registered[domain] = l - return l, false + lw.output(lw.we, LevelFatal, actor, event, attrs) + os.Exit(1) } -// NewFromWriters returns a new logger that writes to the w io.WriteCloser -// for Notice and below and to the e io.WriteCloser for levels above -// Notice. If e is nil, w will be used. If a logger with the same -// domain exists, the logger will set its level to level and return -// the logger; in this case, the registered return value will be true. -func NewFromWriters(domain string, level Level, w, e io.WriteCloser) (l *Logger, registered bool) { - logConfig.lock.Lock() - defer logConfig.lock.Unlock() - - l = logConfig.registered[domain] - if l != nil { - l.SetLevel(level) - return l, true +// Fatal emits a message indicating that the system is in an unsuable +// state, and cannot continue to run. The program will exit with the +// exit code speicfied in the exitcode argument. +// +// Actor specifies the component emitting the message; event indicates +// the event that caused the log message to be emitted. attrs is a map +// of key-value string pairs that can be used to provide additional +// information. +func (lw *LogWriter) FatalCode(exitcode int, actor, event string, attrs map[string]string) { + if lw.lvl > LevelFatal { + return } - - if w == nil { - w = os.Stdout - } - - if e == nil { - e = w - } - - l = &Logger{ - domain: domain, - level: level, - out: w, - err: e, - lock: new(sync.Mutex), - } - - l.Enable() - logConfig.registered[domain] = l - return l, false + lw.output(lw.we, LevelFatal, actor, event, attrs) + os.Exit(exitcode) } -// NewFromFile returns a new logger that opens the files for writing. If -// multiplex is true, output will be multiplexed to standard output -// and standard error as well. -func NewFromFile(domain string, level Level, outFile, errFile string, multiplex bool, flags int) (*Logger, error) { - l := &Logger{ - domain: domain, - level: level, - lock: new(sync.Mutex), +// Fatal emits a message indicating that the system is in an unsuable +// state, and cannot continue to run. The program will not exit; it is +// assumed that the caller has some final clean up to perform. +// +// Actor specifies the component emitting the message; event indicates +// the event that caused the log message to be emitted. attrs is a map +// of key-value string pairs that can be used to provide additional +// information. +func (lw *LogWriter) FatalNoDie(actor, event string, attrs map[string]string) { + if lw.lvl > LevelFatal { + return } - - outf, err := os.OpenFile(outFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY|flags, 0644) - if err != nil { - return nil, err - } - - errf, err := os.OpenFile(errFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY|flags, 0644) - if err != nil { - return nil, err - } - - if multiplex { - l.out = mwc.MultiWriteCloser(outf, os.Stdout) - l.err = mwc.MultiWriteCloser(errf, os.Stderr) - } else { - l.out = outf - l.err = errf - } - - Enable(domain) - return l, nil + lw.output(lw.we, LevelFatal, actor, event, attrs) } -// Enable allows output from the logger. -func (l *Logger) Enable() { - l.lock.Lock() - defer l.lock.Unlock() - l.enabled = true +// Good returns true if the logger is healthy. +func (lw *LogWriter) Good() bool { + return lw.state == nil } -// Enabled returns true if the logger is enabled. -func (l *Logger) Enabled() bool { - return l.enabled +// Status returns an error value from the logger if it isn't healthy, +// or nil if the logger is healthy. +func (lw *LogWriter) Status() error { + return lw.state } -// Suppress ignores output from the logger. -func (l *Logger) Suppress() { - l.lock.Lock() - defer l.lock.Unlock() - l.enabled = false +// SetLevel changes the log level. +func (lw *LogWriter) SetLevel(l Level) { + lw.lvl = l } -// Domain returns the domain of the logger. -func (l *Logger) Domain() string { - return l.domain -} - -// SetLevel changes the level of the logger. -func (l *Logger) SetLevel(level Level) { - l.lock.Lock() - defer l.lock.Unlock() - l.level = level -} +// Close is a no-op that satisfies the Logger interface. +func (lw *LogWriter) Close() {} diff --git a/logging/log_test.go b/logging/log_test.go new file mode 100644 index 0000000..49cfa32 --- /dev/null +++ b/logging/log_test.go @@ -0,0 +1,55 @@ +package logging + +import ( + "bytes" + "fmt" + "os" + "testing" +) + +// A list of implementations that should be tested. +var implementations []Logger + +func init() { + lw := NewLogWriter(&bytes.Buffer{}, nil) + cw := NewConsole() + + implementations = append(implementations, lw) + implementations = append(implementations, cw) +} + +func TestFileSetup(t *testing.T) { + fw1, err := NewFile("fw1.log", true) + if err != nil { + t.Fatalf("failed to create new file logger: %v", err) + } + + fw2, err := NewSplitFile("fw2.log", "fw2.err", true) + if err != nil { + t.Fatalf("failed to create new split file logger: %v", err) + } + + implementations = append(implementations, fw1) + implementations = append(implementations, fw2) +} + +func TestImplementations(t *testing.T) { + for _, l := range implementations { + l.Info("TestImplementations", "Info message", + map[string]string{"type": fmt.Sprintf("%T", l)}) + l.Warn("TestImplementations", "Warning message", + map[string]string{"type": fmt.Sprintf("%T", l)}) + } +} + +func TestCloseLoggers(t *testing.T) { + for _, l := range implementations { + l.Close() + } +} + +func TestDestroyLogFiles(t *testing.T) { + os.Remove("fw1.log") + os.Remove("fw2.log") + os.Remove("fw2.err") +}