@ -0,0 +1,26 @@ | |||
package flog | |||
import ( | |||
"fmt" | |||
"io" | |||
"strconv" | |||
"text/tabwriter" | |||
) | |||
// TextFormatter logs in a human readable format | |||
type TextFormatter struct{} | |||
// FormatMessage implements LogFormatType | |||
func (l TextFormatter) FormatMessage(w io.Writer, entry LogEntry) { | |||
fmt.Fprintln(w, entry.Level, entry.Time.Format(`[2006-01-02 15:04:05]`), entry.Message) | |||
if len(entry.StackTrace) > 0 { | |||
tw := tabwriter.NewWriter(w, 1, 4, 4, ' ', 0) | |||
for _, v := range entry.StackTrace { | |||
fmt.Fprint(tw, ` `, v.Function, "\t", v.File+`:`+strconv.Itoa(v.Line), "\n") | |||
} | |||
tw.Flush() | |||
} | |||
w.Write([]byte{'\n'}) | |||
} |
@ -0,0 +1,93 @@ | |||
package flog | |||
import ( | |||
"fmt" | |||
"runtime" | |||
"strings" | |||
"sync" | |||
"time" | |||
) | |||
// All log functions | |||
var ( | |||
Debug = logFunc(LevelDebug) | |||
Info = logFunc(LevelInfo) | |||
Warning = logFunc(LevelWarning) | |||
Error = logFunc(LevelError) | |||
Critical = logFunc(LevelCritical) | |||
) | |||
func logFunc(level LevelType) func(...interface{}) { | |||
return func(message ...interface{}) { | |||
writeMessage(level, message) | |||
} | |||
} | |||
var mu sync.Mutex | |||
func writeMessage(level LevelType, message []interface{}) { | |||
if MinLevel > level { | |||
return | |||
} | |||
entry := LogEntry{ | |||
Level: level, | |||
Time: time.Now(), | |||
Message: fmt.Sprint(message...), | |||
} | |||
if MinStackLevel <= level { | |||
entry.StackTrace = getStackTrace() | |||
} | |||
mu.Lock() | |||
defer mu.Unlock() | |||
Format.FormatMessage(getOutput(), entry) | |||
} | |||
func getStackTrace() (stackTrace []StackTraceEntry) { | |||
_, filename, _, _ := runtime.Caller(1) | |||
pc := make([]uintptr, 50) | |||
entries := runtime.Callers(2, pc) | |||
frames := runtime.CallersFrames(pc[:entries]) | |||
more, firstLine := true, false | |||
for more { | |||
var frame runtime.Frame | |||
frame, more = frames.Next() | |||
if !firstLine && frame.File == filename { // Skip frames from the flog cal | |||
continue | |||
} | |||
firstLine = true | |||
if frame.Function == `runtime.gopanic` { // If a panic occurred, start at the frame that called panic | |||
stackTrace = nil | |||
continue | |||
} | |||
stackTrace = append(stackTrace, StackTraceEntry{ | |||
Function: cleanFunction(frame.Function), | |||
File: cleanFilename(frame.File), | |||
Line: frame.Line, | |||
}) | |||
} | |||
return | |||
} | |||
func cleanFunction(f string) string { | |||
parts := strings.Split(f, `/vendor/`) | |||
return parts[len(parts)-1] | |||
} | |||
func cleanFilename(file string) string { | |||
parts := strings.Split(file, `/src/`) | |||
if len(parts) < 2 { | |||
return file | |||
} | |||
return strings.Join(parts[1:], `/`) | |||
} |
@ -0,0 +1,92 @@ | |||
package flog | |||
import ( | |||
"bufio" | |||
"bytes" | |||
"os" | |||
"strings" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestGetOutput(t *testing.T) { | |||
assert.Equal(t, os.Stdout, getOutput()) | |||
b := bytes.Buffer{} | |||
Output = &b | |||
assert.Equal(t, &b, getOutput()) | |||
Output = nil | |||
} | |||
func TestLogFormat(t *testing.T) { | |||
b := bytes.Buffer{} | |||
MinLevel, Output = LevelDebug, &b | |||
Debug(`Testtt`) | |||
assert.True(t, strings.HasPrefix(b.String(), `debug [`)) | |||
b.Reset() | |||
Info(`Testtt`) | |||
assert.True(t, strings.HasPrefix(b.String(), `info [`)) | |||
MinLevel, Output = LevelInfo, nil | |||
} | |||
func TestLogLevel(t *testing.T) { | |||
b := bytes.Buffer{} | |||
MinLevel, Output = LevelWarning, &b | |||
Info(`Testtt`) | |||
assert.True(t, b.Len() == 0) | |||
b.Reset() | |||
Warning(`Testtt`) | |||
assert.False(t, b.Len() == 0) | |||
b.Reset() | |||
MinLevel = LevelError | |||
Warning(`Testtt`) | |||
assert.True(t, b.Len() == 0) | |||
b.Reset() | |||
MinLevel, Output = LevelInfo, nil | |||
} | |||
func TestStackTrace(t *testing.T) { | |||
b := bytes.Buffer{} | |||
Output = &b | |||
Error(`Testtt`) | |||
checkStackTrace(t, &b, `git.fuyu.moe/Fuyu/flog.TestStackTrace`) | |||
b.Reset() | |||
func() { | |||
defer func() { | |||
v := recover() | |||
if v != nil { | |||
Critical(v) | |||
} | |||
}() | |||
panic(`Testtt`) | |||
}() | |||
checkStackTrace(t, &b, `git.fuyu.moe/Fuyu/flog.TestStackTrace.func1`) | |||
Output = nil | |||
} | |||
func checkStackTrace(t *testing.T, b *bytes.Buffer, f string) { | |||
rdr := bufio.NewReader(b) | |||
_, _ = rdr.ReadBytes('\n') | |||
data, err := rdr.ReadBytes('\n') | |||
if err != nil { | |||
t.Fatal(`Got error while reading:`, err) | |||
} | |||
parts := strings.Split(strings.TrimSpace(string(data)), ` `) | |||
assert.Equal(t, f, parts[0]) | |||
} |
@ -0,0 +1,23 @@ | |||
package flog | |||
import ( | |||
"io" | |||
"os" | |||
) | |||
// Log settings | |||
var ( | |||
MinLevel = LevelInfo | |||
MinStackLevel = LevelError | |||
Format LogFormatType = TextFormatter{} | |||
Output io.Writer | |||
) | |||
func getOutput() io.Writer { | |||
if Output != nil { | |||
return Output | |||
} | |||
return os.Stdout | |||
} |
@ -0,0 +1,45 @@ | |||
package flog | |||
import ( | |||
"io" | |||
"time" | |||
) | |||
// LevelType defines the level type | |||
type LevelType uint8 | |||
// All possible log levels | |||
const ( | |||
LevelDebug LevelType = iota | |||
LevelInfo | |||
LevelWarning | |||
LevelError | |||
LevelCritical | |||
) | |||
var levels = []string{`debug`, `info`, `warning`, `error`, `critical`} | |||
// String returns the name of the level as string | |||
func (l LevelType) String() string { | |||
return levels[l] | |||
} | |||
// LogEntry is an entry for the log | |||
type LogEntry struct { | |||
Level LevelType `json:"level"` | |||
Time time.Time `json:"time"` | |||
Message string `json:"message"` | |||
StackTrace []StackTraceEntry `json:"stack_trace,omitempty"` | |||
} | |||
// StackTraceEntry is an entry in the stack trace | |||
type StackTraceEntry struct { | |||
Function string `json:"function"` | |||
File string `json:"file"` | |||
Line int `json:"line"` | |||
} | |||
// LogFormatType defines the log format | |||
type LogFormatType interface { | |||
FormatMessage(io.Writer, LogEntry) | |||
} |