package silog

import "go.abhg.dev/log/silog"

Package silog provides a slog.Handler implementation that produces human-readable, logfmt-style output.

Its features include:

Usage

Construct a silog.Handler with NewHandler:

handler := silog.NewHandler(os.Stderr, silog.Options{
	Level:  silog.LevelInfo,
})

Then use it with a slog.Logger:

logger := slog.New(handler)
Example (CustomLevel)

Demonstrates how to introduce a new log level to the logger.

package main

import (
	"context"
	"log/slog"
	"os"

	"go.abhg.dev/log/silog"
)

func main() {
	const LevelTrace = slog.LevelDebug - 4

	style := silog.PlainStyle(nil)
	style.LevelLabels[LevelTrace] = style.LevelLabels[slog.LevelDebug].SetString("TRC")
	style.Messages[LevelTrace] = style.Messages[slog.LevelDebug]

	handler := silog.NewHandler(os.Stdout, &silog.HandlerOptions{
		Style: style,
		Level: LevelTrace,
		// To keep the test output clean easy to test,
		// we will not log the time in this example.
		ReplaceAttr: skipTime,
	})

	logger := slog.New(handler)
	logger.Log(context.Background(), LevelTrace, "This is a trace message")
	logger.Debug("This is a debug message")

}

func skipTime(groups []string, attr slog.Attr) slog.Attr {
	if len(groups) == 0 && attr.Key == slog.TimeKey {
		return slog.Attr{}
	}
	return attr
}

Output:

TRC This is a trace message
DBG This is a debug message
Example (NoLabel)

Demonstrates reserving a log level to be logged without a label before it.

package main

import (
	"context"
	"log/slog"
	"os"

	"github.com/charmbracelet/lipgloss"
	"go.abhg.dev/log/silog"
)

func main() {
	const LevelPlain = slog.LevelDebug - 1

	style := silog.PlainStyle(nil)
	style.LevelLabels[LevelPlain] = lipgloss.NewStyle() // No label
	style.Messages[LevelPlain] = style.Messages[slog.LevelDebug]

	handler := silog.NewHandler(os.Stdout, &silog.HandlerOptions{
		Style: style,
		Level: LevelPlain,
		// To keep the test output clean easy to test,
		// we will not log the time in this example.
		ReplaceAttr: skipTime,
	})

	logger := slog.New(handler)
	logger.Log(context.Background(), LevelPlain, "This is a plain message")
	logger.Debug("This is a debug message")

}

func skipTime(groups []string, attr slog.Attr) slog.Attr {
	if len(groups) == 0 && attr.Key == slog.TimeKey {
		return slog.Attr{}
	}
	return attr
}

Output:

This is a plain message
DBG This is a debug message

Index

Examples

Types

type Handler

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

Handler is a slog.Handler that writes to an io.Writer with colored output.

Output is in a logfmt-style format, with colored levels. Other features include:

func NewHandler

func NewHandler(w io.Writer, opts *HandlerOptions) *Handler

NewHandler constructs a silog Handler for use with slog. Log output is written to the given io.Writer.

The Handler synchronizes writes to the output writer, and is safe to use from multiple goroutines. Each log message is posted to the output writer in a single Writer.Write call.

func (*Handler) Enabled

func (h *Handler) Enabled(_ context.Context, lvl slog.Level) bool

Enabled reports whether the handler is enabled for the given level.

If Enabled returnsf alse, Handle should not be called for a record at that level.

func (*Handler) Handle

func (h *Handler) Handle(_ context.Context, rec slog.Record) error

Handle writes the given log record to the output writer.

The write is synchronized with a mutex, so that multiple copies of the handler (e.g. those made with WithAttrs, WithPrefix, etc.) can be used concurrently without issues as long as they all are built from the same base handler.

func (*Handler) LevelOffset

func (h *Handler) LevelOffset() int

LevelOffset returns the current level offset for this handler, if any.

func (*Handler) Prefix

func (h *Handler) Prefix() string

Prefix returns the current prefix for this handler, if any.

func (*Handler) SetPrefix

func (h *Handler) SetPrefix(prefix string) *Handler

SetPrefix returns a copy of this handler that will use the given prefix for each log message.

If the handler already has a prefix, this will replace it with the new prefix.

Example
package main

import (
	"log/slog"
	"os"

	"go.abhg.dev/log/silog"
)

func main() {
	handler := silog.NewHandler(os.Stdout, &silog.HandlerOptions{
		Style: silog.PlainStyle(nil),
		// To keep the test output clean easy to test,
		// we will not log the time in this example.
		ReplaceAttr: skipTime,
	})

	h1 := handler.SetPrefix("example")
	h2 := h1.SetPrefix("")

	slog.New(h1).Info("Single prefix")
	slog.New(h2).Info("No prefix")

}

func skipTime(groups []string, attr slog.Attr) slog.Attr {
	if len(groups) == 0 && attr.Key == slog.TimeKey {
		return slog.Attr{}
	}
	return attr
}

Output:

INF example: Single prefix
INF No prefix

func (*Handler) WithAttrs

func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs returns a copy of this handler that will always include the given slog attributes in its output.

func (*Handler) WithGroup

func (h *Handler) WithGroup(name string) slog.Handler

WithGroup returns a copy of this handler that will always group the attributes that follow under the given group name.

func (*Handler) WithLevel

func (h *Handler) WithLevel(lvl slog.Leveler) *Handler

