Files
Logging-System/logsys/logger.go
T
weichenandCursor bed0dbf076
ci / vet-and-test (push) Successful in 4s
ci / docker-checkout-hostgateway (push) Successful in 2s
Add a structured logger and collector so runs can be traced across processes.
Sync and async sinks, redaction, and a queryable HTTP service keep concurrent logs tied to a trace id.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 13:51:16 +00:00

343 lines
7.3 KiB
Go

package logsys
import (
"fmt"
"sync"
"time"
)
// Options 构造一个 Logger。构造完成后不要修改切片和映射。
type Options struct {
Service string
Module string
Level Level
ModuleLevels map[string]Level
Sinks []Sink
Redact func(string) string
Fields map[string]any
Now func() time.Time
}
// Logger 是并发安全的日志器。With / Module 返回副本,不改变原对象。
type Logger struct {
service string
module string
min Level
moduleLevels map[string]Level
fields map[string]any
traceID string
sinks []Sink
redact func(string) string
now func() time.Time
nop bool
}
// New 创建日志器。没有 sink 时写入被丢弃(与 Nop 相同,但保留级别与字段)。
func New(opt Options) *Logger {
now := opt.Now
if now == nil {
now = func() time.Time { return time.Now().UTC() }
}
levels := make(map[string]Level, len(opt.ModuleLevels))
for k, v := range opt.ModuleLevels {
levels[k] = v
}
return &Logger{
service: opt.Service,
module: opt.Module,
min: opt.Level,
moduleLevels: levels,
fields: cloneFields(opt.Fields),
sinks: append([]Sink(nil), opt.Sinks...),
redact: opt.Redact,
now: now,
}
}
// Nop 丢弃全部日志。
func Nop() *Logger { return &Logger{nop: true, now: time.Now} }
// Module 返回绑定模块名的子 logger,用于匹配模块级别。
func (l *Logger) Module(name string) *Logger {
if l == nil {
return Nop()
}
n := l.clone()
n.module = name
return n
}
// With 追加字段。键必须是非空字符串,且与值成对;否则 panic。
func (l *Logger) With(kv ...any) *Logger {
if l == nil {
return Nop()
}
n := l.clone()
n.fields = mergeFields(n.fields, fieldsFrom(kv))
return n
}
// WithTraceID 固定本 logger 的追踪标识。
func (l *Logger) WithTraceID(id string) *Logger {
if l == nil {
return Nop()
}
n := l.clone()
n.traceID = id
return n
}
// Enabled 报告该级别是否会写出去。
func (l *Logger) Enabled(level Level) bool {
if l == nil || l.nop {
return false
}
return l.enabled(level)
}
// Trace 记录 trace 级别日志。
func (l *Logger) Trace(msg string, kv ...any) { l.log(LevelTrace, msg, nil, kv...) }
// Debug 记录 debug 级别日志。
func (l *Logger) Debug(msg string, kv ...any) { l.log(LevelDebug, msg, nil, kv...) }
// Info 记录 info 级别日志。
func (l *Logger) Info(msg string, kv ...any) { l.log(LevelInfo, msg, nil, kv...) }
// Warn 记录 warn 级别日志。
func (l *Logger) Warn(msg string, kv ...any) { l.log(LevelWarn, msg, nil, kv...) }
// Error 记录 error 级别日志。err 可为 nil;若实现 ErrorCode/ErrorFields,会展开进条目。
func (l *Logger) Error(msg string, err error, kv ...any) { l.log(LevelError, msg, err, kv...) }
// Fatal 记录 fatal 级别日志,不退出进程。
func (l *Logger) Fatal(msg string, err error, kv ...any) { l.log(LevelFatal, msg, err, kv...) }
// Sync 刷出全部 sink。
func (l *Logger) Sync() error {
if l == nil || l.nop {
return nil
}
var errs []error
for _, s := range l.sinks {
if s == nil {
continue
}
if err := s.Sync(); err != nil {
errs = append(errs, err)
}
}
return joinErrs(errs)
}
// Close 关闭全部 sink。
func (l *Logger) Close() error {
if l == nil || l.nop {
return nil
}
var errs []error
for _, s := range l.sinks {
if s == nil {
continue
}
if err := s.Close(); err != nil {
errs = append(errs, err)
}
}
return joinErrs(errs)
}
func (l *Logger) log(level Level, msg string, err error, kv ...any) {
extra := fieldsFrom(kv)
if l == nil || l.nop || !l.enabled(level) {
return
}
fields := mergeFields(l.fields, extra)
entry := Entry{
Time: l.now(),
Level: level.String(),
Service: l.service,
Module: l.module,
Message: msg,
TraceID: l.traceID,
Fields: fields,
}
applyError(&entry, err)
if l.redact != nil {
entry = redactEntry(entry, l.redact)
}
for _, s := range l.sinks {
if s == nil {
continue
}
_ = s.Write(entry)
}
}
func (l *Logger) enabled(level Level) bool {
min := l.min
if l.module != "" {
if ml, ok := l.moduleLevels[l.module]; ok {
min = ml
}
}
return level >= min
}
func (l *Logger) clone() *Logger {
n := *l
n.fields = cloneFields(l.fields)
n.moduleLevels = l.moduleLevels
n.sinks = l.sinks
return &n
}
func applyError(entry *Entry, err error) {
if err == nil {
return
}
entry.Error = err.Error()
if coded, ok := err.(codedError); ok {
entry.Code = coded.ErrorCode()
for k, v := range coded.ErrorFields() {
if entry.Fields == nil {
entry.Fields = map[string]any{}
}
entry.Fields[k] = v
}
}
}
func redactEntry(entry Entry, redact func(string) string) Entry {
entry.Message = redact(entry.Message)
entry.Error = redact(entry.Error)
entry.Fields = redactFields(entry.Fields, redact)
return entry
}
func redactFields(in map[string]any, redact func(string) string) map[string]any {
if len(in) == 0 {
return nil
}
out := make(map[string]any, len(in))
for k, v := range in {
out[redact(k)] = redactValue(v, redact)
}
return out
}
func redactValue(v any, redact func(string) string) any {
switch t := v.(type) {
case string:
return redact(t)
case error:
if t == nil {
return nil
}
return redact(t.Error())
case map[string]any:
return redactFields(t, redact)
default:
return v
}
}
func fieldsFrom(kv []any) map[string]any {
if len(kv) == 0 {
return nil
}
if len(kv)%2 != 0 {
panic("logsys: odd number of key-value arguments")
}
out := make(map[string]any, len(kv)/2)
for i := 0; i < len(kv); i += 2 {
key, ok := kv[i].(string)
if !ok || key == "" {
panic(fmt.Sprintf("logsys: field key must be a non-empty string, got %T", kv[i]))
}
out[key] = kv[i+1]
}
return out
}
func cloneFields(in map[string]any) map[string]any {
if len(in) == 0 {
return nil
}
out := make(map[string]any, len(in))
for k, v := range in {
out[k] = v
}
return out
}
func mergeFields(base, extra map[string]any) map[string]any {
if len(base) == 0 && len(extra) == 0 {
return nil
}
out := cloneFields(base)
if out == nil {
out = map[string]any{}
}
for k, v := range extra {
out[k] = v
}
return out
}
func joinErrs(errs []error) error {
if len(errs) == 0 {
return nil
}
if len(errs) == 1 {
return errs[0]
}
return &multiErr{errs: errs}
}
type multiErr struct{ errs []error }
func (m *multiErr) Error() string {
var b bytesTrim
for i, err := range m.errs {
if i > 0 {
b.WriteString("; ")
}
b.WriteString(err.Error())
}
return b.String()
}
func (m *multiErr) Unwrap() []error { return m.errs }
// bytesTrim 避免为拼接错误再引入 bytes 与 strings 的混用。
type bytesTrim struct{ b []byte }
func (b *bytesTrim) WriteString(s string) { b.b = append(b.b, s...) }
func (b *bytesTrim) String() string { return string(b.b) }
var (
defaultMu sync.RWMutex
defaultL = Nop()
)
// SetDefault 设置进程默认日志器。nil 等价于 Nop。
func SetDefault(l *Logger) {
if l == nil {
l = Nop()
}
defaultMu.Lock()
defaultL = l
defaultMu.Unlock()
}
// Default 返回进程默认日志器。未设置时为 Nop。
func Default() *Logger {
defaultMu.RLock()
defer defaultMu.RUnlock()
return defaultL
}
// Sync 刷出默认日志器。
func Sync() error { return Default().Sync() }