* 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 EXPRESSIONrelease/v1.9
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