log

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 3 Imported by: 0

README

log

CI CodeQL Coverage Mutation Documentation Go Reference Release Go License

log is a production-oriented toolkit built on Go's standard log/slog types. Applications keep accepting and passing *slog.Logger; this module adds small handlers for composition, redaction, sampling, bounded delivery, test capture, local rotation, and OpenTelemetry correlation.

The package does not define a proprietary logger interface, replace the standard JSON or text encoders, initialize OpenTelemetry, or ship direct vendor drivers.

Requirements

  • Go 1.24 or newer.
  • OpenTelemetry API v1.41 when importing the optional otel bridge.

Install

go get github.com/faustbrian/go-log

Quick start

package main

import (
	"log/slog"
	"os"

	log "github.com/faustbrian/go-log"
)

func main() {
	logger, err := log.New(
		slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}),
		log.WithAttrs(slog.String("service", "orders")),
	)
	if err != nil {
		panic(err)
	}

	logger.Info("service ready", slog.String("component", "http"))
}

All application APIs remain standard:

func RunWorker(logger *slog.Logger) error {
	logger.Info("worker started")
	return nil
}

Packages

Package Purpose
root Standard logger constructors and ordered handler options
handler/stack Synchronous fan-out and inclusive per-sink level routes
handler/redact Structural key and path redaction before value evaluation
handler/sample Concurrent every-N and stable key-based sampling
handler/async Bounded delivery with explicit overflow and shutdown
handler/capture Concurrent record capture and test assertions
handler/rotate Permission-enforced rotating io.WriteCloser
otel Optional trace/span correlation from standard context

For Kubernetes, write standard JSON to stdout or stderr and let the platform forward it to an OpenTelemetry Collector. Configure routing, buffering, retries, and Better Stack, Datadog, or another backend in the Collector. This keeps credentials and vendor transports out of application processes.

Use handler/rotate only where a platform log stream is unavailable, such as a single-host or desktop deployment.

Composition order

Handler order changes guarantees. A typical service pipeline is:

slog.Logger
  -> trace correlation
  -> structural redaction
  -> sampling
  -> bounded async delivery
  -> stack routing
  -> standard JSON/text handlers

Put redaction before every sink that can observe values. Put correlation before async delivery so span IDs are captured while the request context is current. Put sampling before async delivery to avoid consuming queue capacity for dropped records.

Delivery guarantees

handler/async uses a fixed-capacity queue and one worker. Its policies are:

  • Block: wait for space without treating context cancellation as record cancellation, as required by slog.Handler.
  • DropNewest: reject the current record with async.ErrDropped.
  • DropOldest: evict the oldest queued record and accept the current record.
  • SyncFallback: deliver the current record on the caller goroutine.

Flush waits for all records accepted before its call. Shutdown stops new acceptance, drains in the background, is repeatable, and honors each caller's deadline. Stats exposes enqueued, delivered, failed, dropped, fallback, and rejected counts. Applications must call Shutdown during graceful shutdown.

Security defaults

  • Redaction is structural; it never searches rendered strings.
  • Matching keys are case-insensitive and paths are exact structural paths.
  • Matched LogValuer values are replaced without being evaluated.
  • Rotated files default to mode 0600 and enforce the configured mode.
  • Messages are not redacted. Never place secrets or untrusted multiline data in log messages; use attributes and configure redaction rules.

See adoption, recipes, operations, and architecture for complete guidance.

Stability and support

The compatibility promise is documented in docs/compatibility.md. Security issues should follow SECURITY.md. Contributions follow CONTRIBUTING.md.

License

MIT. See LICENSE.

Ecosystem

Use the Golib documentation portal to choose companion packages, supported stacks, recipes, and operations guidance.

Documentation

Overview

Package log provides small constructors for composing standard log/slog loggers and handlers without introducing a replacement logger interface.

