slogcontext

package module
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: Nov 17, 2023 License: MPL-2.0 Imports: 5 Imported by: 30

README

slog-context

tag Go Version GoDoc Build Status Go report Coverage Contributors License

Use golang structured logging (slog) with context. Add and retrieve logger to and from context. Add attributes to context. Automatically read any custom context values, such as OpenTelemetry TraceID.

This library supports two different workflows for using slog and context. These workflows can be used individually or together at the same time.

Attributes Extracted from Context Workflow:

Using the slogcontext.Handler lets us Prepend and Append attributes to log lines, even when a logger is not passed into a function or in code we don't control. This is done without storing the logger in the context; instead the attributes are stored in the context and the Handler picks them up later whenever a new log line is written.

In that same workflow, the HandlerOptions and AttrExtractor types let us extract any custom values from a context and have them automatically be prepended or appended to all log lines using that context. For example, the slogotel.ExtractTraceSpanID extractor will automatically extract the OTEL (OpenTelemetry) TraceID and SpanID, and add them to the log record, while also annotating the Span with an error code if the log is at error level.

Logger in Context Workflow:

Using ToCtx and Logger lets us store the logger itself within a context, and get it back out again. Wrapper methods With/WithGroup/Debug/Info/ Warn/Error/Log/LogAttrs let us work directly with a logger residing with the context (or the default logger if no logger is stored in the context).

Install

go get github.com/veqryn/slog-context

import (
	slogcontext "github.com/veqryn/slog-context"
)

Usage

Logger in Context Workflow
package main

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

	slogcontext "github.com/veqryn/slog-context"
)

// This workflow has us pass the *slog.Logger around inside a context.Context.
// This lets us add attributes and groups to the logger, while naturally
// keeping the logger scoped just like the context itself is scoped.
//
// This eliminates the need to use the default package-level slog, and also
// eliminates the need to add a *slog.Logger as yet another argument to all
// functions.
//
// You can still get the Logger out of the context at any time, and pass it
// around manually if needed, but since contexts are already passed to most
// functions, passing the logger explicitly is now optional.
//
// Attributes and key-value pairs like request-id, trace-id, user-id, etc, can
// be added to the logger in the context, and as the context propagates the
// logger and its attributes will propagate with it, adding these to any log
// lines using that context.
func main() {
	h := slog.NewJSONHandler(os.Stdout, nil)
	slog.SetDefault(slog.New(h))

	// Store the logger inside the context:
	ctx := slogcontext.ToCtx(context.Background(), slog.Default())

	// Get the logger back out again at any time, for manual usage:
	log := slogcontext.Logger(ctx)
	log.Warn("warning")

	// Add attributes directly to the logger in the context:
	ctx = slogcontext.With(ctx, "rootKey", "rootValue")

	// Create a group directly on the logger in the context:
	ctx = slogcontext.WithGroup(ctx, "someGroup")

	// With and wrapper methods have the same args signature as slog methods,
	// and can take a mix of slog.Attr and key-value pairs.
	ctx = slogcontext.With(ctx, slog.String("subKey", "subValue"))

	// Access the logger in the context directly with handy wrappers for Debug/Info/Warn/Error/Log/LogAttrs:
	slogcontext.Info(ctx, "main message", "mainKey", "mainValue")
	/*
		{
			"time":"2023-11-14T00:53:46.363072-07:00",
			"level":"INFO",
			"msg":"main message",
			"rootKey":"rootValue",
			"someGroup":{
				"subKey":"subValue",
				"mainKey":"mainValue"
			}
		}
	*/
}
Attributes Extracted from Context Workflow
Append and Prepend
package main

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

	slogcontext "github.com/veqryn/slog-context"
)

