* 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 EXPRESSIONfor-closed-social
@ -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]) | |||
} |