- internal/logger/ring.go: Entry{time,level,source,msg} + RingBuffer(2000,满覆盖最旧,不阻塞)+ringHandler(slog旁路,Enabled委托内层,sourceOf提取source=再回落Scoped); Options.Ring非nil才包
- gateway/observability_logs.go: GET /admin/observability/logs?level=&limit= (writeAuth owner/admin) → {data:[{time(RFC3339 UTC),level,source,msg}]} 最新在前; level阈值过滤(warn=warn+error,已标注取舍); limit缺省200上限1000; 未知level400/未注入501
- routes.go注册 + observability.go SetLogBuffer(atomic.Pointer) + cmd/cyrene main.go/wireGatewayLogBuffer接线(logger先于gateway)
- 控制台/文件输出零变化; 记录只源自已产出日志,不额外打印敏感
- Docs/api-client v0.35; 测试(ring 10 + observability_logs 8 + cmd端到端2); build/vet/test绿(56)+win/darwin交叉编译
96 lines
2.8 KiB
Go
96 lines
2.8 KiB
Go
// Package logger provides the project-wide structured logging facade built on
|
|
// log/slog. All modules must use this package instead of creating their own
|
|
// logger instances. Development builds render human-friendly TextHandler output;
|
|
// production builds render JSON for machine parsing.
|
|
package logger
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Format selects the structured log output encoding.
|
|
type Format string
|
|
|
|
const (
|
|
// FormatText is the human-readable development format.
|
|
FormatText Format = "text"
|
|
// FormatJSON is the machine-parseable production format.
|
|
FormatJSON Format = "json"
|
|
)
|
|
|
|
// Options configures a Logger.
|
|
type Options struct {
|
|
// Level is the minimum level to record, one of debug, info, warn, error.
|
|
// Unknown values fall back to info.
|
|
Level string
|
|
// Format is the output encoding, one of text, json. Unknown values fall
|
|
// back to text.
|
|
Format string
|
|
// Out is the destination writer. Defaults to os.Stderr when nil.
|
|
Out io.Writer
|
|
// Ring, when non-nil, additionally captures every emitted record into a
|
|
// bounded ring buffer served by the admin observability log tail. The
|
|
// console/file output through Out is unchanged.
|
|
Ring *RingBuffer
|
|
}
|
|
|
|
// New builds a slog.Logger from opts.
|
|
func New(opts Options) *slog.Logger {
|
|
level := ParseLevel(opts.Level)
|
|
out := opts.Out
|
|
if out == nil {
|
|
out = os.Stderr
|
|
}
|
|
|
|
handlerOpts := &slog.HandlerOptions{Level: level}
|
|
var handler slog.Handler
|
|
if Format(opts.Format) == FormatJSON {
|
|
handler = slog.NewJSONHandler(out, handlerOpts)
|
|
} else {
|
|
handler = slog.NewTextHandler(out, handlerOpts)
|
|
}
|
|
|
|
return slog.New(newRingHandler(handler, opts.Ring))
|
|
}
|
|
|
|
// ParseLevel maps a string level name to a slog.Level. Unknown or empty values
|
|
// produce slog.LevelInfo.
|
|
func ParseLevel(s string) slog.Level {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "debug":
|
|
return slog.LevelDebug
|
|
case "info":
|
|
return slog.LevelInfo
|
|
case "warn", "warning":
|
|
return slog.LevelWarn
|
|
case "error":
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|
|
|
|
// WithSource returns a child logger that tags every emitted record with a
|
|
// source so issues can be traced back to the owning module/sub-feature. source
|
|
// is conventionally "module/subfeature", e.g. "runtime/loop" or
|
|
// "channel/adapter". Every module that logs should call this once on its base
|
|
// logger so all records carry provenance.
|
|
func WithSource(l *slog.Logger, source string) *slog.Logger {
|
|
if l == nil {
|
|
return nil
|
|
}
|
|
return l.With(slog.String("source", source))
|
|
}
|
|
|
|
// Scoped returns a child logger tagged with module and subfeature, for finer
|
|
// provenance when a module contains several subsystems.
|
|
func Scoped(l *slog.Logger, module, subfeature string) *slog.Logger {
|
|
if l == nil {
|
|
return nil
|
|
}
|
|
return l.With(slog.String("module", module), slog.String("sub", subfeature))
|
|
}
|