fologger

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 26, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

README

Logger

Go License API Coverage

Overview

Fan-out logger with pluggable output sinks. Main properties:

  1. Each sink runs in its own goroutine, so a slow sink (e.g. saturated DB) doesn't delay a fast one (e.g. stdout).
  2. Named logger (Named(service string)) is a wrapper that holds a pointer to the logger instance and stamps every event with a service tag without allocating a new channel pipeline.
  3. If the main dispatch channel's buffer is full - falls back to a Fallback io.Writer rather than stalling the caller. Same strategy on event fan-out to sink workers' channels.
  4. Dropped/failed log writes are handled with configurable callbacks (onDropped, onWriteFailed, onFallbackFailed) - attach metrics, logging, or noops as needed.
  5. Graceful shutdown drains all in-flight events before closing sinks, with an optional deadline.

Log Event Flow

Log Event flow diagram

Core Types

Logger interface
type Logger interface {
    Debug(ctx context.Context, msg string, fields map[string]any)
    Info(ctx context.Context, msg string, fields map[string]any)
    Warning(ctx context.Context, msg string, fields map[string]any)
    Error(ctx context.Context, msg string, fields map[string]any)
    Critical(ctx context.Context, msg string, fields map[string]any)
}
LogEvent
Field Type Description
Level LoggingLevel Severity level
Msg string Log message
Data map[string]any Arbitrary structured fields (copied on enqueue)
Time time.Time Event timestamp (Config.Now(), injectable for tests)
TraceID string Extracted from context, configured with Config.TraceKey; omitted if absent
Service string Set by Named()
Levels
Constant String
LevelDebug DEBUG
LevelInfo INFO
LevelWarning WARNING
LevelError ERROR
LevelCritical CRITICAL

Creating a Logger

The logger is created via NewAsyncLogger with a Config struct and a slice of SinkWithConfig wrappers.

Config
type Config struct {
    EventBuffer      int                // capacity of the main dispatch channel (default: 4096)
    Fallback         io.Writer          // target when a channel is full (default: os.Stderr)
    TraceKey         any                // context key used to extract a trace/request ID
    OnDropped        func(chName string) // called when an event is dropped due to a full channel (default is noop)
    OnFallbackFailed func()              // called when the fallback writer itself returns an error (default is noop)
    Now              func() time.Time    // timestamp function (default: time.Now)
}

DefaultConfig:

var DefaultConfig = Config{
    EventBuffer:      4096,
    Fallback:         os.Stderr,
    TraceKey:         nil,
    OnDropped:        func(_ string) {},
    OnFallbackFailed: func() {},
    Now:              time.Now,
}

Any field left at its zero value in a custom Config falls back to DefaultConfig.

SinkWithConfig

Each sink is paired with its own buffer capacity and an optional per-sink write-failure callback:

type SinkWithConfig struct {
    Sink         Sink
    EventBuffer  int                  // buffer size for this sink's channel
    OnWriteFailed func(chName string) // called when this sink's Write returns an error (default is noop)
}

OnWriteFailed is per-sink so different sinks can have different failure handling (e.g. one increments a metric counter, another sends an alert). If nil it defaults to a noop.

Example
import (
    "context"
    "os"
    "time"

    "codeberg.org/at375/fologger"
)

type ctxKey struct{}

