436 lines
9.2 KiB
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)
|
|
}
|