WithLevel returns a new handler with the given leveler, retaining all other attributes and groups.

It will write to the same output writer as this handler.

func (*Handler) WithLevelOffset

func (h *Handler) WithLevelOffset(n int) *Handler

WithLevelOffset returns a copy of this handler that will offset the log level by the given number of levels before writing it.

Levels defined in log/slog are 4-levels apart, so you can use 4, or -4 to upgrade or downgrade log levels. For example:

handler = handler.WithLevelOffset(-4)

This will result in the following remapping:

slog.LevelError -> slog.LevelWarn
slog.LevelWarn  -> slog.LevelInfo
slog.LevelInfo  -> slog.LevelDebug
slog.LevelDebug -> slog.LevelDebug - 4

Any existing level offset is retained, so this operation is additive.

Example
package main

import (
	"log/slog"
	"os"

	"go.abhg.dev/log/silog"
)

func main() {
	handler := silog.NewHandler(os.Stdout, &silog.HandlerOptions{
		Level: slog.LevelDebug,
		Style: silog.PlainStyle(nil),
		// To keep the test output clean easy to test,
		// we will not log the time in this example.
		ReplaceAttr: skipTime,
	})

	logger := slog.New(handler.WithLevelOffset(-4)) // downgrade messages
	logger.Error("Downgraded to warning")
	logger.Warn("Downgraded to info")
	logger.Info("Downgraded to debug")
	logger.Debug("Not logged because below configured level")

}

func skipTime(groups []string, attr slog.Attr) slog.Attr {
	if len(groups) == 0 && attr.Key == slog.TimeKey {
		return slog.Attr{}
	}
	return attr
}

Output:

WRN Downgraded to warning
INF Downgraded to info
DBG Downgraded to debug

type HandlerOptions

type HandlerOptions struct {
	// Level is the minimum log level to log.
	// It must be one of the supported log levels.
	// The default is LevelInfo.
	Level slog.Leveler // optional

	// Style is the style to use for the logger.
	// If unset, [DefaultStyle] is used.
	// You may use [PlainStyle] to get output with no colors.
	Style *Style // optional

	// TimeFormat is the format to use when rendering timestamps.
	// If unset, time.Kitchen will be used.
	TimeFormat string // optional

	// ReplaceAttr, if set, is called for each attribute
	// before it is rendered.
	//
	// For time and level, it is called with slog.TimeKey and slog.LevelKey
	// respectively.
	// It is not called if the associated time for the record is zero.
	ReplaceAttr func(groups []string, attr slog.Attr) slog.Attr // optional
}

HandlerOptions defines options for constructing a Handler.

type Style

type Style struct {
	// Key is the style used for the key in key-value pairs.
	Key lipgloss.Style

	// KeyValueDelimiter is the style used for the delimiter
	// separating keys and values in key-value pairs.
	//
	// This SHOULD have a non-empty value (e.g. "=", ": ", etc.)
	// set with lipgloss.Style.SetString.
	//
	// The default value is "=".
	KeyValueDelimiter lipgloss.Style

	// LevelLabels is a map of slog.Level to style
	// for the label of that level.
	//
	// Each style here SHOULD have a non-empty value
	// (e.g. "DBG", "INF", etc.) set with lipgloss.Style.SetString.
	//
	// If a record has a level that is not present in this map,
	// messages of that level will not be labeled.
	LevelLabels map[slog.Level]lipgloss.Style

	// MultilineValuePrefix defines the style for the prefix that is
	// prepended to each line of an indented multi-line attribute value.
	//
	// This SHOULD have a non-empty value (e.g. "| ").
	// The default value is "| ".
	MultilineValuePrefix lipgloss.Style

	// PrefixDelimiter defines the style separating a prefix
	// (specified with Handler.WithPrefix) from the rest of the log message.
	//
	// This SHOULD have a non-empty value (e.g. ": ", " - ", etc.)
	// The default value is ": ".
	PrefixDelimiter lipgloss.Style

	// Time defines the style used for the time of a log record.
	//
	// If ReplaceAttr is used to change the time attribute,
	// the style is also used for the replacement value.
	Time lipgloss.Style

	// Messages defines styling for messages logged at different levels.
	//
	// If a log record has a level that is not present in this map,
	// the message will use plain text style.
	Messages map[slog.Level]lipgloss.Style

	// Values defines the styling for attributes matched by their keys.
	// Attributes with keys that are not present in this map
	// will use a plain text style for their values.
	//
	// DefaultStyle uses this to style the "error" key in red.
	Values map[string]lipgloss.Style
}

Style defines the output styling for the logger.

Any fields of style that are not set will use the default style (no color, no special formatting).

Customization

It's best to construct a style with DefaultStyle or PlainStyle and then modify the fields you want to change. For example:

style := silog.DefaultStyle(nil)
style.KeyValueDelimiter = lipgloss.NewStyle().SetString(": ")

func DefaultStyle

func DefaultStyle(renderer *lipgloss.Renderer) *Style

DefaultStyle is the default style used by Handler. It provides colored output, faint text for debug messages, red errors, etc.

Renderer specifies the lipgloss renderer to use for styling. If unset, the default lipgloss renderer is used.

func PlainStyle

func PlainStyle(renderer *lipgloss.Renderer) *Style

PlainStyle is a style for Handler that performs no styling. It's best when writing to a non-terminal destination.

Renderer specifies the lipgloss renderer to use for styling. If unset, the default lipgloss renderer is used.