first-commit

This commit is contained in:
2025-08-25 15:46:12 +08:00
commit f4d95dfff4
5665 changed files with 705359 additions and 0 deletions

121
modules/log/color.go Normal file
View File

@@ -0,0 +1,121 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"strconv"
)
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 (
resetBytes = ColorBytes(Reset)
fgCyanBytes = ColorBytes(FgCyan)
fgGreenBytes = ColorBytes(FgGreen)
)
type ColoredValue struct {
v any
colors []ColorAttribute
}
var _ fmt.Formatter = (*ColoredValue)(nil)
func (c *ColoredValue) Format(f fmt.State, verb rune) {
_, _ = f.Write(ColorBytes(c.colors...))
s := fmt.Sprintf(fmt.FormatString(f, verb), c.v)
_, _ = f.Write([]byte(s))
_, _ = f.Write(resetBytes)
}
func (c *ColoredValue) Value() any {
return c.v
}
func NewColoredValue(v any, color ...ColorAttribute) *ColoredValue {
return &ColoredValue{v: v, colors: color}
}
// 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
}

View File

@@ -0,0 +1,14 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
// CanColorStdout reports if we can color the Stdout
// Although we could do terminal sniffing and the like - in reality
// most tools on *nix are happy to display ansi colors.
// We will terminal sniff on Windows in console_windows.go
var CanColorStdout = true
// CanColorStderr reports if we can color the Stderr
var CanColorStderr = true

View File

@@ -0,0 +1,20 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !windows
package log
import (
"os"
"github.com/mattn/go-isatty"
)
func init() {
// when running gitea as a systemd unit with logging set to console, the output can not be colorized,
// otherwise it spams the journal / syslog with escape sequences like "#033[0m#033[32mcmd/web.go:102:#033[32m"
// this file covers non-windows platforms.
CanColorStdout = isatty.IsTerminal(os.Stdout.Fd())
CanColorStderr = isatty.IsTerminal(os.Stderr.Fd())
}

View File

@@ -0,0 +1,42 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
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 |= 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())
}
}

View File

@@ -0,0 +1,87 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"time"
)
var statusToColor = map[int][]ColorAttribute{
100: {Bold},
200: {FgGreen},
300: {FgYellow},
304: {FgCyan},
400: {Bold, FgRed},
401: {Bold, FgMagenta},
403: {Bold, FgMagenta},
500: {Bold, BgRed},
}
// ColoredStatus adds colors for HTTP status
func ColoredStatus(status int, s ...string) *ColoredValue {
color, ok := statusToColor[status]
if !ok {
color, ok = statusToColor[(status/100)*100]
}
if !ok {
color = []ColorAttribute{Bold}
}
if len(s) > 0 {
return NewColoredValue(s[0], color...)
}
return NewColoredValue(status, color...)
}
var methodToColor = map[string][]ColorAttribute{
"GET": {FgBlue},
"POST": {FgGreen},
"DELETE": {FgRed},
"PATCH": {FgCyan},
"PUT": {FgYellow, Faint},
"HEAD": {FgBlue, Faint},
}
// ColoredMethod adds colors for HTTP methods on log
func ColoredMethod(method string) *ColoredValue {
color, ok := methodToColor[method]
if !ok {
return NewColoredValue(method, Bold)
}
return NewColoredValue(method, color...)
}
var (
durations = []time.Duration{
10 * time.Millisecond,
100 * time.Millisecond,
1 * time.Second,
5 * time.Second,
10 * time.Second,
}
durationColors = [][]ColorAttribute{
{FgGreen},
{Bold},
{FgYellow},
{FgRed, Bold},
{BgRed},
}
wayTooLong = BgMagenta
)
// ColoredTime converts the provided time to a ColoredValue for logging. The duration is always formatted in milliseconds.
func ColoredTime(duration time.Duration) *ColoredValue {
// the output of duration in Millisecond is more readable:
// * before: "100.1ms" "100.1μs" "100.1s"
// * better: "100.1ms" "0.1ms" "100100.0ms", readers can compare the values at first glance.
str := fmt.Sprintf("%.1fms", float64(duration.Microseconds())/1000)
for i, k := range durations {
if duration < k {
return NewColoredValue(str, durationColors[i]...)
}
}
return NewColoredValue(str, wayTooLong)
}

255
modules/log/event_format.go Normal file
View File

