Includes the current working directory changes. Historical commits and release binaries are excluded to limit Gitea storage. Source base: d7515973fbfd6dd2001fa156c483455b8081c7cb
324 lines
7.6 KiB
Go
324 lines
7.6 KiB
Go
package logger
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
stdlog "log"
|
||
"log/slog"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"cursor/internal/appdata"
|
||
|
||
"github.com/lmittmann/tint"
|
||
"github.com/mattn/go-colorable"
|
||
"github.com/mattn/go-isatty"
|
||
)
|
||
|
||
const (
|
||
appLogMaxLines = 10000
|
||
appLogTrimReserveLine = 1000
|
||
)
|
||
|
||
var (
|
||
initOnce sync.Once
|
||
logFile *os.File
|
||
logFilePath string
|
||
)
|
||
|
||
// Init 配置默认 slog logger,并把标准库 log 接到同一输出。
|
||
func Init() {
|
||
initOnce.Do(func() {
|
||
handlers := []slog.Handler{tint.NewHandler(colorable.NewColorableStdout(), &tint.Options{
|
||
Level: slog.LevelInfo,
|
||
TimeFormat: "15:04:05.000",
|
||
NoColor: disableColor(),
|
||
})}
|
||
fileHandler, path, fileErr := buildFileHandler()
|
||
if fileErr != nil {
|
||
_, _ = fmt.Fprintf(os.Stderr, "[logger] 初始化日志文件失败: %v\n", fileErr)
|
||
} else if fileHandler != nil {
|
||
handlers = append(handlers, fileHandler)
|
||
logFilePath = path
|
||
}
|
||
handler := handlers[0]
|
||
if len(handlers) > 1 {
|
||
handler = &multiHandler{handlers: handlers}
|
||
}
|
||
slog.SetDefault(slog.New(handler))
|
||
stdlog.SetFlags(0)
|
||
if logFilePath != "" {
|
||
slog.Info("应用日志已写入文件", "path", logFilePath, "pid", os.Getpid())
|
||
}
|
||
})
|
||
}
|
||
|
||
// Info 输出 info 级日志。
|
||
func Info(msg string, args ...any) {
|
||
Init()
|
||
slog.Info(msg, args...)
|
||
}
|
||
|
||
// Error 输出 error 级日志。
|
||
func Error(msg string, args ...any) {
|
||
Init()
|
||
slog.Error(msg, args...)
|
||
}
|
||
|
||
// Infof 输出格式化的 info 级日志。
|
||
func Infof(format string, args ...any) {
|
||
Init()
|
||
slog.Info(formatMessage(format, args...))
|
||
}
|
||
|
||
// Errorf 输出格式化的 error 级日志。
|
||
func Errorf(format string, args ...any) {
|
||
Init()
|
||
slog.Error(formatMessage(format, args...))
|
||
}
|
||
|
||
func formatMessage(format string, args ...any) string {
|
||
if len(args) == 0 {
|
||
return strings.TrimSpace(format)
|
||
}
|
||
return strings.TrimSpace(fmt.Sprintf(format, args...))
|
||
}
|
||
|
||
func disableColor() bool {
|
||
if strings.TrimSpace(os.Getenv("NO_COLOR")) != "" {
|
||
return true
|
||
}
|
||
if strings.EqualFold(strings.TrimSpace(os.Getenv("TERM")), "dumb") {
|
||
return true
|
||
}
|
||
fd := os.Stdout.Fd()
|
||
return !isatty.IsTerminal(fd) && !isatty.IsCygwinTerminal(fd)
|
||
}
|
||
|
||
func buildFileHandler() (slog.Handler, string, error) {
|
||
if err := appdata.EnsureAssistantHome(); err != nil {
|
||
return nil, "", err
|
||
}
|
||
path := filepath.Join(appdata.LogsRootPath(), "app.log")
|
||
writer, err := newLineWindowFileWriter(path, appLogMaxLines, appLogTrimReserveLine)
|
||
if err != nil {
|
||
return nil, "", err
|
||
}
|
||
logFile = writer.file
|
||
return tint.NewHandler(writer, &tint.Options{
|
||
Level: slog.LevelInfo,
|
||
TimeFormat: time.RFC3339,
|
||
NoColor: true,
|
||
}), path, nil
|
||
}
|
||
|
||
type lineWindowFileWriter struct {
|
||
mu sync.Mutex
|
||
path string
|
||
file *os.File
|
||
lineCount int
|
||
openLine bool
|
||
maxLines int
|
||
trimReserve int
|
||
}
|
||
|
||
func newLineWindowFileWriter(path string, maxLines int, trimReserve int) (*lineWindowFileWriter, error) {
|
||
writer := &lineWindowFileWriter{
|
||
path: path,
|
||
maxLines: maxLines,
|
||
trimReserve: trimReserve,
|
||
}
|
||
if err := writer.openLocked(); err != nil {
|
||
return nil, err
|
||
}
|
||
lineCount, openLine, err := countFileLines(path)
|
||
if err != nil {
|
||
_ = writer.file.Close()
|
||
return nil, err
|
||
}
|
||
writer.lineCount = lineCount
|
||
writer.openLine = openLine
|
||
if maxLines > 0 && lineCount > maxLines {
|
||
if err := writer.trimToLastLinesLocked(maxLines); err != nil {
|
||
_ = writer.file.Close()
|
||
return nil, err
|
||
}
|
||
}
|
||
return writer, nil
|
||
}
|
||
|
||
func (writer *lineWindowFileWriter) Write(payload []byte) (int, error) {
|
||
writer.mu.Lock()
|
||
defer writer.mu.Unlock()
|
||
if writer == nil || writer.file == nil {
|
||
return 0, fmt.Errorf("log file writer is not initialized")
|
||
}
|
||
newLines := writer.countIncomingLines(payload)
|
||
if writer.maxLines > 0 && newLines > 0 && writer.lineCount+newLines > writer.maxLines {
|
||
target := writer.maxLines - newLines - writer.trimReserve
|
||
if target < 0 {
|
||
target = writer.maxLines - newLines
|
||
}
|
||
if target < 0 {
|
||
target = 0
|
||
}
|
||
if err := writer.trimToLastLinesLocked(target); err != nil {
|
||
return 0, err
|
||
}
|
||
}
|
||
written, err := writer.file.Write(payload)
|
||
writer.lineCount += writer.countIncomingLines(payload[:written])
|
||
if written > 0 {
|
||
writer.openLine = payload[written-1] != '\n'
|
||
}
|
||
return written, err
|
||
}
|
||
|
||
func (writer *lineWindowFileWriter) openLocked() error {
|
||
file, err := os.OpenFile(writer.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
writer.file = file
|
||
logFile = file
|
||
return nil
|
||
}
|
||
|
||
func (writer *lineWindowFileWriter) trimToLastLinesLocked(targetLines int) error {
|
||
if writer.file != nil {
|
||
if err := writer.file.Close(); err != nil {
|
||
return err
|
||
}
|
||
writer.file = nil
|
||
}
|
||
payload, err := os.ReadFile(writer.path)
|
||
if err != nil {
|
||
if reopenErr := writer.openLocked(); reopenErr != nil {
|
||
return errors.Join(err, reopenErr)
|
||
}
|
||
return err
|
||
}
|
||
trimmed, lineCount := lastLinesBytes(payload, targetLines)
|
||
if err := os.WriteFile(writer.path, trimmed, 0o644); err != nil {
|
||
if reopenErr := writer.openLocked(); reopenErr != nil {
|
||
return errors.Join(err, reopenErr)
|
||
}
|
||
return err
|
||
}
|
||
if err := writer.openLocked(); err != nil {
|
||
return err
|
||
}
|
||
writer.lineCount = lineCount
|
||
writer.openLine = len(trimmed) > 0 && trimmed[len(trimmed)-1] != '\n'
|
||
return nil
|
||
}
|
||
|
||
func (writer *lineWindowFileWriter) countIncomingLines(payload []byte) int {
|
||
if len(payload) == 0 {
|
||
return 0
|
||
}
|
||
newlineCount := bytes.Count(payload, []byte{'\n'})
|
||
endsWithNewline := payload[len(payload)-1] == '\n'
|
||
delta := newlineCount
|
||
switch {
|
||
case writer.openLine && endsWithNewline:
|
||
delta--
|
||
case !writer.openLine && !endsWithNewline:
|
||
delta++
|
||
}
|
||
if delta < 0 {
|
||
return 0
|
||
}
|
||
return delta
|
||
}
|
||
|
||
func countFileLines(path string) (int, bool, error) {
|
||
payload, err := os.ReadFile(path)
|
||
if err != nil {
|
||
if errors.Is(err, os.ErrNotExist) {
|
||
return 0, false, nil
|
||
}
|
||
return 0, false, err
|
||
}
|
||
return countBytesLines(payload), len(payload) > 0 && payload[len(payload)-1] != '\n', nil
|
||
}
|
||
|
||
func countBytesLines(payload []byte) int {
|
||
if len(payload) == 0 {
|
||
return 0
|
||
}
|
||
count := bytes.Count(payload, []byte{'\n'})
|
||
if payload[len(payload)-1] != '\n' {
|
||
count++
|
||
}
|
||
return count
|
||
}
|
||
|
||
func lastLinesBytes(payload []byte, targetLines int) ([]byte, int) {
|
||
if len(payload) == 0 || targetLines <= 0 {
|
||
return nil, 0
|
||
}
|
||
lineCount := countBytesLines(payload)
|
||
if lineCount <= targetLines {
|
||
return append([]byte(nil), payload...), lineCount
|
||
}
|
||
dropLines := lineCount - targetLines
|
||
offset := 0
|
||
for i := 0; i < dropLines; i++ {
|
||
next := bytes.IndexByte(payload[offset:], '\n')
|
||
if next < 0 {
|
||
return nil, 0
|
||
}
|
||
offset += next + 1
|
||
}
|
||
trimmed := append([]byte(nil), payload[offset:]...)
|
||
return trimmed, countBytesLines(trimmed)
|
||
}
|
||
|
||
type multiHandler struct {
|
||
handlers []slog.Handler
|
||
}
|
||
|
||
func (h *multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||
for _, handler := range h.handlers {
|
||
if handler.Enabled(ctx, level) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (h *multiHandler) Handle(ctx context.Context, record slog.Record) error {
|
||
var handleErr error
|
||
for _, handler := range h.handlers {
|
||
if !handler.Enabled(ctx, record.Level) {
|
||
continue
|
||
}
|
||
if err := handler.Handle(ctx, record.Clone()); err != nil {
|
||
handleErr = errors.Join(handleErr, err)
|
||
}
|
||
}
|
||
return handleErr
|
||
}
|
||
|
||
func (h *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||
next := make([]slog.Handler, 0, len(h.handlers))
|
||
for _, handler := range h.handlers {
|
||
next = append(next, handler.WithAttrs(attrs))
|
||
}
|
||
return &multiHandler{handlers: next}
|
||
}
|
||
|
||
func (h *multiHandler) WithGroup(name string) slog.Handler {
|
||
next := make([]slog.Handler, 0, len(h.handlers))
|
||
for _, handler := range h.handlers {
|
||
next = append(next, handler.WithGroup(name))
|
||
}
|
||
return &multiHandler{handlers: next}
|
||
}
|