colog/colog.go

436 lines
9.2 KiB
Go

// Package colog implements prefix based logging by setting itself as output of the standard library
// and parsing the log messages. Level prefixes are called headers in CoLog terms to not confuse with
// log.Prefix() which is independent.
// Basic usage only requires registering:
// func main() {
// colog.Register()
// log.Print("info: that's all it takes!")
// }
//
// CoLog requires the standard logger to submit messages without prefix or flags. So it resets them
// while registering and assigns them to itself, unfortunately CoLog cannot be aware of any output
// previously set.
package colog
import (
"bytes"
"fmt"
"io"
"log"
"os"
"runtime"
"sync"
"time"
)
// std is the global singleton
// analog of the standard log.std
var std = NewCoLog(os.Stderr, "", 0)
// CoLog encapsulates our log writer
type CoLog struct {
mu sync.Mutex
host string
prefix []byte
minLevel Level
defaultLevel Level
formatter Formatter
customFmt bool
out io.Writer
}
// Entry represents a message being logged and all attached data
type Entry struct {
Level Level // severity: trace, debug, info, warning, error, alert
Time time.Time // time of the event
Host string // host origin of the message
File string // file where the log was called
Line int // line in the file where the log was called
Message []byte // logged message
}
// Level represents severity level
type Level uint8
// LevelMap links levels with output header bytes
type LevelMap map[Level][]byte
// HeaderMap links input header strings with levels
type HeaderMap map[string]Level
const (
// Unknown severity level
unknown Level = iota
// LDebug represents debug severity level
LDebug
// LInfo represents info severity level
LInfo
// LWarn represents warn severity level
LWarn
// LError represents error severity level
LError
)
// String implements the Stringer interface for levels
func (level Level) String() string {
switch level {
case LDebug:
return "debug"
case LInfo:
return "info"
case LWarn:
return "warn"
case LError:
return "error"
}
return "unknown"
}
var initialMinLevel = LDebug
var initialDefaultLevel = LInfo
// NewCoLog returns CoLog instance ready to be used in logger.SetOutput()
func NewCoLog(out io.Writer, prefix string, flags int) *CoLog {
cl := new(CoLog)
cl.minLevel = initialMinLevel
cl.defaultLevel = initialDefaultLevel
cl.prefix = []byte(prefix)
cl.formatter = &StdFormatter{Flag: flags}
cl.SetOutput(out)
if host, err := os.Hostname(); err != nil {
cl.host = host
}
return cl
}
func (cl *CoLog) Clone() *CoLog {
cl.mu.Lock()
defer cl.mu.Unlock()
item := new(CoLog)
item.minLevel = cl.minLevel
item.defaultLevel = cl.defaultLevel
item.prefix = append([]byte(nil), cl.prefix...)
item.formatter = cl.formatter
item.customFmt = cl.customFmt
item.out = cl.out
item.host = cl.host
return item
}
func Clone() *CoLog {
return std.Clone()
}
// Register sets CoLog as output for the default logger.
// It "hijacks" the standard logger flags and prefix previously set.
// It's not possible to know the output previously set, so the
// default os.Stderr is assumed.
func Register() {
// Inherit standard logger flags and prefix if appropriate
if !std.customFmt {
std.formatter.SetFlags(log.Flags())
}
if log.Prefix() != "" && len(std.prefix) == 0 {
std.SetPrefix(log.Prefix())
}
// Disable all extras
log.SetPrefix("")
log.SetFlags(0)
// Set CoLog as output
log.SetOutput(std)
}
func Get() *CoLog {
return std
}
func Set(cl *CoLog) {
std = cl
}
// SetHost sets the logger hostname assigned to the entries
func (cl *CoLog) SetHost(host string) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.host = host
}
func (cl *CoLog) AddPrefix(prefix string) *CoLog {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.prefix = append(cl.prefix, []byte(prefix)...)
return cl
}
func (cl *CoLog) SetPrefix(prefix string) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.prefix = []byte(prefix)
}
// SetMinLevel sets the minimum level that will be actually logged
func (cl *CoLog) SetMinLevel(l Level) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.minLevel = l
}
// SetDefaultLevel sets the level that will be used when no level is detected
func (cl *CoLog) SetDefaultLevel(l Level) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.defaultLevel = l
}
// SetFormatter sets the formatter to use
func (cl *CoLog) SetFormatter(f Formatter) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.customFmt = true
cl.formatter = f
}
// Flags returns the output flags for the formatter if any
func (cl *CoLog) Flags() int {
cl.mu.Lock()
defer cl.mu.Unlock()
if cl.formatter == nil {
return 0
}
return cl.formatter.Flags()
}
// SetFlags sets the output flags for the formatter if any
func (cl *CoLog) SetFlags(flags int) {
cl.mu.Lock()
defer cl.mu.Unlock()
if cl.formatter == nil {
return
}
cl.formatter.SetFlags(flags)
}
// SetOutput is analog to log.SetOutput sets the output destination.
func (cl *CoLog) SetOutput(w io.Writer) {
cl.mu.Lock()
defer cl.mu.Unlock()
cl.out = w
// if we have a color formatter, notify if new output supports color
if _, ok := cl.formatter.(ColorFormatter); ok {
cl.formatter.(ColorFormatter).ColorSupported(cl.colorSupported())
}
}
// NewLogger returns a colog-enabled logger
func (cl *CoLog) NewLogger() *log.Logger {
cl.mu.Lock()
defer cl.mu.Unlock()
return log.New(cl, "", 0)
}
// Write implements io.Writer interface to that the standard logger uses.
func (cl *CoLog) Write(p []byte) (n int, err error) {
cl.mu.Lock()
defer cl.mu.Unlock()
e := cl.makeEntry(p, LInfo, 5 /*calldepth*/)
if e.Level != unknown && e.Level < cl.minLevel {
return 0, nil
}
if e.Level == unknown && cl.defaultLevel < cl.minLevel {
return 0, nil
}
fp, err := cl.formatter.Format(e)
if err != nil {
fmt.Fprintf(os.Stderr, "colog: failed to format entry: %v\n", err)
return 0, err
}
n, err = cl.out.Write(fp)
if err != nil {
return n, err
}
return len(p), nil
}
func (cl *CoLog) makeEntry(p []byte, level Level, calldepth int) *Entry {
e := &Entry{
Time: time.Now(),
Host: cl.host,
Level: level,
Message: append(cl.prefix, bytes.TrimRight(p, "\n")...),
}
// this is a bit expensive, check is anyone might actually need it
if cl.formatter.Flags()&(log.Lshortfile|log.Llongfile) != 0 {
// release lock while getting caller info - it's expensive
// (makeEntry is called under mutex)
cl.mu.Unlock()
e.File, e.Line = getFileLine(calldepth)
cl.mu.Lock()
}
return e
}
// get file a line where logger was called
func getFileLine(calldepth int) (string, int) {
var file string
var line int
var ok bool
_, file, line, ok = runtime.Caller(calldepth)
if !ok {
file = "???"
line = 0
}
return file, line
}
// figure if output supports color
func (cl *CoLog) colorSupported() bool {
// ColorSupporters can decide themselves
if ce, ok := cl.out.(ColorSupporter); ok {
return ce.ColorSupported()
}
// Windows users need ColorSupporter outputs
if runtime.GOOS == "windows" {
return false
}
// Check for Fd() method
output, ok := cl.out.(interface {
Fd() uintptr
})
// If no file descriptor it's not a TTY
if !ok {
return false
}
return isTerminal(int(output.Fd()))
}
/////////////////////////////////////////////////////////////////////////
func (cl *CoLog) Log(str string) {
cl.Output(LInfo, 4, str)
}
func (cl *CoLog) Logf(format string, v ...interface{}) {
cl.Output(LInfo, 4, fmt.Sprintf(format, v...))
}
func Log(str string) {
std.Output(LInfo, 4, str)
}
func Logf(format string, v ...interface{}) {
std.Output(LInfo, 4, fmt.Sprintf(format, v...))
}
func (cl *CoLog) Debug(str string) {
cl.Output(LDebug, 4, str)
}
func (cl *CoLog) Debugf(format string, v ...interface{}) {
cl.Output(LDebug, 4, fmt.Sprintf(format, v...))
}
func Debug(str string) {
std.Output(LDebug, 4, str)
}
func Debugf(format string, v ...interface{}) {
std.Output(LDebug, 4, fmt.Sprintf(format, v...))
}
func (cl *CoLog) Error(str string) {
cl.Output(LError, 4, str)
}
func (cl *CoLog) Errorf(format string, v ...interface{}) {
cl.Output(LError, 4, fmt.Sprintf(format, v...))
}
func Error(str string) {
std.Output(LError, 4, str)
}
func Errorf(format string, v ...interface{}) {
std.Output(LError, 4, fmt.Sprintf(format, v...))
}
func (cl *CoLog) Warn(str string) {
cl.Output(LWarn, 4, str)
}
func (cl *CoLog) Warnf(format string, v ...interface{}) {
cl.Output(LWarn, 4, fmt.Sprintf(format, v...))
}
func Warn(str string) {
std.Output(LWarn, 4, str)
}
func Warnf(format string, v ...interface{}) {
std.Output(LWarn, 4, fmt.Sprintf(format, v...))
}
func (cl *CoLog) Output(level Level, calldepth int, str string) (err error) {
//Let's do these checks before locking the mutex
if level != unknown && level < cl.minLevel {
return nil
}
if level == unknown && cl.defaultLevel < cl.minLevel {
return nil
}
//not using defer cl.mu.Unlock() because we want to have
//more fine grained control over mutex
cl.mu.Lock()
e := cl.makeEntry([]byte(str), level, calldepth)
fp, err := cl.formatter.Format(e)
if err != nil {
cl.mu.Unlock()
fmt.Fprintf(os.Stderr, "colog: failed to format entry: %v\n", err)
return
}
out := cl.out
cl.mu.Unlock()
_, err = out.Write(fp)
return
}
func Output(level Level, calldepth int, str string) (err error) {
return std.Output(level, calldepth, str)
}