First commit

This commit is contained in:
Pavel Shevaev 2022-10-25 18:00:25 +03:00
commit fa31019ff5
12 changed files with 1242 additions and 0 deletions

435
colog.go Normal file
View File

@ -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)
}

232
colog_test.go Normal file
View File

@ -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)
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.bit5.ru/backend/colog
go 1.13

35
interfaces.go Normal file
View File

@ -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
}

114
json_formatter.go Normal file
View File

@ -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
}

74
json_formatter_test.go Normal file
View File

@ -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)
}
}
}

143
std_formatter.go Normal file
View File

@ -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:]...)
}

101
std_formatter_test.go Normal file
View File

@ -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
}
}

31
tty.go Normal file
View File

@ -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])
}

7
tty_bsd.go Normal file
View File

@ -0,0 +1,7 @@
// +build darwin dragonfly freebsd netbsd openbsd
package colog
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA

5
tty_linux.go Normal file
View File

@ -0,0 +1,5 @@
// +build linux
package colog
const ioctlReadTermios = 0x5401

62
tty_windows.go Normal file
View File

@ -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)
}