123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- package logger
- import (
- "context"
- "errors"
- "io"
- "log/slog"
- "os"
- "slices"
- "sync"
- "time"
- )
- // LevelCritical is a log level for fatal errors
- const LevelCritical = slog.LevelError + 8
- // Format represents the log format
- type Format int
- const (
- // FormatStructured is a key=value structured format
- FormatStructured Format = iota
- // FormatPretty is a human-readable format with colorization
- FormatPretty
- // FormatJSON is a JSON format
- FormatJSON
- // FormatGCP is a JSON format for Google Cloud Platform
- FormatGCP
- )
- // attrGroup represents a named group of attributes.
- //
- // Both the group name and the attributes are optional.
- // Non-empty name means new nested group.
- type attrGroup struct {
- name string
- attrs []slog.Attr
- }
- // Hook is an interface that defines a log hook.
- type Hook interface {
- // Enabled checks if the hook is enabled for the given log level.
- Enabled(lvl slog.Level) bool
- // Fire is a function that gets called on log events.
- //
- // The slice provided in the msg parameter contains the formatted log message,
- // followed by a newline character.
- // It is guaranteed to be available for the duration of the hook call.
- // The hook should not modify the contents of the msg slice except for appending.
- Fire(time time.Time, lvl slog.Level, msg []byte) error
- }
- // Handler is an implementation of [slog.Handler] with support for hooks.
- type Handler struct {
- out io.Writer
- config *Config
- mu *sync.Mutex // Mutex is shared between all instances
- groups []attrGroup
- hooks []Hook
- }
- // NewHandler creates a new [Handler] instance.
- func NewHandler(out io.Writer, config *Config) *Handler {
- return &Handler{
- out: out,
- config: config,
- mu: new(sync.Mutex),
- }
- }
- // AddHook adds a new hook to the handler.
- func (h *Handler) AddHook(hook Hook) {
- if hook == nil {
- return
- }
- h.mu.Lock()
- defer h.mu.Unlock()
- h.hooks = append(h.hooks, hook)
- }
- // Enabled checks if the given log level is enabled.
- func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool {
- if level >= h.config.Level.Level() {
- return true
- }
- for _, hook := range h.hooks {
- if hook.Enabled(level) {
- return true
- }
- }
- return false
- }
- // withGroup returns a new handler with the given attribute group added.
- func (h *Handler) withGroup(group attrGroup) *Handler {
- h2 := *h
- h2.groups = append(slices.Clip(h.groups), group)
- h2.hooks = slices.Clip(h.hooks)
- return &h2
- }
- // WithAttrs returns a new handler with the given attributes added.
- func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
- if len(attrs) == 0 {
- return h
- }
- return h.withGroup(attrGroup{
- name: "",
- attrs: attrs,
- })
- }
- // WithGroup returns a new handler with the given group name added.
- func (h *Handler) WithGroup(name string) slog.Handler {
- if name == "" {
- return h
- }
- return h.withGroup(attrGroup{
- name: name,
- attrs: nil,
- })
- }
- // Handle processes a log record.
- func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
- buf := newBuffer()
- defer func() {
- buf.free()
- }()
- h.format(r, buf)
- h.mu.Lock()
- defer h.mu.Unlock()
- var errs []error
- // Write log entry to output
- _, err := h.out.Write(*buf)
- if err != nil {
- errs = append(errs, err)
- }
- // Fire hooks
- for _, hook := range h.hooks {
- if !hook.Enabled(r.Level) {
- continue
- }
- if err = hook.Fire(r.Time, r.Level, slices.Clip(*buf)); err != nil {
- errs = append(errs, err)
- }
- }
- // If writing to output or firing hooks returned errors,
- // join them, write to STDERR, and return
- if err = h.joinErrors(errs); err != nil {
- h.writeError(err)
- return err
- }
- return nil
- }
- // format formats a log record and writes it to the buffer.
- func (h *Handler) format(r slog.Record, buf *buffer) {
- groups := h.groups
- // If there are no attributes in the record itself,
- // remove empty groups from the end
- if r.NumAttrs() == 0 {
- for len(groups) > 0 && len(groups[len(groups)-1].attrs) == 0 {
- groups = groups[:len(groups)-1]
- }
- }
- // Format the log record according to the format specified in options
- switch h.config.Format {
- case FormatPretty:
- newFormatterPretty(groups, buf).format(r)
- case FormatJSON:
- newFormatterJSON(groups, buf, false).format(r)
- case FormatGCP:
- newFormatterJSON(groups, buf, true).format(r)
- default:
- newFormatterStructured(groups, buf).format(r)
- }
- // Add line break after each log entry
- buf.append('\n')
- }
- func (h *Handler) joinErrors(errs []error) error {
- if len(errs) == 0 {
- return nil
- }
- if len(errs) == 1 {
- return errs[0]
- }
- return errors.Join(errs...)
- }
- // writeError writes a logging error message to STDERR.
- func (h *Handler) writeError(err error) {
- buf := newBuffer()
- defer func() {
- buf.free()
- }()
- r := slog.NewRecord(time.Now(), slog.LevelError, "An error occurred during logging", 0)
- r.Add("error", err)
- h.format(r, buf)
- _, _ = os.Stderr.Write(*buf)
- }
|