logger.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. // Package logging wires the nebula runtime-reconfigurable slog handler used
  2. // by nebula.Main and the nebula CLI binaries. Callers build a logger with
  3. // NewLogger, then call ApplyConfig at startup and from a config reload
  4. // callback to push logging.level, logging.format, and
  5. // logging.disable_timestamp changes onto the logger without rebuilding it.
  6. package logging
  7. import (
  8. "context"
  9. "fmt"
  10. "io"
  11. "log/slog"
  12. "strings"
  13. "sync/atomic"
  14. "time"
  15. )
  16. // Config is the subset of *config.C that ApplyConfig reads. Declaring it
  17. // here keeps the logging package from depending on config directly, which
  18. // would cycle through the shared test helpers (test.NewLogger imports
  19. // logging, and config's tests import test). *config.C satisfies this
  20. // interface structurally with no adapter.
  21. type Config interface {
  22. GetString(key, def string) string
  23. GetBool(key string, def bool) bool
  24. }
  25. // LevelTrace is a custom slog level below Debug, used when logging.level is
  26. // "trace". slog has no builtin trace level; the value is one step below
  27. // slog.LevelDebug in slog's 4-point spacing.
  28. const LevelTrace = slog.Level(-8)
  29. // NewLogger returns a *slog.Logger whose level, format, and timestamp
  30. // emission can be reconfigured at runtime via ApplyConfig and the SSH debug
  31. // commands. The default configuration is info-level text output so log
  32. // calls made before ApplyConfig runs still produce output. Timestamps
  33. // follow slog's default RFC3339Nano format; set logging.disable_timestamp
  34. // in config to suppress them.
  35. //
  36. // ApplyConfig and the SSH commands discover the reconfig surface via
  37. // structural type-assertion on l.Handler(), so replacement implementations
  38. // (tests, platform-specific sinks) need only implement the subset of
  39. // {SetLevel(slog.Level), SetFormat(string) error, SetDisableTimestamp(bool)}
  40. // they care about. Callers that pass a plain *slog.Logger without these
  41. // methods get a silent no-op; reconfiguration is always opt-in.
  42. func NewLogger(w io.Writer) *slog.Logger {
  43. return slog.New(NewHandler(w))
  44. }
  45. // NewHandler builds the *Handler that NewLogger wraps. Exported for
  46. // platform-specific sinks (notably cmd/nebula-service/logs_windows.go)
  47. // that want to wrap the handler with extra behavior, such as tagging each
  48. // record with its Event Log severity, while still benefiting from all the
  49. // level / format / timestamp / WithAttrs machinery implemented here.
  50. func NewHandler(w io.Writer) *Handler {
  51. root := &handlerRoot{}
  52. root.level.Set(slog.LevelInfo)
  53. opts := &slog.HandlerOptions{Level: &root.level}
  54. return &Handler{
  55. root: root,
  56. text: slog.NewTextHandler(w, opts),
  57. json: slog.NewJSONHandler(w, opts),
  58. }
  59. }
  60. // handlerRoot carries the reconfiguration state shared by every logger
  61. // derived from a NewHandler call. All fields are consulted on the log
  62. // path and updated lock-free.
  63. type handlerRoot struct {
  64. level slog.LevelVar
  65. disableTimestamp atomic.Bool
  66. // jsonMode picks which of the pre-derived inner handlers Handler.Handle
  67. // dispatches to. Flipping it propagates instantly to every derived logger
  68. // without rebuilding or chain-replaying anything.
  69. jsonMode atomic.Bool
  70. }
  71. // Handler is the slog.Handler returned by NewHandler. It holds two
  72. // pre-derived slog handlers -- one text, one json -- both built from the
  73. // same accumulated WithAttrs/WithGroup state. Handle picks which one to
  74. // dispatch to based on handlerRoot.jsonMode, so a SetFormat call takes
  75. // effect immediately across the whole process without having to rebuild
  76. // any derived loggers.
  77. type Handler struct {
  78. root *handlerRoot
  79. text slog.Handler
  80. json slog.Handler
  81. }
  82. func (h *Handler) Enabled(_ context.Context, l slog.Level) bool {
  83. return h.root.level.Level() <= l
  84. }
  85. func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
  86. if h.root.disableTimestamp.Load() {
  87. r.Time = time.Time{}
  88. }
  89. if h.root.jsonMode.Load() {
  90. return h.json.Handle(ctx, r)
  91. }
  92. return h.text.Handle(ctx, r)
  93. }
  94. func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
  95. if len(attrs) == 0 {
  96. return h
  97. }
  98. return &Handler{
  99. root: h.root,
  100. text: h.text.WithAttrs(attrs),
  101. json: h.json.WithAttrs(attrs),
  102. }
  103. }
  104. func (h *Handler) WithGroup(name string) slog.Handler {
  105. if name == "" {
  106. return h
  107. }
  108. return &Handler{
  109. root: h.root,
  110. text: h.text.WithGroup(name),
  111. json: h.json.WithGroup(name),
  112. }
  113. }
  114. // SetLevel updates the effective log level. Propagates to every derived
  115. // logger via the shared LevelVar.
  116. func (h *Handler) SetLevel(level slog.Level) { h.root.level.Set(level) }
  117. // GetLevel reports the current log level.
  118. func (h *Handler) GetLevel() slog.Level { return h.root.level.Level() }
  119. // SetFormat flips the output format atomically. Valid formats are "text"
  120. // and "json". Every derived logger sees the new format on its next Handle
  121. // call; no rebuild or registration is required.
  122. func (h *Handler) SetFormat(format string) error {
  123. switch format {
  124. case "text":
  125. h.root.jsonMode.Store(false)
  126. case "json":
  127. h.root.jsonMode.Store(true)
  128. default:
  129. return fmt.Errorf("unknown log format `%s`. possible formats: %s", format, []string{"text", "json"})
  130. }
  131. return nil
  132. }
  133. // GetFormat reports the currently selected format name.
  134. func (h *Handler) GetFormat() string {
  135. if h.root.jsonMode.Load() {
  136. return "json"
  137. }
  138. return "text"
  139. }
  140. // SetDisableTimestamp toggles whether Handle zeroes r.Time before
  141. // dispatching (slog's builtin text/json handlers skip emitting the time
  142. // attribute on a zero time).
  143. func (h *Handler) SetDisableTimestamp(v bool) { h.root.disableTimestamp.Store(v) }
  144. // ApplyConfig reads logging.level, logging.format, and (optionally)
  145. // logging.disable_timestamp from c and applies them to l. The reconfig
  146. // surface is discovered via structural type-assertion on l.Handler(), so
  147. // foreign handlers silently opt out of whichever capabilities they do not
  148. // implement.
  149. //
  150. // nebula.Main does NOT call this function on your behalf; callers that want
  151. // config-driven log level / format / timestamp updates invoke it at
  152. // startup and register it as a reload callback themselves. This keeps the
  153. // library from mutating an embedder's logger without their say-so.
  154. func ApplyConfig(l *slog.Logger, c Config) error {
  155. h := l.Handler()
  156. lvl, err := ParseLevel(strings.ToLower(c.GetString("logging.level", "info")))
  157. if err != nil {
  158. return err
  159. }
  160. if ls, ok := h.(interface{ SetLevel(slog.Level) }); ok {
  161. ls.SetLevel(lvl)
  162. }
  163. format := strings.ToLower(c.GetString("logging.format", "text"))
  164. if fs, ok := h.(interface{ SetFormat(string) error }); ok {
  165. if err := fs.SetFormat(format); err != nil {
  166. return err
  167. }
  168. }
  169. if ts, ok := h.(interface{ SetDisableTimestamp(bool) }); ok {
  170. ts.SetDisableTimestamp(c.GetBool("logging.disable_timestamp", false))
  171. }
  172. return nil
  173. }
  174. // ParseLevel converts a config-string level name ("trace", "debug", "info",
  175. // "warn"/"warning", "error", "fatal"/"panic") to a slog.Level. "fatal" and
  176. // "panic" are accepted for backwards compatibility with pre-slog configs
  177. // and both map to slog.LevelError.
  178. func ParseLevel(s string) (slog.Level, error) {
  179. switch s {
  180. case "trace":
  181. return LevelTrace, nil
  182. case "debug":
  183. return slog.LevelDebug, nil
  184. case "info":
  185. return slog.LevelInfo, nil
  186. case "warn", "warning":
  187. return slog.LevelWarn, nil
  188. case "error":
  189. return slog.LevelError, nil
  190. case "fatal", "panic":
  191. return slog.LevelError, nil
  192. default:
  193. return 0, fmt.Errorf("not a valid logging level: %q", s)
  194. }
  195. }
  196. // LevelName returns a human-readable name for a slog.Level matching the
  197. // strings accepted by ParseLevel.
  198. func LevelName(l slog.Level) string {
  199. switch {
  200. case l <= LevelTrace:
  201. return "trace"
  202. case l <= slog.LevelDebug:
  203. return "debug"
  204. case l <= slog.LevelInfo:
  205. return "info"
  206. case l <= slog.LevelWarn:
  207. return "warn"
  208. default:
  209. return "error"
  210. }
  211. }