* Panic don't fatal on create new logger Fixes #5854 Signed-off-by: Andrew Thornton <art27@cantab.net> * partial broken * Update the logging infrastrcture Signed-off-by: Andrew Thornton <art27@cantab.net> * Reset the skip levels for Fatal and Error Signed-off-by: Andrew Thornton <art27@cantab.net> * broken ncsa * More log.Error fixes Signed-off-by: Andrew Thornton <art27@cantab.net> * Remove nal * set log-levels to lowercase * Make console_test test all levels * switch to lowercased levels * OK now working * Fix vetting issues * Fix lint * Fix tests * change default logging to match current gitea * Improve log testing Signed-off-by: Andrew Thornton <art27@cantab.net> * reset error skip levels to 0 * Update documentation and access logger configuration * Redirect the router log back to gitea if redirect macaron log but also allow setting the log level - i.e. TRACE * Fix broken level caching * Refactor the router log * Add Router logger * Add colorizing options * Adjust router colors * Only create logger if they will be used * update app.ini.sample * rename Attribute ColorAttribute * Change from white to green for function * Set fatal/error levels * Restore initial trace logger * Fix Trace arguments in modules/auth/auth.go * Properly handle XORMLogger * Improve admin/config page * fix fmt * Add auto-compression of old logs * Update error log levels * Remove the unnecessary skip argument from Error, Fatal and Critical * Add stacktrace support * Fix tests * Remove x/sync from vendors? * Add stderr option to console logger * Use filepath.ToSlash to protect against Windows in tests * Remove prefixed underscores from names in colors.go * Remove not implemented database logger This was removed from Gogs on 4 Mar 2016 but left in the configuration since then. * Ensure that log paths are relative to ROOT_PATH * use path.Join * rename jsonConfig to logConfig * Rename "config" to "jsonConfig" to make it clearer * Requested changes * Requested changes: XormLogger * Try to color the windows terminal If successful default to colorizing the console logs * fixup * Colorize initially too * update vendor * Colorize logs on default and remove if this is not a colorizing logger * Fix documentation * fix test * Use go-isatty to detect if on windows we are on msys or cygwin * Fix spelling mistake * Add missing vendors * More changes * Rationalise the ANSI writer protection * Adjust colors on advice from @0x5c * Make Flags a comma separated list * Move to use the windows constant for ENABLE_VIRTUAL_TERMINAL_PROCESSING * Ensure matching is done on the non-colored message - to simpify EXPRESSIONtokarchuk/v1.17
parent
ef2a343e27
commit
704da08fdc
@ -0,0 +1,328 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
// These flags define which text to prefix to each log entry generated
|
||||||
|
// by the Logger. Bits are or'ed together to control what's printed.
|
||||||
|
// There is no control over the order they appear (the order listed
|
||||||
|
// here) or the format they present (as described in the comments).
|
||||||
|
// The prefix is followed by a colon only if more than time is stated
|
||||||
|
// is specified. For example, flags Ldate | Ltime
|
||||||
|
// produce, 2009/01/23 01:23:23 message.
|
||||||
|
// The standard is:
|
||||||
|
// 2009/01/23 01:23:23 ...a/b/c/d.go:23:runtime.Caller() [I]: message
|
||||||
|
const ( |
||||||
|
Ldate = 1 << iota // the date in the local time zone: 2009/01/23
|
||||||
|
Ltime // the time in the local time zone: 01:23:23
|
||||||
|
Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
|
||||||
|
Llongfile // full file name and line number: /a/b/c/d.go:23
|
||||||
|
Lshortfile // final file name element and line number: d.go:23. overrides Llongfile
|
||||||
|
Lfuncname // function name of the caller: runtime.Caller()
|
||||||
|
Lshortfuncname // last part of the function name
|
||||||
|
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
|
||||||
|
Llevelinitial // Initial character of the provided level in brackets eg. [I] for info
|
||||||
|
Llevel // Provided level in brackets [INFO]
|
||||||
|
|
||||||
|
// Last 20 characters of the filename
|
||||||
|
Lmedfile = Lshortfile | Llongfile |
||||||
|
|
||||||
|
// LstdFlags is the initial value for the standard logger
|
||||||
|
LstdFlags = Ldate | Ltime | Lmedfile | Lshortfuncname | Llevelinitial |
||||||
|
) |
||||||
|
|
||||||
|
var flagFromString = map[string]int{ |
||||||
|
"none": 0, |
||||||
|
"date": Ldate, |
||||||
|
"time": Ltime, |
||||||
|
"microseconds": Lmicroseconds, |
||||||
|
"longfile": Llongfile, |
||||||
|
"shortfile": Lshortfile, |
||||||
|
"funcname": Lfuncname, |
||||||
|
"shortfuncname": Lshortfuncname, |
||||||
|
"utc": LUTC, |
||||||
|
"levelinitial": Llevelinitial, |
||||||
|
"level": Llevel, |
||||||
|
"medfile": Lmedfile, |
||||||
|
"stdflags": LstdFlags, |
||||||
|
} |
||||||
|
|
||||||
|
// FlagsFromString takes a comma separated list of flags and returns
|
||||||
|
// the flags for this string
|
||||||
|
func FlagsFromString(from string) int { |
||||||
|
flags := 0 |
||||||
|
for _, flag := range strings.Split(strings.ToLower(from), ",") { |
||||||
|
f, ok := flagFromString[strings.TrimSpace(flag)] |
||||||
|
if ok { |
||||||
|
flags = flags | f |
||||||
|
} |
||||||
|
} |
||||||
|
return flags |
||||||
|
} |
||||||
|
|
||||||
|
type byteArrayWriter []byte |
||||||
|
|
||||||
|
func (b *byteArrayWriter) Write(p []byte) (int, error) { |
||||||
|
*b = append(*b, p...) |
||||||
|
return len(p), nil |
||||||
|
} |
||||||
|
|
||||||
|
// BaseLogger represent a basic logger for Gitea
|
||||||
|
type BaseLogger struct { |
||||||
|
out io.WriteCloser |
||||||
|
mu sync.Mutex |
||||||
|
|
||||||
|
Level Level `json:"level"` |
||||||
|
StacktraceLevel Level `json:"stacktraceLevel"` |
||||||
|
Flags int `json:"flags"` |
||||||
|
Prefix string `json:"prefix"` |
||||||
|
Colorize bool `json:"colorize"` |
||||||
|
Expression string `json:"expression"` |
||||||
|
regexp *regexp.Regexp |
||||||
|
} |
||||||
|
|
||||||
|
func (b *BaseLogger) createLogger(out io.WriteCloser, level ...Level) { |
||||||
|
b.mu.Lock() |
||||||
|
defer b.mu.Unlock() |
||||||
|
b.out = out |
||||||
|
switch b.Flags { |
||||||
|
case 0: |
||||||
|
b.Flags = LstdFlags |
||||||
|
case -1: |
||||||
|
b.Flags = 0 |
||||||
|
} |
||||||
|
if len(level) > 0 { |
||||||
|
b.Level = level[0] |
||||||
|
} |
||||||
|
b.createExpression() |
||||||
|
} |
||||||
|
|
||||||
|
func (b *BaseLogger) createExpression() { |
||||||
|
if len(b.Expression) > 0 { |
||||||
|
var err error |
||||||
|
b.regexp, err = regexp.Compile(b.Expression) |
||||||
|
if err != nil { |
||||||
|
b.regexp = nil |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// GetLevel returns the logging level for this logger
|
||||||
|
func (b *BaseLogger) GetLevel() Level { |
||||||
|
return b.Level |
||||||
|
} |
||||||
|
|
||||||
|
// GetStacktraceLevel returns the stacktrace logging level for this logger
|
||||||
|
func (b *BaseLogger) GetStacktraceLevel() Level { |
||||||
|
return b.StacktraceLevel |
||||||
|
} |
||||||
|
|
||||||
|
// Copy of cheap integer to fixed-width decimal to ascii from logger.
|
||||||
|
func itoa(buf *[]byte, i int, wid int) { |
||||||
|
var b [20]byte |
||||||
|
bp := len(b) - 1 |
||||||
|
for i >= 10 || wid > 1 { |
||||||
|
wid-- |
||||||
|
q := i / 10 |
||||||
|
b[bp] = byte('0' + i - q*10) |
||||||
|
bp-- |
||||||
|
i = q |
||||||
|
} |
||||||
|
// i < 10
|
||||||
|
b[bp] = byte('0' + i) |
||||||
|
*buf = append(*buf, b[bp:]...) |
||||||
|
} |
||||||
|
|
||||||
|
func (b *BaseLogger) createMsg(buf *[]byte, event *Event) { |
||||||
|
*buf = append(*buf, b.Prefix...) |
||||||
|
t := event.time |
||||||
|
if b.Flags&(Ldate|Ltime|Lmicroseconds) != 0 { |
||||||
|
if b.Colorize { |
||||||
|
*buf = append(*buf, fgCyanBytes...) |
||||||
|
} |
||||||
|
if b.Flags&LUTC != 0 { |
||||||
|
t = t.UTC() |
||||||
|
} |
||||||
|
if b.Flags&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 b.Flags&(Ltime|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 b.Flags&Lmicroseconds != 0 { |
||||||
|
*buf = append(*buf, '.') |
||||||
|
itoa(buf, t.Nanosecond()/1e3, 6) |
||||||
|
} |
||||||
|
*buf = append(*buf, ' ') |
||||||
|
} |
||||||
|
if b.Colorize { |
||||||
|
*buf = append(*buf, resetBytes...) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
if b.Flags&(Lshortfile|Llongfile) != 0 { |
||||||
|
if b.Colorize { |
||||||
|
*buf = append(*buf, fgGreenBytes...) |
||||||
|
} |
||||||
|
file := event.filename |
||||||
|
if b.Flags&Lmedfile == Lmedfile { |
||||||
|
startIndex := len(file) - 20 |
||||||
|
if startIndex > 0 { |
||||||
|
file = "..." + file[startIndex:] |
||||||
|
} |
||||||
|
} else if b.Flags&Lshortfile != 0 { |
||||||
|
startIndex := strings.LastIndexByte(file, '/') |
||||||
|
if startIndex > 0 && startIndex < len(file) { |
||||||
|
file = file[startIndex+1:] |
||||||
|
} |
||||||
|
} |
||||||
|
*buf = append(*buf, file...) |
||||||
|
*buf = append(*buf, ':') |
||||||
|
itoa(buf, event.line, -1) |
||||||
|
if b.Flags&(Lfuncname|Lshortfuncname) != 0 { |
||||||
|
*buf = append(*buf, ':') |
||||||
|
} else { |
||||||
|
if b.Colorize { |
||||||
|
*buf = append(*buf, resetBytes...) |
||||||
|
} |
||||||
|
*buf = append(*buf, ' ') |
||||||
|
} |
||||||
|
} |
||||||
|
if b.Flags&(Lfuncname|Lshortfuncname) != 0 { |
||||||
|
if b.Colorize { |
||||||
|
*buf = append(*buf, fgGreenBytes...) |
||||||
|
} |
||||||
|
funcname := event.caller |
||||||
|
if b.Flags&Lshortfuncname != 0 { |
||||||
|
lastIndex := strings.LastIndexByte(funcname, '.') |
||||||
|
if lastIndex > 0 && len(funcname) > lastIndex+1 { |
||||||
|
funcname = funcname[lastIndex+1:] |
||||||
|
} |
||||||
|
} |
||||||
|
*buf = append(*buf, funcname...) |
||||||
|
if b.Colorize { |
||||||
|
*buf = append(*buf, resetBytes...) |
||||||
|
} |
||||||
|
*buf = append(*buf, ' ') |
||||||
|
|
||||||
|
} |
||||||
|
if b.Flags&(Llevel|Llevelinitial) != 0 { |
||||||
|
level := strings.ToUpper(event.level.String()) |
||||||
|
if b.Colorize { |
||||||
|
*buf = append(*buf, levelToColor[event.level]...) |
||||||
|
} |
||||||
|
*buf = append(*buf, '[') |
||||||
|
if b.Flags&Llevelinitial != 0 { |
||||||
|
*buf = append(*buf, level[0]) |
||||||
|
} else { |
||||||
|
*buf = append(*buf, level...) |
||||||
|
} |
||||||
|
*buf = append(*buf, ']') |
||||||
|
if b.Colorize { |
||||||
|
*buf = append(*buf, resetBytes...) |
||||||
|
} |
||||||
|
*buf = append(*buf, ' ') |
||||||
|
} |
||||||
|
|
||||||
|
var msg = []byte(event.msg) |
||||||
|
if len(msg) > 0 && msg[len(msg)-1] == '\n' { |
||||||
|
msg = msg[:len(msg)-1] |
||||||
|
} |
||||||
|
|
||||||
|
pawMode := allowColor |
||||||
|
if !b.Colorize { |
||||||
|
pawMode = removeColor |
||||||
|
} |
||||||
|
|
||||||
|
baw := byteArrayWriter(*buf) |
||||||
|
(&protectedANSIWriter{ |
||||||
|
w: &baw, |
||||||
|
mode: pawMode, |
||||||
|
}).Write([]byte(msg)) |
||||||
|
*buf = baw |
||||||
|
|
||||||
|
if event.stacktrace != "" && b.StacktraceLevel <= event.level { |
||||||
|
lines := bytes.Split([]byte(event.stacktrace), []byte("\n")) |
||||||
|
if len(lines) > 1 { |
||||||
|
for _, line := range lines { |
||||||
|
*buf = append(*buf, "\n\t"...) |
||||||
|
*buf = append(*buf, line...) |
||||||
|
} |
||||||
|
} |
||||||
|
*buf = append(*buf, '\n') |
||||||
|
} |
||||||
|
*buf = append(*buf, '\n') |
||||||
|
} |
||||||
|
|
||||||
|
// LogEvent logs the event to the internal writer
|
||||||
|
func (b *BaseLogger) LogEvent(event *Event) error { |
||||||
|
if b.Level > event.level { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
b.mu.Lock() |
||||||
|
defer b.mu.Unlock() |
||||||
|
if !b.Match(event) { |
||||||
|
return nil |
||||||
|
} |
||||||
|
var buf []byte |
||||||
|
b.createMsg(&buf, event) |
||||||
|
_, err := b.out.Write(buf) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Match checks if the given event matches the logger's regexp expression
|
||||||
|
func (b *BaseLogger) Match(event *Event) bool { |
||||||
|
if b.regexp == nil { |
||||||
|
return true |
||||||
|
} |
||||||
|
if b.regexp.Match([]byte(fmt.Sprintf("%s:%d:%s", event.filename, event.line, event.caller))) { |
||||||
|
return true |
||||||
|
} |
||||||
|
// Match on the non-colored msg - therefore strip out colors
|
||||||
|
var msg []byte |
||||||
|
baw := byteArrayWriter(msg) |
||||||
|
(&protectedANSIWriter{ |
||||||
|
w: &baw, |
||||||
|
mode: removeColor, |
||||||
|
}).Write([]byte(event.msg)) |
||||||
|
msg = baw |
||||||
|
if b.regexp.Match(msg) { |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Close the base logger
|
||||||
|
func (b *BaseLogger) Close() { |
||||||
|
b.mu.Lock() |
||||||
|
defer b.mu.Unlock() |
||||||
|
if b.out != nil { |
||||||
|
b.out.Close() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// GetName returns empty for these provider loggers
|
||||||
|
func (b *BaseLogger) GetName() string { |
||||||
|
return "" |
||||||
|
} |
@ -0,0 +1,277 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
type CallbackWriteCloser struct { |
||||||
|
callback func([]byte, bool) |
||||||
|
} |
||||||
|
|
||||||
|
func (c CallbackWriteCloser) Write(p []byte) (int, error) { |
||||||
|
c.callback(p, false) |
||||||
|
return len(p), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (c CallbackWriteCloser) Close() error { |
||||||
|
c.callback(nil, true) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func TestBaseLogger(t *testing.T) { |
||||||
|
var written []byte |
||||||
|
var closed bool |
||||||
|
|
||||||
|
c := CallbackWriteCloser{ |
||||||
|
callback: func(p []byte, close bool) { |
||||||
|
written = p |
||||||
|
closed = close |
||||||
|
}, |
||||||
|
} |
||||||
|
prefix := "TestPrefix " |
||||||
|
b := BaseLogger{ |
||||||
|
out: c, |
||||||
|
Level: INFO, |
||||||
|
Flags: LstdFlags | LUTC, |
||||||
|
Prefix: prefix, |
||||||
|
} |
||||||
|
location, _ := time.LoadLocation("EST") |
||||||
|
|
||||||
|
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location) |
||||||
|
|
||||||
|
dateString := date.UTC().Format("2006/01/02 15:04:05") |
||||||
|
|
||||||
|
event := Event{ |
||||||
|
level: INFO, |
||||||
|
msg: "TEST MSG", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/FILENAME", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
|
||||||
|
assert.Equal(t, INFO, b.GetLevel()) |
||||||
|
|
||||||
|
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.level = DEBUG |
||||||
|
expected = "" |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
|
||||||
|
event.level = TRACE |
||||||
|
expected = "" |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
|
||||||
|
event.level = WARN |
||||||
|
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.level = ERROR |
||||||
|
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.level = CRITICAL |
||||||
|
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
b.Close() |
||||||
|
assert.Equal(t, true, closed) |
||||||
|
} |
||||||
|
|
||||||
|
func TestBaseLoggerDated(t *testing.T) { |
||||||
|
var written []byte |
||||||
|
var closed bool |
||||||
|
|
||||||
|
c := CallbackWriteCloser{ |
||||||
|
callback: func(p []byte, close bool) { |
||||||
|
written = p |
||||||
|
closed = close |
||||||
|
}, |
||||||
|
} |
||||||
|
prefix := "" |
||||||
|
b := BaseLogger{ |
||||||
|
out: c, |
||||||
|
Level: WARN, |
||||||
|
Flags: Ldate | Ltime | Lmicroseconds | Lshortfile | Llevel, |
||||||
|
Prefix: prefix, |
||||||
|
} |
||||||
|
|
||||||
|
location, _ := time.LoadLocation("EST") |
||||||
|
|
||||||
|
date := time.Date(2019, time.January, 13, 22, 3, 30, 115, location) |
||||||
|
|
||||||
|
dateString := date.Format("2006/01/02 15:04:05.000000") |
||||||
|
|
||||||
|
event := Event{ |
||||||
|
level: WARN, |
||||||
|
msg: "TEST MESSAGE TEST\n", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/FILENAME", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
|
||||||
|
assert.Equal(t, WARN, b.GetLevel()) |
||||||
|
|
||||||
|
expected := fmt.Sprintf("%s%s %s:%d [%s] %s", prefix, dateString, "FILENAME", event.line, strings.ToUpper(event.level.String()), event.msg) |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.level = INFO |
||||||
|
expected = "" |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.level = ERROR |
||||||
|
expected = fmt.Sprintf("%s%s %s:%d [%s] %s", prefix, dateString, "FILENAME", event.line, strings.ToUpper(event.level.String()), event.msg) |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.level = DEBUG |
||||||
|
expected = "" |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.level = CRITICAL |
||||||
|
expected = fmt.Sprintf("%s%s %s:%d [%s] %s", prefix, dateString, "FILENAME", event.line, strings.ToUpper(event.level.String()), event.msg) |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.level = TRACE |
||||||
|
expected = "" |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
b.Close() |
||||||
|
assert.Equal(t, true, closed) |
||||||
|
} |
||||||
|
|
||||||
|
func TestBaseLoggerMultiLineNoFlagsRegexp(t *testing.T) { |
||||||
|
var written []byte |
||||||
|
var closed bool |
||||||
|
|
||||||
|
c := CallbackWriteCloser{ |
||||||
|
callback: func(p []byte, close bool) { |
||||||
|
written = p |
||||||
|
closed = close |
||||||
|
}, |
||||||
|
} |
||||||
|
prefix := "" |
||||||
|
b := BaseLogger{ |
||||||
|
Level: DEBUG, |
||||||
|
StacktraceLevel: ERROR, |
||||||
|
Flags: -1, |
||||||
|
Prefix: prefix, |
||||||
|
Expression: "FILENAME", |
||||||
|
} |
||||||
|
b.createLogger(c) |
||||||
|
|
||||||
|
location, _ := time.LoadLocation("EST") |
||||||
|
|
||||||
|
date := time.Date(2019, time.January, 13, 22, 3, 30, 115, location) |
||||||
|
|
||||||
|
event := Event{ |
||||||
|
level: DEBUG, |
||||||
|
msg: "TEST\nMESSAGE\nTEST", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/FILENAME", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
|
||||||
|
assert.Equal(t, DEBUG, b.GetLevel()) |
||||||
|
|
||||||
|
expected := "TEST\n\tMESSAGE\n\tTEST\n" |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.filename = "ELSEWHERE" |
||||||
|
|
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, "", string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.caller = "FILENAME" |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event = Event{ |
||||||
|
level: DEBUG, |
||||||
|
msg: "TEST\nFILENAME\nTEST", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/ELSEWHERE", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
expected = "TEST\n\tFILENAME\n\tTEST\n" |
||||||
|
b.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func TestBrokenRegexp(t *testing.T) { |
||||||
|
var closed bool |
||||||
|
|
||||||
|
c := CallbackWriteCloser{ |
||||||
|
callback: func(p []byte, close bool) { |
||||||
|
closed = close |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
b := BaseLogger{ |
||||||
|
Level: DEBUG, |
||||||
|
StacktraceLevel: ERROR, |
||||||
|
Flags: -1, |
||||||
|
Prefix: prefix, |
||||||
|
Expression: "\\", |
||||||
|
} |
||||||
|
b.createLogger(c) |
||||||
|
assert.Empty(t, b.regexp) |
||||||
|
b.Close() |
||||||
|
assert.Equal(t, true, closed) |
||||||
|
} |
@ -0,0 +1,348 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
const escape = "\033" |
||||||
|
|
||||||
|
// ColorAttribute defines a single SGR Code
|
||||||
|
type ColorAttribute int |
||||||
|
|
||||||
|
// Base ColorAttributes
|
||||||
|
const ( |
||||||
|
Reset ColorAttribute = iota |
||||||
|
Bold |
||||||
|
Faint |
||||||
|
Italic |
||||||
|
Underline |
||||||
|
BlinkSlow |
||||||
|
BlinkRapid |
||||||
|
ReverseVideo |
||||||
|
Concealed |
||||||
|
CrossedOut |
||||||
|
) |
||||||
|
|
||||||
|
// Foreground text colors
|
||||||
|
const ( |
||||||
|
FgBlack ColorAttribute = iota + 30 |
||||||
|
FgRed |
||||||
|
FgGreen |
||||||
|
FgYellow |
||||||
|
FgBlue |
||||||
|
FgMagenta |
||||||
|
FgCyan |
||||||
|
FgWhite |
||||||
|
) |
||||||
|
|
||||||
|
// Foreground Hi-Intensity text colors
|
||||||
|
const ( |
||||||
|
FgHiBlack ColorAttribute = iota + 90 |
||||||
|
FgHiRed |
||||||
|
FgHiGreen |
||||||
|
FgHiYellow |
||||||
|
FgHiBlue |
||||||
|
FgHiMagenta |
||||||
|
FgHiCyan |
||||||
|
FgHiWhite |
||||||
|
) |
||||||
|
|
||||||
|
// Background text colors
|
||||||
|
const ( |
||||||
|
BgBlack ColorAttribute = iota + 40 |
||||||
|
BgRed |
||||||
|
BgGreen |
||||||
|
BgYellow |
||||||
|
BgBlue |
||||||
|
BgMagenta |
||||||
|
BgCyan |
||||||
|
BgWhite |
||||||
|
) |
||||||
|
|
||||||
|
// Background Hi-Intensity text colors
|
||||||
|
const ( |
||||||
|
BgHiBlack ColorAttribute = iota + 100 |
||||||
|
BgHiRed |
||||||
|
BgHiGreen |
||||||
|
BgHiYellow |
||||||
|
BgHiBlue |
||||||
|
BgHiMagenta |
||||||
|
BgHiCyan |
||||||
|
BgHiWhite |
||||||
|
) |
||||||
|
|
||||||
|
var colorAttributeToString = map[ColorAttribute]string{ |
||||||
|
Reset: "Reset", |
||||||
|
Bold: "Bold", |
||||||
|
Faint: "Faint", |
||||||
|
Italic: "Italic", |
||||||
|
Underline: "Underline", |
||||||
|
BlinkSlow: "BlinkSlow", |
||||||
|
BlinkRapid: "BlinkRapid", |
||||||
|
ReverseVideo: "ReverseVideo", |
||||||
|
Concealed: "Concealed", |
||||||
|
CrossedOut: "CrossedOut", |
||||||
|
FgBlack: "FgBlack", |
||||||
|
FgRed: "FgRed", |
||||||
|
FgGreen: "FgGreen", |
||||||
|
FgYellow: "FgYellow", |
||||||
|
FgBlue: "FgBlue", |
||||||
|
FgMagenta: "FgMagenta", |
||||||
|
FgCyan: "FgCyan", |
||||||
|
FgWhite: "FgWhite", |
||||||
|
FgHiBlack: "FgHiBlack", |
||||||
|
FgHiRed: "FgHiRed", |
||||||
|
FgHiGreen: "FgHiGreen", |
||||||
|
FgHiYellow: "FgHiYellow", |
||||||
|
FgHiBlue: "FgHiBlue", |
||||||
|
FgHiMagenta: "FgHiMagenta", |
||||||
|
FgHiCyan: "FgHiCyan", |
||||||
|
FgHiWhite: "FgHiWhite", |
||||||
|
BgBlack: "BgBlack", |
||||||
|
BgRed: "BgRed", |
||||||
|
BgGreen: "BgGreen", |
||||||
|
BgYellow: "BgYellow", |
||||||
|
BgBlue: "BgBlue", |
||||||
|
BgMagenta: "BgMagenta", |
||||||
|
BgCyan: "BgCyan", |
||||||
|
BgWhite: "BgWhite", |
||||||
|
BgHiBlack: "BgHiBlack", |
||||||
|
BgHiRed: "BgHiRed", |
||||||
|
BgHiGreen: "BgHiGreen", |
||||||
|
BgHiYellow: "BgHiYellow", |
||||||
|
BgHiBlue: "BgHiBlue", |
||||||
|
BgHiMagenta: "BgHiMagenta", |
||||||
|
BgHiCyan: "BgHiCyan", |
||||||
|
BgHiWhite: "BgHiWhite", |
||||||
|
} |
||||||
|
|
||||||
|
func (c *ColorAttribute) String() string { |
||||||
|
return colorAttributeToString[*c] |
||||||
|
} |
||||||
|
|
||||||
|
var colorAttributeFromString = map[string]ColorAttribute{} |
||||||
|
|
||||||
|
// ColorAttributeFromString will return a ColorAttribute given a string
|
||||||
|
func ColorAttributeFromString(from string) ColorAttribute { |
||||||
|
lowerFrom := strings.TrimSpace(strings.ToLower(from)) |
||||||
|
return colorAttributeFromString[lowerFrom] |
||||||
|
} |
||||||
|
|
||||||
|
// ColorString converts a list of ColorAttributes to a color string
|
||||||
|
func ColorString(attrs ...ColorAttribute) string { |
||||||
|
return string(ColorBytes(attrs...)) |
||||||
|
} |
||||||
|
|
||||||
|
// ColorBytes converts a list of ColorAttributes to a byte array
|
||||||
|
func ColorBytes(attrs ...ColorAttribute) []byte { |
||||||
|
bytes := make([]byte, 0, 20) |
||||||
|
bytes = append(bytes, escape[0], '[') |
||||||
|
if len(attrs) > 0 { |
||||||
|
bytes = append(bytes, strconv.Itoa(int(attrs[0]))...) |
||||||
|
for _, a := range attrs[1:] { |
||||||
|
bytes = append(bytes, ';') |
||||||
|
bytes = append(bytes, strconv.Itoa(int(a))...) |
||||||
|
} |
||||||
|
} else { |
||||||
|
bytes = append(bytes, strconv.Itoa(int(Bold))...) |
||||||
|
} |
||||||
|
bytes = append(bytes, 'm') |
||||||
|
return bytes |
||||||
|
} |
||||||
|
|
||||||
|
var levelToColor = map[Level]string{ |
||||||
|
TRACE: ColorString(Bold, FgCyan), |
||||||
|
DEBUG: ColorString(Bold, FgBlue), |
||||||
|
INFO: ColorString(Bold, FgGreen), |
||||||
|
WARN: ColorString(Bold, FgYellow), |
||||||
|
ERROR: ColorString(Bold, FgRed), |
||||||
|
CRITICAL: ColorString(Bold, BgMagenta), |
||||||
|
FATAL: ColorString(Bold, BgRed), |
||||||
|
NONE: ColorString(Reset), |
||||||
|
} |
||||||
|
|
||||||
|
var resetBytes = ColorBytes(Reset) |
||||||
|
var fgCyanBytes = ColorBytes(FgCyan) |
||||||
|
var fgGreenBytes = ColorBytes(FgGreen) |
||||||
|
var fgBoldBytes = ColorBytes(Bold) |
||||||
|
|
||||||
|
type protectedANSIWriterMode int |
||||||
|
|
||||||
|
const ( |
||||||
|
escapeAll protectedANSIWriterMode = iota |
||||||
|
allowColor |
||||||
|
removeColor |
||||||
|
) |
||||||
|
|
||||||
|
type protectedANSIWriter struct { |
||||||
|
w io.Writer |
||||||
|
mode protectedANSIWriterMode |
||||||
|
} |
||||||
|
|
||||||
|
// Write will protect against unusual characters
|
||||||
|
func (c *protectedANSIWriter) Write(bytes []byte) (int, error) { |
||||||
|
end := len(bytes) |
||||||
|
totalWritten := 0 |
||||||
|
normalLoop: |
||||||
|
for i := 0; i < end; { |
||||||
|
lasti := i |
||||||
|
|
||||||
|
if c.mode == escapeAll { |
||||||
|
for i < end && (bytes[i] >= ' ' || bytes[i] == '\n') { |
||||||
|
i++ |
||||||
|
} |
||||||
|
} else { |
||||||
|
for i < end && bytes[i] >= ' ' { |
||||||
|
i++ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if i > lasti { |
||||||
|
written, err := c.w.Write(bytes[lasti:i]) |
||||||
|
totalWritten = totalWritten + written |
||||||
|
if err != nil { |
||||||
|
return totalWritten, err |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
if i >= end { |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
// If we're not just escaping all we should prefix all newlines with a \t
|
||||||
|
if c.mode != escapeAll { |
||||||
|
if bytes[i] == '\n' { |
||||||
|
written, err := c.w.Write([]byte{'\n', '\t'}) |
||||||
|
if written > 0 { |
||||||
|
totalWritten++ |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return totalWritten, err |
||||||
|
} |
||||||
|
i++ |
||||||
|
continue normalLoop |
||||||
|
} |
||||||
|
|
||||||
|
if bytes[i] == escape[0] && i+1 < end && bytes[i+1] == '[' { |
||||||
|
for j := i + 2; j < end; j++ { |
||||||
|
if bytes[j] >= '0' && bytes[j] <= '9' { |
||||||
|
continue |
||||||
|
} |
||||||
|
if bytes[j] == ';' { |
||||||
|
continue |
||||||
|
} |
||||||
|
if bytes[j] == 'm' { |
||||||
|
if c.mode == allowColor { |
||||||
|
written, err := c.w.Write(bytes[i : j+1]) |
||||||
|
totalWritten = totalWritten + written |
||||||
|
if err != nil { |
||||||
|
return totalWritten, err |
||||||
|
} |
||||||
|
} else { |
||||||
|
totalWritten = j |
||||||
|
} |
||||||
|
i = j + 1 |
||||||
|
continue normalLoop |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Process naughty character
|
||||||
|
if _, err := fmt.Fprintf(c.w, `\%#o03d`, bytes[i]); err != nil { |
||||||
|
return totalWritten, err |
||||||
|
} |
||||||
|
i++ |
||||||
|
totalWritten++ |
||||||
|
} |
||||||
|
return totalWritten, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ColoredValue will Color the provided value
|
||||||
|
type ColoredValue struct { |
||||||
|
ColorBytes *[]byte |
||||||
|
ResetBytes *[]byte |
||||||
|
Value *interface{} |
||||||
|
} |
||||||
|
|
||||||
|
// NewColoredValue is a helper function to create a ColoredValue from a Value
|
||||||
|
// If no color is provided it defaults to Bold with standard Reset
|
||||||
|
// If a ColoredValue is provided it is not changed
|
||||||
|
func NewColoredValue(value interface{}, color ...ColorAttribute) *ColoredValue { |
||||||
|
return NewColoredValuePointer(&value, color...) |
||||||
|
} |
||||||
|
|
||||||
|
// NewColoredValuePointer is a helper function to create a ColoredValue from a Value Pointer
|
||||||
|
// If no color is provided it defaults to Bold with standard Reset
|
||||||
|
// If a ColoredValue is provided it is not changed
|
||||||
|
func NewColoredValuePointer(value *interface{}, color ...ColorAttribute) *ColoredValue { |
||||||
|
if val, ok := (*value).(*ColoredValue); ok { |
||||||
|
return val |
||||||
|
} |
||||||
|
if len(color) > 0 { |
||||||
|
bytes := ColorBytes(color...) |
||||||
|
return &ColoredValue{ |
||||||
|
ColorBytes: &bytes, |
||||||
|
ResetBytes: &resetBytes, |
||||||
|
Value: value, |
||||||
|
} |
||||||
|
} |
||||||
|
return &ColoredValue{ |
||||||
|
ColorBytes: &fgBoldBytes, |
||||||
|
ResetBytes: &resetBytes, |
||||||
|
Value: value, |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// NewColoredValueBytes creates a value from the provided value with color bytes
|
||||||
|
// If a ColoredValue is provided it is not changed
|
||||||
|
func NewColoredValueBytes(value interface{}, colorBytes *[]byte) *ColoredValue { |
||||||
|
if val, ok := value.(*ColoredValue); ok { |
||||||
|
return val |
||||||
|
} |
||||||
|
return &ColoredValue{ |
||||||
|
ColorBytes: colorBytes, |
||||||
|
ResetBytes: &resetBytes, |
||||||
|
Value: &value, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Format will format the provided value and protect against ANSI spoofing within the value
|
||||||
|
func (cv *ColoredValue) Format(s fmt.State, c rune) { |
||||||
|
s.Write([]byte(*cv.ColorBytes)) |
||||||
|
fmt.Fprintf(&protectedANSIWriter{w: s}, fmtString(s, c), *(cv.Value)) |
||||||
|
s.Write([]byte(*cv.ResetBytes)) |
||||||
|
} |
||||||
|
|
||||||
|
func fmtString(s fmt.State, c rune) string { |
||||||
|
var width, precision string |
||||||
|
base := make([]byte, 0, 8) |
||||||
|
base = append(base, '%') |
||||||
|
for _, c := range []byte(" +-#0") { |
||||||
|
if s.Flag(int(c)) { |
||||||
|
base = append(base, c) |
||||||
|
} |
||||||
|
} |
||||||
|
if w, ok := s.Width(); ok { |
||||||
|
width = strconv.Itoa(w) |
||||||
|
} |
||||||
|
if p, ok := s.Precision(); ok { |
||||||
|
precision = "." + strconv.Itoa(p) |
||||||
|
} |
||||||
|
return fmt.Sprintf("%s%s%s%c", base, width, precision, c) |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
for attr, from := range colorAttributeToString { |
||||||
|
colorAttributeFromString[strings.ToLower(from)] = attr |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,240 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"net" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func listenReadAndClose(t *testing.T, l net.Listener, expected string) { |
||||||
|
conn, err := l.Accept() |
||||||
|
assert.NoError(t, err) |
||||||
|
defer conn.Close() |
||||||
|
written, err := ioutil.ReadAll(conn) |
||||||
|
|
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func TestConnLogger(t *testing.T) { |
||||||
|
var written []byte |
||||||
|
|
||||||
|
protocol := "tcp" |
||||||
|
address := ":3099" |
||||||
|
|
||||||
|
l, err := net.Listen(protocol, address) |
||||||
|
if err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
defer l.Close() |
||||||
|
|
||||||
|
prefix := "TestPrefix " |
||||||
|
level := INFO |
||||||
|
flags := LstdFlags | LUTC | Lfuncname |
||||||
|
|
||||||
|
logger := NewConn() |
||||||
|
connLogger := logger.(*ConnLogger) |
||||||
|
|
||||||
|
logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"reconnectOnMsg\":%t,\"reconnect\":%t,\"net\":\"%s\",\"addr\":\"%s\"}", prefix, level.String(), flags, true, true, protocol, address)) |
||||||
|
|
||||||
|
assert.Equal(t, flags, connLogger.Flags) |
||||||
|
assert.Equal(t, level, connLogger.Level) |
||||||
|
assert.Equal(t, level, logger.GetLevel()) |
||||||
|
|
||||||
|
location, _ := time.LoadLocation("EST") |
||||||
|
|
||||||
|
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location) |
||||||
|
|
||||||
|
dateString := date.UTC().Format("2006/01/02 15:04:05") |
||||||
|
|
||||||
|
event := Event{ |
||||||
|
level: INFO, |
||||||
|
msg: "TEST MSG", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/FILENAME", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
var wg sync.WaitGroup |
||||||
|
wg.Add(2) |
||||||
|
go func() { |
||||||
|
defer wg.Done() |
||||||
|
listenReadAndClose(t, l, expected) |
||||||
|
}() |
||||||
|
go func() { |
||||||
|
defer wg.Done() |
||||||
|
err := logger.LogEvent(&event) |
||||||
|
assert.NoError(t, err) |
||||||
|
}() |
||||||
|
wg.Wait() |
||||||
|
|
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.level = WARN |
||||||
|
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
wg.Add(2) |
||||||
|
go func() { |
||||||
|
defer wg.Done() |
||||||
|
listenReadAndClose(t, l, expected) |
||||||
|
}() |
||||||
|
go func() { |
||||||
|
defer wg.Done() |
||||||
|
err := logger.LogEvent(&event) |
||||||
|
assert.NoError(t, err) |
||||||
|
}() |
||||||
|
wg.Wait() |
||||||
|
|
||||||
|
logger.Close() |
||||||
|
} |
||||||
|
|
||||||
|
func TestConnLoggerBadConfig(t *testing.T) { |
||||||
|
logger := NewConn() |
||||||
|
|
||||||
|
err := logger.Init("{") |
||||||
|
assert.Equal(t, "unexpected end of JSON input", err.Error()) |
||||||
|
logger.Close() |
||||||
|
} |
||||||
|
|
||||||
|
func TestConnLoggerCloseBeforeSend(t *testing.T) { |
||||||
|
protocol := "tcp" |
||||||
|
address := ":3099" |
||||||
|
|
||||||
|
prefix := "TestPrefix " |
||||||
|
level := INFO |
||||||
|
flags := LstdFlags | LUTC | Lfuncname |
||||||
|
|
||||||
|
logger := NewConn() |
||||||
|
|
||||||
|
logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"reconnectOnMsg\":%t,\"reconnect\":%t,\"net\":\"%s\",\"addr\":\"%s\"}", prefix, level.String(), flags, false, false, protocol, address)) |
||||||
|
logger.Close() |
||||||
|
} |
||||||
|
|
||||||
|
func TestConnLoggerFailConnect(t *testing.T) { |
||||||
|
protocol := "tcp" |
||||||
|
address := ":3099" |
||||||
|
|
||||||
|
prefix := "TestPrefix " |
||||||
|
level := INFO |
||||||
|
flags := LstdFlags | LUTC | Lfuncname |
||||||
|
|
||||||
|
logger := NewConn() |
||||||
|
|
||||||
|
logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"reconnectOnMsg\":%t,\"reconnect\":%t,\"net\":\"%s\",\"addr\":\"%s\"}", prefix, level.String(), flags, false, false, protocol, address)) |
||||||
|
|
||||||
|
assert.Equal(t, level, logger.GetLevel()) |
||||||
|
|
||||||
|
location, _ := time.LoadLocation("EST") |
||||||
|
|
||||||
|
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location) |
||||||
|
|
||||||
|
//dateString := date.UTC().Format("2006/01/02 15:04:05")
|
||||||
|
|
||||||
|
event := Event{ |
||||||
|
level: INFO, |
||||||
|
msg: "TEST MSG", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/FILENAME", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
|
||||||
|
err := logger.LogEvent(&event) |
||||||
|
assert.Error(t, err) |
||||||
|
|
||||||
|
logger.Close() |
||||||
|
} |
||||||
|
|
||||||
|
func TestConnLoggerClose(t *testing.T) { |
||||||
|
var written []byte |
||||||
|
|
||||||
|
protocol := "tcp" |
||||||
|
address := ":3099" |
||||||
|
|
||||||
|
l, err := net.Listen(protocol, address) |
||||||
|
if err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
defer l.Close() |
||||||
|
|
||||||
|
prefix := "TestPrefix " |
||||||
|
level := INFO |
||||||
|
flags := LstdFlags | LUTC | Lfuncname |
||||||
|
|
||||||
|
logger := NewConn() |
||||||
|
connLogger := logger.(*ConnLogger) |
||||||
|
|
||||||
|
logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"reconnectOnMsg\":%t,\"reconnect\":%t,\"net\":\"%s\",\"addr\":\"%s\"}", prefix, level.String(), flags, false, false, protocol, address)) |
||||||
|
|
||||||
|
assert.Equal(t, flags, connLogger.Flags) |
||||||
|
assert.Equal(t, level, connLogger.Level) |
||||||
|
assert.Equal(t, level, logger.GetLevel()) |
||||||
|
location, _ := time.LoadLocation("EST") |
||||||
|
|
||||||
|
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location) |
||||||
|
|
||||||
|
dateString := date.UTC().Format("2006/01/02 15:04:05") |
||||||
|
|
||||||
|
event := Event{ |
||||||
|
level: INFO, |
||||||
|
msg: "TEST MSG", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/FILENAME", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
var wg sync.WaitGroup |
||||||
|
wg.Add(2) |
||||||
|
go func() { |
||||||
|
defer wg.Done() |
||||||
|
err := logger.LogEvent(&event) |
||||||
|
assert.NoError(t, err) |
||||||
|
logger.Close() |
||||||
|
}() |
||||||
|
go func() { |
||||||
|
defer wg.Done() |
||||||
|
listenReadAndClose(t, l, expected) |
||||||
|
}() |
||||||
|
wg.Wait() |
||||||
|
|
||||||
|
logger = NewConn() |
||||||
|
connLogger = logger.(*ConnLogger) |
||||||
|
|
||||||
|
logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"reconnectOnMsg\":%t,\"reconnect\":%t,\"net\":\"%s\",\"addr\":\"%s\"}", prefix, level.String(), flags, false, true, protocol, address)) |
||||||
|
|
||||||
|
assert.Equal(t, flags, connLogger.Flags) |
||||||
|
assert.Equal(t, level, connLogger.Level) |
||||||
|
assert.Equal(t, level, logger.GetLevel()) |
||||||
|
|
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.level = WARN |
||||||
|
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
wg.Add(2) |
||||||
|
go func() { |
||||||
|
defer wg.Done() |
||||||
|
listenReadAndClose(t, l, expected) |
||||||
|
}() |
||||||
|
go func() { |
||||||
|
defer wg.Done() |
||||||
|
err := logger.LogEvent(&event) |
||||||
|
assert.NoError(t, err) |
||||||
|
logger.Close() |
||||||
|
|
||||||
|
}() |
||||||
|
wg.Wait() |
||||||
|
logger.Flush() |
||||||
|
logger.Close() |
||||||
|
} |
@ -0,0 +1,137 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestConsoleLoggerBadConfig(t *testing.T) { |
||||||
|
logger := NewConsoleLogger() |
||||||
|
|
||||||
|
err := logger.Init("{") |
||||||
|
assert.Equal(t, "unexpected end of JSON input", err.Error()) |
||||||
|
logger.Close() |
||||||
|
} |
||||||
|
|
||||||
|
func TestConsoleLoggerMinimalConfig(t *testing.T) { |
||||||
|
for _, level := range Levels() { |
||||||
|
var written []byte |
||||||
|
var closed bool |
||||||
|
|
||||||
|
c := CallbackWriteCloser{ |
||||||
|
callback: func(p []byte, close bool) { |
||||||
|
written = p |
||||||
|
closed = close |
||||||
|
}, |
||||||
|
} |
||||||
|
prefix := "" |
||||||
|
flags := LstdFlags |
||||||
|
|
||||||
|
cw := NewConsoleLogger() |
||||||
|
realCW := cw.(*ConsoleLogger) |
||||||
|
cw.Init(fmt.Sprintf("{\"level\":\"%s\"}", level)) |
||||||
|
nwc := realCW.out.(*nopWriteCloser) |
||||||
|
nwc.w = c |
||||||
|
|
||||||
|
assert.Equal(t, flags, realCW.Flags) |
||||||
|
assert.Equal(t, FromString(level), realCW.Level) |
||||||
|
assert.Equal(t, FromString(level), cw.GetLevel()) |
||||||
|
assert.Equal(t, prefix, realCW.Prefix) |
||||||
|
assert.Equal(t, "", string(written)) |
||||||
|
cw.Close() |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestConsoleLogger(t *testing.T) { |
||||||
|
var written []byte |
||||||
|
var closed bool |
||||||
|
|
||||||
|
c := CallbackWriteCloser{ |
||||||
|
callback: func(p []byte, close bool) { |
||||||
|
written = p |
||||||
|
closed = close |
||||||
|
}, |
||||||
|
} |
||||||
|
prefix := "TestPrefix " |
||||||
|
level := INFO |
||||||
|
flags := LstdFlags | LUTC | Lfuncname |
||||||
|
|
||||||
|
cw := NewConsoleLogger() |
||||||
|
realCW := cw.(*ConsoleLogger) |
||||||
|
realCW.Colorize = false |
||||||
|
nwc := realCW.out.(*nopWriteCloser) |
||||||
|
nwc.w = c |
||||||
|
|
||||||
|
cw.Init(fmt.Sprintf("{\"expression\":\"FILENAME\",\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d}", prefix, level.String(), flags)) |
||||||
|
|
||||||
|
assert.Equal(t, flags, realCW.Flags) |
||||||
|
assert.Equal(t, level, realCW.Level) |
||||||
|
assert.Equal(t, level, cw.GetLevel()) |
||||||
|
|
||||||
|
location, _ := time.LoadLocation("EST") |
||||||
|
|
||||||
|
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location) |
||||||
|
|
||||||
|
dateString := date.UTC().Format("2006/01/02 15:04:05") |
||||||
|
|
||||||
|
event := Event{ |
||||||
|
level: INFO, |
||||||
|
msg: "TEST MSG", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/FILENAME", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
|
||||||
|
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
cw.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
event.level = DEBUG |
||||||
|
expected = "" |
||||||
|
cw.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
|
||||||
|
event.level = TRACE |
||||||
|
expected = "" |
||||||
|
cw.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
|
||||||
|
nonMatchEvent := Event{ |
||||||
|
level: INFO, |
||||||
|
msg: "TEST MSG", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/FI_LENAME", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
event.level = INFO |
||||||
|
expected = "" |
||||||
|
cw.LogEvent(&nonMatchEvent) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
|
||||||
|
event.level = WARN |
||||||
|
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
cw.LogEvent(&event) |
||||||
|
assert.Equal(t, expected, string(written)) |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
written = written[:0] |
||||||
|
|
||||||
|
cw.Close() |
||||||
|
assert.Equal(t, false, closed) |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"os" |
||||||
|
|
||||||
|
"github.com/mattn/go-isatty" |
||||||
|
"golang.org/x/sys/windows" |
||||||
|
) |
||||||
|
|
||||||
|
func enableVTMode(console windows.Handle) bool { |
||||||
|
mode := uint32(0) |
||||||
|
err := windows.GetConsoleMode(console, &mode) |
||||||
|
if err != nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// EnableVirtualTerminalProcessing is the console mode to allow ANSI code
|
||||||
|
// interpretation on the console. See:
|
||||||
|
// https://docs.microsoft.com/en-us/windows/console/setconsolemode
|
||||||
|
// It only works on windows 10. Earlier terminals will fail with an err which we will
|
||||||
|
// handle to say don't color
|
||||||
|
mode = mode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING |
||||||
|
err = windows.SetConsoleMode(console, mode) |
||||||
|
return err == nil |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
if isatty.IsTerminal(os.Stdout.Fd()) { |
||||||
|
CanColorStdout = enableVTMode(windows.Stdout) |
||||||
|
} else { |
||||||
|
CanColorStdout = isatty.IsCygwinTerminal(os.Stderr.Fd()) |
||||||
|
} |
||||||
|
|
||||||
|
if isatty.IsTerminal(os.Stderr.Fd()) { |
||||||
|
CanColorStderr = enableVTMode(windows.Stderr) |
||||||
|
} else { |
||||||
|
CanColorStderr = isatty.IsCygwinTerminal(os.Stderr.Fd()) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,62 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import "fmt" |
||||||
|
|
||||||
|
// ErrTimeout represents a "Timeout" kind of error.
|
||||||
|
type ErrTimeout struct { |
||||||
|
Name string |
||||||
|
Provider string |
||||||
|
} |
||||||
|
|
||||||
|
// IsErrTimeout checks if an error is a ErrTimeout.
|
||||||
|
func IsErrTimeout(err error) bool { |
||||||
|
if err == nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
_, ok := err.(ErrTimeout) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
func (err ErrTimeout) Error() string { |
||||||
|
return fmt.Sprintf("Log Timeout for %s (%s)", err.Name, err.Provider) |
||||||
|
} |
||||||
|
|
||||||
|
// ErrUnknownProvider represents a "Unknown Provider" kind of error.
|
||||||
|
type ErrUnknownProvider struct { |
||||||
|
Provider string |
||||||
|
} |
||||||
|
|
||||||
|
// IsErrUnknownProvider checks if an error is a ErrUnknownProvider.
|
||||||
|
func IsErrUnknownProvider(err error) bool { |
||||||
|
if err == nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
_, ok := err.(ErrUnknownProvider) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
func (err ErrUnknownProvider) Error() string { |
||||||
|
return fmt.Sprintf("Unknown Log Provider \"%s\" (Was it registered?)", err.Provider) |
||||||
|
} |
||||||
|
|
||||||
|
// ErrDuplicateName represents a Duplicate Name error
|
||||||
|
type ErrDuplicateName struct { |
||||||
|
Name string |
||||||
|
} |
||||||
|
|
||||||
|
// IsErrDuplicateName checks if an error is a ErrDuplicateName.
|
||||||
|
func IsErrDuplicateName(err error) bool { |
||||||
|
if err == nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
_, ok := err.(ErrDuplicateName) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
func (err ErrDuplicateName) Error() string { |
||||||
|
return fmt.Sprintf("Duplicate named logger: %s", err.Name) |
||||||
|
} |
@ -0,0 +1,335 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// Event represents a logging event
|
||||||
|
type Event struct { |
||||||
|
level Level |
||||||
|
msg string |
||||||
|
caller string |
||||||
|
filename string |
||||||
|
line int |
||||||
|
time time.Time |
||||||
|
stacktrace string |
||||||
|
} |
||||||
|
|
||||||
|
// EventLogger represents the behaviours of a logger
|
||||||
|
type EventLogger interface { |
||||||
|
LogEvent(event *Event) error |
||||||
|
Close() |
||||||
|
Flush() |
||||||
|
GetLevel() Level |
||||||
|
GetStacktraceLevel() Level |
||||||
|
GetName() string |
||||||
|
} |
||||||
|
|
||||||
|
// ChannelledLog represents a cached channel to a LoggerProvider
|
||||||
|
type ChannelledLog struct { |
||||||
|
name string |
||||||
|
provider string |
||||||
|
queue chan *Event |
||||||
|
loggerProvider LoggerProvider |
||||||
|
flush chan bool |
||||||
|
close chan bool |
||||||
|
closed chan bool |
||||||
|
} |
||||||
|
|
||||||
|
// NewChannelledLog a new logger instance with given logger provider and config.
|
||||||
|
func NewChannelledLog(name, provider, config string, bufferLength int64) (*ChannelledLog, error) { |
||||||
|
if log, ok := providers[provider]; ok { |
||||||
|
l := &ChannelledLog{ |
||||||
|
queue: make(chan *Event, bufferLength), |
||||||
|
flush: make(chan bool), |
||||||
|
close: make(chan bool), |
||||||
|
closed: make(chan bool), |
||||||
|
} |
||||||
|
l.loggerProvider = log() |
||||||
|
if err := l.loggerProvider.Init(config); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
l.name = name |
||||||
|
l.provider = provider |
||||||
|
go l.Start() |
||||||
|
return l, nil |
||||||
|
} |
||||||
|
return nil, ErrUnknownProvider{provider} |
||||||
|
} |
||||||
|
|
||||||
|
// Start processing the ChannelledLog
|
||||||
|
func (l *ChannelledLog) Start() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case event, ok := <-l.queue: |
||||||
|
if !ok { |
||||||
|
l.closeLogger() |
||||||
|
return |
||||||
|
} |
||||||
|
l.loggerProvider.LogEvent(event) |
||||||
|
case _, ok := <-l.flush: |
||||||
|
if !ok { |
||||||
|
l.closeLogger() |
||||||
|
return |
||||||
|
} |
||||||
|
l.loggerProvider.Flush() |
||||||
|
case _, _ = <-l.close: |
||||||
|
l.closeLogger() |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// LogEvent logs an event to this ChannelledLog
|
||||||
|
func (l *ChannelledLog) LogEvent(event *Event) error { |
||||||
|
select { |
||||||
|
case l.queue <- event: |
||||||
|
return nil |
||||||
|
case <-time.After(60 * time.Second): |
||||||
|
// We're blocked!
|
||||||
|
return ErrTimeout{ |
||||||
|
Name: l.name, |
||||||
|
Provider: l.provider, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (l *ChannelledLog) closeLogger() { |
||||||
|
l.loggerProvider.Flush() |
||||||
|
l.loggerProvider.Close() |
||||||
|
l.closed <- true |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Close this ChannelledLog
|
||||||
|
func (l *ChannelledLog) Close() { |
||||||
|
l.close <- true |
||||||
|
<-l.closed |
||||||
|
} |
||||||
|
|
||||||
|
// Flush this ChannelledLog
|
||||||
|
func (l *ChannelledLog) Flush() { |
||||||
|
l.flush <- true |
||||||
|
} |
||||||
|
|
||||||
|
// GetLevel gets the level of this ChannelledLog
|
||||||
|
func (l *ChannelledLog) GetLevel() Level { |
||||||
|
return l.loggerProvider.GetLevel() |
||||||
|
} |
||||||
|
|
||||||
|
// GetStacktraceLevel gets the level of this ChannelledLog
|
||||||
|
func (l *ChannelledLog) GetStacktraceLevel() Level { |
||||||
|
return l.loggerProvider.GetStacktraceLevel() |
||||||
|
} |
||||||
|
|
||||||
|
// GetName returns the name of this ChannelledLog
|
||||||
|
func (l *ChannelledLog) GetName() string { |
||||||
|
return l.name |
||||||
|
} |
||||||
|
|
||||||
|
// MultiChannelledLog represents a cached channel to a LoggerProvider
|
||||||
|
type MultiChannelledLog struct { |
||||||
|
name string |
||||||
|
bufferLength int64 |
||||||
|
queue chan *Event |
||||||
|
mutex sync.Mutex |
||||||
|
loggers map[string]EventLogger |
||||||
|
flush chan bool |
||||||
|
close chan bool |
||||||
|
started bool |
||||||
|
level Level |
||||||
|
stacktraceLevel Level |
||||||
|
closed chan bool |
||||||
|
} |
||||||
|
|
||||||
|
// NewMultiChannelledLog a new logger instance with given logger provider and config.
|
||||||
|
func NewMultiChannelledLog(name string, bufferLength int64) *MultiChannelledLog { |
||||||
|
m := &MultiChannelledLog{ |
||||||
|
name: name, |
||||||
|
queue: make(chan *Event, bufferLength), |
||||||
|
flush: make(chan bool), |
||||||
|
bufferLength: bufferLength, |
||||||
|
loggers: make(map[string]EventLogger), |
||||||
|
level: NONE, |
||||||
|
stacktraceLevel: NONE, |
||||||
|
close: make(chan bool), |
||||||
|
closed: make(chan bool), |
||||||
|
} |
||||||
|
return m |
||||||
|
} |
||||||
|
|
||||||
|
// AddLogger adds a logger to this MultiChannelledLog
|
||||||
|
func (m *MultiChannelledLog) AddLogger(logger EventLogger) error { |
||||||
|
m.mutex.Lock() |
||||||
|
name := logger.GetName() |
||||||
|
if _, has := m.loggers[name]; has { |
||||||
|
m.mutex.Unlock() |
||||||
|
return ErrDuplicateName{name} |
||||||
|
} |
||||||
|
m.loggers[name] = logger |
||||||
|
if logger.GetLevel() < m.level { |
||||||
|
m.level = logger.GetLevel() |
||||||
|
} |
||||||
|
if logger.GetStacktraceLevel() < m.stacktraceLevel { |
||||||
|
m.stacktraceLevel = logger.GetStacktraceLevel() |
||||||
|
} |
||||||
|
m.mutex.Unlock() |
||||||
|
go m.Start() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// DelLogger removes a sub logger from this MultiChannelledLog
|
||||||
|
// NB: If you delete the last sublogger this logger will simply drop
|
||||||
|
// log events
|
||||||
|
func (m *MultiChannelledLog) DelLogger(name string) bool { |
||||||
|
m.mutex.Lock() |
||||||
|
logger, has := m.loggers[name] |
||||||
|
if !has { |
||||||
|
m.mutex.Unlock() |
||||||
|
return false |
||||||
|
} |
||||||
|
delete(m.loggers, name) |
||||||
|
m.internalResetLevel() |
||||||
|
m.mutex.Unlock() |
||||||
|
logger.Flush() |
||||||
|
logger.Close() |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// GetEventLogger returns a sub logger from this MultiChannelledLog
|
||||||
|
func (m *MultiChannelledLog) GetEventLogger(name string) EventLogger { |
||||||
|
m.mutex.Lock() |
||||||
|
defer m.mutex.Unlock() |
||||||
|
return m.loggers[name] |
||||||
|
} |
||||||
|
|
||||||
|
// GetEventLoggerNames returns a list of names
|
||||||
|
func (m *MultiChannelledLog) GetEventLoggerNames() []string { |
||||||
|
m.mutex.Lock() |
||||||
|
defer m.mutex.Unlock() |
||||||
|
var keys []string |
||||||
|
for k := range m.loggers { |
||||||
|
keys = append(keys, k) |
||||||
|
} |
||||||
|
return keys |
||||||
|
} |
||||||
|
|
||||||
|
func (m *MultiChannelledLog) closeLoggers() { |
||||||
|
m.mutex.Lock() |
||||||
|
for _, logger := range m.loggers { |
||||||
|
logger.Flush() |
||||||
|
logger.Close() |
||||||
|
} |
||||||
|
m.mutex.Unlock() |
||||||
|
m.closed <- true |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Start processing the MultiChannelledLog
|
||||||
|
func (m *MultiChannelledLog) Start() { |
||||||
|
m.mutex.Lock() |
||||||
|
if m.started { |
||||||
|
m.mutex.Unlock() |
||||||
|
return |
||||||
|
} |
||||||
|
m.started = true |
||||||
|
m.mutex.Unlock() |
||||||
|
for { |
||||||
|
select { |
||||||
|
case event, ok := <-m.queue: |
||||||
|
if !ok { |
||||||
|
m.closeLoggers() |
||||||
|
return |
||||||
|
} |
||||||
|
m.mutex.Lock() |
||||||
|
for _, logger := range m.loggers { |
||||||
|
err := logger.LogEvent(event) |
||||||
|
if err != nil { |
||||||
|
fmt.Println(err) |
||||||
|
} |
||||||
|
} |
||||||
|
m.mutex.Unlock() |
||||||
|
case _, ok := <-m.flush: |
||||||
|
if !ok { |
||||||
|
m.closeLoggers() |
||||||
|
return |
||||||
|
} |
||||||
|
m.mutex.Lock() |
||||||
|
for _, logger := range m.loggers { |
||||||
|
logger.Flush() |
||||||
|
} |
||||||
|
m.mutex.Unlock() |
||||||
|
case <-m.close: |
||||||
|
m.closeLoggers() |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// LogEvent logs an event to this MultiChannelledLog
|
||||||
|
func (m *MultiChannelledLog) LogEvent(event *Event) error { |
||||||
|
select { |
||||||
|
case m.queue <- event: |
||||||
|
return nil |
||||||
|
case <-time.After(60 * time.Second): |
||||||
|
// We're blocked!
|
||||||
|
return ErrTimeout{ |
||||||
|
Name: m.name, |
||||||
|
Provider: "MultiChannelledLog", |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Close this MultiChannelledLog
|
||||||
|
func (m *MultiChannelledLog) Close() { |
||||||
|
m.close <- true |
||||||
|
<-m.closed |
||||||
|
} |
||||||
|
|
||||||
|
// Flush this ChannelledLog
|
||||||
|
func (m *MultiChannelledLog) Flush() { |
||||||
|
m.flush <- true |
||||||
|
} |
||||||
|
|
||||||
|
// GetLevel gets the level of this MultiChannelledLog
|
||||||
|
func (m *MultiChannelledLog) GetLevel() Level { |
||||||
|
return m.level |
||||||
|
} |
||||||
|
|
||||||
|
// GetStacktraceLevel gets the level of this MultiChannelledLog
|
||||||
|
func (m *MultiChannelledLog) GetStacktraceLevel() Level { |
||||||
|
return m.stacktraceLevel |
||||||
|
} |
||||||
|
|
||||||
|
func (m *MultiChannelledLog) internalResetLevel() Level { |
||||||
|
m.level = NONE |
||||||
|
for _, logger := range m.loggers { |
||||||
|
level := logger.GetLevel() |
||||||
|
if level < m.level { |
||||||
|
m.level = level |
||||||
|
} |
||||||
|
level = logger.GetStacktraceLevel() |
||||||
|
if level < m.stacktraceLevel { |
||||||
|
m.stacktraceLevel = level |
||||||
|
} |
||||||
|
} |
||||||
|
return m.level |
||||||
|
} |
||||||
|
|
||||||
|
// ResetLevel will reset the level of this MultiChannelledLog
|
||||||
|
func (m *MultiChannelledLog) ResetLevel() Level { |
||||||
|
m.mutex.Lock() |
||||||
|
defer m.mutex.Unlock() |
||||||
|
return m.internalResetLevel() |
||||||
|
} |
||||||
|
|
||||||
|
// GetName gets the name of this MultiChannelledLog
|
||||||
|
func (m *MultiChannelledLog) GetName() string { |
||||||
|
return m.name |
||||||
|
} |
@ -0,0 +1,247 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"compress/gzip" |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestFileLoggerFails(t *testing.T) { |
||||||
|
tmpDir, err := ioutil.TempDir("", "TestFileLogger") |
||||||
|
assert.NoError(t, err) |
||||||
|
defer os.RemoveAll(tmpDir) |
||||||
|
|
||||||
|
prefix := "TestPrefix " |
||||||
|
level := INFO |
||||||
|
flags := LstdFlags | LUTC | Lfuncname |
||||||
|
//filename := filepath.Join(tmpDir, "test.log")
|
||||||
|
|
||||||
|
fileLogger := NewFileLogger() |
||||||
|
//realFileLogger, ok := fileLogger.(*FileLogger)
|
||||||
|
//assert.Equal(t, true, ok)
|
||||||
|
|
||||||
|
// Fail if there is bad json
|
||||||
|
err = fileLogger.Init("{") |
||||||
|
assert.Error(t, err) |
||||||
|
|
||||||
|
// Fail if there is no filename
|
||||||
|
err = fileLogger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"filename\":\"%s\"}", prefix, level.String(), flags, "")) |
||||||
|
assert.Error(t, err) |
||||||
|
|
||||||
|
// Fail if the file isn't a filename
|
||||||
|
err = fileLogger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"filename\":\"%s\"}", prefix, level.String(), flags, filepath.ToSlash(tmpDir))) |
||||||
|
assert.Error(t, err) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func TestFileLogger(t *testing.T) { |
||||||
|
tmpDir, err := ioutil.TempDir("", "TestFileLogger") |
||||||
|
assert.NoError(t, err) |
||||||
|
defer os.RemoveAll(tmpDir) |
||||||
|
|
||||||
|
prefix := "TestPrefix " |
||||||
|
level := INFO |
||||||
|
flags := LstdFlags | LUTC | Lfuncname |
||||||
|
filename := filepath.Join(tmpDir, "test.log") |
||||||
|
|
||||||
|
fileLogger := NewFileLogger() |
||||||
|
realFileLogger, ok := fileLogger.(*FileLogger) |
||||||
|
assert.Equal(t, true, ok) |
||||||
|
|
||||||
|
location, _ := time.LoadLocation("EST") |
||||||
|
|
||||||
|
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location) |
||||||
|
|
||||||
|
dateString := date.UTC().Format("2006/01/02 15:04:05") |
||||||
|
|
||||||
|
event := Event{ |
||||||
|
level: INFO, |
||||||
|
msg: "TEST MSG", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/FILENAME", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
|
||||||
|
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
|
||||||
|
fileLogger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"filename\":\"%s\",\"maxsize\":%d,\"compress\":false}", prefix, level.String(), flags, filepath.ToSlash(filename), len(expected)*2)) |
||||||
|
|
||||||
|
assert.Equal(t, flags, realFileLogger.Flags) |
||||||
|
assert.Equal(t, level, realFileLogger.Level) |
||||||
|
assert.Equal(t, level, fileLogger.GetLevel()) |
||||||
|
|
||||||
|
fileLogger.LogEvent(&event) |
||||||
|
fileLogger.Flush() |
||||||
|
logData, err := ioutil.ReadFile(filename) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, expected, string(logData)) |
||||||
|
|
||||||
|
event.level = DEBUG |
||||||
|
expected = expected + "" |
||||||
|
fileLogger.LogEvent(&event) |
||||||
|
fileLogger.Flush() |
||||||
|
logData, err = ioutil.ReadFile(filename) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, expected, string(logData)) |
||||||
|
|
||||||
|
event.level = TRACE |
||||||
|
expected = expected + "" |
||||||
|
fileLogger.LogEvent(&event) |
||||||
|
fileLogger.Flush() |
||||||
|
logData, err = ioutil.ReadFile(filename) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, expected, string(logData)) |
||||||
|
|
||||||
|
event.level = WARN |
||||||
|
expected = expected + fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
fileLogger.LogEvent(&event) |
||||||
|
fileLogger.Flush() |
||||||
|
logData, err = ioutil.ReadFile(filename) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, expected, string(logData)) |
||||||
|
|
||||||
|
// Should rotate
|
||||||
|
fileLogger.LogEvent(&event) |
||||||
|
fileLogger.Flush() |
||||||
|
logData, err = ioutil.ReadFile(filename + fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), 1)) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, expected, string(logData)) |
||||||
|
|
||||||
|
logData, err = ioutil.ReadFile(filename) |
||||||
|
assert.NoError(t, err) |
||||||
|
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
assert.Equal(t, expected, string(logData)) |
||||||
|
|
||||||
|
for num := 2; num <= 999; num++ { |
||||||
|
file, err := os.OpenFile(filename+fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), num), os.O_RDONLY|os.O_CREATE, 0666) |
||||||
|
assert.NoError(t, err) |
||||||
|
file.Close() |
||||||
|
} |
||||||
|
err = realFileLogger.DoRotate() |
||||||
|
assert.Error(t, err) |
||||||
|
|
||||||
|
expected = expected + fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
fileLogger.LogEvent(&event) |
||||||
|
fileLogger.Flush() |
||||||
|
logData, err = ioutil.ReadFile(filename) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, expected, string(logData)) |
||||||
|
|
||||||
|
// Should fail to rotate
|
||||||
|
expected = expected + fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
fileLogger.LogEvent(&event) |
||||||
|
fileLogger.Flush() |
||||||
|
logData, err = ioutil.ReadFile(filename) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, expected, string(logData)) |
||||||
|
|
||||||
|
fileLogger.Close() |
||||||
|
} |
||||||
|
|
||||||
|
func TestCompressFileLogger(t *testing.T) { |
||||||
|
tmpDir, err := ioutil.TempDir("", "TestFileLogger") |
||||||
|
assert.NoError(t, err) |
||||||
|
defer os.RemoveAll(tmpDir) |
||||||
|
|
||||||
|
prefix := "TestPrefix " |
||||||
|
level := INFO |
||||||
|
flags := LstdFlags | LUTC | Lfuncname |
||||||
|
filename := filepath.Join(tmpDir, "test.log") |
||||||
|
|
||||||
|
fileLogger := NewFileLogger() |
||||||
|
realFileLogger, ok := fileLogger.(*FileLogger) |
||||||
|
assert.Equal(t, true, ok) |
||||||
|
|
||||||
|
location, _ := time.LoadLocation("EST") |
||||||
|
|
||||||
|
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location) |
||||||
|
|
||||||
|
dateString := date.UTC().Format("2006/01/02 15:04:05") |
||||||
|
|
||||||
|
event := Event{ |
||||||
|
level: INFO, |
||||||
|
msg: "TEST MSG", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/FILENAME", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
|
||||||
|
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
|
||||||
|
fileLogger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"filename\":\"%s\",\"maxsize\":%d,\"compress\":true}", prefix, level.String(), flags, filepath.ToSlash(filename), len(expected)*2)) |
||||||
|
|
||||||
|
fileLogger.LogEvent(&event) |
||||||
|
fileLogger.Flush() |
||||||
|
logData, err := ioutil.ReadFile(filename) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, expected, string(logData)) |
||||||
|
|
||||||
|
event.level = WARN |
||||||
|
expected = expected + fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
fileLogger.LogEvent(&event) |
||||||
|
fileLogger.Flush() |
||||||
|
logData, err = ioutil.ReadFile(filename) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, expected, string(logData)) |
||||||
|
|
||||||
|
// Should rotate
|
||||||
|
fileLogger.LogEvent(&event) |
||||||
|
fileLogger.Flush() |
||||||
|
|
||||||
|
for num := 2; num <= 999; num++ { |
||||||
|
file, err := os.OpenFile(filename+fmt.Sprintf(".%s.%03d.gz", time.Now().Format("2006-01-02"), num), os.O_RDONLY|os.O_CREATE, 0666) |
||||||
|
assert.NoError(t, err) |
||||||
|
file.Close() |
||||||
|
} |
||||||
|
err = realFileLogger.DoRotate() |
||||||
|
assert.Error(t, err) |
||||||
|
} |
||||||
|
|
||||||
|
func TestCompressOldFile(t *testing.T) { |
||||||
|
tmpDir, err := ioutil.TempDir("", "TestFileLogger") |
||||||
|
assert.NoError(t, err) |
||||||
|
defer os.RemoveAll(tmpDir) |
||||||
|
fname := filepath.Join(tmpDir, "test") |
||||||
|
nonGzip := filepath.Join(tmpDir, "test-nonGzip") |
||||||
|
|
||||||
|
f, err := os.OpenFile(fname, os.O_CREATE|os.O_WRONLY, 0660) |
||||||
|
assert.NoError(t, err) |
||||||
|
ng, err := os.OpenFile(nonGzip, os.O_CREATE|os.O_WRONLY, 0660) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
for i := 0; i < 999; i++ { |
||||||
|
f.WriteString("This is a test file\n") |
||||||
|
ng.WriteString("This is a test file\n") |
||||||
|
} |
||||||
|
f.Close() |
||||||
|
ng.Close() |
||||||
|
|
||||||
|
err = compressOldLogFile(fname, -1) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
_, err = os.Lstat(fname + ".gz") |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
f, err = os.Open(fname + ".gz") |
||||||
|
assert.NoError(t, err) |
||||||
|
zr, err := gzip.NewReader(f) |
||||||
|
assert.NoError(t, err) |
||||||
|
data, err := ioutil.ReadAll(zr) |
||||||
|
assert.NoError(t, err) |
||||||
|
original, err := ioutil.ReadFile(nonGzip) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, original, data) |
||||||
|
} |
@ -0,0 +1,111 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// Level is the level of the logger
|
||||||
|
type Level int |
||||||
|
|
||||||
|
const ( |
||||||
|
// TRACE represents the lowest log level
|
||||||
|
TRACE Level = iota |
||||||
|
// DEBUG is for debug logging
|
||||||
|
DEBUG |
||||||
|
// INFO is for information
|
||||||
|
INFO |
||||||
|
// WARN is for warning information
|
||||||
|
WARN |
||||||
|
// ERROR is for error reporting
|
||||||
|
ERROR |
||||||
|
// CRITICAL is for critical errors
|
||||||
|
CRITICAL |
||||||
|
// FATAL is for fatal errors
|
||||||
|
FATAL |
||||||
|
// NONE is for no logging
|
||||||
|
NONE |
||||||
|
) |
||||||
|
|
||||||
|
var toString = map[Level]string{ |
||||||
|
TRACE: "trace", |
||||||
|
DEBUG: "debug", |
||||||
|
INFO: "info", |
||||||
|
WARN: "warn", |
||||||
|
ERROR: "error", |
||||||
|
CRITICAL: "critical", |
||||||
|
FATAL: "fatal", |
||||||
|
NONE: "none", |
||||||
|
} |
||||||
|
|
||||||
|
var toLevel = map[string]Level{ |
||||||
|
"trace": TRACE, |
||||||
|
"debug": DEBUG, |
||||||
|
"info": INFO, |
||||||
|
"warn": WARN, |
||||||
|
"error": ERROR, |
||||||
|
"critical": CRITICAL, |
||||||
|
"fatal": FATAL, |
||||||
|
"none": NONE, |
||||||
|
} |
||||||
|
|
||||||
|
// Levels returns all the possible logging levels
|
||||||
|
func Levels() []string { |
||||||
|
keys := make([]string, 0) |
||||||
|
for key := range toLevel { |
||||||
|
keys = append(keys, key) |
||||||
|
} |
||||||
|
return keys |
||||||
|
} |
||||||
|
|
||||||
|
func (l Level) String() string { |
||||||
|
s, ok := toString[l] |
||||||
|
if ok { |
||||||
|
return s |
||||||
|
} |
||||||
|
return "info" |
||||||
|
} |
||||||
|
|
||||||
|
// MarshalJSON takes a Level and turns it into text
|
||||||
|
func (l Level) MarshalJSON() ([]byte, error) { |
||||||
|
buffer := bytes.NewBufferString(`"`) |
||||||
|
buffer.WriteString(toString[l]) |
||||||
|
buffer.WriteString(`"`) |
||||||
|
return buffer.Bytes(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// FromString takes a level string and returns a Level
|
||||||
|
func FromString(level string) Level { |
||||||
|
temp, ok := toLevel[strings.ToLower(level)] |
||||||
|
if !ok { |
||||||
|
return INFO |
||||||
|
} |
||||||
|
return temp |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalJSON takes text and turns it into a Level
|
||||||
|
func (l *Level) UnmarshalJSON(b []byte) error { |
||||||
|
var tmp interface{} |
||||||
|
err := json.Unmarshal(b, &tmp) |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "Err: %v", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
switch v := tmp.(type) { |
||||||
|
case string: |
||||||
|
*l = FromString(string(v)) |
||||||
|
case int: |
||||||
|
*l = FromString(Level(v).String()) |
||||||
|
default: |
||||||
|
*l = INFO |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
type testLevel struct { |
||||||
|
Level Level `json:"level"` |
||||||
|
} |
||||||
|
|
||||||
|
func TestLevelMarshalUnmarshalJSON(t *testing.T) { |
||||||
|
levelBytes, err := json.Marshal(testLevel{ |
||||||
|
Level: INFO, |
||||||
|
}) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, string(makeTestLevelBytes(INFO.String())), string(levelBytes)) |
||||||
|
|
||||||
|
var testLevel testLevel |
||||||
|
err = json.Unmarshal(levelBytes, &testLevel) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, INFO, testLevel.Level) |
||||||
|
|
||||||
|
err = json.Unmarshal(makeTestLevelBytes(`FOFOO`), &testLevel) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, INFO, testLevel.Level) |
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(fmt.Sprintf(`{"level":%d}`, 2)), &testLevel) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, INFO, testLevel.Level) |
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(fmt.Sprintf(`{"level":%d}`, 10012)), &testLevel) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, INFO, testLevel.Level) |
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(`{"level":{}}`), &testLevel) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, INFO, testLevel.Level) |
||||||
|
|
||||||
|
assert.Equal(t, INFO.String(), Level(1001).String()) |
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(`{"level":{}`), &testLevel.Level) |
||||||
|
assert.Error(t, err) |
||||||
|
} |
||||||
|
|
||||||
|
func makeTestLevelBytes(level string) []byte { |
||||||
|
return []byte(fmt.Sprintf(`{"level":"%s"}`, level)) |
||||||
|
} |
@ -0,0 +1,154 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func baseConsoleTest(t *testing.T, logger *Logger) (chan []byte, chan bool) { |
||||||
|
written := make(chan []byte) |
||||||
|
closed := make(chan bool) |
||||||
|
|
||||||
|
c := CallbackWriteCloser{ |
||||||
|
callback: func(p []byte, close bool) { |
||||||
|
written <- p |
||||||
|
closed <- close |
||||||
|
}, |
||||||
|
} |
||||||
|
m := logger.MultiChannelledLog |
||||||
|
|
||||||
|
channelledLog := m.GetEventLogger("console") |
||||||
|
assert.NotEmpty(t, channelledLog) |
||||||
|
realChanLog, ok := channelledLog.(*ChannelledLog) |
||||||
|
assert.Equal(t, true, ok) |
||||||
|
realCL, ok := realChanLog.loggerProvider.(*ConsoleLogger) |
||||||
|
assert.Equal(t, true, ok) |
||||||
|
assert.Equal(t, INFO, realCL.Level) |
||||||
|
realCL.out = c |
||||||
|
|
||||||
|
format := "test: %s" |
||||||
|
args := []interface{}{"A"} |
||||||
|
|
||||||
|
logger.Log(0, INFO, format, args...) |
||||||
|
line := <-written |
||||||
|
assert.Contains(t, string(line), fmt.Sprintf(format, args...)) |
||||||
|
assert.Equal(t, false, <-closed) |
||||||
|
|
||||||
|
format = "test2: %s" |
||||||
|
logger.Warn(format, args...) |
||||||
|
line = <-written |
||||||
|
|
||||||
|
assert.Contains(t, string(line), fmt.Sprintf(format, args...)) |
||||||
|
assert.Equal(t, false, <-closed) |
||||||
|
|
||||||
|
format = "testerror: %s" |
||||||
|
logger.Error(format, args...) |
||||||
|
line = <-written |
||||||
|
assert.Contains(t, string(line), fmt.Sprintf(format, args...)) |
||||||
|
assert.Equal(t, false, <-closed) |
||||||
|
return written, closed |
||||||
|
} |
||||||
|
|
||||||
|
func TestNewLoggerUnexported(t *testing.T) { |
||||||
|
level := INFO |
||||||
|
logger := newLogger("UNEXPORTED", 0) |
||||||
|
err := logger.SetLogger("console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String())) |
||||||
|
assert.NoError(t, err) |
||||||
|
out := logger.MultiChannelledLog.GetEventLogger("console") |
||||||
|
assert.NotEmpty(t, out) |
||||||
|
chanlog, ok := out.(*ChannelledLog) |
||||||
|
assert.Equal(t, true, ok) |
||||||
|
assert.Equal(t, "console", chanlog.provider) |
||||||
|
assert.Equal(t, INFO, logger.GetLevel()) |
||||||
|
baseConsoleTest(t, logger) |
||||||
|
} |
||||||
|
|
||||||
|
func TestNewLoggger(t *testing.T) { |
||||||
|
level := INFO |
||||||
|
logger := NewLogger(0, "console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String())) |
||||||
|
|
||||||
|
assert.Equal(t, INFO, GetLevel()) |
||||||
|
assert.Equal(t, false, IsTrace()) |
||||||
|
assert.Equal(t, false, IsDebug()) |
||||||
|
assert.Equal(t, true, IsInfo()) |
||||||
|
assert.Equal(t, true, IsWarn()) |
||||||
|
assert.Equal(t, true, IsError()) |
||||||
|
|
||||||
|
written, closed := baseConsoleTest(t, logger) |
||||||
|
|
||||||
|
format := "test: %s" |
||||||
|
args := []interface{}{"A"} |
||||||
|
|
||||||
|
Log(0, INFO, format, args...) |
||||||
|
line := <-written |
||||||
|
assert.Contains(t, string(line), fmt.Sprintf(format, args...)) |
||||||
|
assert.Equal(t, false, <-closed) |
||||||
|
|
||||||
|
Info(format, args...) |
||||||
|
line = <-written |
||||||
|
assert.Contains(t, string(line), fmt.Sprintf(format, args...)) |
||||||
|
assert.Equal(t, false, <-closed) |
||||||
|
|
||||||
|
go DelLogger("console") |
||||||
|
line = <-written |
||||||
|
assert.Equal(t, "", string(line)) |
||||||
|
assert.Equal(t, true, <-closed) |
||||||
|
} |
||||||
|
|
||||||
|
func TestNewLogggerRecreate(t *testing.T) { |
||||||
|
level := INFO |
||||||
|
NewLogger(0, "console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String())) |
||||||
|
|
||||||
|
assert.Equal(t, INFO, GetLevel()) |
||||||
|
assert.Equal(t, false, IsTrace()) |
||||||
|
assert.Equal(t, false, IsDebug()) |
||||||
|
assert.Equal(t, true, IsInfo()) |
||||||
|
assert.Equal(t, true, IsWarn()) |
||||||
|
assert.Equal(t, true, IsError()) |
||||||
|
|
||||||
|
format := "test: %s" |
||||||
|
args := []interface{}{"A"} |
||||||
|
|
||||||
|
Log(0, INFO, format, args...) |
||||||
|
|
||||||
|
NewLogger(0, "console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String())) |
||||||
|
|
||||||
|
assert.Equal(t, INFO, GetLevel()) |
||||||
|
assert.Equal(t, false, IsTrace()) |
||||||
|
assert.Equal(t, false, IsDebug()) |
||||||
|
assert.Equal(t, true, IsInfo()) |
||||||
|
assert.Equal(t, true, IsWarn()) |
||||||
|
assert.Equal(t, true, IsError()) |
||||||
|
|
||||||
|
Log(0, INFO, format, args...) |
||||||
|
|
||||||
|
assert.Panics(t, func() { |
||||||
|
NewLogger(0, "console", "console", fmt.Sprintf(`{"level":"%s"`, level.String())) |
||||||
|
}) |
||||||
|
|
||||||
|
go DelLogger("console") |
||||||
|
|
||||||
|
// We should be able to redelete without a problem
|
||||||
|
go DelLogger("console") |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func TestNewNamedLogger(t *testing.T) { |
||||||
|
level := INFO |
||||||
|
err := NewNamedLogger("test", 0, "console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String())) |
||||||
|
assert.NoError(t, err) |
||||||
|
logger := NamedLoggers["test"] |
||||||
|
assert.Equal(t, level, logger.GetLevel()) |
||||||
|
|
||||||
|
written, closed := baseConsoleTest(t, logger) |
||||||
|
go DelNamedLogger("test") |
||||||
|
line := <-written |
||||||
|
assert.Equal(t, "", string(line)) |
||||||
|
assert.Equal(t, true, <-closed) |
||||||
|
} |
@ -0,0 +1,156 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"runtime" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// Logger is default logger in the Gitea application.
|
||||||
|
// it can contain several providers and log message into all providers.
|
||||||
|
type Logger struct { |
||||||
|
*MultiChannelledLog |
||||||
|
bufferLength int64 |
||||||
|
} |
||||||
|
|
||||||
|
// newLogger initializes and returns a new logger.
|
||||||
|
func newLogger(name string, buffer int64) *Logger { |
||||||
|
l := &Logger{ |
||||||
|
MultiChannelledLog: NewMultiChannelledLog(name, buffer), |
||||||
|
bufferLength: buffer, |
||||||
|
} |
||||||
|
return l |
||||||
|
} |
||||||
|
|
||||||
|
// SetLogger sets new logger instance with given logger provider and config.
|
||||||
|
func (l *Logger) SetLogger(name, provider, config string) error { |
||||||
|
eventLogger, err := NewChannelledLog(name, provider, config, l.bufferLength) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("Failed to create sublogger (%s): %v", name, err) |
||||||
|
} |
||||||
|
|
||||||
|
l.MultiChannelledLog.DelLogger(name) |
||||||
|
|
||||||
|
err = l.MultiChannelledLog.AddLogger(eventLogger) |
||||||
|
if err != nil { |
||||||
|
if IsErrDuplicateName(err) { |
||||||
|
return fmt.Errorf("Duplicate named sublogger %s %v", name, l.MultiChannelledLog.GetEventLoggerNames()) |
||||||
|
} |
||||||
|
return fmt.Errorf("Failed to add sublogger (%s): %v", name, err) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// DelLogger deletes a sublogger from this logger.
|
||||||
|
func (l *Logger) DelLogger(name string) (bool, error) { |
||||||
|
return l.MultiChannelledLog.DelLogger(name), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Log msg at the provided level with the provided caller defined by skip (0 being the function that calls this function)
|
||||||
|
func (l *Logger) Log(skip int, level Level, format string, v ...interface{}) error { |
||||||
|
if l.GetLevel() > level { |
||||||
|
return nil |
||||||
|
} |
||||||
|
caller := "?()" |
||||||
|
pc, filename, line, ok := runtime.Caller(skip + 1) |
||||||
|
if ok { |
||||||
|
// Get caller function name.
|
||||||
|
fn := runtime.FuncForPC(pc) |
||||||
|
if fn != nil { |
||||||
|
caller = fn.Name() + "()" |
||||||
|
} |
||||||
|
} |
||||||
|
msg := format |
||||||
|
if len(v) > 0 { |
||||||
|
args := make([]interface{}, len(v)) |
||||||
|
for i := 0; i < len(args); i++ { |
||||||
|
args[i] = NewColoredValuePointer(&v[i]) |
||||||
|
} |
||||||
|
msg = fmt.Sprintf(format, args...) |
||||||
|
} |
||||||
|
stack := "" |
||||||
|
if l.GetStacktraceLevel() <= level { |
||||||
|
stack = Stack(skip + 1) |
||||||
|
} |
||||||
|
return l.SendLog(level, caller, strings.TrimPrefix(filename, prefix), line, msg, stack) |
||||||
|
} |
||||||
|
|
||||||
|
// SendLog sends a log event at the provided level with the information given
|
||||||
|
func (l *Logger) SendLog(level Level, caller, filename string, line int, msg string, stack string) error { |
||||||
|
if l.GetLevel() > level { |
||||||
|
return nil |
||||||
|
} |
||||||
|
event := &Event{ |
||||||
|
level: level, |
||||||
|
caller: caller, |
||||||
|
filename: filename, |
||||||
|
line: line, |
||||||
|
msg: msg, |
||||||
|
time: time.Now(), |
||||||
|
stacktrace: stack, |
||||||
|
} |
||||||
|
l.LogEvent(event) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Trace records trace log
|
||||||
|
func (l *Logger) Trace(format string, v ...interface{}) { |
||||||
|
l.Log(1, TRACE, format, v...) |
||||||
|
} |
||||||
|
|
||||||
|
// Debug records debug log
|
||||||
|
func (l *Logger) Debug(format string, v ...interface{}) { |
||||||
|
l.Log(1, DEBUG, format, v...) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// Info records information log
|
||||||
|
func (l *Logger) Info(format string, v ...interface{}) { |
||||||
|
l.Log(1, INFO, format, v...) |
||||||
|
} |
||||||
|
|
||||||
|
// Warn records warning log
|
||||||
|
func (l *Logger) Warn(format string, v ...interface{}) { |
||||||
|
l.Log(1, WARN, format, v...) |
||||||
|
} |
||||||
|
|
||||||
|
// Error records error log
|
||||||
|
func (l *Logger) Error(format string, v ...interface{}) { |
||||||
|
l.Log(1, ERROR, format, v...) |
||||||
|
} |
||||||
|
|
||||||
|
// ErrorWithSkip records error log from "skip" calls back from this function
|
||||||
|
func (l *Logger) ErrorWithSkip(skip int, format string, v ...interface{}) { |
||||||
|
l.Log(skip+1, ERROR, format, v...) |
||||||
|
} |
||||||
|
|
||||||
|
// Critical records critical log
|
||||||
|
func (l *Logger) Critical(format string, v ...interface{}) { |
||||||
|
l.Log(1, CRITICAL, format, v...) |
||||||
|
} |
||||||
|
|
||||||
|
// CriticalWithSkip records critical log from "skip" calls back from this function
|
||||||
|
func (l *Logger) CriticalWithSkip(skip int, format string, v ...interface{}) { |
||||||
|
l.Log(skip+1, CRITICAL, format, v...) |
||||||
|
} |
||||||
|
|
||||||
|
// Fatal records fatal log and exit the process
|
||||||
|
func (l *Logger) Fatal(format string, v ...interface{}) { |
||||||
|
l.Log(1, FATAL, format, v...) |
||||||
|
l.Close() |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
// FatalWithSkip records fatal log from "skip" calls back from this function and exits the process
|
||||||
|
func (l *Logger) FatalWithSkip(skip int, format string, v ...interface{}) { |
||||||
|
l.Log(skip+1, FATAL, format, v...) |
||||||
|
l.Close() |
||||||
|
os.Exit(1) |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
// LoggerProvider represents behaviors of a logger provider.
|
||||||
|
type LoggerProvider interface { |
||||||
|
Init(config string) error |
||||||
|
EventLogger |
||||||
|
} |
||||||
|
|
||||||
|
type loggerProvider func() LoggerProvider |
||||||
|
|
||||||
|
var providers = make(map[string]loggerProvider) |
||||||
|
|
||||||
|
// Register registers given logger provider to providers.
|
||||||
|
func Register(name string, log loggerProvider) { |
||||||
|
if log == nil { |
||||||
|
panic("log: register provider is nil") |
||||||
|
} |
||||||
|
if _, dup := providers[name]; dup { |
||||||
|
panic("log: register called twice for provider \"" + name + "\"") |
||||||
|
} |
||||||
|
providers[name] = log |
||||||
|
} |
@ -0,0 +1,103 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"time" |
||||||
|
|
||||||
|
macaron "gopkg.in/macaron.v1" |
||||||
|
) |
||||||
|
|
||||||
|
var statusToColor = map[int][]byte{ |
||||||
|
100: ColorBytes(Bold), |
||||||
|
200: ColorBytes(FgGreen), |
||||||
|
300: ColorBytes(FgYellow), |
||||||
|
304: ColorBytes(FgCyan), |
||||||
|
400: ColorBytes(Bold, FgRed), |
||||||
|
401: ColorBytes(Bold, FgMagenta), |
||||||
|
403: ColorBytes(Bold, FgMagenta), |
||||||
|
500: ColorBytes(Bold, BgRed), |
||||||
|
} |
||||||
|
|
||||||
|
func coloredStatus(status int, s ...string) *ColoredValue { |
||||||
|
color, ok := statusToColor[status] |
||||||
|
if !ok { |
||||||
|
color, ok = statusToColor[(status/100)*100] |
||||||
|
} |
||||||
|
if !ok { |
||||||
|
color = fgBoldBytes |
||||||
|
} |
||||||
|
if len(s) > 0 { |
||||||
|
return NewColoredValueBytes(s[0], &color) |
||||||
|
} |
||||||
|
return NewColoredValueBytes(status, &color) |
||||||
|
} |
||||||
|
|
||||||
|
var methodToColor = map[string][]byte{ |
||||||
|
"GET": ColorBytes(FgBlue), |
||||||
|
"POST": ColorBytes(FgGreen), |
||||||
|
"DELETE": ColorBytes(FgRed), |
||||||
|
"PATCH": ColorBytes(FgCyan), |
||||||
|
"PUT": ColorBytes(FgYellow, Faint), |
||||||
|
"HEAD": ColorBytes(FgBlue, Faint), |
||||||
|
} |
||||||
|
|
||||||
|
func coloredMethod(method string) *ColoredValue { |
||||||
|
color, ok := methodToColor[method] |
||||||
|
if !ok { |
||||||
|
return NewColoredValueBytes(method, &fgBoldBytes) |
||||||
|
} |
||||||
|
return NewColoredValueBytes(method, &color) |
||||||
|
} |
||||||
|
|
||||||
|
var durations = []time.Duration{ |
||||||
|
10 * time.Millisecond, |
||||||
|
100 * time.Millisecond, |
||||||
|
1 * time.Second, |
||||||
|
5 * time.Second, |
||||||
|
10 * time.Second, |
||||||
|
} |
||||||
|
|
||||||
|
var durationColors = [][]byte{ |
||||||
|
ColorBytes(FgGreen), |
||||||
|
ColorBytes(Bold), |
||||||
|
ColorBytes(FgYellow), |
||||||
|
ColorBytes(FgRed, Bold), |
||||||
|
ColorBytes(BgRed), |
||||||
|
} |
||||||
|
|
||||||
|
var wayTooLong = ColorBytes(BgMagenta) |
||||||
|
|
||||||
|
func coloredTime(duration time.Duration) *ColoredValue { |
||||||
|
for i, k := range durations { |
||||||
|
if duration < k { |
||||||
|
return NewColoredValueBytes(duration, &durationColors[i]) |
||||||
|
} |
||||||
|
} |
||||||
|
return NewColoredValueBytes(duration, &wayTooLong) |
||||||
|
} |
||||||
|
|
||||||
|
// SetupRouterLogger will setup macaron to routing to the main gitea log
|
||||||
|
func SetupRouterLogger(m *macaron.Macaron, level Level) { |
||||||
|
if GetLevel() <= level { |
||||||
|
m.Use(RouterHandler(level)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// RouterHandler is a macaron handler that will log the routing to the default gitea log
|
||||||
|
func RouterHandler(level Level) func(ctx *macaron.Context) { |
||||||
|
return func(ctx *macaron.Context) { |
||||||
|
start := time.Now() |
||||||
|
|
||||||
|
GetLogger("router").Log(0, level, "Started %s %s for %s", coloredMethod(ctx.Req.Method), ctx.Req.RequestURI, ctx.RemoteAddr()) |
||||||
|
|
||||||
|
rw := ctx.Resp.(macaron.ResponseWriter) |
||||||
|
ctx.Next() |
||||||
|
|
||||||
|
status := rw.Status() |
||||||
|
GetLogger("router").Log(0, level, "Completed %s %s %v %s in %v", coloredMethod(ctx.Req.Method), ctx.Req.RequestURI, coloredStatus(status), coloredStatus(status, http.StatusText(rw.Status())), coloredTime(time.Since(start))) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,86 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/smtp" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestSMTPLogger(t *testing.T) { |
||||||
|
prefix := "TestPrefix " |
||||||
|
level := INFO |
||||||
|
flags := LstdFlags | LUTC | Lfuncname |
||||||
|
username := "testuser" |
||||||
|
password := "testpassword" |
||||||
|
host := "testhost" |
||||||
|
subject := "testsubject" |
||||||
|
sendTos := []string{"testto1", "testto2"} |
||||||
|
|
||||||
|
logger := NewSMTPLogger() |
||||||
|
smtpLogger, ok := logger.(*SMTPLogger) |
||||||
|
assert.Equal(t, true, ok) |
||||||
|
|
||||||
|
err := logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"username\":\"%s\",\"password\":\"%s\",\"host\":\"%s\",\"subject\":\"%s\",\"sendTos\":[\"%s\",\"%s\"]}", prefix, level.String(), flags, username, password, host, subject, sendTos[0], sendTos[1])) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
assert.Equal(t, flags, smtpLogger.Flags) |
||||||
|
assert.Equal(t, level, smtpLogger.Level) |
||||||
|
assert.Equal(t, level, logger.GetLevel()) |
||||||
|
|
||||||
|
location, _ := time.LoadLocation("EST") |
||||||
|
|
||||||
|
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location) |
||||||
|
|
||||||
|
dateString := date.UTC().Format("2006/01/02 15:04:05") |
||||||
|
|
||||||
|
event := Event{ |
||||||
|
level: INFO, |
||||||
|
msg: "TEST MSG", |
||||||
|
caller: "CALLER", |
||||||
|
filename: "FULL/FILENAME", |
||||||
|
line: 1, |
||||||
|
time: date, |
||||||
|
} |
||||||
|
|
||||||
|
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
|
||||||
|
var envToHost string |
||||||
|
var envFrom string |
||||||
|
var envTo []string |
||||||
|
var envMsg []byte |
||||||
|
smtpLogger.sendMailFn = func(addr string, a smtp.Auth, from string, to []string, msg []byte) error { |
||||||
|
envToHost = addr |
||||||
|
envFrom = from |
||||||
|
envTo = to |
||||||
|
envMsg = msg |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
err = logger.LogEvent(&event) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, host, envToHost) |
||||||
|
assert.Equal(t, username, envFrom) |
||||||
|
assert.Equal(t, sendTos, envTo) |
||||||
|
assert.Contains(t, string(envMsg), expected) |
||||||
|
|
||||||
|
logger.Flush() |
||||||
|
|
||||||
|
event.level = WARN |
||||||
|
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg) |
||||||
|
err = logger.LogEvent(&event) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, host, envToHost) |
||||||
|
assert.Equal(t, username, envFrom) |
||||||
|
assert.Equal(t, sendTos, envTo) |
||||||
|
assert.Contains(t, string(envMsg), expected) |
||||||
|
|
||||||
|
logger.Close() |
||||||
|
} |
@ -0,0 +1,83 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"runtime" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
unknown = []byte("???") |
||||||
|
) |
||||||
|
|
||||||
|
// Stack will skip back the provided number of frames and return a stack trace with source code.
|
||||||
|
// Although we could just use debug.Stack(), this routine will return the source code and
|
||||||
|
// skip back the provided number of frames - i.e. allowing us to ignore preceding function calls.
|
||||||
|
// A skip of 0 returns the stack trace for the calling function, not including this call.
|
||||||
|
// If the problem is a lack of memory of course all this is not going to work...
|
||||||
|
func Stack(skip int) string { |
||||||
|
buf := new(bytes.Buffer) |
||||||
|
|
||||||
|
// Store the last file we opened as its probable that the preceding stack frame
|
||||||
|
// will be in the same file
|
||||||
|
var lines [][]byte |
||||||
|
var lastFilename string |
||||||
|
for i := skip + 1; ; i++ { // Skip over frames
|
||||||
|
programCounter, filename, lineNumber, ok := runtime.Caller(i) |
||||||
|
// If we can't retrieve the information break - basically we're into go internals at this point.
|
||||||
|
if !ok { |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
// Print equivalent of debug.Stack()
|
||||||
|
fmt.Fprintf(buf, "%s:%d (0x%x)\n", filename, lineNumber, programCounter) |
||||||
|
// Now try to print the offending line
|
||||||
|
if filename != lastFilename { |
||||||
|
data, err := ioutil.ReadFile(filename) |
||||||
|
if err != nil { |
||||||
|
// can't read this sourcefile
|
||||||
|
// likely we don't have the sourcecode available
|
||||||
|
continue |
||||||
|
} |
||||||
|
lines = bytes.Split(data, []byte{'\n'}) |
||||||
|
lastFilename = filename |
||||||
|
} |
||||||
|
fmt.Fprintf(buf, "\t%s: %s\n", functionName(programCounter), source(lines, lineNumber)) |
||||||
|
} |
||||||
|
return buf.String() |
||||||
|
} |
||||||
|
|
||||||
|
// functionName converts the provided programCounter into a function name
|
||||||
|
func functionName(programCounter uintptr) []byte { |
||||||
|
function := runtime.FuncForPC(programCounter) |
||||||
|
if function == nil { |
||||||
|
return unknown |
||||||
|
} |
||||||
|
name := []byte(function.Name()) |
||||||
|
|
||||||
|
// Because we provide the filename we can drop the preceding package name.
|
||||||
|
if lastslash := bytes.LastIndex(name, []byte("/")); lastslash >= 0 { |
||||||
|
name = name[lastslash+1:] |
||||||
|
} |
||||||
|
// And the current package name.
|
||||||
|
if period := bytes.Index(name, []byte(".")); period >= 0 { |
||||||
|
name = name[period+1:] |
||||||
|
} |
||||||
|
// And we should just replace the interpunct with a dot
|
||||||
|
name = bytes.Replace(name, []byte("·"), []byte("."), -1) |
||||||
|
return name |
||||||
|
} |
||||||
|
|
||||||
|
// source returns a space-trimmed slice of the n'th line.
|
||||||
|
func source(lines [][]byte, n int) []byte { |
||||||
|
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
|
||||||
|
if n < 0 || n >= len(lines) { |
||||||
|
return unknown |
||||||
|
} |
||||||
|
return bytes.TrimSpace(lines[n]) |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue