Files

166 lines
5.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package logx 提供带脱敏的日志。
//
// 为什么要专门写一个包,而不是直接用标准库 log:
// 本项目会接触淘宝 Cookie、货憨憨 token 和账号密码。这些东西
// 一旦进了日志文件,就等于泄漏了。所以所有日志都必须经过这里,
// 由 Mask() 统一把敏感内容替换掉。
//
// 规则很简单:往日志里写东西时,永远调用本包的方法,不要直接
// 用 fmt.Println 或 log.Printf。
package logx
import (
"fmt"
"regexp"
"strings"
"sync"
"time"
)
// Level 是日志级别。界面上的运行日志按它上色和过滤。
type Level string
const (
LevelInfo Level = "info" // 普通进度
LevelSuccess Level = "success" // 成功完成一步
LevelWarn Level = "warn" // 有问题但还能继续
LevelError Level = "error" // 失败
)
// Entry 是一条日志。前端「运行日志」窗口直接显示它。
type Entry struct {
Time string `json:"time"` // 形如 15:04:05,只给人看
Level Level `json:"level"` // 前端按它上色和过滤
Message string `json:"message"` // 已经脱敏过的正文
}
// 下面这些正则用来找出不该出现在日志里的内容。
//
// 新增敏感字段时,在这里加一条,并在 logx_test.go 里补一个用例。
var maskPatterns = []struct {
name string
pattern *regexp.Regexp
replace string
}{
// 淘宝 MTOP 令牌,形如 _m_h5_tk=abc123_1699...
{"淘宝 token", regexp.MustCompile(`(?i)(_m_h5_tk(_enc)?=)[^;&\s]+`), "${1}***"},
// 淘宝其它登录相关 Cookie
{"淘宝 cookie", regexp.MustCompile(`(?i)(_tb_token_=|tracknick=)[^;&\s]+`), "${1}***"},
// HTTP 认证头
{"Authorization", regexp.MustCompile(`(?i)(authorization:\s*bearer\s+)\S+`), "${1}***"},
{"API Key", regexp.MustCompile(`(?i)((x-api-key|api[_-]?key)\s*[":=]+\s*"?)[^"\s,}]+`), "${1}***"},
// 整个 Cookie 请求头
{"Cookie 头", regexp.MustCompile(`(?i)(cookie:\s*)\S.*`), "${1}***"},
// 常见的密码字段,覆盖 password=xxx、"password":"xxx"、密码:xxx
{"密码", regexp.MustCompile(`(?i)((password|passwd|pwd)\s*[":=]+\s*"?)[^"\s,}]+`), "${1}***"},
{"中文密码", regexp.MustCompile(`(密码[::]\s*)\S+`), "${1}***"},
// Base64 图片数据,打印出来既没用又刷屏
{"图片 base64", regexp.MustCompile(`data:image/[a-z]+;base64,[A-Za-z0-9+/=]+`), "data:image/*;base64,***"},
}
// Mask 把文本里的敏感内容替换成 ***。
//
// 它只做替换,不判断“这条日志该不该记”。要不要记由调用方决定。
func Mask(text string) string {
for _, p := range maskPatterns {
text = p.pattern.ReplaceAllString(text, p.replace)
}
return text
}
// Logger 是一个内存日志缓冲区。
//
// 为什么放内存:界面上的「运行日志」窗口需要随时能拿到最近的日志。
// 为什么有上限:任务可能跑几个小时,不限制会把内存吃光。
//
// 它是并发安全的,多个下载协程可以同时往里写。
type Logger struct {
mu sync.Mutex
entries []Entry
limit int
// onEntry 在每条日志写入后被调用,用来推送给前端。
// 为 nil 时不推送,方便在单元测试里使用。
onEntry func(Entry)
}
// New 创建一个日志缓冲区。limit 是最多保留多少条,超出后丢弃最旧的。
func New(limit int) *Logger {
if limit <= 0 {
limit = 1000
}
return &Logger{limit: limit}
}
// SetHandler 设置日志推送回调。Wails 启动后用它把日志发给前端。
func (l *Logger) SetHandler(handler func(Entry)) {
l.mu.Lock()
defer l.mu.Unlock()
l.onEntry = handler
}
func (l *Logger) write(level Level, format string, args ...any) {
entry := Entry{
Time: time.Now().Format("15:04:05"),
Level: level,
Message: Mask(fmt.Sprintf(format, args...)),
}
l.mu.Lock()
l.entries = append(l.entries, entry)
if len(l.entries) > l.limit {
// 丢掉最旧的那些。用 copy 而不是切片头部截取,
// 是为了让底层数组能被回收,避免内存一直涨。
drop := len(l.entries) - l.limit
l.entries = append(l.entries[:0], l.entries[drop:]...)
}
handler := l.onEntry
l.mu.Unlock()
if handler != nil {
handler(entry)
}
}
// Info 记录一条普通进度。
func (l *Logger) Info(format string, args ...any) { l.write(LevelInfo, format, args...) }
// Success 记录一条成功。
func (l *Logger) Success(format string, args ...any) { l.write(LevelSuccess, format, args...) }
// Warn 记录一条警告:有问题但任务还能继续。
func (l *Logger) Warn(format string, args ...any) { l.write(LevelWarn, format, args...) }
// Error 记录一条失败。
func (l *Logger) Error(format string, args ...any) { l.write(LevelError, format, args...) }
// Entries 返回当前保留的全部日志副本。
//
// 返回副本而不是内部切片,是为了防止调用方在外面改动它,
// 导致并发读写崩溃。
func (l *Logger) Entries() []Entry {
l.mu.Lock()
defer l.mu.Unlock()
out := make([]Entry, len(l.entries))
copy(out, l.entries)
return out
}
// Clear 清空日志。对应界面上「运行日志」窗口的清空按钮。
func (l *Logger) Clear() {
l.mu.Lock()
defer l.mu.Unlock()
l.entries = nil
}
// Text 把全部日志拼成纯文本,用于「导出日志」。
func (l *Logger) Text() string {
var sb strings.Builder
for _, e := range l.Entries() {
sb.WriteString(e.Time)
sb.WriteString(" ")
sb.WriteString(e.Message)
sb.WriteString("\n")
}
return sb.String()
}