Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

256 lines
6.3 KiB
Go

// Copyright 2026 Hanzo AI, Inc. 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 = "github.com/hanzoai/git/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
}