@@ -0,0 +1,255 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"bytes"
"fmt"
"strings"
"time"
)
type Event struct {
Time time.Time
Caller string
Filename string
Line int
Level Level
MsgSimpleText string
msgFormat string // the format and args is only valid in the caller's goroutine
msgArgs []any // they are discarded before the event is passed to the writer's channel
Stacktrace string
}
type EventFormatted struct {
Origin *Event
Msg any // the message formatted by the writer's formatter, the writer knows its type
}
type EventFormatter func(mode *WriterMode, event *Event, msgFormat string, msgArgs ...any) []byte
type logStringFormatter struct {
v LogStringer
}
var _ fmt.Formatter = logStringFormatter{}
func (l logStringFormatter) Format(f fmt.State, verb rune) {
if f.Flag('#') && verb == 'v' {
_, _ = fmt.Fprintf(f, "%#v", l.v)
return
}
_, _ = f.Write([]byte(l.v.LogString()))
}
// Copy of cheap integer to fixed-width decimal to ascii from logger.
// TODO: legacy bugs: doesn't support negative number, overflow if wid it too large.
func itoa(buf []byte, i, wid int) []byte {
var s [20]byte
bp := len(s) - 1
for i >= 10 || wid > 1 {
wid--
q := i / 10
s[bp] = byte('0' + i - q*10)
bp--
i = q
}
// i < 10
s[bp] = byte('0' + i)
return append(buf, s[bp:]...)
}
func colorSprintf(colorize bool, format string, args ...any) string {
hasColorValue := false
for _, v := range args {
if _, hasColorValue = v.(*ColoredValue); hasColorValue {
break
}
}
if colorize || !hasColorValue {
return fmt.Sprintf(format, args...)
}
noColors := make([]any, len(args))
copy(noColors, args)
for i, v := range args {
if cv, ok := v.(*ColoredValue); ok {
noColors[i] = cv.v
}
}
return fmt.Sprintf(format, noColors...)
}
// EventFormatTextMessage makes the log message for a writer with its mode. This function is a copy of the original package
func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, msgArgs ...any) []byte {
buf := make([]byte, 0, 1024)
buf = append(buf, mode.Prefix...)
t := event.Time
flags := mode.Flags.Bits()
if flags&(Ldate|Ltime|Lmicroseconds) != 0 {
if mode.Colorize {
buf = append(buf, fgCyanBytes...)
}
if flags&LUTC != 0 {
t = t.UTC()
}
if flags&Ldate != 0 {
year, month, day := t.Date()
buf = itoa(buf, year, 4)
buf = append(buf, '/')
buf = itoa(buf, int(month), 2)
buf = append(buf, '/')
buf = itoa(buf, day, 2)
buf = append(buf, ' ')
}
if flags&(Ltime|Lmicroseconds) != 0 {
hour, minNum, sec := t.Clock()
buf = itoa(buf, hour, 2)
buf = append(buf, ':')
buf = itoa(buf, minNum, 2)
buf = append(buf, ':')
buf = itoa(buf, sec, 2)
if flags&Lmicroseconds != 0 {
buf = append(buf, '.')
buf = itoa(buf, t.Nanosecond()/1e3, 6)
}
buf = append(buf, ' ')
}
if mode.Colorize {
buf = append(buf, resetBytes...)
}
}
if flags&(Lshortfile|Llongfile) != 0 && event.Filename != "" {
if mode.Colorize {
buf = append(buf, fgGreenBytes...)
}
file := event.Filename
if flags&Lmedfile == Lmedfile {
fileLen := len(file)
const softLimit = 20
if fileLen > softLimit {
slashIndex := strings.LastIndexByte(file[:fileLen-softLimit], '/')
if slashIndex != -1 {
file = ".../" + file[slashIndex+1:]
}
}
} else if flags&Lshortfile != 0 {
startIndex := strings.LastIndexByte(file, '/')
if startIndex > 0 && startIndex < len(file) {
file = file[startIndex+1:]
}
}
buf = append(buf, file...)
buf = append(buf, ':')
buf = itoa(buf, event.Line, -1)
if flags&(Lfuncname|Lshortfuncname) != 0 {
buf = append(buf, ':')
} else {
if mode.Colorize {
buf = append(buf, resetBytes...)
}
buf = append(buf, ' ')
}
}
if flags&(Lfuncname|Lshortfuncname) != 0 {
if mode.Colorize {
buf = append(buf, fgGreenBytes...)
}
funcName := event.Caller
shortFuncName := funcName
if flags&Lshortfuncname != 0 {
// funcName = "code.gitea.io/gitea/modules/foo/bar.MyFunc.func1.2()"
slashPos := strings.LastIndexByte(funcName, '/')
dotPos := strings.IndexByte(funcName[slashPos+1:], '.')
if dotPos > 0 {
// shortFuncName = "MyFunc.func1.2()"
shortFuncName = funcName[slashPos+1+dotPos+1:]
if strings.Contains(shortFuncName, ".") {
shortFuncName = strings.ReplaceAll(shortFuncName, ".func", ".")
}
}
funcName = shortFuncName
}
buf = append(buf, funcName...)
if mode.Colorize {
buf = append(buf, resetBytes...)
}
buf = append(buf, ' ')
}
if flags&(Llevel|Llevelinitial) != 0 {
level := strings.ToUpper(event.Level.String())
if mode.Colorize {
buf = append(buf, ColorBytes(levelToColor[event.Level]...)...)
}
buf = append(buf, '[')
if flags&Llevelinitial != 0 {
buf = append(buf, level[0])
} else {
buf = append(buf, level...)
}
buf = append(buf, ']')
if mode.Colorize {
buf = append(buf, resetBytes...)
}
buf = append(buf, ' ')
}
var msg []byte
// if the log needs colorizing, do it
if mode.Colorize && len(msgArgs) > 0 {
hasColorValue := false
for _, v := range msgArgs {
if _, hasColorValue = v.(*ColoredValue); hasColorValue {
break
}
}
if hasColorValue {
msg = fmt.Appendf(nil, msgFormat, msgArgs...)
}
}
// try to re-use the pre-formatted simple text message
if len(msg) == 0 {
msg = []byte(event.MsgSimpleText)
}
// if still no message, do the normal Sprintf for the message
if len(msg) == 0 {
msg = []byte(colorSprintf(mode.Colorize, msgFormat, msgArgs...))
}
// remove at most one trailing new line
if len(msg) > 0 && msg[len(msg)-1] == '\n' {
msg = msg[:len(msg)-1]
}
if flags&Lgopid == Lgopid {
deprecatedGoroutinePid := "no-gopid" // use a dummy value to avoid breaking the log format
buf = append(buf, '[')
if mode.Colorize {
buf = append(buf, ColorBytes(FgHiYellow)...)
}
buf = append(buf, deprecatedGoroutinePid...)
if mode.Colorize {
buf = append(buf, resetBytes...)
}
buf = append(buf, ']', ' ')
}
buf = append(buf, msg...)
if event.Stacktrace != "" && mode.StacktraceLevel <= event.Level {
lines := bytes.SplitSeq([]byte(event.Stacktrace), []byte("\n"))
for line := range lines {
buf = append(buf, "\n\t"...)
buf = append(buf, line...)
}
buf = append(buf, '\n')
}
buf = append(buf, '\n')
return buf
}

View File

@@ -0,0 +1,55 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestItoa(t *testing.T) {
b := itoa(nil, 0, 0)
assert.Equal(t, "0", string(b))
b = itoa(nil, 0, 1)
assert.Equal(t, "0", string(b))
b = itoa(nil, 0, 2)
assert.Equal(t, "00", string(b))
}
func TestEventFormatTextMessage(t *testing.T) {
res := EventFormatTextMessage(&WriterMode{Prefix: "[PREFIX] ", Colorize: false, Flags: Flags{defined: true, flags: 0xffffffff}},
&Event{
Time: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
Caller: "caller",
Filename: "filename",
Line: 123,
Level: ERROR,
Stacktrace: "stacktrace",
},
"msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue),
)
assert.Equal(t, `[PREFIX] 2020/01/02 03:04:05.000000 filename:123:caller [E] [no-gopid] msg format: arg0 arg1
stacktrace
`, string(res))
res = EventFormatTextMessage(&WriterMode{Prefix: "[PREFIX] ", Colorize: true, Flags: Flags{defined: true, flags: 0xffffffff}},
&Event{
Time: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
Caller: "caller",
Filename: "filename",
Line: 123,
Level: ERROR,
Stacktrace: "stacktrace",
},
"msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue),
)
assert.Equal(t, "[PREFIX] \x1b[36m2020/01/02 03:04:05.000000 \x1b[0m\x1b[32mfilename:123:\x1b[32mcaller\x1b[0m \x1b[1;31m[E]\x1b[0m [\x1b[93mno-gopid\x1b[0m] msg format: arg0 \x1b[34marg1\x1b[0m\n\tstacktrace\n\n", string(res))
}

View File

@@ -0,0 +1,54 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
)
// EventWriter is the general interface for all event writers
// EventWriterBase is only used as its base interface
// A writer implementation could override the default EventWriterBase functions
// eg: a writer can override the Run to handle events in its own way with its own goroutine
type EventWriter interface {
EventWriterBase
}
// WriterMode is the mode for creating a new EventWriter, it contains common options for all writers
// Its WriterOption field is the specified options for a writer, it should be passed by value but not by pointer
type WriterMode struct {
BufferLen int
Level Level
Prefix string
Colorize bool
Flags Flags
Expression string
StacktraceLevel Level
WriterOption any
}
// EventWriterProvider is the function for creating a new EventWriter
type EventWriterProvider func(writerName string, writerMode WriterMode) EventWriter
var eventWriterProviders = map[string]EventWriterProvider{}
func RegisterEventWriter(writerType string, p EventWriterProvider) {
eventWriterProviders[writerType] = p
}
func HasEventWriter(writerType string) bool {
_, ok := eventWriterProviders[writerType]
return ok
}
func NewEventWriter(name, writerType string, mode WriterMode) (EventWriter, error) {
if p, ok := eventWriterProviders[writerType]; ok {
return p(name, mode), nil
}
return nil, fmt.Errorf("unknown event writer type %q for writer %q", writerType, name)
}

