yall

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Mar 18, 2024 License: MIT Imports: 13 Imported by: 0

README

YALL

YALL is Yet Another Logging Library, a slog backend.

Introduction

slog has a convenient slog.Logger API, but the slog.Handler backend is cumbersome and hard to implement. The handler implementations included in the slog library attempt to do 3 different things at once:

  1. Keep ambient state introduced by WithAttrs and WithGroup.
  2. Format each slog.Record into its text representation.
  3. Send the formatted results to the log output.

As a result standard implementations are complicated and impossible to reuse or extend.

YALL embraces the architecture provided by the slog package but implements the three tasks mentioned above separately.

Formatter

YALL formatters are simple objects with a single function Append. Append takes all the same arguments as slog.Handler.Handle: a context.Context and a slog.Record, and a byte slice. Its job is to append the formatting results to the slice. This enables easy reuse of both existing and custom formatters in the system.

But this is not even the best part. The best part is, a Formatter does not have to necessarily format the whole record. A formatter can choose to specialize on a single attribute or a group of attributes. This allows to compose formatters in a very flexible way.

YALL provides a set of built-in formatters:

  • Time formats slog.Record.Time according to the specified format.
  • Level formats slog.Record.Level using slog.Level.String.
  • Source formats slog.Record.PC in long or short format.
  • Message formats slog.Record.Message with optional quoting.
  • TextAttrs formats slog.Record.Attrs in key=value format with optional value quoting.
  • Layout composes other formatters in a manner of fmt.Sprintf.
  • Conditional is similar to Layout for one argument which only produces output if the inner formatter result is non-empty.

Layout is where the real power of this design comes in. For example, here's a formatter which formats a record exactly how slog.TextHandler does it with source logging enabled:

l := &yall.Layout{
    Format: "time=%s level=%s source=%s msg=%s%s",
    Args: ``yall.Formatter{
        yall.Time{Layout: "2006-01-02T15:04:05.999Z07:00"},
        yall.Level{},
        yall.Source{},
        yall.Message{Quote: yall.QuoteSmart},
        yall.TextAttrs{Quote: yall.QuoteSmart},
    },
}

Here's another, simpler example, but this time the level is formatted to always take 5 characters aligned to the right, e.g. ERROR or INFO:

l := &yall.Layout{
    Format: "`%5s` %s%s",
    Args: ``yall.Formatter{
        yall.Level{},
        yall.Message{},
        yall.Conditional{
            Format: ":%s",
            Inner:  yall.TextAttrs{Quote: yall.QuoteSmart},
        },
    },
}

The DefaultFormat function creates a formatter to produce logs that look like

2020-11-22 12:34:56 INFO Long message foo=bar baz="quote me"

Sink

Sink is responsible for delivering log records to the destination, be it console, a file, or a remote server. In theory, a sink can interpret the records in any way it wants. In YALL, sinks are encouraged to use formatters to convert records to text.

The Sink interface is compatible with slog.Handler. This means that any existing handler can be used as a sink if desired, e.g. slog.Default().Handler().

YALL provides the following sinks:

  • FanOutSink broadcasts log records to any number of other sinks. The list of target sinks can be modified at run time.
  • WriterSink writes records formatted by any Formatter to any io.Writer.

Handler

YALL provides an implementation of slog.Handler which takes care of additional attributes and groups imposed by slog.Logger.With and slog.Logger.WithGroup. It updates each incoming slog.Record accordingly, and sends the final, completed records to a Sink which typically will be a FanOutSink, but can also be a WriterSink or even one of the existing slog handlers like slog.TextHandler.

Use the NewHandler function to create an instance of the handler.

Examples

Please see yall_test.go for some usage examples.

Documentation

Overview

Package yall is Yet Another Logging Library, a slog backend.

Introduction

slog has a convenient slog.Logger API, but the slog.Handler backend is cumbersome and hard to implement. The handler implementations included in the slog library attempt to do 3 different things at once:

  1. Keep ambient state introduced by WithAttrs and WithGroup.
  2. Format each slog.Record into its text representation.
  3. Send the formatted results to the log output.

As a result standard implementations are complicated and impossible to reuse or extend.

YALL embraces the architecture provided by the slog package but implements the three tasks mentioned above separately.

Formatter

YALL formatters are simple objects with a single function Append. Append takes all the same arguments as slog.Handler.Handle: a context.Context and a slog.Record, and a byte slice. Its job is to append the formatting results to the slice. This enables easy reuse of both existing and custom formatters in the system.

But this is not even the best part. The best part is, a Formatter does not have to necessarily format the whole record. A formatter can choose to specialize on a single attribute or a group of attributes. This allows to compose formatters in a very flexible way.