Example (BoundedAsync)
package main

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

	"github.com/faustbrian/go-log/handler/async"
	"github.com/faustbrian/go-log/handler/capture"
)

func main() {
	sink := capture.New()
	handler, err := async.New(sink, async.Options{
		Capacity: 16,
		Overflow: async.Block,
	})
	if err != nil {
		panic(err)
	}
	slog.New(handler).Info("queued")
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := handler.Shutdown(ctx); err != nil {
		panic(err)
	}

	fmt.Println(sink.Len(), handler.Stats().Delivered)

}
Output:
1 1
Example (RotatingStandardJSON)
package main

import (
	"fmt"
	"log/slog"
	"os"

	"github.com/faustbrian/go-log/handler/rotate"
)

func main() {
	directory, err := os.MkdirTemp("", "log-example")
	if err != nil {
		panic(err)
	}
	defer func() {
		_ = os.RemoveAll(directory)
	}()
	writer, err := rotate.New(rotate.Options{
		Path:     directory + "/service.log",
		MaxBytes: 1 << 20,
		Backups:  2,
	})
	if err != nil {
		panic(err)
	}
	logger := slog.New(slog.NewJSONHandler(writer, &slog.HandlerOptions{ReplaceAttr: removeTime}))
	logger.Info("local")
	if err := writer.Close(); err != nil {
		panic(err)
	}
	contents, err := os.ReadFile(directory + "/service.log")
	if err != nil {
		panic(err)
	}
	fmt.Print(string(contents))

}

func removeTime(_ []string, attr slog.Attr) slog.Attr {
	if attr.Key == slog.TimeKey {
		return slog.Attr{}
	}

	return attr
}
Output:
{"level":"INFO","msg":"local"}
Example (StackRouting)
package main

import (
	"bytes"
	"fmt"
	"log/slog"
	"strings"

	"github.com/faustbrian/go-log/handler/stack"
)

func main() {
	var application bytes.Buffer
	var failures bytes.Buffer
	options := &slog.HandlerOptions{ReplaceAttr: removeTime}
	handler, err := stack.New(
		stack.Route{
			Handler:  slog.NewTextHandler(&application, options),
			MinLevel: slog.LevelInfo,
		},
		stack.Route{
			Handler:  slog.NewTextHandler(&failures, options),
			MinLevel: slog.LevelError,
		},
	)
	if err != nil {
		panic(err)
	}
	logger := slog.New(handler)
	logger.Info("accepted")
	logger.Error("failed")

	fmt.Println(strings.TrimSpace(application.String()))
	fmt.Println(strings.TrimSpace(failures.String()))

}

func removeTime(_ []string, attr slog.Attr) slog.Attr {
	if attr.Key == slog.TimeKey {
		return slog.Attr{}
	}

	return attr
}
Output:
level=INFO msg=accepted
level=ERROR msg=failed
level=ERROR msg=failed
Example (StructuralRedaction)
package main

import (
	"bytes"
	"fmt"
	"log/slog"

	"github.com/faustbrian/go-log/handler/redact"
)

func main() {
	var output bytes.Buffer
	next := slog.NewJSONHandler(&output, &slog.HandlerOptions{ReplaceAttr: removeTime})
	handler, err := redact.New(next, &redact.Options{
		Rules: []redact.Rule{redact.Keys("password", "authorization")},
	})
	if err != nil {
		panic(err)
	}
	slog.New(handler).Info("login",
		slog.String("user", "alice"),
		slog.String("password", "secret"),
	)

	fmt.Print(output.String())

}

func removeTime(_ []string, attr slog.Attr) slog.Attr {
	if attr.Key == slog.TimeKey {
		return slog.Attr{}
	}

	return attr
}
Output:
{"level":"INFO","msg":"login","user":"alice","password":"[REDACTED]"}
Example (TraceCorrelation)
package main

