* 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