View File

@@ -0,0 +1,169 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"context"
"fmt"
"io"
"regexp"
"runtime/pprof"
"time"
)
// EventWriterBase is the base interface for most event writers
// It provides default implementations for most methods
type EventWriterBase interface {
Base() *EventWriterBaseImpl
GetWriterType() string
GetWriterName() string
GetLevel() Level
Run(ctx context.Context)
}
type EventWriterBaseImpl struct {
writerType string
Name string
Mode *WriterMode
Queue chan *EventFormatted
FormatMessage EventFormatter // format the Event to a message and write it to output
OutputWriteCloser io.WriteCloser // it will be closed when the event writer is stopped
GetPauseChan func() chan struct{}
shared bool
stopped chan struct{}
}
var _ EventWriterBase = (*EventWriterBaseImpl)(nil)
func (b *EventWriterBaseImpl) Base() *EventWriterBaseImpl {
return b
}
func (b *EventWriterBaseImpl) GetWriterType() string {
return b.writerType
}
func (b *EventWriterBaseImpl) GetWriterName() string {
return b.Name
}
func (b *EventWriterBaseImpl) GetLevel() Level {
return b.Mode.Level
}
// Run is the default implementation for EventWriter.Run
func (b *EventWriterBaseImpl) Run(ctx context.Context) {
defer b.OutputWriteCloser.Close()
var exprRegexp *regexp.Regexp
if b.Mode.Expression != "" {
var err error
if exprRegexp, err = regexp.Compile(b.Mode.Expression); err != nil {
FallbackErrorf("unable to compile expression %q for writer %q: %v", b.Mode.Expression, b.Name, err)
}
}
handlePaused := func() {
if pause := b.GetPauseChan(); pause != nil {
select {
case <-pause:
case <-ctx.Done():
}
}
}
for {
select {
case <-ctx.Done():
return
case event, ok := <-b.Queue:
if !ok {
return
}
handlePaused()
if exprRegexp != nil {
fileLineCaller := fmt.Sprintf("%s:%d:%s", event.Origin.Filename, event.Origin.Line, event.Origin.Caller)
matched := exprRegexp.MatchString(fileLineCaller) || exprRegexp.MatchString(event.Origin.MsgSimpleText)
if !matched {
continue
}
}
var err error
switch msg := event.Msg.(type) {
case string:
_, err = b.OutputWriteCloser.Write([]byte(msg))
case []byte:
_, err = b.OutputWriteCloser.Write(msg)
case io.WriterTo:
_, err = msg.WriteTo(b.OutputWriteCloser)
default:
_, err = fmt.Fprint(b.OutputWriteCloser, msg)
}
if err != nil {
FallbackErrorf("unable to write log message of %q (%v): %v", b.Name, err, event.Msg)
}
}
}
}
func NewEventWriterBase(name, writerType string, mode WriterMode) *EventWriterBaseImpl {
if mode.BufferLen == 0 {
mode.BufferLen = 1000
}
if mode.Level == UNDEFINED {
mode.Level = INFO
}
if mode.StacktraceLevel == UNDEFINED {
mode.StacktraceLevel = NONE
}
b := &EventWriterBaseImpl{
writerType: writerType,
Name: name,
Mode: &mode,
Queue: make(chan *EventFormatted, mode.BufferLen),
GetPauseChan: GetManager().GetPauseChan, // by default, use the global pause channel
FormatMessage: EventFormatTextMessage,
}
return b
}
// eventWriterStartGo use "go" to start an event worker's Run method
func eventWriterStartGo(ctx context.Context, w EventWriter, shared bool) {
if w.Base().stopped != nil {
return // already started
}
w.Base().shared = shared
w.Base().stopped = make(chan struct{})
ctxDesc := "Logger: EventWriter: " + w.GetWriterName()
if shared {
ctxDesc = "Logger: EventWriter (shared): " + w.GetWriterName()
}
writerCtx, writerCancel := newProcessTypedContext(ctx, ctxDesc)
go func() {
defer writerCancel()
defer close(w.Base().stopped)
pprof.SetGoroutineLabels(writerCtx)
w.Run(writerCtx)
}()
}
// eventWriterStopWait stops an event writer and waits for it to finish flushing (with a timeout)
func eventWriterStopWait(w EventWriter) {
close(w.Base().Queue)
select {
case <-w.Base().stopped:
case <-time.After(2 * time.Second):
FallbackErrorf("unable to stop log writer %q in time, skip", w.GetWriterName())
}
}

View File

@@ -0,0 +1,111 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"io"
"net"
)
type WriterConnOption struct {
Addr string
Protocol string
Reconnect bool
ReconnectOnMsg bool
}
type eventWriterConn struct {
*EventWriterBaseImpl
connWriter connWriter
}
var _ EventWriter = (*eventWriterConn)(nil)
func NewEventWriterConn(writerName string, writerMode WriterMode) EventWriter {
w := &eventWriterConn{EventWriterBaseImpl: NewEventWriterBase(writerName, "conn", writerMode)}
opt := writerMode.WriterOption.(WriterConnOption)
w.connWriter = connWriter{
ReconnectOnMsg: opt.ReconnectOnMsg,
Reconnect: opt.Reconnect,
Net: opt.Protocol,
Addr: opt.Addr,
}
w.OutputWriteCloser = &w.connWriter
return w
}
func init() {
RegisterEventWriter("conn", NewEventWriterConn)
}
// below is copied from old code
type connWriter struct {
innerWriter io.WriteCloser
ReconnectOnMsg bool
Reconnect bool
Net string `json:"net"`
Addr string `json:"addr"`
}
var _ io.WriteCloser = (*connWriter)(nil)
// Close the inner writer
func (i *connWriter) Close() error {
if i.innerWriter != nil {
return i.innerWriter.Close()
}
return nil
}
// Write the data to the connection
func (i *connWriter) Write(p []byte) (int, error) {
if i.neededConnectOnMsg() {
if err := i.connect(); err != nil {
return 0, err
}
}
if i.ReconnectOnMsg {
defer i.innerWriter.Close()
}
return i.innerWriter.Write(p)
}
func (i *connWriter) neededConnectOnMsg() bool {
if i.Reconnect {
i.Reconnect = false
return true
}
if i.innerWriter == nil {
return true
}
return i.ReconnectOnMsg
}
func (i *connWriter) connect() error {
if i.innerWriter != nil {
_ = i.innerWriter.Close()
i.innerWriter = nil
}
conn, err := net.Dial(i.Net, i.Addr)
if err != nil {
return err
}
if tcpConn, ok := conn.(*net.TCPConn); ok {
err = tcpConn.SetKeepAlive(true)
if err != nil {
return err
}
}
i.innerWriter = conn
return nil
}

