Files
2026-10-28 17:39:33 +08:00

271 lines
6.5 KiB
Go

// Package logging 是桌面程序自带的日志设施。
//
// 日志同时写两处:磁盘上的 app.log(长期留存、方便排查)和内存里的
// 环形缓冲(最近若干行)。界面上的「日志」按钮直接读这份缓冲,
// 因此程序完全不需要控制台窗口就能查看运行日志。
package logging
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
)
// Buffer 是保存最近若干行日志的环形缓冲。
type Buffer struct {
mu sync.Mutex
lines []string
cap int
seq uint64
}
// NewBuffer 创建一个最多保留 capacity 行的缓冲。
func NewBuffer(capacity int) *Buffer {
if capacity <= 0 {
capacity = 2000
}
return &Buffer{lines: make([]string, 0, capacity), cap: capacity}
}
// Append 追加一行日志,超过容量时丢弃最旧的一行。
func (b *Buffer) Append(line string) {
b.mu.Lock()
defer b.mu.Unlock()
if len(b.lines) == b.cap {
copy(b.lines, b.lines[1:])
b.lines = b.lines[:len(b.lines)-1]
}
b.lines = append(b.lines, line)
b.seq++
}
// Text 返回缓存中的全部日志文本。
func (b *Buffer) Text() string {
b.mu.Lock()
defer b.mu.Unlock()
if len(b.lines) == 0 {
return ""
}
return strings.Join(b.lines, "\n")
}
// Lines 返回缓存中日志行的副本。
func (b *Buffer) Lines() []string {
b.mu.Lock()
defer b.mu.Unlock()
out := make([]string, len(b.lines))
copy(out, b.lines)
return out
}
// Seq 返回内容版本号:每写入一行或清空一次就加一。
// 界面靠它判断内容有没有变化,避免每次都重建整段文本。
func (b *Buffer) Seq() uint64 {
b.mu.Lock()
defer b.mu.Unlock()
return b.seq
}
// Clear 清空缓存。
func (b *Buffer) Clear() {
b.mu.Lock()
defer b.mu.Unlock()
b.lines = b.lines[:0]
b.seq++
}
// Writer 把日志同时写进文件和内存缓冲。
type Writer struct {
mu sync.Mutex
file *os.File
buf *Buffer
}
// New 返回一个只写内存的写入器,用于日志文件打不开时的兜底。
func New(buf *Buffer) *Writer { return &Writer{buf: buf} }
// Open 打开(追加写入)日志文件并与 buf 绑定。
// 打开失败时返回一个仍然可用的写入器(只写内存)和错误。
func Open(path string, buf *Buffer) (*Writer, error) {
if dir := filepath.Dir(path); dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
return New(buf), fmt.Errorf("创建日志目录失败: %w", err)
}
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return New(buf), fmt.Errorf("打开日志文件失败: %w", err)
}
return &Writer{file: f, buf: buf}, nil
}
// Write 实现 io.Writer。slog 的每条记录是一次 Write 调用,这里按行落盘并缓存。
func (w *Writer) Write(p []byte) (int, error) {
line := strings.TrimRight(string(p), "\r\n")
if line == "" {
return len(p), nil
}
w.mu.Lock()
defer w.mu.Unlock()
if w.file != nil {
if _, err := w.file.WriteString(line + "\n"); err != nil {
return len(p), err
}
}
if w.buf != nil {
w.buf.Append(line)
}
return len(p), nil
}
// Close 关闭日志文件。
func (w *Writer) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.file == nil {
return nil
}
err := w.file.Close()
w.file = nil
return err
}
// handler 是自定义的 slog.Handler:把每条记录格式化成一行,
// 再交给 Writer 同时落盘并缓存,供界面直接查看。
type handler struct {
w *Writer
level slog.Level
attrs []slog.Attr
groups []string
}
// NewHandler 创建一个把日志写进 w 的 slog 处理器。
// level 以下级别的记录会被丢弃。
func NewHandler(w *Writer, level slog.Level) slog.Handler {
return &handler{w: w, level: level}
}
func (h *handler) Enabled(_ context.Context, l slog.Level) bool {
return l >= h.level
}
func (h *handler) Handle(_ context.Context, r slog.Record) error {
var sb strings.Builder
sb.WriteString(r.Time.Format("2006-01-02 15:04:05.000"))
sb.WriteString(" ")
sb.WriteString(levelLabel(r.Level))
sb.WriteString(" ")
sb.WriteString(r.Message)
var rec []slog.Attr
r.Attrs(func(a slog.Attr) bool {
rec = append(rec, a)
return true
})
// 记录里的属性按当前组栈展开;WithAttrs 存下来的属性已经展开过。
for _, a := range flatten(h.groups, rec) {
appendAttr(&sb, a)
}
for _, a := range h.attrs {
appendAttr(&sb, a)
}
sb.WriteString("\n")
_, err := h.w.Write([]byte(sb.String()))
return err
}
// WithAttrs 返回一个带上额外属性的处理器。
func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler {
if len(attrs) == 0 {
return h
}
nh := *h
nh.attrs = append(append([]slog.Attr(nil), h.attrs...), flatten(h.groups, attrs)...)
return &nh
}
// WithGroup 返回一个把后续属性放进命名组的处理器。
func (h *handler) WithGroup(name string) slog.Handler {
if name == "" {
return h
}
nh := *h
nh.groups = append(append([]string(nil), h.groups...), name)
return &nh
}
// flatten 把带组的属性展开成扁平的键值对,键名带上组前缀。
func flatten(groups []string, attrs []slog.Attr) []slog.Attr {
out := make([]slog.Attr, 0, len(attrs))
for _, a := range attrs {
if a.Value.Kind() == slog.KindGroup {
g := a.Value.Group()
if len(g) == 0 {
continue
}
sub := groups
if a.Key != "" {
sub = append(append([]string(nil), groups...), a.Key)
}
out = append(out, flatten(sub, g)...)
continue
}
if a.Key == "" {
continue
}
key := a.Key
if len(groups) > 0 {
key = strings.Join(groups, ".") + "." + key
}
out = append(out, slog.Attr{Key: key, Value: a.Value})
}
return out
}
// appendAttr 把一个扁平属性写进日志行。
func appendAttr(sb *strings.Builder, a slog.Attr) {
sb.WriteString(" ")
sb.WriteString(a.Key)
sb.WriteString("=")
sb.WriteString(formatValue(a.Value))
}
// formatValue 把属性值格式化成一行日志里好读的样子。
func formatValue(v slog.Value) string {
v = v.Resolve()
switch v.Kind() {
case slog.KindString:
s := v.String()
if strings.ContainsAny(s, " \t\"=") {
return strconv.Quote(s)
}
return s
case slog.KindTime:
return v.Time().Format("15:04:05.000")
case slog.KindDuration:
return v.Duration().String()
default:
return fmt.Sprintf("%v", v.Any())
}
}
// levelLabel 返回定宽的级别标签。
func levelLabel(l slog.Level) string {
switch {
case l >= slog.LevelError:
return "ERROR"
case l >= slog.LevelWarn:
return "WARN "
case l >= slog.LevelInfo:
return "INFO "
default:
return "DEBUG"
}
}