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:
- Colored output
- Custom log levels
- Multi-line messages and attributes
- Prefixes for log messages
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
-
type Handler
- func NewHandler(w io.Writer, opts *HandlerOptions) *Handler
- func (h *Handler) Enabled(_ context.Context, lvl slog.Level) bool
- func (h *Handler) Handle(_ context.Context, rec slog.Record) error
- func (h *Handler) LevelOffset() int
- func (h *Handler) Prefix() string
- func (h *Handler) SetPrefix(prefix string) *Handler
- func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler
- func (h *Handler) WithGroup(name string) slog.Handler
- func (h *Handler) WithLevel(lvl slog.Leveler) *Handler
- func (h *Handler) WithLevelOffset(n int) *Handler
- type HandlerOptions
- type Style
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:
- rendering of trace level
- multi-line fields are indented and aligned
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.
Output: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
}
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.
Output: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
}
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.