// This workflow lets us use slog as normal, while adding the ability to put
// slog attributes into the context which will then show up at the start or end
// of log lines.
//
// This is useful when you are not passing a *slog.Logger around to different
// functions (because you are making use of the default package-level slog),
// but you are passing a context.Context around.
//
// This can also be used when a library or vendor code you don't control is
// using the default log methods, default logger, or doesn't accept a slog
// Logger to all functions you wish to add attributes to.
//
// Attributes and key-value pairs like request-id, trace-id, user-id, etc, can
// be added to the context, and the *slogcontext.Handler will make sure they
// are prepended to the start, or appended to the end, of any log lines using
// that context.
func main() {
	// Create the *slogcontext.Handler middleware
	h := slogcontext.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)
	slog.SetDefault(slog.New(h))

	ctx := context.Background()

	// Prepend some slog attributes to the start of future log lines:
	ctx = slogcontext.Prepend(ctx, "prependKey", "prependValue")

	// Append some slog attributes to the end of future log lines:
	// Prepend and Append have the same args signature as slog methods,
	// and can take a mix of slog.Attr and key-value pairs.
	ctx = slogcontext.Append(ctx, slog.String("appendKey", "appendValue"))

	// Use the logger like normal:
	slog.WarnContext(ctx, "main message", "mainKey", "mainValue")
	/*
		{
			"time": "2023-11-15T18:43:23.290798-07:00",
			"level": "WARN",
			"msg": "main message",
			"prependKey": "prependValue",
			"mainKey": "mainValue",
			"appendKey": "appendValue"
		}
	*/

	// Use the logger like normal; add attributes, create groups, pass it around:
	log := slog.With("rootKey", "rootValue")
	log = log.WithGroup("someGroup")
	log = log.With("subKey", "subValue")

	// The prepended/appended attributes end up in all log lines that use that context
	log.InfoContext(ctx, "main message", "mainKey", "mainValue")
	/*
		{
			"time": "2023-11-14T00:37:03.805196-07:00",
			"level": "INFO",
			"msg": "main message",
			"prependKey": "prependValue",
			"rootKey": "rootValue",
			"someGroup": {
				"subKey": "subValue",
				"mainKey": "mainValue",
				"appendKey": "appendValue"
			}
		}
	*/
}
Custom Context Value Extractor
package main

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

	slogcontext "github.com/veqryn/slog-context"
)

type ctxKey struct{}

func customExtractor(ctx context.Context, _ time.Time, _ slog.Level, _ string) []slog.Attr {
	if v, ok := ctx.Value(ctxKey{}).(string); ok {
		return []slog.Attr{slog.String("my-key", v)}
	}
	return nil
}

// This workflow lets us use slog as normal, while letting us extract any
// custom values we want from any context, and having them added to the start
// or end of the log record.
func main() {
	// Create the *slogcontext.Handler middleware
	h := slogcontext.NewHandler(
		slog.NewJSONHandler(os.Stdout, nil), // The next handler in the chain
		&slogcontext.HandlerOptions{
			// Prependers stays as default (leaving as nil would accomplish the same)
			Prependers: []slogcontext.AttrExtractor{
				slogcontext.ExtractPrepended,
			},
			// Appenders first appends anything added with slogcontext.Append,
			// then appends our custom ctx value
			Appenders: []slogcontext.AttrExtractor{
				slogcontext.ExtractAppended,
				customExtractor,
			},
		},
	)
	slog.SetDefault(slog.New(h))

	// Add a value to the context
	ctx := context.WithValue(context.Background(), ctxKey{}, "my-value")

	// Use the logger like normal:
	slog.WarnContext(ctx, "main message", "mainKey", "mainValue")
	/*
		{
			"time": "2023-11-17T04:35:30.333732-07:00",
			"level": "WARN",
			"msg": "main message",
			"mainKey": "mainValue",
			"my-key": "my-value"
		}
	*/
}
OpenTelemetry TraceID SpanID Extractor

In order to avoid making all users of this repo require all the OTEL libraries, the OTEL extractor is in a separate module in this repo:

go get github.com/veqryn/slog-context/otel

package main

import (
	"context"
	"log/slog"
	"net/http"
	"os"
	"time"

	slogcontext "github.com/veqryn/slog-context"
	slogotel "github.com/veqryn/slog-context/otel"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
	"go.opentelemetry.io/otel/trace"
)

func init() {
	// Create the *slogcontext.Handler middleware
	h := slogcontext.NewHandler(
		slog.NewJSONHandler(os.Stdout, nil), // The next handler in the chain
		&slogcontext.HandlerOptions{
			// Prependers will first add the OTEL Trace ID,
			// then anything else Prepended to the ctx
			Prependers: []slogcontext.AttrExtractor{
				slogotel.ExtractTraceSpanID,
				slogcontext.ExtractPrepended,
			},
			// Appenders stays as default (leaving as nil would accomplish the same)
			Appenders: []slogcontext.AttrExtractor{
				slogcontext.ExtractAppended,
			},
		},
	)
	slog.SetDefault(slog.New(h))

	setupOTEL()
}

func main() {
	// Handle OTEL shutdown properly so nothing leaks
	defer traceProvider.Shutdown(context.Background())

	// Demonstrate the slogotel.ExtractTraceSpanID with a http server
	http.HandleFunc("/hello", helloHandler)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}

