mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
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]>
256 lines
6.3 KiB
Go
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
|
|
}
|