logger.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. package logger
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "log"
  7. "log/slog"
  8. "os"
  9. "path/filepath"
  10. "strings"
  11. "sync"
  12. "sync/atomic"
  13. "time"
  14. "go.uber.org/zap"
  15. "go.uber.org/zap/zapcore"
  16. "gopkg.in/natefinch/lumberjack.v2"
  17. )
  18. type Level = zapcore.Level
  19. const (
  20. LevelDebug = zapcore.DebugLevel
  21. LevelInfo = zapcore.InfoLevel
  22. LevelWarn = zapcore.WarnLevel
  23. LevelError = zapcore.ErrorLevel
  24. LevelFatal = zapcore.FatalLevel
  25. // OpsSystemLogSkipField keeps an event in the standard logger while
  26. // preventing the database-backed Ops system-log sink from indexing it.
  27. OpsSystemLogSkipField = "ops_system_log_skip"
  28. )
  29. type Sink interface {
  30. WriteLogEvent(event *LogEvent)
  31. }
  32. type LogEvent struct {
  33. Time time.Time
  34. Level string
  35. Component string
  36. Message string
  37. LoggerName string
  38. Fields map[string]any
  39. }
  40. var (
  41. mu sync.RWMutex
  42. global atomic.Pointer[zap.Logger]
  43. sugar atomic.Pointer[zap.SugaredLogger]
  44. atomicLevel zap.AtomicLevel
  45. initOptions InitOptions
  46. currentSink atomic.Value // sinkState
  47. stdLogUndo func()
  48. bootstrapOnce sync.Once
  49. )
  50. type sinkState struct {
  51. sink Sink
  52. }
  53. func InitBootstrap() {
  54. bootstrapOnce.Do(func() {
  55. if err := Init(bootstrapOptions()); err != nil {
  56. _, _ = fmt.Fprintf(os.Stderr, "logger bootstrap init failed: %v\n", err)
  57. }
  58. })
  59. }
  60. func Init(options InitOptions) error {
  61. mu.Lock()
  62. defer mu.Unlock()
  63. return initLocked(options)
  64. }
  65. func initLocked(options InitOptions) error {
  66. normalized := options.normalized()
  67. zl, al, err := buildLogger(normalized)
  68. if err != nil {
  69. return err
  70. }
  71. prev := global.Load()
  72. global.Store(zl)
  73. sugar.Store(zl.Sugar())
  74. atomicLevel = al
  75. initOptions = normalized
  76. bridgeSlogLocked()
  77. bridgeStdLogLocked()
  78. if prev != nil {
  79. _ = prev.Sync()
  80. }
  81. return nil
  82. }
  83. func Reconfigure(mutator func(*InitOptions) error) error {
  84. mu.Lock()
  85. defer mu.Unlock()
  86. next := initOptions
  87. if mutator != nil {
  88. if err := mutator(&next); err != nil {
  89. return err
  90. }
  91. }
  92. return initLocked(next)
  93. }
  94. func SetLevel(level string) error {
  95. lv, ok := parseLevel(level)
  96. if !ok {
  97. return fmt.Errorf("invalid log level: %s", level)
  98. }
  99. mu.Lock()
  100. defer mu.Unlock()
  101. atomicLevel.SetLevel(lv)
  102. initOptions.Level = strings.ToLower(strings.TrimSpace(level))
  103. return nil
  104. }
  105. func CurrentLevel() string {
  106. mu.RLock()
  107. defer mu.RUnlock()
  108. if global.Load() == nil {
  109. return "info"
  110. }
  111. return atomicLevel.Level().String()
  112. }
  113. func SetSink(sink Sink) {
  114. currentSink.Store(sinkState{sink: sink})
  115. }
  116. func loadSink() Sink {
  117. v := currentSink.Load()
  118. if v == nil {
  119. return nil
  120. }
  121. state, ok := v.(sinkState)
  122. if !ok {
  123. return nil
  124. }
  125. return state.sink
  126. }
  127. // WriteSinkEvent 直接写入日志 sink,不经过全局日志级别门控。
  128. // 用于需要“可观测性入库”与“业务输出级别”解耦的场景(例如 ops 系统日志索引)。
  129. func WriteSinkEvent(level, component, message string, fields map[string]any) {
  130. sink := loadSink()
  131. if sink == nil {
  132. return
  133. }
  134. level = strings.ToLower(strings.TrimSpace(level))
  135. if level == "" {
  136. level = "info"
  137. }
  138. component = strings.TrimSpace(component)
  139. message = strings.TrimSpace(message)
  140. if message == "" {
  141. return
  142. }
  143. eventFields := make(map[string]any, len(fields)+1)
  144. for k, v := range fields {
  145. eventFields[k] = v
  146. }
  147. if component != "" {
  148. if _, ok := eventFields["component"]; !ok {
  149. eventFields["component"] = component
  150. }
  151. }
  152. sink.WriteLogEvent(&LogEvent{
  153. Time: time.Now(),
  154. Level: level,
  155. Component: component,
  156. Message: message,
  157. LoggerName: component,
  158. Fields: eventFields,
  159. })
  160. }
  161. func L() *zap.Logger {
  162. if l := global.Load(); l != nil {
  163. return l
  164. }
  165. return zap.NewNop()
  166. }
  167. func S() *zap.SugaredLogger {
  168. if s := sugar.Load(); s != nil {
  169. return s
  170. }
  171. return zap.NewNop().Sugar()
  172. }
  173. func With(fields ...zap.Field) *zap.Logger {
  174. return L().With(fields...)
  175. }
  176. func Sync() {
  177. l := global.Load()
  178. if l != nil {
  179. _ = l.Sync()
  180. }
  181. }
  182. func bridgeStdLogLocked() {
  183. if stdLogUndo != nil {
  184. stdLogUndo()
  185. stdLogUndo = nil
  186. }
  187. prevFlags := log.Flags()
  188. prevPrefix := log.Prefix()
  189. prevWriter := log.Writer()
  190. log.SetFlags(0)
  191. log.SetPrefix("")
  192. base := global.Load()
  193. if base == nil {
  194. base = zap.NewNop()
  195. }
  196. log.SetOutput(newStdLogBridge(base.Named("stdlog")))
  197. stdLogUndo = func() {
  198. log.SetOutput(prevWriter)
  199. log.SetFlags(prevFlags)
  200. log.SetPrefix(prevPrefix)
  201. }
  202. }
  203. func bridgeSlogLocked() {
  204. base := global.Load()
  205. if base == nil {
  206. base = zap.NewNop()
  207. }
  208. slog.SetDefault(slog.New(newSlogZapHandler(base.Named("slog"))))
  209. }
  210. func buildLogger(options InitOptions) (*zap.Logger, zap.AtomicLevel, error) {
  211. level, _ := parseLevel(options.Level)
  212. atomic := zap.NewAtomicLevelAt(level)
  213. encoderCfg := zapcore.EncoderConfig{
  214. TimeKey: "time",
  215. LevelKey: "level",
  216. NameKey: "logger",
  217. CallerKey: "caller",
  218. MessageKey: "msg",
  219. StacktraceKey: "stacktrace",
  220. LineEnding: zapcore.DefaultLineEnding,
  221. EncodeLevel: zapcore.CapitalLevelEncoder,
  222. EncodeTime: zapcore.ISO8601TimeEncoder,
  223. EncodeDuration: zapcore.MillisDurationEncoder,
  224. EncodeCaller: zapcore.ShortCallerEncoder,
  225. }
  226. var enc zapcore.Encoder
  227. if options.Format == "console" {
  228. enc = zapcore.NewConsoleEncoder(encoderCfg)
  229. } else {
  230. enc = zapcore.NewJSONEncoder(encoderCfg)
  231. }
  232. sinkCore := newSinkCore()
  233. cores := make([]zapcore.Core, 0, 3)
  234. if options.Output.ToStdout {
  235. infoPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
  236. return lvl >= atomic.Level() && lvl < zapcore.WarnLevel
  237. })
  238. errPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
  239. return lvl >= atomic.Level() && lvl >= zapcore.WarnLevel
  240. })
  241. cores = append(cores, zapcore.NewCore(enc, zapcore.Lock(os.Stdout), infoPriority))
  242. cores = append(cores, zapcore.NewCore(enc, zapcore.Lock(os.Stderr), errPriority))
  243. }
  244. if options.Output.ToFile {
  245. fileCore, filePath, fileErr := buildFileCore(enc, atomic, options)
  246. if fileErr != nil {
  247. _, _ = fmt.Fprintf(os.Stderr, "time=%s level=WARN msg=\"日志文件输出初始化失败,降级为仅标准输出\" path=%s err=%v\n",
  248. time.Now().Format(time.RFC3339Nano),
  249. filePath,
  250. fileErr,
  251. )
  252. } else {
  253. cores = append(cores, fileCore)
  254. }
  255. }
  256. if len(cores) == 0 {
  257. cores = append(cores, zapcore.NewCore(enc, zapcore.Lock(os.Stdout), atomic))
  258. }
  259. core := zapcore.NewTee(cores...)
  260. if options.Sampling.Enabled {
  261. core = zapcore.NewSamplerWithOptions(core, samplingTick(), options.Sampling.Initial, options.Sampling.Thereafter)
  262. }
  263. core = sinkCore.Wrap(core)
  264. stacktraceLevel, _ := parseStacktraceLevel(options.StacktraceLevel)
  265. zapOpts := make([]zap.Option, 0, 5)
  266. if options.Caller {
  267. zapOpts = append(zapOpts, zap.AddCaller())
  268. }
  269. if stacktraceLevel <= zapcore.FatalLevel {
  270. zapOpts = append(zapOpts, zap.AddStacktrace(stacktraceLevel))
  271. }
  272. logger := zap.New(core, zapOpts...).With(
  273. zap.String("service", options.ServiceName),
  274. zap.String("env", options.Environment),
  275. )
  276. return logger, atomic, nil
  277. }
  278. func buildFileCore(enc zapcore.Encoder, atomic zap.AtomicLevel, options InitOptions) (zapcore.Core, string, error) {
  279. filePath := options.Output.FilePath
  280. if strings.TrimSpace(filePath) == "" {
  281. filePath = resolveLogFilePath("")
  282. }
  283. dir := filepath.Dir(filePath)
  284. if err := os.MkdirAll(dir, 0o755); err != nil {
  285. return nil, filePath, err
  286. }
  287. lj := &lumberjack.Logger{
  288. Filename: filePath,
  289. MaxSize: options.Rotation.MaxSizeMB,
  290. MaxBackups: options.Rotation.MaxBackups,
  291. MaxAge: options.Rotation.MaxAgeDays,
  292. Compress: options.Rotation.Compress,
  293. LocalTime: options.Rotation.LocalTime,
  294. }
  295. return zapcore.NewCore(enc, zapcore.AddSync(lj), atomic), filePath, nil
  296. }
  297. type sinkCore struct {
  298. core zapcore.Core
  299. fields []zapcore.Field
  300. }
  301. func newSinkCore() *sinkCore {
  302. return &sinkCore{}
  303. }
  304. func (s *sinkCore) Wrap(core zapcore.Core) zapcore.Core {
  305. cp := *s
  306. cp.core = core
  307. return &cp
  308. }
  309. func (s *sinkCore) Enabled(level zapcore.Level) bool {
  310. return s.core.Enabled(level)
  311. }
  312. func (s *sinkCore) With(fields []zapcore.Field) zapcore.Core {
  313. nextFields := append([]zapcore.Field{}, s.fields...)
  314. nextFields = append(nextFields, fields...)
  315. return &sinkCore{
  316. core: s.core.With(fields),
  317. fields: nextFields,
  318. }
  319. }
  320. func (s *sinkCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
  321. // Delegate to inner core (tee) so each sub-core's level enabler is respected.
  322. // Then add ourselves for sink forwarding only.
  323. ce = s.core.Check(entry, ce)
  324. if ce != nil {
  325. ce = ce.AddCore(entry, s)
  326. }
  327. return ce
  328. }
  329. func (s *sinkCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
  330. // Only handle sink forwarding — the inner cores write via their own
  331. // Write methods (added to CheckedEntry by s.core.Check above).
  332. sink := loadSink()
  333. if sink == nil {
  334. return nil
  335. }
  336. enc := zapcore.NewMapObjectEncoder()
  337. for _, f := range s.fields {
  338. f.AddTo(enc)
  339. }
  340. for _, f := range fields {
  341. f.AddTo(enc)
  342. }
  343. event := &LogEvent{
  344. Time: entry.Time,
  345. Level: strings.ToLower(entry.Level.String()),
  346. Component: entry.LoggerName,
  347. Message: entry.Message,
  348. LoggerName: entry.LoggerName,
  349. Fields: enc.Fields,
  350. }
  351. sink.WriteLogEvent(event)
  352. return nil
  353. }
  354. func (s *sinkCore) Sync() error {
  355. return s.core.Sync()
  356. }
  357. type stdLogBridge struct {
  358. logger *zap.Logger
  359. }
  360. func newStdLogBridge(l *zap.Logger) io.Writer {
  361. if l == nil {
  362. l = zap.NewNop()
  363. }
  364. return &stdLogBridge{logger: l}
  365. }
  366. func (b *stdLogBridge) Write(p []byte) (int, error) {
  367. msg := normalizeStdLogMessage(string(p))
  368. if msg == "" {
  369. return len(p), nil
  370. }
  371. level := inferStdLogLevel(msg)
  372. entry := b.logger.WithOptions(zap.AddCallerSkip(4))
  373. switch level {
  374. case LevelDebug:
  375. entry.Debug(msg, zap.Bool("legacy_stdlog", true))
  376. case LevelWarn:
  377. entry.Warn(msg, zap.Bool("legacy_stdlog", true))
  378. case LevelError, LevelFatal:
  379. entry.Error(msg, zap.Bool("legacy_stdlog", true))
  380. default:
  381. entry.Info(msg, zap.Bool("legacy_stdlog", true))
  382. }
  383. return len(p), nil
  384. }
  385. func normalizeStdLogMessage(raw string) string {
  386. msg := strings.TrimSpace(strings.ReplaceAll(raw, "\n", " "))
  387. if msg == "" {
  388. return ""
  389. }
  390. return strings.Join(strings.Fields(msg), " ")
  391. }
  392. func inferStdLogLevel(msg string) Level {
  393. lower := strings.ToLower(strings.TrimSpace(msg))
  394. if lower == "" {
  395. return LevelInfo
  396. }
  397. if strings.HasPrefix(lower, "[debug]") || strings.HasPrefix(lower, "debug:") {
  398. return LevelDebug
  399. }
  400. if strings.HasPrefix(lower, "[warn]") || strings.HasPrefix(lower, "[warning]") || strings.HasPrefix(lower, "warn:") || strings.HasPrefix(lower, "warning:") {
  401. return LevelWarn
  402. }
  403. if strings.HasPrefix(lower, "[error]") || strings.HasPrefix(lower, "error:") || strings.HasPrefix(lower, "fatal:") || strings.HasPrefix(lower, "panic:") {
  404. return LevelError
  405. }
  406. if strings.Contains(lower, " failed") || strings.Contains(lower, "error") || strings.Contains(lower, "panic") || strings.Contains(lower, "fatal") {
  407. return LevelError
  408. }
  409. if strings.Contains(lower, "warning") || strings.Contains(lower, "warn") || strings.Contains(lower, " queue full") || strings.Contains(lower, "fallback") {
  410. return LevelWarn
  411. }
  412. return LevelInfo
  413. }
  414. // LegacyPrintf 用于平滑迁移历史的 printf 风格日志到结构化 logger。
  415. func LegacyPrintf(component, format string, args ...any) {
  416. msg := normalizeStdLogMessage(fmt.Sprintf(format, args...))
  417. if msg == "" {
  418. return
  419. }
  420. initialized := global.Load() != nil
  421. if !initialized {
  422. // 在日志系统未初始化前,回退到标准库 log,避免测试/工具链丢日志。
  423. log.Print(msg)
  424. return
  425. }
  426. l := L()
  427. if component != "" {
  428. l = l.With(zap.String("component", component))
  429. }
  430. l = l.WithOptions(zap.AddCallerSkip(1))
  431. switch inferStdLogLevel(msg) {
  432. case LevelDebug:
  433. l.Debug(msg, zap.Bool("legacy_printf", true))
  434. case LevelWarn:
  435. l.Warn(msg, zap.Bool("legacy_printf", true))
  436. case LevelError, LevelFatal:
  437. l.Error(msg, zap.Bool("legacy_printf", true))
  438. default:
  439. l.Info(msg, zap.Bool("legacy_printf", true))
  440. }
  441. }
  442. type contextKey string
  443. const loggerContextKey contextKey = "ctx_logger"
  444. func IntoContext(ctx context.Context, l *zap.Logger) context.Context {
  445. if ctx == nil {
  446. ctx = context.Background()
  447. }
  448. if l == nil {
  449. l = L()
  450. }
  451. return context.WithValue(ctx, loggerContextKey, l)
  452. }
  453. func FromContext(ctx context.Context) *zap.Logger {
  454. if ctx == nil {
  455. return L()
  456. }
  457. if l, ok := ctx.Value(loggerContextKey).(*zap.Logger); ok && l != nil {
  458. return l
  459. }
  460. return L()
  461. }