// helloHandler starts an OTEL Span, then begins a long-running calculation.
// The calculation will fail, and the logging at Error level will mark the span
// as codes.Error.
func helloHandler(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracer.Start(r.Context(), "helloHandler")
	defer span.End()

	slogcontext.Info(ctx, "starting long calculation...")
	/*
		{
			"time": "2023-11-17T03:11:20.584592-07:00",
			"level": "INFO",
			"msg": "starting long calculation...",
			"TraceID": "15715df45965b4a2db6dc103a76e52ae",
			"SpanID": "76d364cdd598c895"
		}
	*/

	time.Sleep(5 * time.Second)
	slogcontext.Error(ctx, "something failed...")
	/*
		{
			"time": "2023-11-17T03:11:25.586464-07:00",
			"level": "ERROR",
			"msg": "something failed...",
			"TraceID": "15715df45965b4a2db6dc103a76e52ae",
			"SpanID": "76d364cdd598c895"
		}
	*/

	w.WriteHeader(http.StatusInternalServerError)
	// The OTEL exporter will output the trace, which will include this and much more:
	/*
		{
			"Name": "helloHandler",
			"SpanContext": {
				"TraceID": "15715df45965b4a2db6dc103a76e52ae",
				"SpanID": "76d364cdd598c895"
			},
			"Status": {
				"Code": "Error",
				"Description": "something failed..."
			}
		}
	*/
}

var (
	tracer        trace.Tracer
	traceProvider *sdktrace.TracerProvider
)

// OTEL setup
func setupOTEL() {
	exp, err := stdouttrace.New()
	if err != nil {
		panic(err)
	}

	// Create a new tracer provider with a batch span processor and the given exporter.
	traceProvider = newTraceProvider(exp)

	// Set as global trace provider
	otel.SetTracerProvider(traceProvider)

	// Finally, set the tracer that can be used for this package.
	tracer = traceProvider.Tracer("ExampleService")
}

// OTEL tracer provider setup
func newTraceProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
	r, err := resource.Merge(
		resource.Default(),
		resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceName("ExampleService"),
		),
	)
	if err != nil {
		panic(err)
	}

	return sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exp),
		sdktrace.WithResource(r),
	)
}

Documentation

Overview

Package slogcontext lets you use golang structured logging (slog) with context. Add and retrieve logger to and from context. Add attributes to context. Automatically read any custom context values, such as OpenTelemetry TraceID.

This library supports two different workflows for using slog and context. These workflows can be used individually or together at the same time.

Attributes Extracted from Context Workflow:

Using the slogcontext.NewHandler lets us Prepend and Append attributes to log lines, even when a logger is not passed into a function or in code we don't control. This is done without storing the logger in the context; instead the attributes are stored in the context and the Handler picks them up later whenever a new log line is written.

In that same workflow, the HandlerOptions and AttrExtractor types let us extract any custom values from a context and have them automatically be prepended or appended to all log lines using that context. For example, the slogotel.ExtractTraceSpanID extractor will automatically extract the OTEL (OpenTelemetry) TraceID and SpanID, and add them to the log record, while also annotating the Span with an error code if the log is at error level.

Logger in Context Workflow:

Using ToCtx and Logger lets us store the logger itself within a context, and get it back out again. Wrapper methods With / WithGroup / Debug / Info / Warn / Error / Log / LogAttrs let us work directly with a logger residing with the context (or the default logger if no logger is stored in the context).

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Append

func Append(parent context.Context, args ...any) context.Context

Append adds the attribute arguments to the end of the group that will be appended to the end of the log record when it is handled. This means that the attributes could be in a group or sub-group, if the log has used WithGroup at some point.

func Debug

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

Debug calls Logger.DebugContext on the logger stored in the context, or if there isn't any, on the default logger. slog.Logger.DebugContext logs at LevelDebug with the given context.

func Error

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

Error calls Logger.ErrorContext on the logger stored in the context, or if there isn't any, on the default logger. slog.Logger.ErrorContext logs at LevelError with the given context.

func ExtractAppended added in v0.2.0

func ExtractAppended(ctx context.Context, _ time.Time, _ slog.Level, _ string) []slog.Attr

ExtractAppended is an AttrExtractor that returns the appended attributes stored in the context. The returned slice should not be appended to or modified in any way. Doing so will cause a race condition.

func ExtractPrepended added in v0.2.0

func ExtractPrepended(ctx context.Context, _ time.Time, _ slog.Level, _ string) []slog.Attr

ExtractPrepended is an AttrExtractor that returns the prepended attributes stored in the context. The returned slice should not be appended to or modified in any way. Doing so will cause a race condition.

