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