View File

@@ -0,0 +1,74 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"io"
"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 := io.ReadAll(conn)
assert.NoError(t, err)
assert.Equal(t, expected, string(written))
}
func TestConnLogger(t *testing.T) {
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 := NewLoggerWithWriters(t.Context(), "test", NewEventWriterConn("test-conn", WriterMode{
Level: level,
Prefix: prefix,
Flags: FlagsFromBits(flags),
WriterOption: WriterConnOption{Addr: address, Protocol: protocol, Reconnect: true, ReconnectOnMsg: true},
}))
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,
MsgSimpleText: "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.MsgSimpleText)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
listenReadAndClose(t, l, expected)
}()
logger.SendLogEvent(&event)
wg.Wait()
logger.Close()
}

View File

@@ -0,0 +1,35 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"os"
"code.gitea.io/gitea/modules/util"
)
type WriterConsoleOption struct {
Stderr bool
}
type eventWriterConsole struct {
*EventWriterBaseImpl
}
var _ EventWriter = (*eventWriterConsole)(nil)
func NewEventWriterConsole(name string, mode WriterMode) EventWriter {
w := &eventWriterConsole{EventWriterBaseImpl: NewEventWriterBase(name, "console", mode)}
opt := mode.WriterOption.(WriterConsoleOption)
if opt.Stderr {
w.OutputWriteCloser = util.NopCloser{Writer: os.Stderr}
} else {
w.OutputWriteCloser = util.NopCloser{Writer: os.Stdout}
}
return w
}
func init() {
RegisterEventWriter("console", NewEventWriterConsole)
}

View File

@@ -0,0 +1,54 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"io"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/util/rotatingfilewriter"
)
type WriterFileOption struct {
FileName string
MaxSize int64
LogRotate bool
DailyRotate bool
MaxDays int
Compress bool
CompressionLevel int
}
type eventWriterFile struct {
*EventWriterBaseImpl
fileWriter io.WriteCloser
}
var _ EventWriter = (*eventWriterFile)(nil)
func NewEventWriterFile(name string, mode WriterMode) EventWriter {
w := &eventWriterFile{EventWriterBaseImpl: NewEventWriterBase(name, "file", mode)}
opt := mode.WriterOption.(WriterFileOption)
var err error
w.fileWriter, err = rotatingfilewriter.Open(opt.FileName, &rotatingfilewriter.Options{
Rotate: opt.LogRotate,
MaximumSize: opt.MaxSize,
RotateDaily: opt.DailyRotate,
KeepDays: opt.MaxDays,
Compress: opt.Compress,
CompressionLevel: opt.CompressionLevel,
})
if err != nil {
// if the log file can't be opened, what should it do? panic/exit? ignore logs? fallback to stderr?
// it seems that "fallback to stderr" is slightly better than others ....
FallbackErrorf("unable to open log file %q: %v", opt.FileName, err)
w.fileWriter = util.NopCloser{Writer: LoggerToWriter(FallbackErrorf)}
}
w.OutputWriteCloser = w.fileWriter
return w
}
func init() {
RegisterEventWriter("file", NewEventWriterFile)
}

134
modules/log/flags.go Normal file
View File

@@ -0,0 +1,134 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"sort"
"strings"
"code.gitea.io/gitea/modules/json"
)
// 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/logger/c/d.go:23:runtime.Caller() [I]: message
const (
Ldate uint32 = 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/logger/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]
Lgopid // the Goroutine-PID of the context, deprecated and it is always a const value
Lmedfile = Lshortfile | Llongfile // last 20 characters of the filename
LstdFlags = Ldate | Ltime | Lmedfile | Lshortfuncname | Llevelinitial // default
)
const Ldefault = LstdFlags
type Flags struct {
defined bool
flags uint32
}
var flagFromString = map[string]uint32{
"date": Ldate,
"time": Ltime,
"microseconds": Lmicroseconds,
"longfile": Llongfile,
"shortfile": Lshortfile,
"funcname": Lfuncname,
"shortfuncname": Lshortfuncname,
"utc": LUTC,
"levelinitial": Llevelinitial,
"level": Llevel,
"gopid": Lgopid,
"medfile": Lmedfile,
"stdflags": LstdFlags,
}
var flagComboToString = []struct {
flag uint32
name string
}{
// name with more bits comes first
{LstdFlags, "stdflags"},
{Lmedfile, "medfile"},
{Ldate, "date"},
{Ltime, "time"},
{Lmicroseconds, "microseconds"},
{Llongfile, "longfile"},
{Lshortfile, "shortfile"},
{Lfuncname, "funcname"},
{Lshortfuncname, "shortfuncname"},
{LUTC, "utc"},
{Llevelinitial, "levelinitial"},
{Llevel, "level"},
{Lgopid, "gopid"},
}
func (f Flags) Bits() uint32 {
if !f.defined {
return Ldefault
}
return f.flags
}
func (f Flags) String() string {
flags := f.Bits()
var flagNames []string
for _, it := range flagComboToString {
if flags&it.flag == it.flag {
flags &^= it.flag
flagNames = append(flagNames, it.name)
}
}
if len(flagNames) == 0 {
return "none"
}
sort.Strings(flagNames)
return strings.Join(flagNames, ",")
}
func (f *Flags) UnmarshalJSON(bytes []byte) error {
var s string
if err := json.Unmarshal(bytes, &s); err != nil {
return err
}
*f = FlagsFromString(s)
return nil
}
func (f Flags) MarshalJSON() ([]byte, error) {
return []byte(`"` + f.String() + `"`), nil
}
func FlagsFromString(from string, def ...uint32) Flags {
from = strings.TrimSpace(from)
if from == "" && len(def) > 0 {
return Flags{defined: true, flags: def[0]}
}
flags := uint32(0)
for flag := range strings.SplitSeq(strings.ToLower(from), ",") {
flags |= flagFromString[strings.TrimSpace(flag)]
}
return Flags{defined: true, flags: flags}
}
func FlagsFromBits(flags uint32) Flags {
return Flags{defined: true, flags: flags}
}

30
modules/log/flags_test.go Normal file
View File

@@ -0,0 +1,30 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"testing"
"code.gitea.io/gitea/modules/json"
"github.com/stretchr/testify/assert"
)
func TestFlags(t *testing.T) {
assert.Equal(t, Ldefault, Flags{}.Bits())
assert.EqualValues(t, 0, FlagsFromString("").Bits())
assert.Equal(t, Lgopid, FlagsFromString("", Lgopid).Bits())
assert.EqualValues(t, 0, FlagsFromString("none", Lgopid).Bits())
assert.Equal(t, Ldate|Ltime, FlagsFromString("date,time", Lgopid).Bits())
assert.Equal(t, "stdflags", FlagsFromString("stdflags").String())
assert.Equal(t, "medfile", FlagsFromString("medfile").String())
bs, err := json.Marshal(FlagsFromString("utc,level"))
assert.NoError(t, err)
assert.Equal(t, `"level,utc"`, string(bs))
var flags Flags
assert.NoError(t, json.Unmarshal(bs, &flags))
assert.Equal(t, LUTC|Llevel, flags.Bits())
}