import (
	"context"
	"fmt"
	"log/slog"

	"github.com/faustbrian/go-log/handler/capture"

	logotel "github.com/faustbrian/go-log/otel"
	"go.opentelemetry.io/otel/trace"
)

func main() {
	sink := capture.New()
	handler, err := logotel.New(sink, logotel.Options{})
	if err != nil {
		panic(err)
	}
	spanContext := trace.NewSpanContext(trace.SpanContextConfig{
		TraceID: trace.TraceID{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
		SpanID:  trace.SpanID{1, 0, 0, 0, 0, 0, 0, 1},
	})
	ctx := trace.ContextWithSpanContext(context.Background(), spanContext)
	slog.New(handler).InfoContext(ctx, "correlated")

	record, _ := sink.Last()
	record.Attrs(func(attr slog.Attr) bool {
		fmt.Printf("%s=%s\n", attr.Key, attr.Value.String())
		return true
	})

}
Output:
trace_id=01000000000000000000000000000001
span_id=0100000000000001

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrNilHandler = errors.New("log: nil handler")

ErrNilHandler is returned when New is called without a handler.

Functions

func JSON

func JSON(writer io.Writer, options *slog.HandlerOptions) *slog.Logger

JSON constructs a standard slog JSON logger.

func New

func New(handler slog.Handler, options ...Option) (*slog.Logger, error)

New constructs a standard slog.Logger from handler.

New keeps *slog.Logger as the application-facing type. It returns the first option error and never constructs a logger around a nil handler.

Example
package main

import (
	"log/slog"
	"os"

	log "github.com/faustbrian/go-log"
)

func main() {
	options := &slog.HandlerOptions{ReplaceAttr: removeTime}
	logger, err := log.New(
		slog.NewTextHandler(os.Stdout, options),
		log.WithAttrs(slog.String("service", "orders")),
	)
	if err != nil {
		panic(err)
	}
	logger.Info("ready")

}

func removeTime(_ []string, attr slog.Attr) slog.Attr {
	if attr.Key == slog.TimeKey {
		return slog.Attr{}
	}

	return attr
}
Output:
level=INFO msg=ready service=orders

func Text

func Text(writer io.Writer, options *slog.HandlerOptions) *slog.Logger

Text constructs a standard slog text logger.

Types

type Option

type Option func(slog.Handler) (slog.Handler, error)

Option decorates a slog handler while constructing a logger.

Options are applied in the order supplied to New. An option should return an immutable derived handler and leave its input unchanged.

func WithAttrs

func WithAttrs(attrs ...slog.Attr) Option

WithAttrs returns an option that adds attrs to every record.

func WithGroup

func WithGroup(name string) Option

WithGroup returns an option that qualifies subsequent attributes with name.

Directories

Path Synopsis
handler
async
Package async provides bounded asynchronous delivery for standard log/slog handlers.
Package async provides bounded asynchronous delivery for standard log/slog handlers.
capture
Package capture provides an in-memory slog handler and lightweight test assertions.
Package capture provides an in-memory slog handler and lightweight test assertions.
redact
Package redact provides structural attribute redaction for standard log/slog handlers.
Package redact provides structural attribute redaction for standard log/slog handlers.
rotate
Package rotate provides an optional bounded local-file writer for use with the standard log/slog JSON and text handlers.
Package rotate provides an optional bounded local-file writer for use with the standard log/slog JSON and text handlers.
sample
Package sample provides deterministic and rate-based sampling decorators for standard log/slog handlers.
Package sample provides deterministic and rate-based sampling decorators for standard log/slog handlers.
stack
Package stack provides synchronous fan-out and per-sink level routing for standard log/slog handlers.
Package stack provides synchronous fan-out and per-sink level routing for standard log/slog handlers.
Package otel correlates slog records with the standard OpenTelemetry span context produced by telemetry.
Package otel correlates slog records with the standard OpenTelemetry span context produced by telemetry.

Jump to

Keyboard shortcuts

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