logfilter

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Mar 3, 2026 License: MIT Imports: 11 Imported by: 1

README

slog-logfilter

A dynamic, filter-based logging library for Go's log/slog. Enables runtime log level control and attribute-based filtering without code changes.

Features

  • Dynamic log levels - Change global log level at runtime via LevelVar
  • Filter-based overrides - Elevate or suppress logs based on attribute values
  • Context extraction - Filter on values stored in context.Context
  • Source-based filtering - Filter by file path or function name (Rust-style module filtering)
  • Simple pattern matching - Fast glob-style patterns (prefix*, *suffix, contains)
  • Optional expiry - Temporary filters with automatic expiration
  • First match wins - Predictable filter ordering
  • Thread-safe - Safe for concurrent use

Installation

go get github.com/jmylchreest/slog-logfilter

Quick Start

package main

import (
    "log/slog"
    "github.com/jmylchreest/slog-logfilter"
)

func main() {
    // Create a filtered logger
    logger := logfilter.New(
        logfilter.WithLevel(slog.LevelInfo),
        logfilter.WithFormat("json"),
    )

    // Add filters at runtime
    logfilter.SetFilters([]logfilter.LogFilter{
        {Type: "job_id", Pattern: "debug_*", Level: "debug", Enabled: true},
    })

    // Logs with matching attributes use filter's level
    logger.Debug("processing", "job_id", "debug_123") // Emitted (filter matches)
    logger.Debug("processing", "job_id", "normal_456") // Suppressed (no match)
    logger.Info("status", "job_id", "normal_456")      // Emitted (INFO >= global)
}

Filter Configuration

LogFilter Structure
type LogFilter struct {
    Type        string     `json:"type"`         // Attribute key or special prefix
    Pattern     string     `json:"pattern"`      // Glob pattern for value
    Level       string     `json:"level"`        // Minimum threshold: debug, info, warn, error
    OutputLevel string     `json:"output_level"` // Optional: transform output level
    Enabled     bool       `json:"enabled"`      // Whether filter is active
    ExpiresAt   *time.Time `json:"expires_at"`   // Optional expiry (nil = never)
}
Field Defaults and Behavior
Field Default Description
type (required) Attribute key, or special prefix (context:, source:file, source:function)
pattern (required) Glob pattern: exact, prefix*, *suffix, *contains*
level "info" Minimum threshold. Logs below this level are suppressed.
output_level (pass-through) If omitted/empty, preserves original log level. If set, transforms output.
enabled false Filter is only active when true
expires_at (never) If omitted/null, filter never expires

Important:

  • level="" defaults to "info", which suppresses DEBUG logs. Use level="debug" to allow all levels.
  • output_level="" or omitted means pass-through - the original log level is preserved in output.
Filter Types
Type Description Example Pattern
attribute_name Match log attribute value "job_*" matches job_id="job_123"
context:key Match value from context.Context "user_*" matches context user_id
source:file Match source file path (relative) "internal/service/*"
source:function Match function name "*Extraction*"
Pattern Matching
Pattern Match Type Example
value Exact "job_123" matches only "job_123"
prefix* Prefix "job_*" matches "job_123", "job_abc"
*suffix Suffix "*_prod" matches "job_prod", "task_prod"
*contains* Contains "*error*" matches "big_error_here"
Example Filters
[
  {"type": "job_id", "pattern": "job_abc*", "level": "debug", "enabled": true},
  {"type": "user_id", "pattern": "user_123", "level": "debug", "enabled": true},
  {"type": "context:user_id", "pattern": "debug_user_*", "level": "debug", "enabled": true},
  {"type": "source:file", "pattern": "internal/service/*", "level": "debug", "enabled": true},
  {"type": "source:function", "pattern": "*Extraction*", "level": "debug", "enabled": true},
  {"type": "endpoint", "pattern": "/api/v1/extract", "level": "debug", "enabled": true,
   "expires_at": "2024-01-15T00:00:00Z"}
]

