* 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