func main() {
    // --- Config ---
    cfg := fologger.Config{
        EventBuffer: 4096,
        Fallback:    os.Stderr,
        TraceKey:    ctxKey{},
        OnDropped: func(sinkName string) {
            // e.g. increment a metric counter
        },
        OnFallbackFailed: func() {
            // e.g. increment a metric counter
        },
    }

    // --- Build sinks ---
    stdoutSink := fologger.NewStdoutSink("stdout", fologger.LevelDebug)

    fileSink, err := fologger.NewFileSink("file", "/var/log/myapp", fologger.LevelInfo)
    if err != nil {
        // handle error (e.g. directory not writable)
        panic(err)
    }

    sinks := []fologger.SinkWithConfig{
        {Sink: stdoutSink, EventBuffer: 64},
        {Sink: fileSink, EventBuffer: 128, OnWriteFailed: func(name string) {
            // e.g. increment a metric counter
        }},
    }

    // --- Create logger ---
    l := fologger.NewAsyncLogger(cfg, sinks)
    log := l.Named("my-service")

    ctx := context.WithValue(context.Background(), ctxKey{}, "trace-abc-123")
    log.Info(ctx, "started", map[string]any{"port": 8080})

    // --- Graceful shutdown ---
    timeoutCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := l.Shutdown(timeoutCtx); err != nil {
        // deadline exceeded
    }
}

Sinks

All sinks implement the Sink interface:

type Sink interface {
    Write(ctx context.Context, event LogEvent) error
    Format(LogEvent) string
    Close() error
    Name() string
}

Write is called from the sink's dedicated goroutine only - implementations do not need to be safe against concurrent Write calls (unless you need a direct Write call to the sink from outside the sinkWorker for some reason). Close is always called after the last Write has returned (guaranteed by sinkWorker waiting on run() to exit), so no Write + Close race is possible.

This means though, that Write should respect its context, because a Write that ignores cancellation will cause a deadlined shutdown to block until all Write calls for events left in the buffer complete naturally regardless of the context timeout.

Implementations' Close should flush any buffers and release resources.

Name is used to label sinks (e.g. for metrics or error reporting).

Format returns the string representation of a log event. Package provides DefaultFormatter.

StdoutSink

Writes human-readable lines to os.Stdout. Close is a noop.

s := fologger.NewStdoutSink("stdout", fologger.LevelInfo)
FileSink

Appends log lines to daily files under a configurable root directory, organized into subdirectories by year and month. Rotates automatically at midnight. Subdirectories are created on the first write of each month. Validates root directory writability at construction time with a probe write.

s, err := fologger.NewFileSink("file", "/var/log/app", fologger.LevelDebug)

Output layout:

<log_path>/2026/01/15.log
<log_path>/2026/01/16.log
<log_path>/2026/02/01.log

Log Format

Each sink has a Format(LogEvent) string method. The default implementation (DefaultFormatter) produces:

[YYYY-MM-DD HH:MM:SS,mmm] - LEVEL - service - message
  └─ {"field":"value",...}

The data line is omitted when there are no fields.


Backpressure & Fallback

The logger has two points where it can drop to the fallback writer rather than block:

  1. Dispatch channel full - the calling goroutine writes the event directly to the fallback and calls onDropped("dispatch").
  2. Sink channel full - the dispatch goroutine writes the event to the fallback and calls onDropped("<sink name>").

If the fallback write itself fails, onFallbackFailed() is called and a last-resort line is printed to os.Stderr.


Time Injection

Config.Now is a func() time.Time that defaults to time.Now.

cfg := fologger.Config{
    Now: func() time.Time {
        return time.Date(2026, 1, 15, 10, 30, 45, 123000000, time.UTC)
    },
}

Error Callbacks

In Config
Field Signature When called
OnDropped func(chName string) An event was dropped because a channel was full
OnFallbackFailed func() The fallback writer itself returned an error

Both are initialised to noops in DefaultConfig.

In SinkWithConfig
Field Signature When called
OnWriteFailed func(chName string) This sink's Write returned a non-nil error

Per-sink, so different sinks can have independent failure handling.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultConfig = Config{
	EventBuffer:      4096,
	Fallback:         os.Stderr,
	TraceKey:         nil,
	OnDropped:        func(_ string) {},
	OnFallbackFailed: func() {},
	Now:              time.Now,
}

Functions

func DefaultFormatter

func DefaultFormatter(e LogEvent) string

Types

type AsyncLogger