Context Filtering

Filter on values stored in context (useful for request-scoped data):

// Register a context extractor
logfilter.RegisterContextExtractor("user_id", func(ctx context.Context) (string, bool) {
    if v := ctx.Value(UserIDKey); v != nil {
        if s, ok := v.(string); ok {
            return s, true
        }
    }
    return "", false
})

// Use "context:" prefix in filter type
logfilter.AddFilter(logfilter.LogFilter{
    Type:    "context:user_id",
    Pattern: "debug_user_*",
    Level:   "debug",
    Enabled: true,
})

// Logs will check context for user_id
ctx := context.WithValue(context.Background(), UserIDKey, "debug_user_123")
logger.DebugContext(ctx, "user action") // Emitted (context matches)

Source-Based Filtering

Filter logs based on where they originate in your code (similar to Rust's RUST_LOG module filtering):

// Enable debug logging for all files in the service package
logfilter.AddFilter(logfilter.LogFilter{
    Type:    "source:file",
    Pattern: "internal/service/*",
    Level:   "debug",
    Enabled: true,
})

// Enable debug logging for all Extraction-related functions
logfilter.AddFilter(logfilter.LogFilter{
    Type:    "source:function",
    Pattern: "*Extraction*",
    Level:   "debug",
    Enabled: true,
})
Source Filter Details
  • source:file - Matches against the source file path
  • source:function - Matches against the function name (e.g., (*ExtractionService).Extract)

Path formats for source:file:

  • Local files (within your project): relative path like internal/service/extraction.go
  • External packages: prefixed with @ like @github.com/user/repo/pkg/file.go

This allows you to filter logs from specific external dependencies:

[
  {"type": "source:file", "pattern": "internal/*", "level": "debug", "enabled": true},
  {"type": "source:file", "pattern": "@github.com/jmylchreest/refyne/*", "level": "debug", "enabled": true}
]
Performance

Source extraction only occurs when source-based filters are configured. If you have no source:file or source:function filters, there's zero overhead from this feature.

Example: Debug a Specific Package
[
  {"type": "source:file", "pattern": "internal/service/*", "level": "debug", "enabled": true},
  {"type": "source:file", "pattern": "internal/worker/*", "level": "debug", "enabled": true}
]
Example: Debug Specific Functions
[
  {"type": "source:function", "pattern": "*Handler*", "level": "debug", "enabled": true},
  {"type": "source:function", "pattern": "Process*", "level": "debug", "enabled": true}
]

Runtime API

// Change global level
logfilter.SetLevel(slog.LevelDebug)
level := logfilter.GetLevel()

// Manage filters
logfilter.SetFilters(filters)           // Replace all filters
logfilter.AddFilter(filter)             // Add single filter
logfilter.RemoveFilter("job_id", "abc*") // Remove by type+pattern
logfilter.ClearFilters()                // Remove all filters
filters := logfilter.GetFilters()       // Get current filters

Filter Behavior

Elevation (DEBUG when global is INFO)
// Global level: INFO
// Filter: {type: "job_id", pattern: "debug_*", level: "debug"}

logger.Debug("msg", "job_id", "debug_123") // Emitted (filter elevates)
logger.Debug("msg", "job_id", "normal_456") // Suppressed (no filter match)
Suppression (WARN when global is INFO)
// Global level: INFO
// Filter: {type: "job_id", pattern: "noisy_*", level: "warn"}

logger.Info("msg", "job_id", "noisy_123")  // Suppressed (filter sets WARN)
logger.Warn("msg", "job_id", "noisy_123")  // Emitted (WARN >= filter level)
logger.Info("msg", "job_id", "normal_456") // Emitted (no filter, uses global)
First Match Wins

Filters are checked in order. First matching filter determines the level:

filters := []logfilter.LogFilter{
    {Type: "job_id", Pattern: "job_*", Level: "debug", Enabled: true},   // First
    {Type: "job_id", Pattern: "job_123", Level: "error", Enabled: true}, // Never used for job_123
}
// "job_123" matches first filter, uses DEBUG (not ERROR)
Output Level Transformation

Use output_level to transform the emitted log level. This is useful when you want verbose debugging but don't want DEBUG-level noise in your log aggregator:

// Global level: INFO
// Filter: {type: "job_id", pattern: "debug_*", level: "debug", output_level: "info"}

logger.Debug("detailed trace", "job_id", "debug_123")
// Result: Emitted as INFO (passes filter, level transformed)
// Output: level=INFO msg="detailed trace" job_id=debug_123

Use cases:

  • Elevate debug logs to INFO so they appear in production log streams
  • Make important debug info visible without lowering global log level
  • Tag certain debug logs as WARN for alerting systems
[
  {"type": "job_id", "pattern": "critical_*", "level": "debug", "output_level": "warn", "enabled": true},
  {"type": "source:file", "pattern": "internal/payment/*", "level": "debug", "output_level": "info", "enabled": true}
]

Integration Example

Load filters from JSON config (e.g., from S3):

func loadFilters(data []byte) error {
    var filters []logfilter.LogFilter
    if err := json.Unmarshal(data, &filters); err != nil {
        return err
    }
    logfilter.SetFilters(filters)
    return nil
}

// Periodic refresh
func refreshFilters(ctx context.Context, s3Client *s3.Client) {
    ticker := time.NewTicker(5 * time.Minute)
    for {
        select {
        case <-ticker.C:
            data, err := s3Client.GetObject(ctx, "config/logfilters.json")
            if err == nil {
                loadFilters(data)
            }
        case <-ctx.Done():
            return
        }
    }
}

Performance

The handler is optimized for minimal overhead:

  • Fast path: If log level >= global level, no filter checking needed
  • Cached lowest level: Quick check if any filter could match
  • Simple patterns: No regex, just string prefix/suffix/contains
  • Lock-free reads: RWMutex for concurrent filter access
  • Lazy source extraction: Source file/function only extracted when source filters are configured

License

MIT License - see LICENSE for details.

Documentation

Overview

Package logfilter provides a dynamic, filter-based logging system for Go's slog.

It supports:

  • Dynamic log level changes at runtime via LevelVar
  • Filter-based level overrides (elevate or suppress logs based on attributes)
  • Context value extraction for filtering
  • Simple glob-style pattern matching (prefix*, *suffix, *contains*)
  • Optional filter expiry for temporary debugging

Basic usage:

// Create a filtered logger
logger := logfilter.New(
    logfilter.WithLevel(slog.LevelInfo),
    logfilter.WithFormat("json"),
)

// Add filters at runtime
logfilter.SetFilters([]logfilter.LogFilter{
    {Type: "job_id", Pattern: "job_abc*", Level: "debug", Enabled: true},
})

// Logs with matching attributes will use filter's level
logger.Debug("processing", "job_id", "job_abc123") // Emitted (filter matches)
logger.Debug("processing", "job_id", "job_xyz")    // Suppressed (no match)

Context filtering:

// Register a context extractor
logfilter.RegisterContextExtractor("user_id", func(ctx context.Context) (string, bool) {
    if v := ctx.Value(UserIDKey); v != nil {
        return v.(string), true
    }
    return "", false
})

// Use context: prefix in filter type
logfilter.AddFilter(logfilter.LogFilter{
    Type: "context:user_id", Pattern: "user_123", Level: "debug", Enabled: true,
})

Index

Constants

View Source
const (
	ContextPrefix        = "context:"
	SourceFilePrefix     = "source:file"
	SourceFunctionPrefix = "source:function"
)

Source filter type prefixes.

Variables

This section is empty.

Functions

func AddFilter

func AddFilter(filter LogFilter)

AddFilter adds a filter to the global handler.

func ClearContextExtractors

func ClearContextExtractors()

ClearContextExtractors removes all registered context extractors. Useful for testing.

func ClearFilters

func ClearFilters()

ClearFilters removes all filters from the global handler.

func ContextExtractorKeys

func ContextExtractorKeys() []string

ContextExtractorKeys returns the keys of all registered context extractors.

func GetLevel

func GetLevel() slog.Level

GetLevel returns the current global log level.

func New

func New(opts ...Option) *slog.Logger

New creates a new slog.Logger with filter support. The returned logger uses the global filter handler, so filters can be updated at runtime using SetFilters, AddFilter, etc.

func ParseLevel

func ParseLevel(level string) slog.Level

ParseLevel converts a level string to slog.Level.

func RegisterContextExtractor

func RegisterContextExtractor(key string, extractor ContextExtractor)

RegisterContextExtractor registers a function to extract a value from context for the given key. This is used by filters with type "context:key".

Example:

logfilter.RegisterContextExtractor("job_id", func(ctx context.Context) (string, bool) {
    if v := ctx.Value(JobIDKey); v != nil {
        if s, ok := v.(string); ok {
            return s, true
        }
    }
    return "", false
})

func RemoveFilter

func RemoveFilter(filterType, pattern string)

RemoveFilter removes filters matching the given type and pattern.

func SetDefault

func SetDefault(opts ...Option) *slog.Logger

SetDefault creates a new logger with the given options and sets it as the default slog logger.

func SetFilters

func SetFilters(filters []LogFilter)

SetFilters replaces all filters on the global handler. Filters are applied in order; first match wins.

func SetLevel

func SetLevel(level slog.Level)

SetLevel changes the global log level at runtime.

func UnregisterContextExtractor

func UnregisterContextExtractor(key string)

UnregisterContextExtractor removes a context extractor for the given key.

Types

type ContextExtractor

type ContextExtractor func(ctx context.Context) (string, bool)

ContextExtractor is a function that extracts a string value from context. It should return the value and true if found, or empty string and false if not.

func GetContextExtractor

func GetContextExtractor(key string) ContextExtractor

GetContextExtractor returns the extractor for the given key, or nil if not registered.

type Handler

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

Handler is an slog.Handler that supports dynamic log levels and filter-based level overrides. It wraps an inner handler and checks filters before delegating.

func GetHandler

func GetHandler() *Handler

GetHandler returns the global filter handler. This can be used to wrap with additional handlers or for testing.

func NewHandler

func NewHandler(inner slog.Handler, globalLevel *slog.LevelVar) *Handler

NewHandler creates a new filter-aware handler wrapping the given inner handler. The globalLevel is used as the default log level when no filters match.

func (*Handler) AddFilter

func (h *Handler) AddFilter(filter LogFilter)

AddFilter adds a filter to the end of the filter list.

func (*Handler) ClearFilters

func (h *Handler) ClearFilters()

ClearFilters removes all filters.

func (*Handler) Enabled

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

Enabled reports whether the handler handles records at the given level. It returns true if either: - The level is >= the global level, OR - There are active filters that might match at this level

func (*Handler) GetFilters

func (h *Handler) GetFilters() []LogFilter

GetFilters returns a copy of the current filters.

func (*Handler) Handle

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

Handle processes a log record, applying filters to determine the effective level. If a matching filter has OutputLevel set, the record's level is transformed before emission.

func (*Handler) RemoveFilter

func (h *Handler) RemoveFilter(filterType, pattern string)

RemoveFilter removes filters matching the given type and pattern.

func (*Handler) SetFilters

func (h *Handler) SetFilters(filters []LogFilter)

SetFilters replaces all filters with the given list. Filters are applied in order; first match wins.

func (*Handler) WithAttrs

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

WithAttrs returns a new Handler with the given attributes added.

func (*Handler) WithGroup

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

WithGroup returns a new Handler with the given group name.

type LogFilter

type LogFilter struct {
	// Type is the attribute key to match (e.g., "job_id", "user_id", "package").
	// Special prefixes:
	//   - "context:key" for context values (e.g., "context:job_id")
	//   - "source:file" for source file path filtering
	//   - "source:function" for function name filtering
	Type string `json:"type"`

	// Pattern for matching the attribute value.
	// Supports simple glob-style patterns:
	//   - "value"    exact match
	//   - "prefix*"  prefix match
	//   - "*suffix"  suffix match
	//   - "*contains*" contains match
	Pattern string `json:"pattern"`

	// Level is the minimum threshold for logs matching this filter.
	// Logs below this level are suppressed, logs at or above pass through.
	// Valid values: "debug", "info", "warn", "error"
	Level string `json:"level"`

	// OutputLevel optionally transforms the log level in the output.
	// If set, matching logs are emitted at this level instead of their original level.
	// This is useful for elevating debug logs to info so they appear in normal log streams.
	// If empty, the original log level is preserved.
	// Valid values: "", "debug", "info", "warn", "error"
	OutputLevel string `json:"output_level,omitempty"`

	// Enabled controls whether this filter is active.
	Enabled bool `json:"enabled"`

	// ExpiresAt is an optional expiry time for temporary filters.
	// If nil or zero, the filter never expires.
	ExpiresAt *time.Time `json:"expires_at,omitempty"`
	// contains filtered or unexported fields
}

LogFilter defines a log level override based on attribute matching.

func GetFilters

func GetFilters() []LogFilter

GetFilters returns a copy of the current filters.

func (*LogFilter) AttributeKey

func (f *LogFilter) AttributeKey() string

AttributeKey returns the attribute key for attribute filters. Returns the type as-is for non-context and non-source filters.

func (*LogFilter) ContextKey

func (f *LogFilter) ContextKey() string

ContextKey returns the context key for context filters. Returns empty string if not a context filter.

func (*LogFilter) GetOutputLevel

func (f *LogFilter) GetOutputLevel(originalLevel slog.Level) slog.Level

GetOutputLevel returns the parsed output level, or the original level if not set.

func (*LogFilter) HasOutputLevel

func (f *LogFilter) HasOutputLevel() bool

HasOutputLevel returns true if this filter transforms the output level.

func (*LogFilter) IsActive

func (f *LogFilter) IsActive() bool

IsActive returns true if the filter is enabled and not expired.

func (*LogFilter) IsContextFilter

func (f *LogFilter) IsContextFilter() bool

IsContextFilter returns true if this filter checks context values.

func (*LogFilter) IsExpired

func (f *LogFilter) IsExpired() bool

IsExpired returns true if the filter has expired.

func (*LogFilter) IsSourceFileFilter

func (f *LogFilter) IsSourceFileFilter() bool

IsSourceFileFilter returns true if this filter checks source file path.

func (*LogFilter) IsSourceFilter

func (f *LogFilter) IsSourceFilter() bool

IsSourceFilter returns true if this filter checks source file or function.

func (*LogFilter) IsSourceFunctionFilter

func (f *LogFilter) IsSourceFunctionFilter() bool

IsSourceFunctionFilter returns true if this filter checks function name.

func (*LogFilter) Matches

func (f *LogFilter) Matches(value string) bool

Matches checks if the given value matches the filter pattern. Returns true if the pattern matches.

type Option

type Option func(*options)

Option configures the logger.

func WithFilters

func WithFilters(filters []LogFilter) Option

WithFilters sets the initial filters.

func WithFormat

func WithFormat(format string) Option

WithFormat sets the output format ("json" or "text").

func WithLevel

func WithLevel(level slog.Level) Option

WithLevel sets the initial log level.

func WithOutput

func WithOutput(w io.Writer) Option

WithOutput sets the output writer (default: os.Stdout).

func WithSource

func WithSource(enabled bool) Option

WithSource enables source file:line in log output.

Jump to

Keyboard shortcuts

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