1
0
mirror of https://github.com/imgproxy/imgproxy.git synced 2025-12-23 22:11:10 +02:00
Files
imgproxy/logger/handler.go
2025-09-21 20:13:27 +06:00

226 lines
4.7 KiB
Go

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)
}