type AsyncLogger struct {
	// contains filtered or unexported fields
}

AsyncLogger fans events from a single dispatch channel to N sink workers concurrently. Never blocks callers - full channels trigger the fallback.

func NewAsyncLogger

func NewAsyncLogger(cfg Config, sinks []SinkWithConfig) *AsyncLogger

func (*AsyncLogger) Named

func (l *AsyncLogger) Named(service string) Logger

func (*AsyncLogger) Shutdown

func (l *AsyncLogger) Shutdown(ctx context.Context) error

Shutdown drains the logger and closes all sinks gracefully. Safe to call multiple times, only the first call has effect. Pass a context with a deadline to bound the shutdown time.

type Config

type Config struct {
	EventBuffer      int
	Fallback         io.Writer
	TraceKey         any
	OnDropped        func(chName string)
	OnFallbackFailed func()
	Now              func() time.Time
}

type FileSink

type FileSink struct {
	// contains filtered or unexported fields
}

func NewFileSink

func NewFileSink(name, logDir string, l LoggingLevel) (*FileSink, error)

NewFileSink opens (or creates/appends) the file at path and returns a reference to FileSink

func (*FileSink) Close

func (s *FileSink) Close() error

func (*FileSink) Format

func (s *FileSink) Format(e LogEvent) string

func (*FileSink) Name

func (s *FileSink) Name() string

func (*FileSink) Write

func (s *FileSink) Write(_ context.Context, e LogEvent) error

type LogEvent

type LogEvent struct {
	Level   LoggingLevel   `json:"level"`
	Msg     string         `json:"msg"`
	Data    map[string]any `json:"data,omitempty"`
	Time    time.Time      `json:"time"`
	TraceID string         `json:"trace_id,omitempty"`
	Service string         `json:"service,omitempty"`
}

type Logger

type Logger interface {
	Debug(ctx context.Context, msg string, fields map[string]any)
	Info(ctx context.Context, msg string, fields map[string]any)
	Warning(ctx context.Context, msg string, fields map[string]any)
	Error(ctx context.Context, msg string, fields map[string]any)
	Critical(ctx context.Context, msg string, fields map[string]any)
}

type LoggingLevel

type LoggingLevel int
const (
	LevelDebug LoggingLevel = iota
	LevelInfo
	LevelWarning
	LevelError
	LevelCritical
)

func (LoggingLevel) String

func (l LoggingLevel) String() string

type Sink

type Sink interface {
	Write(ctx context.Context, event LogEvent) error
	Close() error
	Format(LogEvent) string
	Name() string
}

`Write` is called from the sink's dedicated goroutine only - implementations do not need to be safe against concurrent `Write` calls (unless you need a direct `Write` call to the sink from outside the `sinkWorker` for some reason). `Close` is always called after the last `Write` has returned (guaranteed by `sinkWorker` waiting on `run()` to exit), so no `Write` + `Close` race is possible.

This means though, that `Write` should respect it's context, because a `Write` that ignores cancellation will cause deadlined shutdown to block until all `Write` calls for events left in buffer complete naturally regardless of the context timeout.

Close is called once during shutdown, after the sink's channel is drained. Implementations' Close should flush any buffers and release resources.

type SinkWithConfig

type SinkWithConfig struct {
	Sink        Sink
	EventBuffer int

	// If nil will deafault to noop
	OnWriteFailed func(chName string)
}

type StdoutSink

type StdoutSink struct {
	// contains filtered or unexported fields
}

func NewStdoutSink

func NewStdoutSink(name string, l LoggingLevel) *StdoutSink

func (*StdoutSink) Close

func (s *StdoutSink) Close() error

func (*StdoutSink) Format

func (s *StdoutSink) Format(e LogEvent) string

func (*StdoutSink) Name

func (s *StdoutSink) Name() string

func (*StdoutSink) Write

func (s *StdoutSink) Write(ctx context.Context, e LogEvent) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL