slog-context

repository·main·Indexed 41 days ago

https://github.com/veqryn/slog-context

A utility library for Go 1.21+ that enables structured logging with context. It allows developers to attach loggers and attributes to a context.Context to ensure metadata like TraceIDs or request-scoped attributes are automatically propagated. It features interoperability between slog and logr, gRPC logging interceptors via the sloggrpc package, and OpenTelemetry integration for TraceID and SpanID extraction.

Tokens
9.7K
Snippets
42
Records
48
Agent score
51%

What's inside slog-context

  1. Attributes Extracted from Context Workflow

    main

    This workflow allows you to use standard slog methods (like slog.InfoContext) while automatically injecting attributes from the context.Context into the log lines. This is ideal when you are using the default package-level logger but passing a context around.

    Append and Prepend

    Use slogctx.Prepend(ctx, ...) to add attributes to the start of log lines, or slogctx.Append(ctx, ...) to add them to the end. Both accept a mix of slog.Attr and key-value pairs.

    Custom Context Value Extractor

    You can define custom logic to extract values from a context using the AttrExtractor type. Register these in slogctx.HandlerOptions under Prependers or Appenders.

    OpenTelemetry TraceID/SpanID Extractor

    To include OTEL tracing information, install the sub-module: go get github.com/veqryn/slog-context/otel Then use slogotel.ExtractTraceSpanID in your HandlerOptions.

    HTTP Middleware and Extractor

    For web servers, use the sloghttp package. The sloghttp.AttrCollection middleware allows you to collect attributes (like request IDs) that will be available to all log calls within that request's context.

    // Example: Custom Extractor
    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
    }
    
    func main() {
    	h := slogctx.NewHandler(
    		slog.NewJSONHandler(os.Stdout, nil),
    		&slogctx.HandlerOptions{
    			Prependers: []slogctx.AttrExtractor{slogctx.ExtractPrepended},
    			Appenders: []slogctx.AttrExtractor{
    				slogctx.ExtractAppended,
    				customExtractor,
    			},
    		},
    	)
    	slog.SetDefault(slog.New(h))
    }
  2. Interoperability between slog and logr

    main

    slog-context is compatible with both the standard library slog and the logr interface.

    • If your application only uses slog, only *slog.Logger instances are stored in the context.
    • If your application uses both, *slog.Logger instances will be automatically converted to logr.Logger and vice versa. This ensures full interoperability across different libraries in your dependency stack that might use either logging frontend.
  3. How attributes extracted from context work

    main

    The Attributes Extracted from Context Workflow allows you to attach attributes to a context.Context and have them automatically included in log lines, even if a specific logger isn't passed through the call stack.

    This is achieved using a custom Handler that uses Prepend and Append logic. Instead of storing a logger in the context, the attributes themselves are stored in the context, and the Handler picks them up whenever a log record is written.

    Key components include:

    • HandlerOptions and AttrExtractor types: Used to define how custom values are pulled from a context.
    • slogotel.ExtractTraceSpanID: An optional extractor that automatically adds OpenTelemetry TraceID and SpanID to log records, including error annotation for error-level logs.
    • sloghttp.ExtractAttrCollection: An optional extractor that automatically adds attributes added via sloghttp.With after the sloghttp.AttrCollection middleware.
  4. Logger in Context Workflow

    main

    This workflow involves storing a *slog.Logger instance directly inside a context.Context. This allows you to add attributes and groups to the logger while keeping the logger scoped to the context's lifecycle. It eliminates the need to pass a logger as an explicit argument to every function, as you can simply pass the context.

    Key operations:

    • Initialize: Use slogctx.NewCtx(ctx, logger) to store a logger in a context.
    • Retrieve: Use slogctx.FromCtx(ctx) to get the logger back out for manual use.
    • Modify Context Logger: Use slogctx.With(ctx, ...) to add attributes or slogctx.WithGroup(ctx, ...) to create a group on the logger within the context.
    • Log via Context: Use slogctx.Error(ctx, ...) (or other level wrappers like Info, Warn, Debug) to log using the logger contained in the context. These wrappers support slog.Attr and key-value pairs.
    package main
    
    import (
    	"context"
    	"errors"
    	"log/slog"
    	"os"
    
    	slogctx "github.com/veqryn/slog-context"
    )
    
    func main() {
    	h := slogctx.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)
    	slog.SetDefault(slog.New(h))
    
    	// Store the logger inside the context
    	ctx := slogctx.NewCtx(context.Background(), slog.Default())
    
    	// Get the logger back out for manual usage
    	log := slogctx.FromCtx(ctx)
    	log.Warn("warning")
    
    	// Add attributes directly to the logger in the context
    	ctx = slogctx.With(ctx, "rootKey", "rootValue")
    
    	// Create a group directly on the logger in the context
    	ctx = slogctx.WithGroup(ctx, "someGroup")
    
    	// Add mix of slog.Attr and key-value pairs
    	ctx = slogctx.With(ctx, slog.String("subKey", "subValue"), slog.Bool("someBool", true))
    
    	err := errors.New("an error")
    
    	// Access the logger in the context directly with wrappers
    	slogctx.Error(ctx, "main message",
    		slogctx.Err(err),
    		slog.String("mainKey", "mainValue"))
    }
  5. How the logger in context workflow works

    main

    The Logger in Context Workflow allows you to store an actual *slog.Logger instance directly within a context.Context.

    This workflow provides several utility functions to interact with the logger stored in the context:

    • NewCtx: Stores a logger in a context.
    • FromCtx: Retrieves a logger from a context (falling back to the default logger if none is found).
    • Wrapper methods: With, WithGroup, Debug, Info, Warn, Error, Log, and LogAttrs allow you to call logging methods directly on the context, which internally uses the logger residing within it.
  6. Configure gRPC interceptors with Options

    main
    All Slog*Interceptor functions accept a variadic number of Option arguments. These options allow you to customize the logging behavior, such as providing an InterceptorFilter to skip logging for specific methods based on otelgrpc.InterceptorInfo.
  7. Migrate from v0.4.0 to v0.5.0

    main

    If you are upgrading from version 0.4.0 to 0.5.0, you must account for the following breaking changes:

    1. Function Renames:
      • ToCtx is now NewCtx.
      • Logger is now FromCtx.
    2. Package Rename:
      • The package name has changed from slogcontext to slogctx.

    Note that named imports are unaffected by the package rename.

    // Old way (v0.4.0)
    import "github.com/veqryn/slog-context"
    var h = slogcontext.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)
    
    // New way (v0.5.0+)
    import "github.com/veqryn/slog-context"
    var h = slogctx.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)
  8. Interoperate with slog-multi

    main

    The library provides a convenience method to work with github.com/samber/slog-multi. This allows you to build complex logging pipelines (fanout, routing, failover) that include slog-context middleware.

    slog.SetDefault(slog.New(slogmulti.
    	Pipe(slogctx.NewMiddleware(&slogctx.HandlerOptions{})).
    	Pipe(slogdedup.NewOverwriteMiddleware(&slogdedup.OverwriteHandlerOptions{})).
    	Handler(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{})),
    ))
  9. Use gRPC Logging Interceptors

    main

    The sloggrpc package provides interceptors for both Unary and Stream gRPC calls (Server and Client sides). These interceptors automatically log RPC metadata such as service name, method, role, and request/response payloads.

    Server Interceptors:

    • sloggrpc.SlogUnaryServerInterceptor(...)
    • sloggrpc.SlogStreamServerInterceptor(...)

    Client Interceptors:

    • sloggrpc.SlogUnaryClientInterceptor(...)
    • sloggrpc.SlogStreamClientInterceptor(...)

    Configuration Options:

    • sloggrpc.WithAppendToAttributes(option): Controls which attributes are added. Use sloggrpc.AppendToAttributesAll for full detail or sloggrpc.AppendToAttributesDefault for a slimmer version.
    • sloggrpc.WithInterceptorFilter(filter): Use sloggrpc.InterceptorFilterIgnoreReflection to skip logging for reflection calls.
    • sloggrpc.WithErrorToLevel(level): (Mentioned in code) Customize the log level for errors.
    • sloggrpc.WithLogger(logger): (Mentioned in code) Provide a specific logger.
    // Server Interceptor Example
    grpcServer := grpc.NewServer(
    	grpc.ChainUnaryInterceptor(sloggrpc.SlogUnaryServerInterceptor(
    		sloggrpc.WithAppendToAttributes(sloggrpc.AppendToAttributesAll),
    		sloggrpc.WithInterceptorFilter(sloggrpc.InterceptorFilterIgnoreReflection),
    	))),
    )
  10. Configure slog-context with HandlerOptions

    main

    Use HandlerOptions to define how attributes are extracted from the context.Context and injected into log lines.

    • Prependers: A slice of AttrExtractor functions. Attributes returned by these functions are added to the beginning of the log record. If nil, the default ExtractPrepended function is used.
    • Appenders: A slice of AttrExtractor functions. Attributes returned by these functions are added to the end of the log record. If nil, the default ExtractAppended function is used.
    opts := &slogctx.HandlerOptions{
        Prependers: []slogctx.AttrExtractor{myPrependFunc},
        Appenders:   []slogctx.AttrExtractor{myAppendFunc},
    }
  11. Use SlogUnaryClientInterceptor for gRPC client unary calls

    main

    Use SlogUnaryClientInterceptor to automatically log gRPC unary requests and responses on the client side. It can be passed to grpc.NewClient or used within grpc.WithChainUnaryInterceptor. The interceptor logs the request payload, the response payload, and the elapsed time/error status of the call.

    import (
    	"google.golang.org/grpc"
    	"github.com/veqryn/veqryn/slog-context/sloggrpc"
    )
    
    // Example usage with grpc.NewClient
    conn, err := grpc.NewClient(
    	"target:port",
    	grpc.WithUnaryInterceptor(sloggrpc.SlogUnaryClientInterceptor()),
    )