mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 03:27:02 +08:00
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}
|
|
}
|