Logy|Docs

Custom handlers

Implement a handler for custom log destinations

Use a custom handler when built-in console, file, or syslog output is not the right destination. Common cases include audit streams, internal observability pipelines, test collectors, message queues, or platform-specific log transports.

Custom handlers receive a Record, decide whether it should be written, and send it to the destination they own.

Handler interface

handler.go
type Handler interface {
	Handle(record Record) error
	SetLevel(level Level)
	Level() Level
	SetEnabled(enabled bool)
	IsEnabled() bool
	IsLoggable(record Record) bool
}

Handle writes a single record. SetLevel and Level control the minimum level for that handler. SetEnabled and IsEnabled allow configuration to turn the handler on or off. IsLoggable is the guard that combines enabled state and level checks before expensive work happens.

The record contains the values a handler needs to format and route the message:

record.go
type Record struct {
	Time       time.Time
	Level      Level
	Message    string
	Context    context.Context
	LoggerName string
	StackTrace string
	Error      error
	Caller     Caller
}

Implement a handler

Keep the implementation small and push destination-specific details behind the handler. This example stores records in memory, which is useful for tests or for adapting the same shape to another sink.

audit_handler.go
type AuditHandler struct {
	enabled bool
	level   logy.Level
	records []logy.Record
}

func NewAuditHandler() *AuditHandler {
	return &AuditHandler{
		enabled: true,
		level:   logy.LevelInfo,
	}
}

func (h *AuditHandler) Handle(record logy.Record) error {
	if !h.IsLoggable(record) {
		return nil
	}

	h.records = append(h.records, record)

	return nil
}

func (h *AuditHandler) SetLevel(level logy.Level) {
	h.level = level
}

func (h *AuditHandler) Level() logy.Level {
	return h.level
}

func (h *AuditHandler) SetEnabled(enabled bool) {
	h.enabled = enabled
}

func (h *AuditHandler) IsEnabled() bool {
	return h.enabled
}

func (h *AuditHandler) IsLoggable(record logy.Record) bool {
	return h.enabled && record.Level >= h.level
}

Register the handler

Register the handler before configuration loads. The built-in names console, file, and syslog are reserved, so choose an application-specific name.

handler.go
func init() {
	logy.Register("audit", NewAuditHandler())
}

After registration, reference the handler by name.

main.go
err := logy.LoadConfig(&logy.Config{
	Handlers: logy.Handlers{"console", "audit"},
})
if err != nil {
	panic(err)
}

Custom handlers should keep Handle fast and predictable. If the destination is slow, buffer or queue work outside the logging call path.