44
modules/log/init.go Normal file
View File

@@ -0,0 +1,44 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"context"
"runtime"
"strings"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/util/rotatingfilewriter"
)
var projectPackagePrefix string
func init() {
_, filename, _, _ := runtime.Caller(0)
projectPackagePrefix = strings.TrimSuffix(filename, "modules/log/init.go")
if projectPackagePrefix == filename {
// in case the source code file is moved, we can not trim the suffix, the code above should also be updated.
panic("unable to detect correct package prefix, please update file: " + filename)
}
rotatingfilewriter.ErrorPrintf = FallbackErrorf
process.TraceCallback = func(skip int, start bool, pid process.IDType, description string, parentPID process.IDType, typ string) {
if start && parentPID != "" {
Log(skip+1, TRACE, "Start %s: %s (from %s) (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(parentPID, FgYellow), NewColoredValue(typ, Reset))
} else if start {
Log(skip+1, TRACE, "Start %s: %s (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(typ, Reset))
} else {
Log(skip+1, TRACE, "Done %s: %s", NewColoredValue(pid, FgHiYellow), NewColoredValue(description, Reset))
}
}
}
func newProcessTypedContext(parent context.Context, desc string) (ctx context.Context, cancel context.CancelFunc) {
// the "process manager" also calls "log.Trace()" to output logs, so if we want to create new contexts by the manager, we need to disable the trace temporarily
process.TraceLogDisable(true)
defer process.TraceLogDisable(false)
ctx, _, cancel = process.GetManager().AddTypedContext(parent, desc, process.SystemProcessType, false)
return ctx, cancel
}

116
modules/log/level.go Normal file
View File

@@ -0,0 +1,116 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"bytes"
"strings"
"code.gitea.io/gitea/modules/json"
)
// Level is the level of the logger
type Level int
const (
UNDEFINED Level = iota
TRACE
DEBUG
INFO
WARN
ERROR
FATAL
NONE
)
const CRITICAL = ERROR // most logger frameworks doesn't support CRITICAL, and it doesn't seem useful
var toString = map[Level]string{
UNDEFINED: "undefined",
TRACE: "trace",
DEBUG: "debug",
INFO: "info",
WARN: "warn",
ERROR: "error",
FATAL: "fatal",
NONE: "none",
}
var toLevel = map[string]Level{
"undefined": UNDEFINED,
"trace": TRACE,
"debug": DEBUG,
"info": INFO,
"warn": WARN,
"warning": WARN,
"error": ERROR,
"fatal": FATAL,
"none": NONE,
}
var levelToColor = map[Level][]ColorAttribute{
TRACE: {Bold, FgCyan},
DEBUG: {Bold, FgBlue},
INFO: {Bold, FgGreen},
WARN: {Bold, FgYellow},
ERROR: {Bold, FgRed},
FATAL: {Bold, BgRed},
NONE: {Reset},
}
func (l Level) String() string {
s, ok := toString[l]
if ok {
return s
}
return "info"
}
func (l Level) ColorAttributes() []ColorAttribute {
color, ok := levelToColor[l]
if ok {
return color
}
none := levelToColor[NONE]
return none
}
// 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
}
// UnmarshalJSON takes text and turns it into a Level
func (l *Level) UnmarshalJSON(b []byte) error {
var tmp any
err := json.Unmarshal(b, &tmp)
if err != nil {
return err
}
switch v := tmp.(type) {
case string:
*l = LevelFromString(v)
case int:
*l = LevelFromString(Level(v).String())
default:
*l = INFO
}
return nil
}
// LevelFromString takes a level string and returns a Level
func LevelFromString(level string) Level {
if l, ok := toLevel[strings.ToLower(level)]; ok {
return l
}
return INFO
}

55
modules/log/level_test.go Normal file
View File

@@ -0,0 +1,55 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"testing"
"code.gitea.io/gitea/modules/json"
"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(fmt.Appendf(nil, `{"level":%d}`, 2), &testLevel)
assert.NoError(t, err)
assert.Equal(t, INFO, testLevel.Level)
err = json.Unmarshal(fmt.Appendf(nil, `{"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 fmt.Appendf(nil, `{"level":"%s"}`, level)
}

50
modules/log/logger.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package log provides logging capabilities for Gitea.
// Concepts:
//
// * Logger: a Logger provides logging functions and dispatches log events to all its writers
//
// * EventWriter: written log Event to a destination (eg: file, console)
// - EventWriterBase: the base struct of a writer, it contains common fields and functions for all writers
// - WriterType: the type name of a writer, eg: console, file
// - WriterName: aka Mode Name in document, the name of a writer instance, it's usually defined by the config file.
// It is called "mode name" because old code use MODE as config key, to keep compatibility, keep this concept.
//
// * WriterMode: the common options for all writers, eg: log level.
// - WriterConsoleOption and others: the specified options for a writer, eg: file path, remote address.
//
// Call graph:
// -> log.Info()
// -> LoggerImpl.Log()
// -> LoggerImpl.SendLogEvent, then the event goes into writer's goroutines
// -> EventWriter.Run() handles the events
package log
// BaseLogger provides the basic logging functions
type BaseLogger interface {
Log(skip int, event *Event, format string, v ...any)
GetLevel() Level
}
// LevelLogger provides level-related logging functions
type LevelLogger interface {
LevelEnabled(level Level) bool
Trace(format string, v ...any)
Debug(format string, v ...any)
Info(format string, v ...any)
Warn(format string, v ...any)
Error(format string, v ...any)
Critical(format string, v ...any)
}
type Logger interface {
BaseLogger
LevelLogger
}
type LogStringer interface { //nolint:revive // export stutter
LogString() string
}

View File

@@ -0,0 +1,85 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"os"
)
// FallbackErrorf is the last chance to show an error if the logger has internal errors
func FallbackErrorf(format string, args ...any) {
_, _ = fmt.Fprintf(os.Stderr, format+"\n", args...)
}
func GetLevel() Level {
return GetLogger(DEFAULT).GetLevel()
}
func Log(skip int, level Level, format string, v ...any) {
GetLogger(DEFAULT).Log(skip+1, &Event{Level: level}, format, v...)
}
func Trace(format string, v ...any) {
Log(1, TRACE, format, v...)
}
func IsTrace() bool {
return GetLevel() <= TRACE
}
func Debug(format string, v ...any) {
Log(1, DEBUG, format, v...)
}
func IsDebug() bool {
return GetLevel() <= DEBUG
}
func Info(format string, v ...any) {
Log(1, INFO, format, v...)
}
func Warn(format string, v ...any) {
Log(1, WARN, format, v...)
}
func Error(format string, v ...any) {
Log(1, ERROR, format, v...)
}
func ErrorWithSkip(skip int, format string, v ...any) {
Log(skip+1, ERROR, format, v...)
}
func Critical(format string, v ...any) {
Log(1, ERROR, format, v...)
}
var OsExiter = os.Exit
// Fatal records fatal log and exit process
func Fatal(format string, v ...any) {
Log(1, FATAL, format, v...)
GetManager().Close()
OsExiter(1)
}
func GetLogger(name string) Logger {
return GetManager().GetLogger(name)
}
func IsLoggerEnabled(name string) bool {
return GetManager().GetLogger(name).IsEnabled()
}
func SetConsoleLogger(loggerName, writerName string, level Level) {
writer := NewEventWriterConsole(writerName, WriterMode{
Level: level,
Flags: FlagsFromBits(LstdFlags),
Colorize: CanColorStdout,
WriterOption: WriterConsoleOption{},
})
GetManager().GetLogger(loggerName).ReplaceAllWriters(writer)
}

249
modules/log/logger_impl.go Normal file
View File

@@ -0,0 +1,249 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"context"
"reflect"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"
)
type LoggerImpl struct {
LevelLogger
ctx context.Context
ctxCancel context.CancelFunc
level atomic.Int32
stacktraceLevel atomic.Int32
eventWriterMu sync.RWMutex
eventWriters map[string]EventWriter
}
var (
_ BaseLogger = (*LoggerImpl)(nil)
_ LevelLogger = (*LoggerImpl)(nil)
)
// SendLogEvent sends a log event to all writers
func (l *LoggerImpl) SendLogEvent(event *Event) {
l.eventWriterMu.RLock()
defer l.eventWriterMu.RUnlock()
if len(l.eventWriters) == 0 {
FallbackErrorf("[no logger writer]: %s", event.MsgSimpleText)
return
}
// the writers have their own goroutines, the message arguments (with Stringer) shouldn't be used in other goroutines
// so the event message must be formatted here
msgFormat, msgArgs := event.msgFormat, event.msgArgs
event.msgFormat, event.msgArgs = "(already processed by formatters)", nil
for _, w := range l.eventWriters {
if event.Level < w.GetLevel() {
continue
}
formatted := &EventFormatted{
Origin: event,
Msg: w.Base().FormatMessage(w.Base().Mode, event, msgFormat, msgArgs...),
}
select {
case w.Base().Queue <- formatted:
default:
bs, _ := json.Marshal(event)
FallbackErrorf("log writer %q queue is full, event: %v", w.GetWriterName(), string(bs))
}
}
}
// syncLevelInternal syncs the level of the logger with the levels of the writers
func (l *LoggerImpl) syncLevelInternal() {
lowestLevel := NONE
for _, w := range l.eventWriters {
if w.GetLevel() < lowestLevel {
lowestLevel = w.GetLevel()
}
}
l.level.Store(int32(lowestLevel))
lowestLevel = NONE
for _, w := range l.eventWriters {
if w.Base().Mode.StacktraceLevel < lowestLevel {
lowestLevel = w.GetLevel()
}
}
l.stacktraceLevel.Store(int32(lowestLevel))
}
// removeWriterInternal removes a writer from the logger, and stops it if it's not shared
func (l *LoggerImpl) removeWriterInternal(w EventWriter) {
if !w.Base().shared {
eventWriterStopWait(w) // only stop non-shared writers, shared writers are managed by the manager
}
delete(l.eventWriters, w.GetWriterName())
}
// AddWriters adds writers to the logger, and starts them. Existing writers will be replaced by new ones.
func (l *LoggerImpl) AddWriters(writer ...EventWriter) {
l.eventWriterMu.Lock()
defer l.eventWriterMu.Unlock()
l.addWritersInternal(writer...)
}
func (l *LoggerImpl) addWritersInternal(writer ...EventWriter) {
for _, w := range writer {
if old, ok := l.eventWriters[w.GetWriterName()]; ok {
l.removeWriterInternal(old)
}
}
for _, w := range writer {
l.eventWriters[w.GetWriterName()] = w
eventWriterStartGo(l.ctx, w, false)
}
l.syncLevelInternal()
}
// RemoveWriter removes a writer from the logger, and the writer is closed and flushed if it is not shared
func (l *LoggerImpl) RemoveWriter(modeName string) error {
l.eventWriterMu.Lock()
defer l.eventWriterMu.Unlock()
w, ok := l.eventWriters[modeName]
if !ok {
return util.ErrNotExist
}
l.removeWriterInternal(w)
l.syncLevelInternal()
return nil
}
// ReplaceAllWriters replaces all writers from the logger, non-shared writers are closed and flushed
func (l *LoggerImpl) ReplaceAllWriters(writer ...EventWriter) {
l.eventWriterMu.Lock()
defer l.eventWriterMu.Unlock()
for _, w := range l.eventWriters {
l.removeWriterInternal(w)
}
l.eventWriters = map[string]EventWriter{}
l.addWritersInternal(writer...)
}
// DumpWriters dumps the writers as a JSON map, it's used for debugging and display purposes.
func (l *LoggerImpl) DumpWriters() map[string]any {
l.eventWriterMu.RLock()
defer l.eventWriterMu.RUnlock()
writers := make(map[string]any, len(l.eventWriters))
for k, w := range l.eventWriters {
bs, err := json.Marshal(w.Base().Mode)
if err != nil {
FallbackErrorf("marshal writer %q to dump failed: %v", k, err)
continue
}
m := map[string]any{}
_ = json.Unmarshal(bs, &m)
m["WriterType"] = w.GetWriterType()
writers[k] = m
}
return writers
}
// Close closes the logger, non-shared writers are closed and flushed
func (l *LoggerImpl) Close() {
l.ReplaceAllWriters()
l.ctxCancel()
}
// IsEnabled returns true if the logger is enabled: it has a working level and has writers
// Fatal is not considered as enabled, because it's a special case and the process just exits
func (l *LoggerImpl) IsEnabled() bool {
l.eventWriterMu.RLock()
defer l.eventWriterMu.RUnlock()
return l.level.Load() < int32(FATAL) && len(l.eventWriters) > 0
}
func asLogStringer(v any) LogStringer {
if s, ok := v.(LogStringer); ok {
return s
} else if a := reflect.ValueOf(v); a.Kind() == reflect.Struct {
// in case the receiver is a pointer, but the value is a struct
vp := reflect.New(a.Type())
vp.Elem().Set(a)
if s, ok := vp.Interface().(LogStringer); ok {
return s
}
}
return nil
}
// Log prepares the log event, if the level matches, the event will be sent to the writers
func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any) {
if Level(l.level.Load()) > event.Level {
return
}
if event.Time.IsZero() {
event.Time = time.Now()
}
if event.Caller == "" {
pc, filename, line, ok := runtime.Caller(skip + 1)
if ok {
fn := runtime.FuncForPC(pc)
if fn != nil {
fnName := fn.Name()
event.Caller = strings.ReplaceAll(fnName, "[...]", "") + "()" // generic function names are "foo[...]"
}
}
event.Filename, event.Line = strings.TrimPrefix(filename, projectPackagePrefix), line
if l.stacktraceLevel.Load() <= int32(event.Level) {
event.Stacktrace = Stack(skip + 1)
}
}
// get a simple text message without color
msgArgs := make([]any, len(logArgs))
copy(msgArgs, logArgs)
// handle LogStringer values
for i, v := range msgArgs {
if cv, ok := v.(*ColoredValue); ok {
if ls := asLogStringer(cv.v); ls != nil {
cv.v = logStringFormatter{v: ls}
}
} else if ls := asLogStringer(v); ls != nil {
msgArgs[i] = logStringFormatter{v: ls}
}
}
event.MsgSimpleText = colorSprintf(false, format, msgArgs...)
event.msgFormat = format
event.msgArgs = msgArgs
l.SendLogEvent(event)
}
func (l *LoggerImpl) GetLevel() Level {
return Level(l.level.Load())
}
func NewLoggerWithWriters(ctx context.Context, name string, writer ...EventWriter) *LoggerImpl {
l := &LoggerImpl{}
l.ctx, l.ctxCancel = newProcessTypedContext(ctx, "Logger: "+name)
l.LevelLogger = BaseLoggerToGeneralLogger(l)
l.eventWriters = map[string]EventWriter{}
l.AddWriters(writer...)
return l
}

179
modules/log/logger_test.go Normal file
View File

@@ -0,0 +1,179 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"regexp"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
type dummyWriter struct {
*EventWriterBaseImpl
delay time.Duration
mu sync.Mutex
logs []string
}
func (d *dummyWriter) Write(p []byte) (n int, err error) {
if d.delay > 0 {
time.Sleep(d.delay)
}
d.mu.Lock()
defer d.mu.Unlock()
d.logs = append(d.logs, string(p))
return len(p), nil
}
func (d *dummyWriter) Close() error {
return nil
}
func (d *dummyWriter) FetchLogs() []string {
d.mu.Lock()
defer d.mu.Unlock()
logs := d.logs
d.logs = nil
return logs
}
func newDummyWriter(name string, level Level, delay time.Duration) *dummyWriter {
w := &dummyWriter{
EventWriterBaseImpl: NewEventWriterBase(name, "dummy", WriterMode{Level: level, Flags: FlagsFromBits(0)}),
}
w.delay = delay
w.Base().OutputWriteCloser = w
return w
}
func TestLogger(t *testing.T) {
logger := NewLoggerWithWriters(t.Context(), "test")
dump := logger.DumpWriters()
assert.Empty(t, dump)
assert.Equal(t, NONE, logger.GetLevel())
assert.False(t, logger.IsEnabled())
w1 := newDummyWriter("dummy-1", DEBUG, 0)
logger.AddWriters(w1)
assert.Equal(t, DEBUG, logger.GetLevel())
w2 := newDummyWriter("dummy-2", WARN, 200*time.Millisecond)
logger.AddWriters(w2)
assert.Equal(t, DEBUG, logger.GetLevel())
dump = logger.DumpWriters()
assert.Len(t, dump, 2)
logger.Trace("trace-level") // this level is not logged
logger.Debug("debug-level")
logger.Error("error-level")
// w2 is slow, so only w1 has logs
time.Sleep(100 * time.Millisecond)
assert.Equal(t, []string{"debug-level\n", "error-level\n"}, w1.FetchLogs())
assert.Empty(t, w2.FetchLogs())
logger.Close()
// after Close, all logs are flushed
assert.Empty(t, w1.FetchLogs())
assert.Equal(t, []string{"error-level\n"}, w2.FetchLogs())
}
func TestLoggerPause(t *testing.T) {
logger := NewLoggerWithWriters(t.Context(), "test")
w1 := newDummyWriter("dummy-1", DEBUG, 0)
logger.AddWriters(w1)
GetManager().PauseAll()
time.Sleep(50 * time.Millisecond)
logger.Info("info-level")
time.Sleep(100 * time.Millisecond)
assert.Empty(t, w1.FetchLogs())
GetManager().ResumeAll()
time.Sleep(100 * time.Millisecond)
assert.Equal(t, []string{"info-level\n"}, w1.FetchLogs())
logger.Close()
}
type testLogString struct {
Field string
}
func (t testLogString) LogString() string {
return "log-string"
}
type testLogStringPtrReceiver struct {
Field string
}
func (t *testLogStringPtrReceiver) LogString() string {
return "log-string-ptr-receiver"
}
func genericFunc[T any](logger Logger, v T) {
logger.Info("from genericFunc: %v", v)
}
func TestLoggerOutput(t *testing.T) {
t.Run("LogString", func(t *testing.T) {
logger := NewLoggerWithWriters(t.Context(), "test")
w1 := newDummyWriter("dummy-1", DEBUG, 0)
w1.Mode.Colorize = true
logger.AddWriters(w1)
logger.Info("%s %s %#v %v", testLogString{}, &testLogString{}, testLogString{Field: "detail"}, NewColoredValue(testLogString{}, FgRed))
logger.Info("%s %s %#v %v", testLogStringPtrReceiver{}, &testLogStringPtrReceiver{}, testLogStringPtrReceiver{Field: "detail"}, NewColoredValue(testLogStringPtrReceiver{}, FgRed))
logger.Close()
assert.Equal(t, []string{
"log-string log-string log.testLogString{Field:\"detail\"} \x1b[31mlog-string\x1b[0m\n",
"log-string-ptr-receiver log-string-ptr-receiver &log.testLogStringPtrReceiver{Field:\"detail\"} \x1b[31mlog-string-ptr-receiver\x1b[0m\n",
}, w1.FetchLogs())
})
t.Run("Caller", func(t *testing.T) {
logger := NewLoggerWithWriters(t.Context(), "test")
w1 := newDummyWriter("dummy-1", DEBUG, 0)
w1.EventWriterBaseImpl.Mode.Flags.flags = Lmedfile | Lshortfuncname
logger.AddWriters(w1)
anonymousFunc := func(logger Logger) {
logger.Info("from anonymousFunc")
}
genericFunc(logger, "123")
anonymousFunc(logger)
logger.Close()
logs := w1.FetchLogs()
assert.Len(t, logs, 2)
assert.Regexp(t, `modules/log/logger_test.go:\w+:`+regexp.QuoteMeta(`genericFunc() from genericFunc: 123`), logs[0])
assert.Regexp(t, `modules/log/logger_test.go:\w+:`+regexp.QuoteMeta(`TestLoggerOutput.2.1() from anonymousFunc`), logs[1])
})
}
func TestLoggerExpressionFilter(t *testing.T) {
logger := NewLoggerWithWriters(t.Context(), "test")
w1 := newDummyWriter("dummy-1", DEBUG, 0)
w1.Mode.Expression = "foo.*"
logger.AddWriters(w1)
logger.Info("foo")
logger.Info("bar")
logger.Info("foo bar")
logger.SendLogEvent(&Event{Level: INFO, Filename: "foo.go", MsgSimpleText: "by filename"})
logger.Close()
assert.Equal(t, []string{"foo\n", "foo bar\n", "by filename\n"}, w1.FetchLogs())
}

142
modules/log/manager.go Normal file
View File

@@ -0,0 +1,142 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"context"
"fmt"
"sync"
"sync/atomic"
)
const DEFAULT = "default"
// LoggerManager manages loggers and shared event writers
type LoggerManager struct {
ctx context.Context
ctxCancel context.CancelFunc
mu sync.Mutex
writers map[string]EventWriter
loggers map[string]*LoggerImpl
defaultLogger atomic.Pointer[LoggerImpl]
pauseMu sync.RWMutex
pauseChan chan struct{}
}
// GetLogger returns a logger with the given name. If the logger doesn't exist, a new empty one will be created.
func (m *LoggerManager) GetLogger(name string) *LoggerImpl {
if name == DEFAULT {
if logger := m.defaultLogger.Load(); logger != nil {
return logger
}
}
m.mu.Lock()
defer m.mu.Unlock()
logger := m.loggers[name]
if logger == nil {
logger = NewLoggerWithWriters(m.ctx, name)
m.loggers[name] = logger
if name == DEFAULT {
m.defaultLogger.Store(logger)
}
}
return logger
}
// PauseAll pauses all event writers
func (m *LoggerManager) PauseAll() {
m.pauseMu.Lock()
m.pauseChan = make(chan struct{})
m.pauseMu.Unlock()
}
// ResumeAll resumes all event writers
func (m *LoggerManager) ResumeAll() {
m.pauseMu.Lock()
close(m.pauseChan)
m.pauseChan = nil
m.pauseMu.Unlock()
}
// GetPauseChan returns a channel for writer pausing
func (m *LoggerManager) GetPauseChan() chan struct{} {
m.pauseMu.RLock()
defer m.pauseMu.RUnlock()
return m.pauseChan
}
// Close closes the logger manager, all loggers and writers will be closed, the messages are flushed.
func (m *LoggerManager) Close() {
m.mu.Lock()
defer m.mu.Unlock()
for _, logger := range m.loggers {
logger.Close()
}
m.loggers = map[string]*LoggerImpl{}
for _, writer := range m.writers {
eventWriterStopWait(writer)
}
m.writers = map[string]EventWriter{}
m.ctxCancel()
}
// DumpLoggers returns a map of all loggers and their event writers, for debugging and display purposes.
func (m *LoggerManager) DumpLoggers() map[string]any {
m.mu.Lock()
defer m.mu.Unlock()
dump := map[string]any{}
for name, logger := range m.loggers {
loggerDump := map[string]any{
"IsEnabled": logger.IsEnabled(),
"EventWriters": logger.DumpWriters(),
}
dump[name] = loggerDump
}
return dump
}
// NewSharedWriter creates a new shared event writer, it can be used by multiple loggers, and a shared writer won't be closed if a logger is closed.
func (m *LoggerManager) NewSharedWriter(writerName, writerType string, mode WriterMode) (writer EventWriter, err error) {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.writers[writerName]; ok {
return nil, fmt.Errorf("log event writer %q has been added before", writerName)
}
if writer, err = NewEventWriter(writerName, writerType, mode); err != nil {
return nil, err
}
m.writers[writerName] = writer
eventWriterStartGo(m.ctx, writer, true)
return writer, nil
}
func (m *LoggerManager) GetSharedWriter(writerName string) EventWriter {
m.mu.Lock()
defer m.mu.Unlock()
return m.writers[writerName]
}
var loggerManager = NewManager()
func GetManager() *LoggerManager {
return loggerManager
}
func NewManager() *LoggerManager {
m := &LoggerManager{writers: map[string]EventWriter{}, loggers: map[string]*LoggerImpl{}}
m.ctx, m.ctxCancel = newProcessTypedContext(context.Background(), "LoggerManager")
return m
}

View File

@@ -0,0 +1,42 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSharedWorker(t *testing.T) {
RegisterEventWriter("dummy", func(writerName string, writerMode WriterMode) EventWriter {
return newDummyWriter(writerName, writerMode.Level, 0)
})
m := NewManager()
_, err := m.NewSharedWriter("dummy-1", "dummy", WriterMode{Level: DEBUG, Flags: FlagsFromBits(0)})
assert.NoError(t, err)
w := m.GetSharedWriter("dummy-1")
assert.NotNil(t, w)
loggerTest := m.GetLogger("test")
loggerTest.AddWriters(w)
loggerTest.Info("msg-1")
loggerTest.ReplaceAllWriters() // the shared writer is not closed here
loggerTest.Info("never seen")
// the shared writer can still be used later
w = m.GetSharedWriter("dummy-1")
assert.NotNil(t, w)
loggerTest.AddWriters(w)
loggerTest.Info("msg-2")
m.GetLogger("test-another").AddWriters(w)
m.GetLogger("test-another").Info("msg-3")
m.Close()
logs := w.(*dummyWriter).FetchLogs()
assert.Equal(t, []string{"msg-1\n", "msg-2\n", "msg-3\n"}, logs)
}

78
modules/log/misc.go Normal file
View File

@@ -0,0 +1,78 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"io"
)
type baseToLogger struct {
base BaseLogger
}
// BaseLoggerToGeneralLogger wraps a BaseLogger (which only has Log() function) to a Logger (which has Info() function)
func BaseLoggerToGeneralLogger(b BaseLogger) Logger {
l := &baseToLogger{base: b}
return l
}
var _ Logger = (*baseToLogger)(nil)
func (s *baseToLogger) Log(skip int, event *Event, format string, v ...any) {
s.base.Log(skip+1, event, format, v...)
}
func (s *baseToLogger) GetLevel() Level {
return s.base.GetLevel()
}
func (s *baseToLogger) LevelEnabled(level Level) bool {
return s.base.GetLevel() <= level
}
func (s *baseToLogger) Trace(format string, v ...any) {
s.base.Log(1, &Event{Level: TRACE}, format, v...)
}
func (s *baseToLogger) Debug(format string, v ...any) {
s.base.Log(1, &Event{Level: DEBUG}, format, v...)
}
func (s *baseToLogger) Info(format string, v ...any) {
s.base.Log(1, &Event{Level: INFO}, format, v...)
}
func (s *baseToLogger) Warn(format string, v ...any) {
s.base.Log(1, &Event{Level: WARN}, format, v...)
}
func (s *baseToLogger) Error(format string, v ...any) {
s.base.Log(1, &Event{Level: ERROR}, format, v...)
}
func (s *baseToLogger) Critical(format string, v ...any) {
s.base.Log(1, &Event{Level: CRITICAL}, format, v...)
}
type PrintfLogger struct {
Logf func(format string, args ...any)
}
func (p *PrintfLogger) Printf(format string, args ...any) {
p.Logf(format, args...)
}
type loggerToWriter struct {
logf func(format string, args ...any)
}
func (p *loggerToWriter) Write(bs []byte) (int, error) {
p.logf("%s", string(bs))
return len(bs), nil
}
// LoggerToWriter wraps a log function to an io.Writer
func LoggerToWriter(logf func(format string, args ...any)) io.Writer {
return &loggerToWriter{logf: logf}
}

80
modules/log/stack.go Normal file
View File

@@ -0,0 +1,80 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"bytes"
"fmt"
"os"
"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 := os.ReadFile(filename)
if err != nil {
// can't read this source file
// 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.ReplaceAll(name, []byte("·"), []byte("."))
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])
}