From 3cbefefec543a845074a38926d06bc134b9782e3 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Tue, 8 May 2018 16:30:55 +0200 Subject: [PATCH] Initial commit --- formatter.go | 26 +++++++++++++++ log.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++ log_test.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++ settings.go | 23 +++++++++++++ type.go | 45 +++++++++++++++++++++++++ 5 files changed, 279 insertions(+) create mode 100644 formatter.go create mode 100644 log.go create mode 100644 log_test.go create mode 100644 settings.go create mode 100644 type.go diff --git a/formatter.go b/formatter.go new file mode 100644 index 0000000..5f91916 --- /dev/null +++ b/formatter.go @@ -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'}) +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..64878f1 --- /dev/null +++ b/log.go @@ -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:], `/`) +} diff --git a/log_test.go b/log_test.go new file mode 100644 index 0000000..e5a4c60 --- /dev/null +++ b/log_test.go @@ -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]) +} diff --git a/settings.go b/settings.go new file mode 100644 index 0000000..705ea74 --- /dev/null +++ b/settings.go @@ -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 +} diff --git a/type.go b/type.go new file mode 100644 index 0000000..00ca386 --- /dev/null +++ b/type.go @@ -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) +}