YALL provides a set of built-in formatters:

Layout is where the real power of this design comes in. For example, here's a formatter which formats a record exactly how slog.TextHandler does it with source logging enabled:

l := &yall.Layout{
	Format: "time=%s level=%s source=%s msg=%s%s",
	Args: []yall.Formatter{
		yall.Time{Layout: "2006-01-02T15:04:05.999Z07:00"},
		yall.Level{},
		yall.Source{},
		yall.Message{Quote: yall.QuoteSmart},
		yall.TextAttrs{Quote: yall.QuoteSmart},
	},
}

Here's another, simpler example, but this time the level is formatted to always take 5 characters aligned to the right, e.g. [ERROR] or [ INFO]:

l := &yall.Layout{
	Format: "[%5s] %s%s",
	Args: []yall.Formatter{
		yall.Level{},
		yall.Message{},
		yall.Conditional{
			Format: ":%s",
			Inner:  yall.TextAttrs{Quote: yall.QuoteSmart},
		},
	},
}

The DefaultFormat function creates a formatter to produce logs that look like

2020-11-22 12:34:56 INFO Long message foo=bar baz="quote me"

Sink

Sink is responsible for delivering log records to the destination, be it console, a file, or a remote server. In theory, a sink can interpret the records in any way it wants. In YALL, sinks are encouraged to use formatters to convert records to text.

The Sink interface is compatible with slog.Handler. This means that any existing handler can be used as a sink if desired, e.g. slog.Default().Handler().

YALL provides the following sinks:

  • FanOutSink broadcasts log records to any number of other sinks. The list of target sinks can be modified at run time.
  • WriterSink writes records formatted by any Formatter to any io.Writer.

Handler

YALL provides an implementation of slog.Handler which takes care of additional attributes and groups imposed by slog.Logger.With and slog.Logger.WithGroup. It updates each incoming slog.Record accordingly, and sends the final, completed records to a Sink which typically will be a FanOutSink, but can also be a WriterSink or even one of the existing slog handlers like slog.TextHandler.

Use the NewHandler function to create an instance of the handler.

Example
package main

import (
	"bytes"
	"github.com/snake-scaly/yall"
	"log/slog"
	"math"
	"os"
)

func main() {
	// This example configures YALL to log into two different targets,
	// with different formats and configurations.

	// Configure the STDOUT sink to log in "[  LVL] Message: args" format,
	// ignoring events with levels below Info, with smart quoting of arg values.
	stdoutSink := yall.WriterSink{
		Writer: os.Stdout,
		Level:  slog.LevelInfo,
		Format: &yall.Layout{
			Format: "[%5s] %s%s",
			Args: []yall.Formatter{
				yall.Level{},
				yall.Message{},
				yall.Conditional{
					Format: ":%s",
					Inner:  yall.TextAttrs{Quote: yall.QuoteSmart},
				},
			},
		},
	}

	// Configure an in-memory sink to log in a format similar to slog.TextHandler.
	// Accept events of all levels.
	memoryTarget := bytes.Buffer{}
	memorySink := yall.WriterSink{
		Writer: &memoryTarget,
		Level:  slog.Level(math.MinInt),
		Format: &yall.Layout{
			Format: "time=%s level=%s source=%s msg=%s%s",
			Args: []yall.Formatter{
				yall.Time{Layout: "2006-01-02T15:04:05.999Z07:00"},
				yall.Level{},
				yall.Source{},
				yall.Message{Quote: yall.QuoteSmart},
				yall.TextAttrs{Quote: yall.QuoteSmart},
			},
		},
	}

	// Create a FanOutSink to send logging events to both target sinks.
	fanOutSink := yall.NewFanOutSink(&stdoutSink, &memorySink)

	// Handler takes care of args and groups so that you don't have to.
	handler := yall.NewHandler(fanOutSink)

	// Finnally, all this plugs into the standard Logger.
	logger := slog.New(handler)
	logger.Info("Just a message")

	// Logger can be used as normal.
	logger2 := logger.With("outer", "exampli gratia").WithGroup("inner")
	logger2.Error("Message with args", "answer", 42)

}
Output:

[ INFO] Just a message
[ERROR] Message with args: inner.answer=42 outer="exampli gratia"

Index

Examples

Constants

View Source
const (
	QuoteNever  = QuoteType(iota) // Do not add quotes.
	QuoteAlways                   // Always quote using [strconv.Quote].
	QuoteSmart                    // Only quote empty strings and strings containing spaces and/or equal signs.
)

Variables

This section is empty.

Functions

func NewHandler

func NewHandler(sink Sink) slog.Handler

NewHandler creates an implementation of slog.Handler that sends logging events to a Sink.

