更新代码
This commit is contained in:
1 parent
eda7e8bc96
commit
6ff632e0b0
19 files changed
+2033
-197
No files matched your search
@@ -0,0 +1,270 @@
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user