mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-01-09 10:07:17 +02:00
265 lines
5.9 KiB
Go
265 lines
5.9 KiB
Go
package logger
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"sync"
|
|
|
|
"github.com/pocketbase/pocketbase/tools/types"
|
|
)
|
|
|
|
var _ slog.Handler = (*BatchHandler)(nil)
|
|
|
|
// BatchOptions are options for the BatchHandler.
|
|
type BatchOptions struct {
|
|
// WriteFunc processes the batched logs.
|
|
WriteFunc func(ctx context.Context, logs []*Log) error
|
|
|
|
// BeforeAddFunc is optional function that is invoked every time
|
|
// before a new log is added to the batch queue.
|
|
//
|
|
// Return false to skip adding the log into the batch queue.
|
|
BeforeAddFunc func(ctx context.Context, log *Log) bool
|
|
|
|
// Level reports the minimum level to log.
|
|
// Levels with lower levels are discarded.
|
|
// If nil, the Handler uses [slog.LevelInfo].
|
|
Level slog.Leveler
|
|
|
|
// BatchSize specifies how many logs to accumulate before calling WriteFunc.
|
|
// If not set or 0, fallback to 100 by default.
|
|
BatchSize int
|
|
}
|
|
|
|
// NewBatchHandler creates a slog compatible handler that writes JSON
|
|
// logs on batches (default to 100), using the given options.
|
|
//
|
|
// Panics if [BatchOptions.WriteFunc] is not defined.
|
|
//
|
|
// Example:
|
|
//
|
|
// l := slog.New(logger.NewBatchHandler(logger.BatchOptions{
|
|
// WriteFunc: func(ctx context.Context, logs []*Log) error {
|
|
// for _, l := range logs {
|
|
// fmt.Println(l.Level, l.Message, l.Data)
|
|
// }
|
|
// return nil
|
|
// }
|
|
// }))
|
|
// l.Info("Example message", "title", "lorem ipsum")
|
|
func NewBatchHandler(options BatchOptions) *BatchHandler {
|
|
h := &BatchHandler{
|
|
mux: &sync.Mutex{},
|
|
options: &options,
|
|
}
|
|
|
|
if h.options.WriteFunc == nil {
|
|
panic("options.WriteFunc must be set")
|
|
}
|
|
|
|
if h.options.Level == nil {
|
|
h.options.Level = slog.LevelInfo
|
|
}
|
|
|
|
if h.options.BatchSize == 0 {
|
|
h.options.BatchSize = 100
|
|
}
|
|
|
|
h.logs = make([]*Log, 0, h.options.BatchSize)
|
|
|
|
return h
|
|
}
|
|
|
|
// BatchHandler is a slog handler that writes records on batches.
|
|
//
|
|
// The log records attributes are formatted in JSON.
|
|
//
|
|
// Requires the [BatchOptions.WriteFunc] option to be defined.
|
|
type BatchHandler struct {
|
|
mux *sync.Mutex
|
|
parent *BatchHandler
|
|
options *BatchOptions
|
|
group string
|
|
attrs []slog.Attr
|
|
logs []*Log
|
|
}
|
|
|
|
// Enabled reports whether the handler handles records at the given level.
|
|
//
|
|
// The handler ignores records whose level is lower.
|
|
func (h *BatchHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
|
return level >= h.options.Level.Level()
|
|
}
|
|
|
|
// WithGroup returns a new BatchHandler that starts a group.
|
|
//
|
|
// All logger attributes will be resolved under the specified group name.
|
|
func (h *BatchHandler) WithGroup(name string) slog.Handler {
|
|
if name == "" {
|
|
return h
|
|
}
|
|
|
|
return &BatchHandler{
|
|
parent: h,
|
|
mux: h.mux,
|
|
options: h.options,
|
|
group: name,
|
|
}
|
|
}
|
|
|
|
// WithAttrs returns a new BatchHandler loaded with the specified attributes.
|
|
func (h *BatchHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
if len(attrs) == 0 {
|
|
return h
|
|
}
|
|
|
|
return &BatchHandler{
|
|
parent: h,
|
|
mux: h.mux,
|
|
options: h.options,
|
|
attrs: attrs,
|
|
}
|
|
}
|
|
|
|
// Handle formats the slog.Record argument as JSON object and adds it
|
|
// to the batch queue.
|
|
//
|
|
// If the batch queue threshold has been reached, the WriteFunc option
|
|
// is invoked with the accumulated logs which in turn will reset the batch queue.
|
|
func (h *BatchHandler) Handle(ctx context.Context, r slog.Record) error {
|
|
if h.group != "" {
|
|
h.mux.Lock()
|
|
attrs := make([]any, 0, len(h.attrs)+r.NumAttrs())
|
|
for _, a := range h.attrs {
|
|
attrs = append(attrs, a)
|
|
}
|
|
h.mux.Unlock()
|
|
|
|
r.Attrs(func(a slog.Attr) bool {
|
|
attrs = append(attrs, a)
|
|
return true
|
|
})
|
|
|
|
r = slog.NewRecord(r.Time, r.Level, r.Message, r.PC)
|
|
r.AddAttrs(slog.Group(h.group, attrs...))
|
|
} else if len(h.attrs) > 0 {
|
|
r = r.Clone()
|
|
|
|
h.mux.Lock()
|
|
r.AddAttrs(h.attrs...)
|
|
h.mux.Unlock()
|
|
}
|
|
|
|
if h.parent != nil {
|
|
return h.parent.Handle(ctx, r)
|
|
}
|
|
|
|
data := make(map[string]any, r.NumAttrs())
|
|
|
|
r.Attrs(func(a slog.Attr) bool {
|
|
if err := h.resolveAttr(data, a); err != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
log := &Log{
|
|
Time: r.Time,
|
|
Level: r.Level,
|
|
Message: r.Message,
|
|
Data: types.JsonMap(data),
|
|
}
|
|
|
|
if h.options.BeforeAddFunc != nil && !h.options.BeforeAddFunc(ctx, log) {
|
|
return nil
|
|
}
|
|
|
|
h.mux.Lock()
|
|
h.logs = append(h.logs, log)
|
|
totalLogs := len(h.logs)
|
|
h.mux.Unlock()
|
|
|
|
if totalLogs >= h.options.BatchSize {
|
|
if err := h.WriteAll(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetLevel updates the handler options level to the specified one.
|
|
func (h *BatchHandler) SetLevel(level slog.Level) {
|
|
h.mux.Lock()
|
|
h.options.Level = level
|
|
h.mux.Unlock()
|
|
}
|
|
|
|
// WriteAll writes all accumulated Log entries and resets the batch queue.
|
|
func (h *BatchHandler) WriteAll(ctx context.Context) error {
|
|
if h.parent != nil {
|
|
// invoke recursively the parent level handler since the most
|
|
// top level one is holding the logs queue.
|
|
return h.parent.WriteAll(ctx)
|
|
}
|
|
|
|
h.mux.Lock()
|
|
|
|
totalLogs := len(h.logs)
|
|
|
|
// no logs to write
|
|
if totalLogs == 0 {
|
|
h.mux.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// create a copy of the logs slice to prevent blocking during write
|
|
logs := make([]*Log, totalLogs)
|
|
copy(logs, h.logs)
|
|
h.logs = h.logs[:0] // reset
|
|
|
|
h.mux.Unlock()
|
|
|
|
return h.options.WriteFunc(ctx, logs)
|
|
}
|
|
|
|
// resolveAttr writes attr into data.
|
|
func (h *BatchHandler) resolveAttr(data map[string]any, attr slog.Attr) error {
|
|
// ensure that the attr value is resolved before doing anything else
|
|
attr.Value = attr.Value.Resolve()
|
|
|
|
if attr.Equal(slog.Attr{}) {
|
|
return nil // ignore empty attrs
|
|
}
|
|
|
|
switch attr.Value.Kind() {
|
|
case slog.KindGroup:
|
|
attrs := attr.Value.Group()
|
|
if len(attrs) == 0 {
|
|
return nil // ignore empty groups
|
|
}
|
|
|
|
// create a submap to wrap the resolved group attributes
|
|
groupData := make(map[string]any, len(attrs))
|
|
|
|
for _, subAttr := range attrs {
|
|
h.resolveAttr(groupData, subAttr)
|
|
}
|
|
|
|
if len(groupData) > 0 {
|
|
data[attr.Key] = groupData
|
|
}
|
|
default:
|
|
v := attr.Value.Any()
|
|
|
|
if err, ok := v.(error); ok {
|
|
data[attr.Key] = err.Error()
|
|
} else {
|
|
data[attr.Key] = v
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|