The handler takes care of slog.Handler.WithAttrs and slog.Handler.WithGroup and always sends a complete slog.Record to the Sink. The Sink still needs to resolve and handle the attrs.

Types

type Conditional

type Conditional struct {
	Format string
	Inner  Formatter
}

Conditional is a Formatter that wraps the output of the Inner formatter in the fmt.Sprintf-like format only if Inner produces a non-empty string. Internally this works by passing Inner to fmt.Appendf as a []byte slice. A reasonable format will contain one format specifier, one of %s, %q, %x, or %X.

func (Conditional) Append

func (co Conditional) Append(b []byte, c context.Context, r slog.Record) []byte

type FanOutSink

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

FanOutSink is a Sink that broadcasts logging events to a dynamic list of other sinks.

func NewFanOutSink

func NewFanOutSink(sinks ...Sink) *FanOutSink

NewFanOutSink creates a FanOutSink instance with the given initial list of sinks.

func (*FanOutSink) AddSink

func (f *FanOutSink) AddSink(s Sink)

AddSink adds a Sink to the FanOutSink's list of sinks.

func (*FanOutSink) Enabled

func (f *FanOutSink) Enabled(ctx context.Context, level slog.Level) bool

func (*FanOutSink) Handle

func (f *FanOutSink) Handle(ctx context.Context, record slog.Record) error

func (*FanOutSink) RemoveSink

func (f *FanOutSink) RemoveSink(s Sink) bool

RemoveSink removes the first Sink that matches s from the FanOutSink's list of sinks.

type Formatter

type Formatter interface {
	// Append performs the formatting, appends the result to the given buffer as a sequence
	// of UTF8 bytes, and returns the modified buffer.
	// Append may be called concurrently from multiple goroutines.
	Append(b []byte, c context.Context, r slog.Record) []byte
}

Formatter creates a text representation of a slog.Record.

func DefaultFormat

func DefaultFormat() Formatter

DefaultFormat returns a Formatter that mimics the default log format of slog.

type Layout

type Layout struct {
	Format string
	Args   []Formatter
}

Layout is a Formatter that combines other formatters in the manner of fmt.Sprintf. Internally this works by passing Args to fmt.Appendf as []byte slices. Therefore only %s, %q, %x, and %X format specifiers make sense in the Format.

func (Layout) Append

func (l Layout) Append(b []byte, c context.Context, r slog.Record) []byte

type Level

type Level struct{}

Level is a Formatter that formats slog.Record.Level using slog.Level.String.

func (Level) Append

func (l Level) Append(b []byte, _ context.Context, r slog.Record) []byte

type Message

type Message struct {
	Quote QuoteType
}

Message is a Formatter that formats slog.Record.Message. Message is quoted according to Quote.

func (Message) Append

func (m Message) Append(b []byte, _ context.Context, r slog.Record) []byte

type QuoteType

type QuoteType int

QuoteType defines the type of string quotation.

type Sink

type Sink interface {
	Enabled(c context.Context, l slog.Level) bool
	Handle(c context.Context, r slog.Record) error
}

Sink receives complete logging events. Note that any slog.Handler can be used as a Sink, e.g.

var s Sink = slog.Default().Handler()

type Source

type Source struct {
	Short bool
}

Source is a Formatter that formats slog.Record.PC. Set Short to true to only print source file name without path.

func (Source) Append

func (s Source) Append(b []byte, _ context.Context, r slog.Record) []byte

type TextAttrs

type TextAttrs struct {
	Quote QuoteType
}

TextAttrs is a Formatter that formats slog.Record.Attrs as "key=value" pairs. Values are quoted according to Quote. When the result is non-empty, it includes a leading space.

func (TextAttrs) Append

func (t TextAttrs) Append(b []byte, _ context.Context, r slog.Record) []byte

type Time

type Time struct {
	Layout string
}

Time is a Formatter that formats slog.Record.Time according to Layout. See time.Layout and related constants for a list of existing formats and a discussion on how to create your own. The zero value formats with time.DateTime.

func (Time) Append

func (t Time) Append(b []byte, _ context.Context, r slog.Record) []byte

type WriterSink

type WriterSink struct {
	Writer io.Writer
	Level  slog.Leveler
	Format Formatter
	// contains filtered or unexported fields
}

WriterSink is a sink that writes logs to an io.Writer. Each log event is terminated with a new line and is written as a single write on the Writer.

func (*WriterSink) Enabled

func (s *WriterSink) Enabled(_ context.Context, l slog.Level) bool

func (*WriterSink) Handle

func (s *WriterSink) Handle(c context.Context, r slog.Record) error

Jump to

Keyboard shortcuts

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