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:
- Keep ambient state introduced by WithAttrs and WithGroup.
- Format each slog.Record into its text representation.
- 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.
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 ¶
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 ¶
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 ¶
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.
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) 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 ¶
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.
type Level ¶
type Level struct{}
Level is a Formatter that formats slog.Record.Level using slog.Level.String.
type Message ¶
type Message struct {
Quote QuoteType
}
Message is a Formatter that formats slog.Record.Message. Message is quoted according to Quote.
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.
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.
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.