func Info

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

Info calls Logger.InfoContext on the logger stored in the context, or if there isn't any, on the default logger. slog.Logger.InfoContext logs at LevelInfo with the given context.

func Log

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

Log calls Logger.Log on the logger stored in the context, or if there isn't any, on the default logger. slog.Logger.Log emits a log record with the current time and the given level and message. The Record's Attrs consist of the Logger's attributes followed by the Attrs specified by args.

The attribute arguments are processed as follows:

  • If an argument is an Attr, it is used as is.
  • If an argument is a string and this is not the last argument, the following argument is treated as the value and the two are combined into an Attr.
  • Otherwise, the argument is treated as a value with key "!BADKEY".

func LogAttrs

func LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr)

LogAttrs calls Logger.LogAttrs on the logger stored in the context, or if there isn't any, on the default logger. slog.Logger.LogAttrs is a more efficient version of slog.Logger.Log that accepts only Attrs.

func Logger

func Logger(ctx context.Context) *slog.Logger

Logger returns the slog.Logger associated with the ctx. If no logger is associated, or the logger or ctx are nil, slog.Default() is returned.

func Prepend

func Prepend(parent context.Context, args ...any) context.Context

Prepend adds the attribute arguments to the end of the group that will be prepended to the start of the log record when it is handled. This means that these attributes will be at the root level, and not in any groups.

func ToCtx

func ToCtx(parent context.Context, logger *slog.Logger) context.Context

ToCtx returns a copy of ctx with the logger attached. The parent context will be unaffected. Passing in a nil logger will force future calls of Logger(ctx) on the returned context to return the slog.Default() logger.

Example
package main

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

	slogcontext "github.com/veqryn/slog-context"
)

func main() {
	// This workflow has us pass the *slog.Logger around inside a context.Context.
	// This lets us add attributes and groups to the logger, while naturally
	// keeping the logger scoped just like the context itself is scoped.
	//
	// This eliminates the need to use the default package-level slog, and also
	// eliminates the need to add a *slog.Logger as yet another argument to all
	// functions.
	//
	// You can still get the Logger out of the context at any time, and pass it
	// around manually if needed, but since contexts are already passed to most
	// functions, passing the logger explicitly is now optional.
	//
	// Attributes and key-value pairs like request-id, trace-id, user-id, etc, can
	// be added to the logger in the context, and as the context propagates the
	// logger and its attributes will propagate with it, adding these to any log
	// lines using that context.

	h := slog.NewJSONHandler(os.Stdout, nil)
	slog.SetDefault(slog.New(h))

	// Store the logger inside the context:
	ctx := slogcontext.ToCtx(context.Background(), slog.Default())

	// Get the logger back out again at any time, for manual usage:
	log := slogcontext.Logger(ctx)
	log.Warn("warning")

	// Add attributes directly to the logger in the context:
	ctx = slogcontext.With(ctx, "rootKey", "rootValue")

	// Create a group directly on the logger in the context:
	ctx = slogcontext.WithGroup(ctx, "someGroup")

	// With and wrapper methods have the same args signature as slog methods,
	// and can take a mix of slog.Attr and key-value pairs.
	ctx = slogcontext.With(ctx, slog.String("subKey", "subValue"))

	// Access the logger in the context directly with handy wrappers for Debug/Info/Warn/Error/Log/LogAttrs:
	slogcontext.Info(ctx, "main message", "mainKey", "mainValue")
	/*
		{
			"time":"2023-11-14T00:53:46.363072-07:00",
			"level":"INFO",
			"msg":"main message",
			"rootKey":"rootValue",
			"someGroup":{
				"subKey":"subValue",
				"mainKey":"mainValue"
			}
		}
	*/
}
Output:

func Warn

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

Warn calls Logger.WarnContext on the logger stored in the context, or if there isn't any, on the default logger. slog.Logger.WarnContext logs at LevelWarn with the given context.

func With

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

With calls Logger.With on the logger stored in the context, or if there isn't any, on the default logger. This new logger is stored in a child context and the new context is returned. slog.Logger.With returns a Logger that includes the given attributes in each output operation. Arguments are converted to attributes as if by Logger.Log.

func WithGroup

func WithGroup(ctx context.Context, name string) context.Context

WithGroup calls Logger.WithGroup on the logger stored in the context, or if there isn't any, on the default logger. This new logger is stored in a child context and the new context is returned. slog.Logger.WithGroup returns a Logger that starts a group, if name is non-empty. The keys of all attributes added to the Logger will be qualified by the given name. (How that qualification happens depends on the Handler.WithGroup method of the Logger's Handler.)

