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>
343 lines
7.3 KiB
Go
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() }
|