commit 248ff19dbd0645564d664b35c30205fd5b1f4246 Author: Noah Petherbridge Date: Sat Jun 10 17:01:22 2017 -0700 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cabbc44 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Noah Petherbridge + +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/README.md b/README.md new file mode 100644 index 0000000..fefbc3f --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Go Log + +This is Yet Another Logger for Go programs. + +![Screenshot](https://raw.githubusercontent.com/kirsle/golog/master/screenshot.png) + +This is a logging package designed for local interactive shells running text +based Go programs. To that end, this prints colorful log lines with customizable +themes. + +The color options for the log lines are `NoColor` (default), `ANSIColor` +which limits the color codes to the standard 16 ANSI colors, and +`ExtendedColor` which supports the 256-color palette of `xterm` and other +modern terminal emulators. The theming engine supports defining colors using +hex codes, supported by [tomnomnom/xtermcolor](https://github.com/tomnomnom/xtermcolor). + +This module is still a work in progress and will be extended and improved as I +use it for other personal Go projects. + +# Usage + +```go +package main + +import "github.com/kirsle/golog" + +var log golog.Logger + +func init() { + // Get a named logger and configure it. Note: you can call GetLogger any + // number of times from any place in your codebase. It implements the + // singleton pattern. + log = golog.GetLogger("main") + log.Configure(&golog.Config{ + Colors: golog.ExtendedColor, + Theme: golog.DarkTheme, + }) +} + +func main() { + // The log functions work like `fmt.Printf` + log.Debug("Running on %s", runtime.GOOS) + log.Info("Hello, world!") +} +``` + +# License + +``` +The MIT License (MIT) + +Copyright (c) 2017 Noah Petherbridge + +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/ansi/colors.go b/ansi/colors.go new file mode 100644 index 0000000..f0c5b8c --- /dev/null +++ b/ansi/colors.go @@ -0,0 +1,23 @@ +package ansi + +// Names and escape codes for the standard ANSI colors. +const ( + Black = `30` + BrightBlack = `30;1` + Red = `31` + BrightRed = `31;1` + Green = `32` + BrightGreen = `32;1` + Yellow = `33` + BrightYellow = `33;1` + Blue = `34` + BrightBlue = `34;1` + Magenta = `35` + BrightMagenta = `35;1` + Cyan = `36` + BrightCyan = `36;1` + White = `37` + BrightWhite = `37;1` + + Reset = `0` +) diff --git a/colors.go b/colors.go new file mode 100644 index 0000000..3a22524 --- /dev/null +++ b/colors.go @@ -0,0 +1,34 @@ +package golog + +import ( + "fmt" + + "github.com/tomnomnom/xtermcolor" +) + +type colorLevel int + +// Options for color support in your logger. +const ( + // NoColor doesn't use any color codes at all (plain text). + NoColor colorLevel = iota + + // ANSIColor uses the standard 16 colors supported by most terminals. This + // option is the most portable across platforms. + ANSIColor + + // ExtendedColor allows the use of 256 colors supported by most modern + // terminals (24-bit color codes). + ExtendedColor +) + +// HexColor is a convenient wrapper around `xtermcolor.FromHexStr` to define colors +// for themes for xterm-256color codes. +func HexColor(hex string) string { + code, err := xtermcolor.FromHexStr(hex) + if err != nil { + code = 201 // bright magenta seems like a good default + } + + return fmt.Sprintf("38;5;%d", code) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..20e80fb --- /dev/null +++ b/config.go @@ -0,0 +1,72 @@ +package golog + +import "io" + +// Config stores settings that control the logger's behavior. +type Config struct { + // Level is one of DebugLevel, InfoLevel, WarnLevel, ErrorLevel or FatalLevel. + // Messages emitted by the logger must be 'at least' this level to be logged. + Level logLevel + + // What colors are supported? Default is NoColor. Use ANSIColor to support + // legacy terminal emulators, or ExtendedColor for modern 256-color support. + Colors colorLevel + + // Which color theme are you using? The default is DarkTheme. + Theme Theme + + // Where to write the log messages to? If not defined with a custom io.Writer, + // the default goes to standard output for Debug and Info messages and + // standard error for warnings, errors, and fatal messages. + Writer *io.Writer + + // How do you want to format your log lines? This should be a Go text format + // string, with the following variable placeholders: + // + // {{.Time}} inserts the date/time stamp for the log message. + // {{.Level}} inserts a label for the log level, e.g. "INFO" or "WARN" + // {{.Message}} inserts the text of the log message itself. + // {{.Primary}} inserts the color sequence for the primary color based + // on the log level for the message. + // {{.Secondary}} inserts the color sequence for the secondary color. + // {{.Reset}} inserts the 'reset' color sequence to stop coloring + // the rest of the text that follows. + // + // The default log format is as follows: + // + // {{.Secondary}}{{.Time}}{{.Reset}} {{.Primary}}[{{.Level}}]{{.Reset}} {{.Message}} + Format string + + // How do you want to format your time stamps? (The `{{.Time}}`). This uses + // the Go `time` module, so the TimeFormat should use their reference date/time. + // The default TimeFormat is: `2006-01-02 15:04:05` + TimeFormat string +} + +// DefaultConfig returns a Config with the default values filled in. +func DefaultConfig() *Config { + return &Config{ + Theme: DarkTheme, + Format: DefaultFormat, + TimeFormat: DefaultTime, + } +} + +// Configure applies the configuration to the logger. If any of the following +// keys are not defined (or have zero-values), the default value for the key will +// be used instead: +// +// Format +// TimeFormat +func (l *Logger) Configure(cfg *Config) { + // Important keys and their defaults. + if cfg.Format == "" { + cfg.Format = DefaultFormat + } + if cfg.TimeFormat == "" { + cfg.TimeFormat = DefaultTime + } + + l.Config = cfg + l.template = nil +} diff --git a/formatter.go b/formatter.go new file mode 100644 index 0000000..a355d7d --- /dev/null +++ b/formatter.go @@ -0,0 +1,102 @@ +package golog + +import ( + "bytes" + "fmt" + "text/template" + "time" + + "github.com/kirsle/golog/ansi" +) + +// Convenient log formats to use in your logger. +const ( + // DefaultFormat: shows the date in the secondary (dark) color, the label + // in the bright color, and the message text in the normal color. + DefaultFormat = `{{.Secondary}}{{.Time}}{{.Reset}} {{.Primary}}[{{.Level}}]{{.Reset}} {{.Message}}` + + // ColorfulFormat: like the DefaultFormat, but the message itself is also + // colored using the secondary color. + ColorfulFormat = `{{.Secondary}}{{.Time}}{{.Reset}} {{.Primary}}[{{.Level}}]{{.Reset}} {{.Secondary}}{{.Message}}{{.Reset}}` +) + +// Convenient time formats to use in your logger. +const ( + // DefaultTime is the default, in `yyyy-mm-dd hh:mm:ss` format. + DefaultTime = `2006-01-02 15:04:05` + + // FriendlyTime is a human readable `Jan 2 15:04:05 2006` format. + FriendlyTime = `Jan 2 15:04:05 2006` +) + +// formatter provides the variables that can be used in the log format. +type formatter struct { + Time string + Level string + Message string + Primary string + Secondary string + Reset string +} + +// Format and return a log message. +func (l *Logger) Format(level logLevel, tmpl string, args ...interface{}) string { + // Prepare the variables to apply to the log message format. + format := formatter{ + Time: time.Now().Format(l.Config.TimeFormat), + Level: levelNames[level], + Message: fmt.Sprintf(tmpl, args...), + } + + // Find the theme color to use. + if l.Config.Colors != NoColor { + var ( + primary ThemeColor + secondary ThemeColor + ) + + switch level { + case DebugLevel: + primary = l.Config.Theme.Debug + secondary = l.Config.Theme.DebugSecondary + case InfoLevel: + primary = l.Config.Theme.Info + secondary = l.Config.Theme.InfoSecondary + case WarnLevel: + primary = l.Config.Theme.Warn + secondary = l.Config.Theme.WarnSecondary + case ErrorLevel: + primary = l.Config.Theme.Error + secondary = l.Config.Theme.ErrorSecondary + } + + // What color level are we supporting? + if l.Config.Colors == ANSIColor { + format.Primary = fmt.Sprintf("\x1B[%sm", primary.ANSI) + format.Secondary = fmt.Sprintf("\x1B[%sm", secondary.ANSI) + format.Reset = fmt.Sprintf("\x1B[%sm", ansi.Reset) + } else if l.Config.Colors == ExtendedColor { + format.Primary = fmt.Sprintf("\x1B[%sm", primary.Extended) + format.Secondary = fmt.Sprintf("\x1B[%sm", secondary.Extended) + format.Reset = fmt.Sprintf("\x1B[%sm", ansi.Reset) + } + } + + // Do we have the template cached? + if l.template == nil { + template, err := template.New("golog").Parse(l.Config.Format) + if err != nil { + return fmt.Sprintf("[GoLog format error: %s]", err) + } + l.template = template + } + + // Evaluate the template. + var buf bytes.Buffer + err := l.template.Execute(&buf, format) + if err != nil { + return fmt.Sprintf("[GoLog template error: %s]", err) + } + + return buf.String() +} diff --git a/golog.go b/golog.go new file mode 100644 index 0000000..64836b9 --- /dev/null +++ b/golog.go @@ -0,0 +1,43 @@ +package golog + +import ( + "sync" + "text/template" +) + +// An internal map of named loggers. This allows for GetLogger() to be called +// many times from anywhere in your code base, but for only one logger instance +// to be created for it. +var ( + loggers map[string]*Logger + loggerMutex sync.Mutex +) + +func init() { + loggers = map[string]*Logger{} +} + +// Logger stores the configuration for a named logger instance. +type Logger struct { + Name string + Config *Config + + // Private cached text/template, the first time the formatter is used. + template *template.Template +} + +// GetLogger initializes and returns a new Logger. +func GetLogger(name string) *Logger { + loggerMutex.Lock() + defer loggerMutex.Unlock() + + // Initialize the logger the first time we ask for it. + if _, ok := loggers[name]; !ok { + loggers[name] = &Logger{ + Name: name, + Config: DefaultConfig(), + } + } + + return loggers[name] +} diff --git a/golog_test.go b/golog_test.go new file mode 100644 index 0000000..239f7a4 --- /dev/null +++ b/golog_test.go @@ -0,0 +1,54 @@ +package golog + +import "testing" + +func TestColors(t *testing.T) { + log := GetLogger("test") + + // Helper function to emit all the log types. + emitLogs := func(message string) { + log.Debug(message) + log.Info(message) + log.Warn(message) + log.Error(message) + } + + log.Configure(&Config{ + Theme: DarkTheme, + Colors: ANSIColor, + }) + emitLogs("With standard 16-color ANSI codes.") + + log.Configure(&Config{ + Theme: DarkTheme, + Colors: ExtendedColor, + }) + emitLogs("With xterm-256color codes.") + + log.Configure(&Config{ + Theme: DarkTheme, + Colors: ExtendedColor, + Format: ColorfulFormat, + }) + emitLogs("Colorful format.") +} + +func TestLogLevels(t *testing.T) { + log := GetLogger("levels") + + // Helper function to emit all the log types. + emitLogs := func(message string) { + log.Debug(message) + log.Info(message) + log.Warn(message) + log.Error(message) + } + + emitLogs("Default log level=debug") + log.Config.Level = InfoLevel + emitLogs("With Level=Info") + log.Config.Level = WarnLevel + emitLogs("With Level=Warn") + log.Config.Level = ErrorLevel + emitLogs("With Level=Error") +} diff --git a/levels.go b/levels.go new file mode 100644 index 0000000..725d258 --- /dev/null +++ b/levels.go @@ -0,0 +1,70 @@ +package golog + +import ( + "fmt" + "os" +) + +type logLevel int + +// Log levels for controlling whether or not logs of certain types will be +// emitted by your logger. +const ( + DebugLevel logLevel = iota + InfoLevel + WarnLevel + ErrorLevel +) + +// Map log levels to human readable labels. +var levelNames = map[logLevel]string{ + DebugLevel: "DEBUG", + InfoLevel: "INFO", + WarnLevel: "WARN", + ErrorLevel: "ERROR", +} + +// emit is the general purpose log line emitter. +func (l *Logger) emit(level logLevel, tmpl string, args ...interface{}) { + message := l.Format(level, tmpl, args...) + + // If we have a log writer, send it there. + if l.Config.Writer != nil { + // l.Config.Writer.Write(message) + } else { + // No writer given so we default to standard out/error. + if level <= InfoLevel { + fmt.Fprintln(os.Stdout, message) + } else { + fmt.Fprintln(os.Stderr, message) + } + } +} + +// Debug emits a debug-level message from the logger. +func (l *Logger) Debug(tmpl string, args ...interface{}) { + if l.Config.Level <= DebugLevel { + l.emit(DebugLevel, tmpl, args...) + } +} + +// Info emits an informational message. +func (l *Logger) Info(tmpl string, args ...interface{}) { + if l.Config.Level <= InfoLevel { + l.emit(InfoLevel, tmpl, args...) + } +} + +// Warn emits a warning message. +func (l *Logger) Warn(tmpl string, args ...interface{}) { + if l.Config.Level <= WarnLevel { + l.emit(WarnLevel, tmpl, args...) + } +} + +// Error emits an error message. +func (l *Logger) Error(tmpl string, args ...interface{}) { + if l.Config.Level <= ErrorLevel { + l.emit(ErrorLevel, tmpl, args...) + } +} diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..643376a Binary files /dev/null and b/screenshot.png differ diff --git a/themes.go b/themes.go new file mode 100644 index 0000000..f161d6e --- /dev/null +++ b/themes.go @@ -0,0 +1,37 @@ +package golog + +import "github.com/kirsle/golog/ansi" + +// Theme defines the color scheme for a logger. Each log level has two colors: +// a primary (for the label itself) and a secondary color. For example, if your +// log lines include a date/time this could be colored using the secondary +// color. +type Theme struct { + Debug ThemeColor + DebugSecondary ThemeColor + Info ThemeColor + InfoSecondary ThemeColor + Warn ThemeColor + WarnSecondary ThemeColor + Error ThemeColor + ErrorSecondary ThemeColor +} + +// ThemeColor defines a color tuple for ANSI (legacy) support and modern +// 256-color support. +type ThemeColor struct { + ANSI string + Extended string +} + +// DarkTheme is a suitable default theme for dark terminal backgrounds. +var DarkTheme = Theme{ + Debug: ThemeColor{ansi.BrightCyan, HexColor("#FF99FF")}, + DebugSecondary: ThemeColor{ansi.Cyan, HexColor("#996699")}, + Info: ThemeColor{ansi.BrightGreen, HexColor("#0099FF")}, + InfoSecondary: ThemeColor{ansi.Green, HexColor("#006699")}, + Warn: ThemeColor{ansi.BrightYellow, HexColor("#FF9900")}, + WarnSecondary: ThemeColor{ansi.Yellow, HexColor("#996600")}, + Error: ThemeColor{ansi.BrightRed, HexColor("#FF0000")}, + ErrorSecondary: ThemeColor{ansi.Red, HexColor("#CC0000")}, +}