diff --git a/lib/defs.go b/lib/defs.go new file mode 100644 index 0000000..c3227eb --- /dev/null +++ b/lib/defs.go @@ -0,0 +1,11 @@ +package lib + +// Various constants used throughout the tools. + +const ( + // ExitSuccess is the successful exit status. + ExitSuccess = 0 + + // ExitFailure is the failing exit status. + ExitFailure = 1 +) diff --git a/lib/lib.go b/lib/lib.go new file mode 100644 index 0000000..42178fc --- /dev/null +++ b/lib/lib.go @@ -0,0 +1,47 @@ +// Package lib contains functions useful for most programs. +package lib + +import ( + "fmt" + "os" + "path/filepath" +) + +var progname = filepath.Base(os.Args[0]) + +// Warnx displays a formatted error message to standard error, à la +// warnx(3). +func Warnx(format string, a ...interface{}) (int, error) { + format = fmt.Sprintf("[%s] %s", progname, format) + format += "\n" + return fmt.Fprintf(os.Stderr, format, a...) +} + +// Warn displays a formatted error message to standard output, +// appending the error string, à la warn(3). +func Warn(err error, format string, a ...interface{}) (int, error) { + format = fmt.Sprintf("[%s] %s", progname, format) + format += ": %v\n" + a = append(a, err) + return fmt.Fprintf(os.Stderr, format, a...) +} + +// Errx displays a formatted error message to standard error and exits +// with the status code from `exit`, à la errx(3). +func Errx(exit int, format string, a ...interface{}) { + format = fmt.Sprintf("[%s] %s", progname, format) + format += "\n" + fmt.Fprintf(os.Stderr, format, a...) + os.Exit(exit) +} + +// Err displays a formatting error message to standard error, +// appending the error string, and exits with the status code from +// `exit`, à la err(3). +func Err(exit int, err error, format string, a ...interface{}) { + format = fmt.Sprintf("[%s] %s", progname, format) + format += ": %v\n" + a = append(a, err) + fmt.Fprintf(os.Stderr, format, a...) + os.Exit(exit) +} diff --git a/logging/example/logger.go b/logging/example/logger.go new file mode 100644 index 0000000..12f083b --- /dev/null +++ b/logging/example/logger.go @@ -0,0 +1,31 @@ +package main + +import "github.com/kisom/goutils/logging" + +var log = logging.Init() +var olog = logging.New("subsystem #42", logging.LevelNotice) + +func main() { + log.Notice("Hello, world.") + log.Warning("this program is about to end") + + olog.Print("now online") + logging.Suppress("olog") + olog.Print("extraneous information") + + logging.Enable("olog") + olog.Print("relevant now") + + 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") +} diff --git a/logging/levels.go b/logging/levels.go new file mode 100644 index 0000000..209cac1 --- /dev/null +++ b/logging/levels.go @@ -0,0 +1,200 @@ +package logging + +import ( + "fmt" + "os" + "time" +) + +// A Level represents a logging level. +type Level uint8 + +// The following constants represent logging levels in increasing levels of seriousness. +const ( + // LevelDebug are debug output useful during program testing + // and debugging. + LevelDebug = 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. + LevelWarning + + // LevelError is for messages indicating an error of some + // kind. + LevelError + + // 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 +) + +func writeToOut(level Level) bool { + if level < LevelWarning { + return true + } + return false +} + +var levelPrefix = [...]string{ + LevelDebug: "[DEBUG] ", + LevelInfo: "[INFO] ", + LevelNotice: "[NOTICE] ", + LevelWarning: "[WARNING] ", + LevelError: "[ERROR] ", + LevelCritical: "[CRITICAL] ", + LevelAlert: "[ALERT] ", + LevelFatal: "[FATAL] ", +} + +var DateFormat = "2006-01-02T15:03:04-0700" + +func (l *Logger) outputf(level Level, format string, v []interface{}) { + if !logConfig.registered[l.domain] { + return + } + + if level >= l.level { + format = fmt.Sprintf("%s %s: %s%s\n", + time.Now().Format(DateFormat), + l.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 !logConfig.registered[l.domain] { + return + } + + if level >= l.level { + format := fmt.Sprintf("%s %s: %s", + time.Now().Format(DateFormat), + l.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. +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) +} diff --git a/logging/log.go b/logging/log.go new file mode 100644 index 0000000..1337eaf --- /dev/null +++ b/logging/log.go @@ -0,0 +1,165 @@ +// Package logging is an adaptation of the CFSSL logging library. It +// operates on domains, which are components for which logging can be +// selectively enabled or disabled. It also differentiates between +// normal messages (which are sent to standard output) and errors, +// which are sent to standard error. +package logging + +import ( + "io" + "os" + "path/filepath" + "sync" + + "github.com/kisom/goutils/mwc" +) + +var logConfig = struct { + registered map[string]bool + lock *sync.Mutex +}{ + registered: map[string]bool{}, + lock: new(sync.Mutex), +} + +// 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 { + return New(filepath.Base(os.Args[0]), DefaultLevel) +} + +// A Logger writes logs on behalf of a particular domain at a certain +// level. +type Logger struct { + 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 + } + + return l.err.Close() +} + +// Suppress ignores logs from a specific domain. +func Suppress(domain string) { + logConfig.lock.Lock() + defer logConfig.lock.Unlock() + logConfig.registered[domain] = false +} + +// SuppressAll suppresses all logging output. +func SuppressAll() { + logConfig.lock.Lock() + defer logConfig.lock.Unlock() + for domain := range logConfig.registered { + logConfig.registered[domain] = false + } +} + +// Enable enables logs from a specific domain. +func Enable(domain string) { + logConfig.lock.Lock() + defer logConfig.lock.Unlock() + logConfig.registered[domain] = true +} + +// EnableAll enables all domains. +func EnableAll() { + logConfig.lock.Lock() + defer logConfig.lock.Unlock() + for domain := range logConfig.registered { + logConfig.registered[domain] = true + } +} + +// New returns a new logger that writes to standard output for Notice +// and below and standard error for levels above Notice. +func New(domain string, level Level) *Logger { + l := &Logger{ + domain: domain, + level: level, + out: os.Stdout, + err: os.Stderr, + } + + Enable(domain) + return l +} + +// NewWriters 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. +func NewWriters(domain string, level Level, w, e io.WriteCloser) *Logger { + if e == nil { + e = w + } + + l := &Logger{ + domain: domain, + level: level, + out: w, + err: e, + } + + Enable(domain) + return l +} + +// NewFile 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 NewFile(domain string, level Level, outFile, errFile string, multiplex bool) (*Logger, error) { + l := &Logger{ + domain: domain, + level: level, + } + + var err error + l.out, err = os.OpenFile(outFile, os.O_APPEND|os.O_CREATE, 0644) + if err != nil { + return nil, err + } + + l.err, err = os.OpenFile(errFile, os.O_APPEND|os.O_CREATE, 0644) + if err != nil { + return nil, err + } + + if multiplex { + l.out = mwc.MultiWriteCloser(l.out, os.Stdout) + l.err = mwc.MultiWriteCloser(l.err, os.Stderr) + } + + Enable(domain) + return l, nil +} + +// Enable allows output from the logger. +func (l *Logger) Enable() { + Enable(l.domain) +} + +// Suppress ignores output from the logger. +func (l *Logger) Suppress() { + Suppress(l.domain) +} + +// 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.level = level +} diff --git a/mwc/mwc.go b/mwc/mwc.go new file mode 100644 index 0000000..64a023c --- /dev/null +++ b/mwc/mwc.go @@ -0,0 +1,42 @@ +// Package mwc implements MultiWriteClosers. +package mwc + +import "io" + +type mwc struct { + wcs []io.WriteCloser +} + +// Write implements the Writer interface. +func (t *mwc) Write(p []byte) (n int, err error) { + for _, w := range t.wcs { + n, err = w.Write(p) + if err != nil { + return + } + if n != len(p) { + err = io.ErrShortWrite + return + } + } + return len(p), nil +} + +// Close implements the Closer interface. +func (t *mwc) Close() error { + for _, wc := range t.wcs { + err := wc.Close() + if err != nil { + return err + } + } + return nil +} + +// MultiWriteClose creates a WriteCloser that duplicates its writes to +// all the provided writers, similar to the Unix tee(1) command. +func MultiWriteCloser(wc ...io.WriteCloser) io.WriteCloser { + wcs := make([]io.WriteCloser, len(wc)) + copy(wcs, wc) + return &mwc{wcs} +}