slogctx

package module
v0.0.0-...-14de9af Latest Latest
Warning

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

Go to latest
Published: Feb 3, 2023 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package slogctx provides tools for working with contexts with the proposed log/slog package.

The package provides alternatives to slog logging functions that require a context with a logs: slogctx.Info replaces slog.Info, slogctx.Error replaces slog.Error, etc. The struct *slogctx.Logger replaces *slog.Logger. This is useful to ensure a context is always included. Usage:

slogctx.Info(ctx, "got a request")
logger := slogctx.Default() // or logger := slogctx.NewLogger(slog.Default())
logger.Info(ctx, "found something special")

The package supports storing extra attributes in a context. All logs using the context created by slogctx.WithAttrs will include the extra attributes. This is useful to include a requestID with all logs. Usage:

ctx = slogctx.WithAttrs(ctx, "requestID", 1234)
slogctx.Info(ctx, "processing request")
slog.Default().WithContext(ctx).Info("processing more") // also works with plain slog

The package supports overriding the minimum log level. All logs using the context created by slogctx.WithMinimumLevel will use the supplied level. This is useful to debug specific requests. Usage:

ctx = slogctx.WithMinimumLevel(ctx, slog.LevelDebug)
slogctx.Debug(ctx, "low-level information")

Using WithAttrs and WithMinimumLevel requires wrapping the underlying slog.Handler using slogctx.CtxHandler. This can be done globally for the default logger using slogctx.WrapDefaultLoggerWithCtxHandler.

Example
package main

import (
	"context"
	"os"
	"time"

	"github.com/jellevandenhooff/slogctx"
	"golang.org/x/exp/slog"
)

func SetupSlogForExample() func() {
	original := slog.Default()

	slog.SetDefault(slog.New(slog.HandlerOptions{
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if a.Key == "time" {
				return slog.Time("time", time.Date(2022, 1, 29, 15, 10, 0, 0, time.UTC))
			}
			return a
		},
	}.NewTextHandler(os.Stdout)))

	return func() {
		slog.SetDefault(original)
	}
}

func main() {
	cleanup := SetupSlogForExample()
	defer cleanup()

	ctx := context.Background()

	// Setup slogctx for slog.Default(). This is required to make slog.WithAttrs
	// and slog.WithMinimumLevel work.
	slogctx.WrapDefaultLoggerWithCtxHandler()

	// The log functions slogctx.Info, slogctx.Error, etc. are alternatives to
	// slog.Info, slog.Error, etc. that take a context as first argument.
	// This can be useful to ensure all logger calls will supply a context.
	slogctx.Info(ctx, "doing a small log")

	// The slogctx.Logger is an alternative to slog.Logger where the log
	// functions take a context as first argument.
	// This can be useful to ensure all logger calls will supply a context.
	logger := slogctx.NewLogger(slog.Default())
	logger.Info(ctx, "from the logger")
	logger = logger.With("loggerAttr", "extra attr")
	logger.Info(ctx, "extra logger attr")

	// Using WithAttrs extra attributes can be included in a context.
	// This can be useful to include eg. a request ID with all future logs.
	ctx = slogctx.WithAttrs(ctx, "requestID", "1234")
	slogctx.Info(ctx, "extra context attr")

	// WithAttrs also works with the standard slog log functions.
	slog.Default().WithContext(ctx).Info("from default logger")

	// Using WithMinimumLevel the default log level can be overriden.
	// This can be useful to trace in detail what happens with a specific request.
	slogctx.Debug(ctx, "log ignored")
	ctx = slogctx.WithMinimumLevel(ctx, slog.LevelDebug)
	slogctx.Debug(ctx, "log not ignored")

}
Output:

time=2022-01-29T15:10:00.000Z level=INFO msg="doing a small log"
time=2022-01-29T15:10:00.000Z level=INFO msg="from the logger"
time=2022-01-29T15:10:00.000Z level=INFO msg="extra logger attr" loggerAttr="extra attr"
time=2022-01-29T15:10:00.000Z level=INFO msg="extra context attr" requestID=1234
time=2022-01-29T15:10:00.000Z level=INFO msg="from default logger" requestID=1234
time=2022-01-29T15:10:00.000Z level=DEBUG msg="log not ignored" requestID=1234
Example (Whole_program)
package main

import (
	"context"
	"encoding/json"
	"net/http"
	"os"
	"time"

	"github.com/google/uuid"
	"github.com/jellevandenhooff/slogctx"
	"golang.org/x/exp/slog"
)

func NewRequestIDMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestID := uuid.NewString()
		ctx := r.Context()
		ctx = slogctx.WithAttrs(ctx, "requestID", requestID)
		r = r.WithContext(ctx)
		next.ServeHTTP(w, r)
	})
}

type WebServer struct {
	// WebServer always has a context when logging. Use a *slogctx.Logger for convenience.
	logger *slogctx.Logger

	db *DB
}

