First commit
This commit is contained in:
commit
fa31019ff5
|
@ -0,0 +1,435 @@
|
|||
// 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)
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
package colog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type outputTest struct {
|
||||
in string
|
||||
out string
|
||||
}
|
||||
|
||||
var outputTests = []outputTest{
|
||||
{"Info should be green %s", "[INF] Info should be green %s\n"},
|
||||
}
|
||||
|
||||
func TestColors(t *testing.T) {
|
||||
|
||||
log.SetFlags(log.LstdFlags)
|
||||
Register()
|
||||
|
||||
Get().SetMinLevel(LDebug)
|
||||
Get().SetDefaultLevel(LDebug)
|
||||
|
||||
for _, tt := range outputTests {
|
||||
tt.in = fmt.Sprintf(tt.in, "")
|
||||
log.Println(tt.in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultLevel(t *testing.T) {
|
||||
|
||||
tw := new(mockWriter)
|
||||
log.SetFlags(0)
|
||||
Register()
|
||||
Get().SetOutput(tw)
|
||||
Get().SetFormatter(&StdFormatter{Colors: false})
|
||||
Get().SetDefaultLevel(LDebug)
|
||||
|
||||
log.Println("no prefix text")
|
||||
if "[INF] no prefix text\n" != tw.String() {
|
||||
t.Fatalf("Default level failed: %s", tw.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinDefaultLevel(t *testing.T) {
|
||||
|
||||
tw := new(mockWriter)
|
||||
log.SetFlags(0)
|
||||
Register()
|
||||
Get().SetOutput(tw)
|
||||
Get().SetFormatter(&StdFormatter{Colors: false})
|
||||
|
||||
Get().SetMinLevel(LDebug)
|
||||
Get().SetDefaultLevel(LDebug)
|
||||
log.Println("no prefix text")
|
||||
if "[INF] no prefix text\n" != tw.String() {
|
||||
t.Fatalf("Default level failed: %s", tw.String())
|
||||
}
|
||||
|
||||
Get().SetMinLevel(LError)
|
||||
log.Println("should not print")
|
||||
if "[INF] no prefix text\n" != tw.String() {
|
||||
t.Fatalf("Default level failed: %s", tw.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefix(t *testing.T) {
|
||||
|
||||
tw := new(mockWriter)
|
||||
cl := NewCoLog(tw, "abc ", 0)
|
||||
cl.SetFormatter(&StdFormatter{Colors: false})
|
||||
|
||||
logger := cl.NewLogger()
|
||||
logger.Println("some text")
|
||||
if "[INF] abc some text\n" != tw.String() {
|
||||
t.Fatalf("Prefix output failed: %s", tw.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleOutput(t *testing.T) {
|
||||
|
||||
tw := new(mockWriter)
|
||||
cl := NewCoLog(tw, "", 0)
|
||||
cl.SetFormatter(&StdFormatter{Colors: false})
|
||||
logger := cl.NewLogger()
|
||||
for k, tt := range outputTests {
|
||||
|
||||
seq := randSeq(k)
|
||||
tt.in = fmt.Sprintf(tt.in, seq)
|
||||
tt.out = fmt.Sprintf(tt.out, seq)
|
||||
|
||||
logger.Println(tt.in)
|
||||
if tt.out != tw.String() {
|
||||
t.Fatalf("Simple output not found:\n %s\n %s", tt.out, string(tw.Data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputRace(t *testing.T) {
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
go testStdLoggerOutput(t, &wg)
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go testNewLoggerOutput(t, &wg)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func testStdLoggerOutput(t *testing.T, wg *sync.WaitGroup) {
|
||||
|
||||
tb := new(mockBufferWriter)
|
||||
tb.Data = make(map[string][]byte, len(outputTests))
|
||||
log.SetFlags(0)
|
||||
Register()
|
||||
Get().SetOutput(tb)
|
||||
Get().SetMinLevel(LDebug)
|
||||
Get().SetDefaultLevel(LDebug)
|
||||
Get().SetFormatter(&StdFormatter{Colors: false})
|
||||
|
||||
for k, tt := range outputTests {
|
||||
wg.Add(1)
|
||||
go func(tt outputTest, k int) {
|
||||
|
||||
seq := randSeq(k)
|
||||
tt.in = fmt.Sprintf(tt.in, seq)
|
||||
tt.out = fmt.Sprintf(tt.out, seq)
|
||||
|
||||
log.Println(tt.in)
|
||||
if !tb.IsWritten(tt.out) {
|
||||
t.Errorf("Raced std output not found: %s", tt.out)
|
||||
}
|
||||
wg.Done()
|
||||
}(tt, k)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
func testNewLoggerOutput(t *testing.T, wg *sync.WaitGroup) {
|
||||
|
||||
tb := new(mockBufferWriter)
|
||||
tb.Data = make(map[string][]byte, len(outputTests))
|
||||
cl := NewCoLog(tb, "", 0)
|
||||
cl.SetFormatter(&StdFormatter{Colors: false})
|
||||
logger := cl.NewLogger()
|
||||
|
||||
for k, tt := range outputTests {
|
||||
wg.Add(1)
|
||||
go func(tt outputTest, k int) {
|
||||
|
||||
seq := randSeq(k)
|
||||
tt.in = fmt.Sprintf(tt.in, seq)
|
||||
tt.out = fmt.Sprintf(tt.out, seq)
|
||||
|
||||
logger.Println(tt.in)
|
||||
if !tb.IsWritten(tt.out) {
|
||||
t.Errorf("Raced logger output not found: %s", tt.out)
|
||||
}
|
||||
wg.Done()
|
||||
}(tt, k)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
type mockWriter struct {
|
||||
mux sync.Mutex
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (tw *mockWriter) Write(p []byte) (n int, err error) {
|
||||
tw.mux.Lock()
|
||||
defer tw.mux.Unlock()
|
||||
tw.Data = p
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (tw *mockWriter) String() string {
|
||||
tw.mux.Lock()
|
||||
defer tw.mux.Unlock()
|
||||
return string(tw.Data)
|
||||
}
|
||||
|
||||
type mockBufferWriter struct {
|
||||
mux sync.Mutex
|
||||
Data map[string][]byte
|
||||
}
|
||||
|
||||
func (tb *mockBufferWriter) Write(p []byte) (n int, err error) {
|
||||
tb.mux.Lock()
|
||||
defer tb.mux.Unlock()
|
||||
tb.Data[string(p)] = p
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (tb *mockBufferWriter) IsWritten(s string) bool {
|
||||
tb.mux.Lock()
|
||||
defer tb.mux.Unlock()
|
||||
_, ok := tb.Data[s]
|
||||
return ok
|
||||
}
|
||||
|
||||
type mockHook struct {
|
||||
entry *Entry
|
||||
levels []Level
|
||||
}
|
||||
|
||||
func (h *mockHook) Levels() []Level {
|
||||
return h.levels
|
||||
}
|
||||
|
||||
func (h *mockHook) Fire(e *Entry) error {
|
||||
h.entry = e
|
||||
return nil
|
||||
}
|
||||
|
||||
func randSeq(n int) string {
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package colog
|
||||
|
||||
// Hook is the interface to be implemented by event hooks
|
||||
type Hook interface {
|
||||
Levels() []Level // returns the set of levels for which the hook should be triggered
|
||||
Fire(*Entry) error // triggers the hook, this function will be called for every eligible log entry
|
||||
}
|
||||
|
||||
// Formatter interface must be implemented by message formatters
|
||||
// Format(*Entry) will be called and the resulting bytes sent to output
|
||||
type Formatter interface {
|
||||
Format(*Entry) ([]byte, error) // The actual formatter called every time
|
||||
SetFlags(flags int) // Like the standard log.SetFlags(flags int)
|
||||
Flags() int // Like the standard log.Flags() int
|
||||
}
|
||||
|
||||
// ColorFormatter interface can be implemented by formatters
|
||||
// to get notifications on whether the output supports color
|
||||
type ColorFormatter interface {
|
||||
Formatter
|
||||
ColorSupported(yes bool)
|
||||
}
|
||||
|
||||
// ColorSupporter interface can be implemented by "smart"
|
||||
// outputs that want to handle color display themselves
|
||||
type ColorSupporter interface {
|
||||
ColorSupported() bool
|
||||
}
|
||||
|
||||
// Extractor interface must be implemented by data extractors
|
||||
// the extractor reads the message and tries to extract key-value
|
||||
// pairs from the message and sets the in the entry
|
||||
type Extractor interface {
|
||||
Extract(*Entry) error
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package colog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// JSONFormatter serializes entries to JSON
|
||||
// TimeFormat can be any Go time format, if empty
|
||||
// it will mimic the standard logger format
|
||||
// LevelAsNum will use a numeric string "1", "2",...
|
||||
// for as levels instead of "trace", "debug", ..
|
||||
type JSONFormatter struct {
|
||||
TimeFormat string
|
||||
LevelAsNum bool
|
||||
Flag int
|
||||
}
|
||||
|
||||
// JSONEntry is an entry with the final JSON field types
|
||||
// We can not just implement the Marshaller interface since
|
||||
// some of the process depends on runtime options
|
||||
type JSONEntry struct {
|
||||
Level string `json:"level,omitempty"`
|
||||
Time string `json:"time,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
File string `json:"file,omitempty"`
|
||||
Line int `json:"line,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// Format takes and entry and returns the formatted output in bytes
|
||||
func (jf *JSONFormatter) Format(e *Entry) ([]byte, error) {
|
||||
|
||||
file, line := jf.fileLine(e)
|
||||
date := jf.date(e)
|
||||
|
||||
var level string
|
||||
if jf.LevelAsNum {
|
||||
level = strconv.Itoa(int(e.Level))
|
||||
} else {
|
||||
level = e.Level.String()
|
||||
}
|
||||
|
||||
je := &JSONEntry{
|
||||
Level: level,
|
||||
Time: date,
|
||||
Host: e.Host,
|
||||
File: file,
|
||||
Line: line,
|
||||
Message: string(e.Message),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(je)
|
||||
return append(data, '\n'), err
|
||||
}
|
||||
|
||||
// Flags returns the output flags for the formatter.
|
||||
func (jf *JSONFormatter) Flags() int {
|
||||
return jf.Flag
|
||||
}
|
||||
|
||||
// SetFlags sets the output flags for the formatter.
|
||||
func (jf *JSONFormatter) SetFlags(flags int) {
|
||||
jf.Flag = flags
|
||||
}
|
||||
|
||||
func (jf *JSONFormatter) fileLine(e *Entry) (file string, line int) {
|
||||
if jf.Flag&(log.Lshortfile|log.Llongfile) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
file = e.File
|
||||
line = e.Line
|
||||
if jf.Flag&log.Lshortfile != 0 {
|
||||
short := file
|
||||
for i := len(file) - 1; i > 0; i-- {
|
||||
if file[i] == '/' {
|
||||
short = file[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
file = short
|
||||
}
|
||||
|
||||
return file, line
|
||||
}
|
||||
|
||||
func (jf *JSONFormatter) date(e *Entry) (date string) {
|
||||
if jf.TimeFormat != "" {
|
||||
return e.Time.Format(jf.TimeFormat)
|
||||
}
|
||||
|
||||
if jf.Flag&(log.Ldate|log.Ltime|log.Lmicroseconds) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if jf.Flag&log.Ldate != 0 {
|
||||
year, month, day := e.Time.Date()
|
||||
date = fmt.Sprintf("%d/%d/%d", year, month, day)
|
||||
}
|
||||
|
||||
if jf.Flag&(log.Ltime|log.Lmicroseconds) != 0 {
|
||||
hour, min, sec := e.Time.Clock()
|
||||
date = fmt.Sprintf("%s %d:%d:%d", date, hour, min, sec)
|
||||
if jf.Flag&log.Lmicroseconds != 0 {
|
||||
date = fmt.Sprintf("%s.%d", date, e.Time.Nanosecond())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return date
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package colog
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type JSONFormatTest struct {
|
||||
entry Entry
|
||||
prefix string
|
||||
flags int
|
||||
tfmt string
|
||||
lnum bool
|
||||
output string
|
||||
}
|
||||
|
||||
var JSONFormatTests = []JSONFormatTest{
|
||||
{
|
||||
entry: Entry{
|
||||
Level: LInfo,
|
||||
Message: []byte("some message"),
|
||||
},
|
||||
output: `{"level":"info","message":"some message"}` + "\n",
|
||||
},
|
||||
{
|
||||
entry: Entry{
|
||||
Time: TTime,
|
||||
Level: LDebug,
|
||||
Message: []byte("some message"),
|
||||
},
|
||||
flags: log.Ldate,
|
||||
output: `{"level":"debug","time":"2015/8/1","message":"some message"}` + "\n",
|
||||
},
|
||||
{
|
||||
entry: Entry{
|
||||
Time: TTime,
|
||||
Level: LError,
|
||||
File: "/src/file.go",
|
||||
Line: 142,
|
||||
Message: []byte("some message"),
|
||||
},
|
||||
flags: log.Ldate | log.Llongfile,
|
||||
output: `{"level":"error","time":"2015/8/1","file":"/src/file.go","line":142,"message":"some message"}` + "\n",
|
||||
},
|
||||
{
|
||||
entry: Entry{
|
||||
Time: TTime,
|
||||
Level: LDebug,
|
||||
Message: []byte("some message"),
|
||||
},
|
||||
lnum: true,
|
||||
flags: log.Ldate,
|
||||
output: `{"level":"1","time":"2015/8/1","message":"some message"}` + "\n",
|
||||
},
|
||||
}
|
||||
|
||||
func TestJSONFormatter(t *testing.T) {
|
||||
for _, tt := range JSONFormatTests {
|
||||
f := JSONFormatter{
|
||||
Flag: tt.flags,
|
||||
LevelAsNum: tt.lnum,
|
||||
TimeFormat: tt.tfmt,
|
||||
}
|
||||
|
||||
b, err := f.Format(&tt.entry)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(b) != tt.output {
|
||||
t.Errorf("Unexpected JSON formatter output: %s", b)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package colog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
var colorLabels = LevelMap{
|
||||
LDebug: []byte("[\x1b[0;36mDBG\x1b[0m] "),
|
||||
LInfo: []byte("[\x1b[0;32mINF\x1b[0m] "),
|
||||
LWarn: []byte("\x1b[0;35m[WARN]\x1b[0m "),
|
||||
LError: []byte("\x1b[0;31m[ERR]\x1b[0m "),
|
||||
}
|
||||
|
||||
var plainLabels = LevelMap{
|
||||
LDebug: []byte("[DBG] "),
|
||||
LInfo: []byte("[INF] "),
|
||||
LWarn: []byte("[WARN] "),
|
||||
LError: []byte("[ERR] "),
|
||||
}
|
||||
|
||||
// StdFormatter supports plain and color level headers
|
||||
// and bold/padded fields
|
||||
type StdFormatter struct {
|
||||
Flag int
|
||||
Colors bool // Force enable colors
|
||||
NoColors bool // Force disable colors (has preference)
|
||||
colorSupported bool
|
||||
}
|
||||
|
||||
// Format takes and entry and returns the formatted output in bytes
|
||||
func (sf *StdFormatter) Format(e *Entry) ([]byte, error) {
|
||||
|
||||
// Normal headers. time, file, etc
|
||||
var header, message []byte
|
||||
sf.stdHeader(&header, e.Time, e.File, e.Line)
|
||||
|
||||
// Level headers
|
||||
headers := sf.levelHeaders()
|
||||
message = append(headers[e.Level], append(header, e.Message...)...)
|
||||
|
||||
return append(message, '\n'), nil
|
||||
}
|
||||
|
||||
// levelHeaders returns plain or color level headers
|
||||
// depending on user preference and output support
|
||||
func (sf *StdFormatter) levelHeaders() LevelMap {
|
||||
switch {
|
||||
case sf.NoColors:
|
||||
return plainLabels
|
||||
case sf.Colors:
|
||||
return colorLabels
|
||||
case sf.colorSupported:
|
||||
return colorLabels
|
||||
}
|
||||
return plainLabels
|
||||
}
|
||||
|
||||
// Flags returns the output flags for the formatter.
|
||||
func (sf *StdFormatter) Flags() int {
|
||||
return sf.Flag
|
||||
}
|
||||
|
||||
// SetFlags sets the output flags for the formatter.
|
||||
func (sf *StdFormatter) SetFlags(flags int) {
|
||||
sf.Flag = flags
|
||||
}
|
||||
|
||||
// ColorSupported enables or disables the colors, this will be called on every
|
||||
func (sf *StdFormatter) ColorSupported(supp bool) {
|
||||
sf.Colors = supp
|
||||
}
|
||||
|
||||
// Adapted replica of log.Logger.formatHeader
|
||||
func (sf *StdFormatter) stdHeader(buf *[]byte, t time.Time, file string, line int) {
|
||||
if sf.Flag&(log.Ldate|log.Ltime|log.Lmicroseconds) != 0 {
|
||||
if sf.Flag&log.Ldate != 0 {
|
||||
year, month, day := t.Date()
|
||||
itoa(buf, year, 4)
|
||||
*buf = append(*buf, '/')
|
||||
itoa(buf, int(month), 2)
|
||||
*buf = append(*buf, '/')
|
||||
itoa(buf, day, 2)
|
||||
*buf = append(*buf, ' ')
|
||||
}
|
||||
if sf.Flag&(log.Ltime|log.Lmicroseconds) != 0 {
|
||||
hour, min, sec := t.Clock()
|
||||
itoa(buf, hour, 2)
|
||||
*buf = append(*buf, ':')
|
||||
itoa(buf, min, 2)
|
||||
*buf = append(*buf, ':')
|
||||
itoa(buf, sec, 2)
|
||||
if sf.Flag&log.Lmicroseconds != 0 {
|
||||
*buf = append(*buf, '.')
|
||||
itoa(buf, t.Nanosecond()/1e3, 6)
|
||||
}
|
||||
*buf = append(*buf, ' ')
|
||||
}
|
||||
}
|
||||
if sf.Flag&(log.Lshortfile|log.Llongfile) != 0 {
|
||||
if sf.Flag&log.Lshortfile != 0 {
|
||||
short := file
|
||||
for i := len(file) - 1; i > 0; i-- {
|
||||
if file[i] == '/' {
|
||||
short = file[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
file = short
|
||||
} else {
|
||||
file = filepath.Base(filepath.Dir(file)) + "/" + filepath.Base(file)
|
||||
}
|
||||
|
||||
if sf.Colors {
|
||||
file = fmt.Sprintf("\x1b[1;30m%s:%d:\x1b[0m ", file, line)
|
||||
} else {
|
||||
file = fmt.Sprintf("%s:%d: ", file, line)
|
||||
}
|
||||
|
||||
*buf = append(*buf, file...)
|
||||
}
|
||||
}
|
||||
|
||||
// Replica of log.Logger.itoa
|
||||
func itoa(buf *[]byte, i int, wid int) {
|
||||
var u = uint(i)
|
||||
if u == 0 && wid <= 1 {
|
||||
*buf = append(*buf, '0')
|
||||
return
|
||||
}
|
||||
|
||||
// Assemble decimal in reverse order.
|
||||
var b [32]byte
|
||||
bp := len(b)
|
||||
for ; u > 0 || wid > 0; u /= 10 {
|
||||
bp--
|
||||
wid--
|
||||
b[bp] = byte(u%10) + '0'
|
||||
}
|
||||
*buf = append(*buf, b[bp:]...)
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package colog
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type formatTest struct {
|
||||
entry Entry
|
||||
prefix string
|
||||
flags int
|
||||
width int
|
||||
colors bool
|
||||
output string
|
||||
}
|
||||
|
||||
// TTime is the fixed point in time for all formatting tests
|
||||
var TTime = time.Date(2015, time.August, 1, 20, 45, 30, 9999, time.UTC)
|
||||
|
||||
var formatterTests = []formatTest{
|
||||
{
|
||||
entry: Entry{
|
||||
Level: LInfo,
|
||||
Message: []byte("some message"),
|
||||
},
|
||||
output: "[INF] some message\n",
|
||||
},
|
||||
{
|
||||
entry: Entry{
|
||||
Time: TTime,
|
||||
Level: LDebug,
|
||||
Message: []byte("some message"),
|
||||
},
|
||||
flags: log.Ldate,
|
||||
output: "[DBG] 2015/08/01 some message\n",
|
||||
},
|
||||
{
|
||||
entry: Entry{
|
||||
Time: TTime,
|
||||
Level: LDebug,
|
||||
Message: []byte("some message"),
|
||||
},
|
||||
colors: true,
|
||||
width: 40,
|
||||
flags: log.Ldate,
|
||||
output: "[\x1b[0;36mDBG\x1b[0m] 2015/08/01 some message\n",
|
||||
},
|
||||
{
|
||||
entry: Entry{
|
||||
Time: TTime,
|
||||
Level: LDebug,
|
||||
Message: []byte("some message"),
|
||||
},
|
||||
colors: true,
|
||||
width: 140,
|
||||
flags: log.Ldate,
|
||||
output: "[\x1b[0;36mDBG\x1b[0m] 2015/08/01 some message\n"},
|
||||
{
|
||||
entry: Entry{
|
||||
Time: TTime,
|
||||
Level: LDebug,
|
||||
File: "/src/file.go",
|
||||
Line: 142,
|
||||
Message: []byte("some message"),
|
||||
},
|
||||
colors: true,
|
||||
width: 140,
|
||||
flags: log.Llongfile,
|
||||
output: "[\x1b[0;36mDBG\x1b[0m] \x1b[1;30msrc/file.go:142:\x1b[0m some message\n",
|
||||
},
|
||||
}
|
||||
|
||||
func TestStdFormatter(t *testing.T) {
|
||||
for _, tt := range formatterTests {
|
||||
f := StdFormatter{
|
||||
Flag: tt.flags,
|
||||
Colors: tt.colors,
|
||||
}
|
||||
|
||||
if tt.width > 0 {
|
||||
terminalWidth = fixedWidthTerminal(tt.width)
|
||||
}
|
||||
|
||||
b, err := f.Format(&tt.entry)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(b) != tt.output {
|
||||
t.Errorf("Unexpected formatter output:\n%s\nVS\n%s", b, tt.output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stub for terminal width function
|
||||
func fixedWidthTerminal(width int) func(int) int {
|
||||
return func(fd int) int {
|
||||
return width
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// +build !windows
|
||||
|
||||
package colog
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Use variable indirection for test stubbing
|
||||
var isTerminal = isTerminalFunc
|
||||
var terminalWidth = terminalWidthFunc
|
||||
|
||||
// isTerminalFunc returns true if the given file descriptor is a terminal.
|
||||
func isTerminalFunc(fd int) bool {
|
||||
var termios syscall.Termios
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
|
||||
return err == 0
|
||||
}
|
||||
|
||||
// terminalWidthFunc returns the width in characters of the terminal.
|
||||
func terminalWidthFunc(fd int) (width int) {
|
||||
var dimensions [4]uint16
|
||||
|
||||
_, _, errno := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0)
|
||||
if errno != 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
return int(dimensions[1])
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// +build darwin dragonfly freebsd netbsd openbsd
|
||||
|
||||
package colog
|
||||
|
||||
import "syscall"
|
||||
|
||||
const ioctlReadTermios = syscall.TIOCGETA
|
|
@ -0,0 +1,5 @@
|
|||
// +build linux
|
||||
|
||||
package colog
|
||||
|
||||
const ioctlReadTermios = 0x5401
|
|
@ -0,0 +1,62 @@
|
|||
package colog
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Use variable indirection for test stubbing
|
||||
var isTerminal = isTerminalFunc
|
||||
var terminalWidth = terminalWidthFunc
|
||||
|
||||
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
var procInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
||||
var procMode = kernel32.NewProc("GetConsoleMode")
|
||||
|
||||
// Not applicable in windows
|
||||
// define constant to avoid compilation error
|
||||
const ioctlReadTermios = 0x0
|
||||
|
||||
// isTerminalFunc returns true if the given file descriptor is a terminal.
|
||||
func isTerminalFunc(fd int) bool {
|
||||
var st uint32
|
||||
r, _, errno := syscall.Syscall(procMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
|
||||
if errno != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return r != 0
|
||||
}
|
||||
|
||||
type short int16
|
||||
type word uint16
|
||||
|
||||
type coord struct {
|
||||
x short
|
||||
y short
|
||||
}
|
||||
type rectangle struct {
|
||||
left short
|
||||
top short
|
||||
right short
|
||||
bottom short
|
||||
}
|
||||
|
||||
type termInfo struct {
|
||||
size coord
|
||||
cursorPosition coord
|
||||
attributes word
|
||||
window rectangle
|
||||
maximumWindowSize coord
|
||||
}
|
||||
|
||||
// terminalWidthFunc returns the width in characters of the terminal.
|
||||
func terminalWidthFunc(fd int) (width int) {
|
||||
var info termInfo
|
||||
_, _, errno := syscall.Syscall(procInfo.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&info)), 0)
|
||||
if errno != 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
return int(info.size.x)
|
||||
}
|
Loading…
Reference in New Issue