If name is empty, WithGroup returns the receiver.

Types

type AttrExtractor added in v0.2.0

type AttrExtractor func(ctx context.Context, recordT time.Time, recordLvl slog.Level, recordMsg string) []slog.Attr

AttrExtractor is a function that retrieves or creates slog.Attr's based information/values found in the context.Context and the slog.Record's basic attributes.

type Handler

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

Handler is a slog.Handler middleware that will Prepend and Append attributes to log lines. The attributes are extracted out of the log record's context by the provided AttrExtractor methods. It passes the final record and attributes off to the next handler when finished.

func NewHandler

func NewHandler(next slog.Handler, opts *HandlerOptions) *Handler

NewHandler creates a Handler slog.Handler middleware that will Prepend and Append attributes to log lines. The attributes are extracted out of the log record's context by the provided AttrExtractor methods. It passes the final record and attributes off to the next handler when finished. If opts is nil, the default options are used.

Example
package main

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

	slogcontext "github.com/veqryn/slog-context"
)

func main() {
	// This workflow lets us use slog as normal, while adding the ability to put
	// slog attributes into the context which will then show up at the start or end
	// of log lines.
	//
	// This is useful when you are not passing a *slog.Logger around to different
	// functions (because you are making use of the default package-level slog),
	// but you are passing a context.Context around.
	//
	// This can also be used when a library or vendor code you don't control is
	// using the default log methods, default logger, or doesn't accept a slog
	// Logger to all functions you wish to add attributes to.
	//
	// Attributes and key-value pairs like request-id, trace-id, user-id, etc, can
	// be added to the context, and the *slogcontext.Handler will make sure they
	// are prepended to the start, or appended to the end, of any log lines using
	// that context.

	// Create the *slogcontext.Handler middleware
	h := slogcontext.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)
	slog.SetDefault(slog.New(h))

	ctx := context.Background()

	// Prepend some slog attributes to the start of future log lines:
	ctx = slogcontext.Prepend(ctx, "prependKey", "prependValue")

	// Append some slog attributes to the end of future log lines:
	// Prepend and Append have the same args signature as slog methods,
	// and can take a mix of slog.Attr and key-value pairs.
	ctx = slogcontext.Append(ctx, slog.String("appendKey", "appendValue"))

	// Use the logger like normal:
	slog.WarnContext(ctx, "main message", "mainKey", "mainValue")
	/*
		{
			"time": "2023-11-15T18:43:23.290798-07:00",
			"level": "WARN",
			"msg": "main message",
			"prependKey": "prependValue",
			"mainKey": "mainValue",
			"appendKey": "appendValue"
		}
	*/

	// Use the logger like normal; add attributes, create groups, pass it around:
	log := slog.With("rootKey", "rootValue")
	log = log.WithGroup("someGroup")
	log = log.With("subKey", "subValue8")

	// The prepended/appended attributes end up in all log lines that use that context
	log.InfoContext(ctx, "main message", "mainKey", "mainValue")
	/*
		{
			"time": "2023-11-14T00:37:03.805196-07:00",
			"level": "INFO",
			"msg": "main message",
			"prependKey": "prependValue",
			"rootKey": "rootValue",
			"someGroup": {
				"subKey": "subValue",
				"mainKey": "mainValue",
				"appendKey": "appendValue"
			}
		}
	*/
}
Output:

func (*Handler) Enabled

func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool

Enabled reports whether the next handler handles records at the given level. The handler ignores records whose level is lower.

func (*Handler) Handle

func (h *Handler) Handle(ctx context.Context, r slog.Record) error

Handle de-duplicates all attributes and groups, then passes the new set of attributes to the next handler.

func (*Handler) WithAttrs

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

WithAttrs returns a new AppendHandler whose attributes consists of h's attributes followed by attrs.

func (*Handler) WithGroup

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

WithGroup returns a new AppendHandler that still has h's attributes, but any future attributes added will be namespaced.

type HandlerOptions

type HandlerOptions struct {
	// A list of functions to be called, each of which will return attributes
	// that should be prepended to the start of every log line with this context.
	// If left nil, the default ExtractPrepended function will be used only.
	Prependers []AttrExtractor

	// A list of functions to be called, each of which will return attributes
	// that should be appended to the end of every log line with this context.
	// If left nil, the default ExtractAppended function will be used only.
	Appenders []AttrExtractor
}

HandlerOptions are options for a Handler

Directories

Path Synopsis
examples module
otel module

Jump to

Keyboard shortcuts

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