func (s *WebServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	s.logger.Info(ctx, "got messages request", "url", r.URL.String(), "remoteAddr", r.RemoteAddr)

	response, err := s.db.QueryMessages(ctx)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	json.NewEncoder(w).Encode(response)
}

type DB struct {
	// DB sometimes has a context when logging, sometimes does not. Use a *slog.Logger.
	logger *slog.Logger
}

func (db *DB) QueryMessages(ctx context.Context) ([]string, error) {
	db.logger.WithContext(ctx).Debug("querying db")

	return []string{"hello world", "another message"}, nil
}

func (db *DB) PrintStats() {
	t := time.NewTicker(10 * time.Second)

	for range t.C {
		db.logger.Info("stats", "numDBConnections", 10)
	}
}

func main() {
	// This example shows a way of passing around *slog(ctx).Loggers as well and
	// extra attributes (or trace IDs) with a context.
	//
	// At program start time, the main function sets up the logger(s), DB
	// clients, server structs, and glues them altogether.
	//
	// At runtime, handlers pass around context.Context which gets passed down
	// to the loggers.

	// Setup slog
	slog.SetDefault(slog.New(slog.HandlerOptions{
		AddSource: true,
	}.NewTextHandler(os.Stderr)))
	slogctx.WrapDefaultLoggerWithCtxHandler()

	// Build DB client.
	db := &DB{
		logger: slog.Default().With("dbURL", "sqlite://foo"),
	}
	go db.PrintStats()

	// Build web server.
	webServer := &WebServer{
		logger: slogctx.NewLogger(slog.Default()),
		db:     db,
	}

	// Wrap web server with middleware.
	handler := NewRequestIDMiddleware(webServer)

	// Run the server.
	http.ListenAndServe(":8080", handler)
}
Output:

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Debug

func Debug(ctx context.Context, msg string, args ...any)

Debug calls Logger.WithContext(ctx).Debug on the default logger.

func Error

func Error(ctx context.Context, msg string, err error, args ...any)

Debug calls Logger.WithContext(ctx).Error on the default logger.

func Info

func Info(ctx context.Context, msg string, args ...any)

Debug calls Logger.WithContext(ctx).Info on the default logger.

func Warn

func Warn(ctx context.Context, msg string, args ...any)

Debug calls Logger.WithContext(ctx).Warn on the default logger.

func WithAttrs

func WithAttrs(ctx context.Context, args ...any) context.Context

WithAttrs attaches the given attributes (as in slog.Logger.With) to the context.

Requires a slog.Handler wrapped with WrapWithCtxHandler.

func WithMinimumLevel

func WithMinimumLevel(ctx context.Context, level slog.Level) context.Context

WithMinimumLevel overrides the minimum logging level for all log calls using this context.

Requires a slog.Handler wrapped with WrapWithCtxHandler.

func WrapDefaultLoggerWithCtxHandler

func WrapDefaultLoggerWithCtxHandler()

WrapDefaultLoggerWithCtxHandler wraps the handler used by slog.Default() with WrapWithCtxHandler.

func WrapWithCtxHandler

func WrapWithCtxHandler(inner slog.Handler) slog.Handler

WrapWithCtxHandler wraps a slog.Handler with support for WithAttrs and WithMinimumLevel.

Use WrapDefaultLoggerWithCtxHandler to wrap the handler used by slog.Default.

Types

type Logger

type Logger struct {
	Inner slog.Logger
}

Logger is a slog.Logger wrapper with a mandatory context argument.

func Default

func Default() *Logger

Default returns a Logger created from slog.Default.

func NewLogger

func NewLogger(logger *slog.Logger) *Logger

NewLogger creates a new Logger from a slog.Logger.

func (*Logger) Debug

func (l *Logger) Debug(ctx context.Context, msg string, args ...any)

Debug logs at LevelDebug.

func (*Logger) Enabled

func (l *Logger) Enabled(ctx context.Context, level slog.Level) bool

Enabled reports whether l emits log records at the given level.

func (*Logger) Error

func (l *Logger) Error(ctx context.Context, msg string, err error, args ...any)

Error logs at LevelError. If err is non-nil, Error appends Any(ErrorKey, err) to the list of attributes.

func (*Logger) Info

func (l *Logger) Info(ctx context.Context, msg string, args ...any)

Info logs at LevelInfo.

func (*Logger) Log

func (l *Logger) Log(ctx context.Context, level slog.Level, msg string, args ...any)

Log emits a log record, like slog.Logger.Log.

func (*Logger) Warn

func (l *Logger) Warn(ctx context.Context, msg string, args ...any)

Warn logs at LevelWarn.

func (*Logger) With

func (l *Logger) With(args ...any) *Logger

With returns a new Logger that includes the given arguments, like slog.Logger.With.

Jump to

Keyboard shortcuts

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