logging.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. // Package logging provides LocalGo's structured logging wrapper around
  2. // log/slog. It exposes the same printf-style surface as the previous zap
  3. // sugared logger so call sites only depend on this package.
  4. package logging
  5. import (
  6. "context"
  7. "fmt"
  8. "io"
  9. "log/slog"
  10. "os"
  11. "path/filepath"
  12. "strings"
  13. )
  14. var (
  15. globalLogger *Logger
  16. globalSugar *Logger
  17. )
  18. // ANSI colour codes
  19. const (
  20. colourReset = "\033[0m"
  21. colourRed = "\033[31m"
  22. colourYellow = "\033[33m"
  23. colourCyan = "\033[36m"
  24. colourGrey = "\033[90m"
  25. )
  26. // Logger wraps *slog.Logger and mirrors the zap SugaredLogger method surface.
  27. type Logger struct {
  28. l *slog.Logger
  29. }
  30. // Init initialises the global slog logger.
  31. //
  32. // - verbose: enable debug-level output and also log to stdout
  33. // - jsonFmt: output newline-delimited JSON instead of human-readable text
  34. // - noColor: disable ANSI color escape sequences in log output
  35. func Init(verbose, jsonFmt, noColor bool) *Logger {
  36. level := slog.LevelInfo
  37. if verbose {
  38. level = slog.LevelDebug
  39. }
  40. stateDir := ""
  41. if xdgState := os.Getenv("XDG_STATE_HOME"); xdgState != "" {
  42. stateDir = filepath.Join(xdgState, "localgo")
  43. } else if home, err := os.UserHomeDir(); err == nil {
  44. stateDir = filepath.Join(home, ".local", "state", "localgo")
  45. }
  46. var fileWs io.Writer
  47. if stateDir != "" {
  48. os.MkdirAll(stateDir, 0700)
  49. logPath := filepath.Join(stateDir, "app.log")
  50. if f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600); err == nil {
  51. fileWs = f
  52. }
  53. }
  54. if fileWs == nil {
  55. fileWs = os.Stderr
  56. }
  57. opts := &slog.HandlerOptions{Level: level}
  58. if jsonFmt {
  59. opts.ReplaceAttr = func(_ []string, a slog.Attr) slog.Attr {
  60. if a.Key == slog.LevelKey {
  61. if l, ok := a.Value.Any().(slog.Level); ok {
  62. a.Value = slog.StringValue(strings.ToLower(l.String()))
  63. }
  64. }
  65. return a
  66. }
  67. fileHandler := slog.NewJSONHandler(fileWs, opts)
  68. if verbose {
  69. consoleHandler := newConsoleHandler(level, noColor)
  70. return setGlobal(slog.New(multiHandler{handlers: []slog.Handler{fileHandler, consoleHandler}}))
  71. }
  72. return setGlobal(slog.New(fileHandler))
  73. }
  74. fileHandler := slog.NewTextHandler(fileWs, opts)
  75. if verbose {
  76. consoleHandler := newConsoleHandler(level, noColor)
  77. return setGlobal(slog.New(multiHandler{handlers: []slog.Handler{fileHandler, consoleHandler}}))
  78. }
  79. return setGlobal(slog.New(fileHandler))
  80. }
  81. func setGlobal(l *slog.Logger) *Logger {
  82. globalLogger = &Logger{l: l}
  83. globalSugar = globalLogger
  84. return globalSugar
  85. }
  86. // NewQuiet returns a no-op logger that discards all output.
  87. func NewQuiet() *Logger {
  88. return &Logger{l: slog.New(slog.DiscardHandler)}
  89. }
  90. // Global returns the global logger, or a no-op if Init has not been called.
  91. func Global() *Logger {
  92. if globalSugar != nil {
  93. return globalSugar
  94. }
  95. return NewQuiet()
  96. }
  97. func (g *Logger) Infof(format string, a ...any) { g.l.Info(fmt.Sprintf(format, a...)) }
  98. func (g *Logger) Warnf(format string, a ...any) { g.l.Warn(fmt.Sprintf(format, a...)) }
  99. func (g *Logger) Errorf(format string, a ...any) { g.l.Error(fmt.Sprintf(format, a...)) }
  100. func (g *Logger) Debugf(format string, a ...any) { g.l.Debug(fmt.Sprintf(format, a...)) }
  101. func (g *Logger) Info(m string) { g.l.Info(m) }
  102. func (g *Logger) Warn(m string) { g.l.Warn(m) }
  103. func (g *Logger) Error(m string) { g.l.Error(m) }
  104. func (g *Logger) Debug(m string) { g.l.Debug(m) }
  105. func (g *Logger) Infow(m string, kv ...any) { g.l.Info(m, kv...) }
  106. func (g *Logger) Warnw(m string, kv ...any) { g.l.Warn(m, kv...) }
  107. func (g *Logger) Errorw(m string, kv ...any) { g.l.Error(m, kv...) }
  108. func (g *Logger) Debugw(m string, kv ...any) { g.l.Debug(m, kv...) }
  109. // multiHandler fans records out to several slog handlers.
  110. type multiHandler struct {
  111. handlers []slog.Handler
  112. }
  113. func (m multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
  114. for _, h := range m.handlers {
  115. if h.Enabled(ctx, level) {
  116. return true
  117. }
  118. }
  119. return false
  120. }
  121. func (m multiHandler) Handle(ctx context.Context, r slog.Record) error {
  122. for _, h := range m.handlers {
  123. if err := h.Handle(ctx, r.Clone()); err != nil {
  124. return err
  125. }
  126. }
  127. return nil
  128. }
  129. func (m multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
  130. hs := make([]slog.Handler, len(m.handlers))
  131. for i, h := range m.handlers {
  132. hs[i] = h.WithAttrs(attrs)
  133. }
  134. return multiHandler{handlers: hs}
  135. }
  136. func (m multiHandler) WithGroup(name string) slog.Handler {
  137. hs := make([]slog.Handler, len(m.handlers))
  138. for i, h := range m.handlers {
  139. hs[i] = h.WithGroup(name)
  140. }
  141. return multiHandler{handlers: hs}
  142. }
  143. // consoleHandler renders human-readable lines to stdout, e.g.
  144. //
  145. // 15:04:05 INF server started
  146. type consoleHandler struct {
  147. level slog.Level
  148. noColor bool
  149. w io.Writer
  150. }
  151. func newConsoleHandler(level slog.Level, noColor bool) *consoleHandler {
  152. return &consoleHandler{level: level, noColor: noColor, w: os.Stdout}
  153. }
  154. func (h *consoleHandler) Enabled(_ context.Context, level slog.Level) bool {
  155. return level >= h.level
  156. }
  157. func (h *consoleHandler) Handle(_ context.Context, r slog.Record) error {
  158. buf := make([]byte, 0, 128)
  159. buf = r.Time.AppendFormat(buf, "15:04:05")
  160. buf = append(buf, ' ', ' ')
  161. levelStr := r.Level.String()
  162. if !h.noColor {
  163. levelStr = colourLevel(r.Level) + levelStr + colourReset
  164. }
  165. buf = append(buf, levelStr...)
  166. buf = append(buf, ' ', ' ')
  167. buf = append(buf, r.Message...)
  168. if r.NumAttrs() > 0 {
  169. buf = append(buf, ' ')
  170. r.Attrs(func(a slog.Attr) bool {
  171. buf = append(buf, a.Key...)
  172. buf = append(buf, '=', '\'')
  173. buf = append(buf, fmt.Sprint(a.Value.Any())...)
  174. buf = append(buf, '\'', ' ')
  175. return true
  176. })
  177. }
  178. buf = append(buf, '\n')
  179. _, err := h.w.Write(buf)
  180. return err
  181. }
  182. func (h *consoleHandler) WithAttrs(_ []slog.Attr) slog.Handler {
  183. return h
  184. }
  185. func (h *consoleHandler) WithGroup(_ string) slog.Handler {
  186. return h
  187. }
  188. func colourLevel(l slog.Level) string {
  189. switch {
  190. case l >= slog.LevelError:
  191. return colourRed
  192. case l >= slog.LevelWarn:
  193. return colourYellow
  194. case l >= slog.LevelInfo:
  195. return colourCyan
  196. default:
  197. return colourGrey
  198. }
  199. }