Documentation
¶
Overview ¶
Package logg is a thin wrapper around log/slog to decorate a slog.Logger with commonly-needed metadata for applications and events. Events will have the same keys described by the slog package for built-in attributes; ie: "time", "level", "msg", "source".
Additionally, these default top-level keys may be present:
- "application_metadata": []slog.Attr. ie: version control data, build times, etc.
- "trace_id": string. A tracing ID, may be present for output from loggers created via New
- "data": []slog.Attr. Event-specific attributes, may be present for output from loggers created with New.
These key names may be configured with the SetDefaults function.
Example ¶
Setup the application logger with some preset fields and a logging destination. Then log.
package main
import (
"errors"
"fmt"
"log/slog"
"os"
"github.com/rafaelespinoza/logg"
)
func main() {
logg.SetDefaults(
slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}),
&logg.Settings{
ApplicationMetadata: []slog.Attr{
slog.String("branch_name", "main"),
slog.String("build_time", "20060102T150415"),
slog.String("commit_hash", "deadbeef"),
},
},
)
//
// Log at the INFO level.
//
slog.Info("hello world")
// Want message string interpolation? Do this beforehand.
variable := "there"
slog.Info(fmt.Sprintf("hello %s", variable))
// Log message with attributes
slog.Info("hello with attributes", slog.Bool("simple", true), slog.Int("rating", 42))
//
// Log at the ERROR level.
//
err := errors.New("example")
slog.Error("uh-oh", slog.Any("error", err))
}
Output:
Example (Context) ¶
A demo of context-based loggers. For brevity's sake, this example lacks some things you may want in a real application, such as using the context to transmit an existing tracing ID or generating a tracing ID in case the context doesn't have one. That exercise is up to you.
package main
import (
"context"
"log/slog"
"os"
"github.com/rafaelespinoza/logg"
)
// Use values of these types as keys for accessing values on a context. It's
// best practice to use unexported types to reduce the likelihood of collisions.
type (
traceContextKey struct{}
loggerContextKey struct{}
)
// SetContextLoggerWithID creates a new logger with a tracing ID and dataAttrs,
// and returns a new context with the logger and tracing ID. Retrieve the logger
// with GetContextLogger.
func SetContextLoggerWithID(ctx context.Context, traceID string, dataAttrs ...slog.Attr) context.Context {
lgr := logg.New(traceID, dataAttrs...)
outCtx := context.WithValue(ctx, loggerContextKey{}, lgr)
outCtx = context.WithValue(outCtx, traceContextKey{}, traceID)
return outCtx
}
// GetContextLogger fetches the logger on the context, which would have been set
// by SetContextLoggerWithID. If no logger is found, then it returns the default
// logger from the slog package.
func GetContextLogger(ctx context.Context) *slog.Logger {
lgr, found := ctx.Value(loggerContextKey{}).(*slog.Logger)
if found {
return lgr
}
return slog.Default()
}
// A demo of context-based loggers. For brevity's sake, this example lacks some
// things you may want in a real application, such as using the context to
// transmit an existing tracing ID or generating a tracing ID in case the
// context doesn't have one. That exercise is up to you.
func main() {
defaultHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
// Remove the time key so that output is consistent.
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if len(groups) < 1 && a.Key == slog.TimeKey {
a = slog.Attr{}
}
return a
},
})
logg.SetDefaults(defaultHandler, &logg.Settings{
ApplicationMetadata: []slog.Attr{slog.String("branch", "main")},
ApplicationMetadataKey: "version",
})
aCtx := SetContextLoggerWithID(context.Background(), "unique_id")
aLogger := GetContextLogger(aCtx)
aLogger.Info("Avalanches in Atlanta")
bCtx := SetContextLoggerWithID(context.Background(), "example_with_data_attrs", slog.String("foo", "bar"))
bLogger := GetContextLogger(bCtx)
bLogger.Info("Blizzards in Bakersfield")
}
Output: level=INFO msg="Avalanches in Atlanta" version.branch=main trace_id=unique_id level=INFO msg="Blizzards in Bakersfield" version.branch=main trace_id=example_with_data_attrs data.foo=bar
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func New ¶
New creates a slog.Logger, with the same configuration and handler set via SetDefaults. The traceID, if non-empty, will be at a top-level key for subsequent logging output with the created Logger. Similarly, dataAttrs will be grouped at a top-level key.
Example ¶
Initialize the logger with or without data fields.
package main
import (
"log/slog"
"github.com/rafaelespinoza/logg"
)
func main() {
// Writes with the same handler provisioned in SetupDefaults.
// This logger has its own data fields.
logger := logg.New("", slog.Bool("whiskey", true), slog.Float64("tango", 1.23), slog.Int("foxtrot", 10))
logger.Info("hello, world")
// This logger also writes to the package configured handler, but doesn't
// have any data fields of its own.
loggerNoFields := logg.New("")
loggerNoFields.Info("no data fields here")
// This logger has its own tracing ID.
loggerWithTraceID := logg.New("unique_trace_id")
loggerWithTraceID.Info("that happened")
}
Output:
func SetDefaults ¶ added in v0.1.0
SetDefaults initializes a package-level prototype logger from which subsequent logs are based upon. This function is intended to be called only once, shortly after application startup. It will also call the slog.SetDefault function, thereby affecting all output functionality in that package. Loggers created with New consider values set in this function as defaults. When the handler input h, is empty, then the current Handler value of slog.Default is used. When the input settings is empty, then defaults are used. Default settings are mentioned in the package documentation.
The side effects of this function may put this data at top-level keys for every log entry:
- application metadata.
- a trace ID value, only present when a logger is created via New.
- event-specific data attributes.
Example ¶
In a real application, SetDefaults probably only needs to be called once.
package main
import (
"io"
"log/slog"
"os"
"github.com/rafaelespinoza/logg"
)
func main() {
//
// Customize keys of metadata attributes.
//
logg.SetDefaults(
slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}),
&logg.Settings{
ApplicationMetadata: []slog.Attr{
slog.String("branch_name", "main"),
slog.String("build_time", "now"),
slog.Any("foo", "bar"),
},
ApplicationMetadataKey: "application_metadata",
TraceIDKey: "request_uuid",
DataKey: "event_data",
},
)
//
// Output text format.
//
logg.SetDefaults(
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}),
&logg.Settings{ApplicationMetadata: []slog.Attr{slog.String("branch_name", "main")}},
)
//
// Write to standard error as well as some file.
//
var file io.Writer
sink := io.MultiWriter(os.Stderr, file)
logg.SetDefaults(
slog.NewJSONHandler(sink, &slog.HandlerOptions{Level: slog.LevelInfo}),
&logg.Settings{ApplicationMetadata: []slog.Attr{slog.String("branch_name", "main")}},
)
}
Output:
Types ¶
type Settings ¶ added in v0.1.0
type Settings struct {
ApplicationMetadata []slog.Attr
ApplicationMetadataKey string
TraceIDKey string
DataKey string
}
Settings is package configuration data for overriding package defaults via SetDefaults. ApplicationMetadata could be versioning metadata, ie: commit hash, build time, etc. The placement of this data within a logging event can be set with ApplicationMetadataKey. The location of event tracing IDs can be set with TraceIDKey. Other event-specific attributes would be placed at DataKey.