filewatcher

package module
v2.4.2 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 27 Imported by: 0

README

go-filewatcher

A high-performance, composable file system watcher for Go.

Go Reference CI

Documentation · API Reference


Built on fsnotify. Eliminates the boilerplate of raw fsnotify with sensible defaults, automatic recursion, 17+ composable filters, 18 middleware, and production-grade resilience.

Why?

Raw fsnotify gives you events and nothing else. Every real-world file watcher needs the same infrastructure:

  • Recursion — you must walk directories and add each one manually
  • Filtering — you write the same extension/ignore/pattern logic every time
  • Debouncing — editors trigger 5 events on a single save; you coalesce them yourself
  • Resilience — ENOSPC crashes your watcher; you handle it or you don't
  • Observability — no stats, no metrics, no tracing hooks

go-filewatcher handles all of this. Built for production, tested with -race, and deployed in real systems.

Comparison

Feature Raw fsnotify Other wrappers go-filewatcher
Recursive watching Partial ✓
Built-in filters Few 17+
Middleware chains 18
Debouncing Partial Global+PerPath
.gitignore-aware ✓
ENOSPC resilience ✓
NFS/FUSE polling ✓
Self-healing ✓
Prometheus + OTel ✓

How it works

  1. Create — New(paths, opts...) validates paths, walks directories, applies options, and registers inotify watches
  2. Watch — Watch(ctx) starts the event loop goroutine, returns a read-only event channel
  3. Pipeline — each event passes through your filter chain (AND/OR/NOT composition) and middleware (logging, recovery, rate limiting, metrics)
  4. Deliver — filtered events arrive on the channel with path, op, timestamp, size, modtime, and optional content hash

Install

go get github.com/larsartmann/go-filewatcher/v2

Requires Go 1.26.7 or later (the version pinned in go.mod and the CI matrix).

Usage

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    filewatcher "github.com/larsartmann/go-filewatcher/v2"
)

func main() {
    watcher, err := filewatcher.New(
        []string{"./src"},
        filewatcher.WithExtensions(".go"),
        filewatcher.WithDebounce(500*time.Millisecond),
        filewatcher.WithIgnoreDirs("vendor", "node_modules"),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer watcher.Close()

    events, err := watcher.Watch(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    for event := range events {
        fmt.Printf("%s: %s\n", event.Op, event.Path)
    }
}

With Middleware

watcher, err := filewatcher.New(
    []string{"./src"},
    filewatcher.WithExtensions(".go"),
    filewatcher.WithMiddleware(
        filewatcher.MiddlewareRecovery(),   // Runs LAST (innermost)
        filewatcher.MiddlewareLogging(nil), // Runs FIRST (outermost)
    ),
)

With Custom Filters

filter := filewatcher.FilterAnd(
    filewatcher.FilterExtensions(".go"),
    filewatcher.FilterNot(filewatcher.FilterIgnoreDirs("vendor")),
    filewatcher.FilterOperations(filewatcher.Write, filewatcher.Create),
)

watcher, err := filewatcher.New(
    []string{"./src"},
    filewatcher.WithFilter(filter),
)

Filtering Generated Code

Exclude auto-generated Go files via gogenfilter:

watcher, err := filewatcher.New(
    []string{"./src"},
    filewatcher.WithFilter(filewatcher.FilterGeneratedCode()),
)

Detects sqlc, protobuf, templ, mockgen, stringer, and 13 more generators.

sqlc note (gogenfilter v3.6): weak filenames like models.go are NOT treated as sqlc-generated on their own (hand-written models.go is too common). Strong *.sql.go patterns still match by filename, and the // Code generated by sqlc. DO NOT EDIT. content marker is detected when content checking is enabled.

Configuration Options

Option Description Default
WithDebounce(d) Global debounce — all events coalesced into one emission after delay 0 (disabled)
WithPerPathDebounce(d) Per-path debounce — each file debounced independently 0 (disabled)
WithFilter(f) Add a custom filter function —
WithExtensions(exts...) Only emit events for given file extensions —
WithIgnoreDirs(dirs...) Discard events from given directory names (also skips during walk) —
WithIgnorePatterns(pats...) Discard events for files matching glob patterns (filename only) —
WithIgnoreHidden() Discard events for hidden files/dirs (dot prefix) —
WithRecursive(b) Enable/disable recursive directory watching true
WithMiddleware(m...) Add middleware to the event processing pipeline —
WithErrorHandler(fn) Set custom error handler for watcher errors stderr logging
WithOnError(fn) ⚠️ Deprecated. Use WithErrorHandler. Simplified callback (func(error)) —
WithSkipDotDirs(skip) Skip directories starting with a dot during walking true
WithBuffer(size) Event channel buffer size for handling bursts 64
WithOnAdd(fn) Callback invoked when a new path is added to the watcher —
WithLazyIsDir() Skip os.Stat calls in event conversion (IsDir always false) false
WithPolling(fallback) Supplement fsnotify with periodic polling (NFS/FUSE/Docker volumes) false
WithPollInterval(d) Polling interval (requires WithPolling(true)) 2s when polling enabled
WithDebug(logger) Enable verbose structured debug logging —
WithFollowSymlinks(b) Follow symbolic links during directory walking false
WithGitignore(b) .gitignore-aware walk filtering (skips ignored subtrees) true
WithExcludePaths(paths...) Exclude absolute paths (and subtrees) during walk (prefix matching) —
WithMaxWatches(n) Override inotify watch budget (auto-detected from /proc/sys/... on Linux) auto-detected
WithContentHashing() Populate Event.Hash with SHA-256 of file content (capped 10 MiB) false
WithSelfHeal(interval) Auto-retry failed watch registrations at the given interval 0 (disabled)

Filters

Filters determine which events are emitted. Return true to keep, false to discard.

Filter Description
FilterExtensions(exts...) Only files with given extensions
FilterIgnoreExtensions(exts...) Exclude files with given extensions
FilterIgnoreDirs(dirs...) Exclude files within given directory names
FilterExcludePaths(paths...) Exclude files within given absolute paths (prefix match)
FilterIgnoreHidden() Exclude hidden files/directories
FilterIgnoreGlobs(patterns...) Exclude files matching glob patterns (filename only)
FilterOperations(ops...) Only given operation types
FilterNotOperations(ops...) Exclude given operation types
FilterGlob(pattern) Match file name against glob pattern
FilterRegex(pattern) Match path against regex pattern
FilterMinSize(bytes) Only files at least the given size
FilterMaxSize(bytes) Only files at most the given size
FilterMinAge(age) Only files older than given duration
FilterModifiedSince(t) Only files modified after given time
FilterContentHash(expectedHex) Only files matching expected SHA-256
FilterGitignore(repoRoot) Exclude files ignored by .gitignore in repoRoot
FilterGeneratedCode(gens...) Exclude auto-generated Go files (sqlc, protobuf, ...)

Compose with FilterAnd, FilterOr, and FilterNot.

Middleware

Middleware wraps event handlers for cross-cutting concerns. Applied in reverse order (last added runs first).

Middleware Description
MiddlewareLogging(logger) Log all events with structured logging (slog)
MiddlewareRecovery() Recover from panics, log stack trace
MiddlewareFilter(filter) Filter events (same as WithFilter)
MiddlewareOnError(handler) Handle errors from downstream handlers
MiddlewareRateLimit(maxEvents) ⚠️ Deprecated. Use MiddlewareThrottle(maxEvents, maxEvents) instead
MiddlewareSlidingWindowRateLimit(n, win) Sliding-window rate limiting
MiddlewareThrottle(maxEvents, burst) Token-bucket rate limiting via golang.org/x/time/rate
MiddlewareMetrics(counter) Count processed events by operation
MiddlewareDeduplicate(window) Drop duplicate events within a time window
MiddlewareBatch(window, maxSize, flush) Batch events over a window or size threshold
MiddlewareWriteFileLog(path) Write events to file for audit trail
MiddlewareCircuitBreaker(maxFail, reset) Fault tolerance with closed/open/half-open states
MiddlewareExponentialBackoff(maxF, init, max) Configurable backoff for event processing
MiddlewareErrorRateLimit(maxErrs, window) Per-error-type rate limiting
MiddlewareErrorRecovery(strategy) Recoverable error handling with custom strategies
MiddlewareErrorCorrelation(idGen) Attach correlation IDs for request tracing
MiddlewareErrorSanitization(sanitize) Safe error message scrubbing preserving error chains
MiddlewareErrorBatch(window, maxSize, flush) Batch errors for analytics

Event

type Event struct {
    Path      string    // Absolute path of changed file/directory
    Op        Op        // Create, Write, Remove, or Rename
    Timestamp time.Time // When the event was detected
    IsDir     bool      // True if directory, false if file
    Size      int64     // File size in bytes (0 if unavailable)
    ModTime   time.Time // File modification time (zero if unavailable)
    Hash      string    // SHA-256 hex digest (populated only with WithContentHashing)
}

Event priority when multiple operations coalesce: Create > Write > Remove > Rename.

Full JSON marshaling and slog.LogValuer support. Chmod events are ignored.

Resilience

Built for large, long-running watchers:

watcher, err := filewatcher.New(
    []string{"./large-monorepo"},
    filewatcher.WithGitignore(true),                   // default: true
    filewatcher.WithExcludePaths("/home/me/forks"),    // skip subtrees
    filewatcher.WithMaxWatches(524288),                 // override inotify budget
    filewatcher.WithSelfHeal(30 * time.Second),         // auto-retry failures
    filewatcher.WithFollowSymlinks(true),               // follow symlinks
)

When the inotify budget is exhausted, directories are skipped silently and counted in Stats.WatchErrors — the watcher starts in degraded mode instead of failing entirely.

NFS/FUSE Support

watcher, err := filewatcher.New(
    []string{"/mnt/nfs/share"},
    filewatcher.WithPolling(true),
    filewatcher.WithPollInterval(2 * time.Second),
)

Filesystem Compatibility

Filesystems differ in two ways that silently break naive watchers: case sensitivity (NTFS/APFS are case-insensitive, ext4/XFS are case-sensitive) and Unicode normalization (macOS stores filenames as decomposed NFD; most other systems use composed NFC). go-filewatcher handles both automatically.

watcher, err := filewatcher.New(
    []string{"./project"},
    // Auto-detects from the OS: case-insensitive on macOS/Windows,
    // case-sensitive on Linux. Override explicitly when needed:
    filewatcher.WithCaseSensitivity(filewatcher.CaseSensitivityAuto),
)

How it works:

  • NFC normalization is always on. Every path key (Add, Remove, debounce, exclude-path, poll snapshot, and gitignore matching) is normalized to NFC, so an NFD path from the macOS filesystem and an NFC path from user input resolve to the same key. ASCII paths are unaffected (zero-cost fast path).
  • Case-sensitivity is configurable. In case-insensitive mode, path keys are lowercased so File.txt and file.txt collide — preventing duplicate watches, missed Remove() calls, and case-only-rename phantom events.
  • Check the resolved mode at runtime: watcher.EffectiveCaseSensitivity() returns the type-safe FilesystemCaseSensitivity enum, or stats.CaseSensitivity for the string form.

Filter-level wrappers (without changing the watcher-wide mode):

// Case-insensitive: lowercase + NFC-normalize before the inner filter:
filter := filewatcher.FilterCaseInsensitive(filewatcher.FilterExtensions(".go"))

// Case-sensitive: NFC-normalize only (useful on macOS NFD paths):
filter := filewatcher.FilterCaseSensitive(filewatcher.FilterRegex(`^/Project/.*\.go$`))

Observability

Stats

stats := watcher.Stats()
fmt.Printf("watching %d/%d paths (%.1f%% budget), %d add failures\n",
    stats.WatchCount, stats.WatchBudgetCap,
    stats.WatchBudgetUsed*100, stats.WatchErrors)

Prometheus

The PrometheusCollector is dependency-free: it exposes Counters() and Gauges() that you adapt to your own metrics registry. Here is a complete adapter using the standard prometheus/client_golang package:

import "github.com/prometheus/client_golang/prometheus"

// filewatcherCollector implements prometheus.Collector by delegating
// to filewatcher.PrometheusCollector. Prometheus calls Collect on each
// scrape — no polling loop needed.
type filewatcherCollector struct {
    coll  *filewatcher.PrometheusCollector
    descs map[string]*prometheus.Desc
}

func newFilewatcherCollector(coll *filewatcher.PrometheusCollector) *filewatcherCollector {
    descs := make(map[string]*prometheus.Desc)
    for _, c := range coll.Counters() {
        descs[c.Name] = prometheus.NewDesc(c.Name, c.Help, nil, nil)
    }
    for _, g := range coll.Gauges() {
        descs[g.Name] = prometheus.NewDesc(g.Name, g.Help, nil, nil)
    }
    return &filewatcherCollector{coll: coll, descs: descs}
}

func (f *filewatcherCollector) Describe(ch chan<- *prometheus.Desc) {
    for _, desc := range f.descs {
        ch <- desc
    }
}

func (f *filewatcherCollector) Collect(ch chan<- prometheus.Metric) {
    for _, c := range f.coll.Counters() {
        ch <- prometheus.MustNewConstMetric(f.descs[c.Name], prometheus.CounterValue, float64(c.Value))
    }
    for _, g := range f.coll.Gauges() {
        ch <- prometheus.MustNewConstMetric(f.descs[g.Name], prometheus.GaugeValue, g.Value)
    }
}

// Register once at startup — Prometheus scrapes on its own schedule:
coll := filewatcher.NewPrometheusCollector(watcher.Stats)
prometheus.MustRegister(newFilewatcherCollector(coll))

The filewatcher_case_sensitivity gauge encodes the resolved mode as a number: 0 = case-sensitive, 1 = case-insensitive, 2 = auto (will resolve to 0 or 1 at runtime). Use this to alert when a watcher is unexpectedly in the wrong mode.

OpenTelemetry

OTelMiddleware is zero-dependency: you provide an OTelSpan adapter that bridges to the real go.opentelemetry.io/otel/trace.Span. Spans are created per event, with filewatcher.path, filewatcher.op, and filewatcher.is_dir attributes attached. Status is set to ok or error automatically.

import (
    "context"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
)

// otelSpanAdapter bridges trace.Span to filewatcher.OTelSpan.
type otelSpanAdapter struct {
    span trace.Span
}

func (a *otelSpanAdapter) End() { a.span.End() }

func (a *otelSpanAdapter) SetStatus(code, desc string) {
    if code == "error" {
        a.span.SetStatus(codes.Error, desc)
    } else {
        a.span.SetStatus(codes.Ok, desc)
    }
}

func (a *otelSpanAdapter) SetAttributes(attrs ...filewatcher.Attribute) {
    converted := make([]attribute.KeyValue, len(attrs))
    for i, attr := range attrs {
        converted[i] = attribute.String(attr.Key, attr.Value)
    }
    a.span.SetAttributes(converted...)
}

// Wire it into the watcher pipeline:
watcher, err := filewatcher.New(paths,
    filewatcher.WithMiddleware(
        filewatcher.OTelMiddleware(func(path, op string) filewatcher.OTelSpan {
            _, span := otel.Tracer("filewatcher").Start(
                context.Background(), "filewatcher.event",
            )
            return &otelSpanAdapter{span: span}
        }),
    ),
)

To see spans in a real exporter, initialize the OTel SDK with a stdout or Jaeger exporter at program startup. Each file event will produce a span that propagates to the configured exporter:

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

// stdout exporter for development — pretty-prints spans as JSON:
exp, _ := stdouttrace.New(stdouttrace.WithPrettyPrint())
tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exp))
otel.SetTracerProvider(tp)

// Each filewatcher event now produces a span you can see in the trace output.

Benchmarks

Performance characteristics on Apple M2 (arm64):

Benchmark Operations/sec Time/op Allocations
New/SinglePath 53,822 30.9 µs 18 allocs
New/WithOptions 31,879 34.3 µs 28 allocs
ConvertEvent/Create 179,262 7.5 µs 3 allocs
ConvertEvent/Chmod 178,305,804 10.8 ns 0 allocs
PassesFilters/Single 26,671,284 61.4 ns 0 allocs
PassesFilters/Complex 2,325,330 595 ns 0 allocs
BuildMiddleware/None 7,333,308 302 ns 2 allocs
BuildMiddleware/Three 1,000,000 1.37 µs 11 allocs
Stats/Empty 21,545,258 51.0 ns 0 allocs
WatchList/Copy 444,613 6.4 µs 1 alloc

Run benchmarks: nix run .#bench or go test -bench=. -benchmem

Dependencies

Dependency Purpose
fsnotify/fsnotify Core file watching (v1.10.1)
LarsArtmann/gogenfilter/v3 Generated code detection (v3.6.1)
sabhiram/go-gitignore .gitignore pattern matching (zero transitive deps)
golang.org/x/text/unicode/norm NFC Unicode normalization in path keys
golang.org/x/time/rate Token-bucket rate limiting for MiddlewareThrottle

Design Decisions

  • Functional Options — clean, extensible configuration API
  • Channel Streaming — natural Go concurrency patterns, no callbacks
  • Middleware Chains — composable cross-cutting concerns, applied in reverse order
  • Sentinel Errors — errors.Is() for error checking, typed WatcherError with codes and categories
  • Context First — context.Context for cancellation and timeouts
  • Composition — filters and middleware compose elegantly with AND/OR/NOT
  • Minimal Dependencies — only fsnotify, gogenfilter, gitignore, x/text/norm, and x/time/rate
  • Phantom Types — EventPath, RootPath, DebounceKey for compile-time type safety

Error Handling

All errors are wrapped and checkable with errors.Is:

watcher, err := filewatcher.New(paths)
if errors.Is(err, filewatcher.ErrPathNotFound) {
    // Handle missing path
}

11 sentinel errors, 11 error codes, and structured WatcherError with transient/permanent categorization. Access runtime errors via watcher.Errors() channel.

Development

This project uses Nix Flakes for reproducible builds:

nix develop              # Enter development shell
nix run .#check          # Full quality: vet + lint + test
nix run .#ci             # Full CI: tidy + fmt + vet + lint + test
nix run .#lint-fix       # Auto-fix linter issues
nix run .#test           # Run tests with -race
nix flake check          # Run all quality gates

Related docs: Features · Roadmap · API Stability · Troubleshooting · Migration Guide · Changelog

Examples

Runnable examples in the examples/ directory:

go run ./examples/basic              # Simplest usage
go run ./examples/per-path-debounce   # Each file independently
go run ./examples/middleware          # Logging, recovery, metrics
  • go-daemon — building blocks for long-lived daemons served over unix domain sockets: socket lifecycle, sd_notify readiness/watchdog, graceful drain, JSON/CBOR/SSE. The natural companion when a watcher outgrows its process: start watching in its Lifecycle.OnListen hook and stop in OnShutdown, gate systemd readiness on established watches, stream Events over the socket, and back a health endpoint with Stats(). Its systemd watchdog pairs well with WithSelfHeal — self-heal recovers transient failures in-process, the watchdog catches hard hangs.

API Stability

This library follows Go module versioning. The core New/Watch/Event API is stable and unlikely to change. See API_STABILITY.md for details.

License

MIT © Lars Artmann

Documentation

Overview

Package filewatcher provides a high-level, composable file system watcher built on top of fsnotify.

Quick Start

Create a watcher, start watching, and process events:

watcher, err := filewatcher.New(
    []string{"./src"},
    filewatcher.WithExtensions(".go"),
    filewatcher.WithDebounce(500*time.Millisecond),
    filewatcher.WithIgnoreDirs("vendor", "node_modules"),
)
if err != nil {
    log.Fatal(err)
}
defer watcher.Close()

events, err := watcher.Watch(ctx)
for event := range events {
    fmt.Printf("%s: %s\n", event.Op, event.Path)
}

Design Principles

Following go-cqrs-lite conventions:

  • Functional options for configuration
  • Sentinel errors with stdlib errors package
  • Explicit error handling; panics only for programmer errors (invalid arguments)
  • Context as first parameter
  • Channel-based event streaming
  • Middleware chains for cross-cutting concerns
  • Composition over inheritance

Filters

Built-in filters can be combined with AND/OR/NOT logic:

filter := filewatcher.FilterAnd(
    filewatcher.FilterExtensions(".go"),
    filewatcher.FilterNot(filewatcher.FilterIgnoreDirs("vendor")),
    filewatcher.FilterOperations(filewatcher.Write, filewatcher.Create),
)

Middleware

Middleware wraps event handlers for cross-cutting concerns:

watcher, _ := filewatcher.New(paths,
    filewatcher.WithMiddleware(
        filewatcher.MiddlewareRecovery(),
        filewatcher.MiddlewareLogging(nil),
    ),
)

Debounce

Two debounce modes:

  • WithDebounce: global debounce (all events coalesced)
  • WithPerPathDebounce: per-path debounce (independent per file)

Filesystem Compatibility

Different platforms and filesystems have different semantics for filename case and Unicode normalization. The watcher handles this transparently:

watcher, _ := filewatcher.New(paths,
    // Auto-detects: case-insensitive on Windows/macOS, case-sensitive on Linux.
    // Override with CaseSensitive or CaseInsensitive for non-default filesystems.
    filewatcher.WithCaseSensitivity(filewatcher.CaseSensitivityAuto),
)

Internally, all path comparisons (watch dedup, Remove matching, debounce keys, exclude paths, gitignore matching, poll loop snapshots) use a canonical pathKey that applies NFC Unicode normalization and optional case-folding. This fixes the invisible mismatches that occur when macOS stores filenames as NFD (decomposed) but user-configured paths are NFC (composed).

Example: the filename "café" is stored on macOS as the bytes 63 61 66 65 CC 81 (NFD: "cafe" + combining acute U+0301), while user input is typically 63 61 66 C3 A9 (NFC: precomposed é U+00E9). Without NFC normalization, these two byte sequences never compare equal, causing exclude-path matching, debounce deduplication, and gitignore prefix checks to silently fail.

For filter-level case-insensitivity without changing the watcher-wide mode, wrap any Filter with FilterCaseInsensitive (see the example). For NFC normalization without case-folding, use FilterCaseSensitive.

Check the resolved mode at runtime via EffectiveCaseSensitivity() (returns the FilesystemCaseSensitivity enum) or Stats().CaseSensitivityMode.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrWatcherClosed is returned when operations are attempted on a closed watcher.
	ErrWatcherClosed = errors.New("watcher is closed")

	// ErrNoPaths is returned when no paths are provided to the watcher.
	ErrNoPaths = errors.New("at least one path is required")

	// ErrPathNotFound is returned when a specified path does not exist.
	ErrPathNotFound = errors.New("path not found")

	// ErrPathNotDir is returned when a path is not a directory.
	ErrPathNotDir = errors.New("path is not a directory")

	// ErrWatcherRunning is returned when Watch() is called on an already running watcher.
	ErrWatcherRunning = errors.New("watcher is already running")

	// ErrUnknownOp is returned when parsing an unknown operation string.
	ErrUnknownOp = errors.New("unknown operation")

	// ErrFsnotifyFailed is returned when the underlying fsnotify watcher fails.
	ErrFsnotifyFailed = errors.New("fsnotify operation failed")

	// ErrWalkFailed is returned when directory traversal fails.
	ErrWalkFailed = errors.New("directory walk failed")

	// ErrPathResolveFailed is returned when path resolution fails.
	ErrPathResolveFailed = errors.New("path resolution failed")

	// ErrEventProcessingFailed is returned when event processing fails.
	ErrEventProcessingFailed = errors.New("event processing failed")

	// ErrMiddlewareFailed is returned when middleware execution fails.
	ErrMiddlewareFailed = errors.New("middleware execution failed")
)

Sentinel errors for common failure modes.

View Source
var DefaultIgnoreDirs = []string{
	".git", ".hg", ".svn",
	"vendor", "node_modules",
	"dist", buildDir, "bin", "out",
	"__pycache__", ".cache",
}

DefaultIgnoreDirs returns a copy of the commonly ignored directory names. The returned slice is safe to modify without affecting the defaults.

Functions

func DefaultIgnoreDirsCopy

func DefaultIgnoreDirsCopy() []string

DefaultIgnoreDirsCopy returns a defensive copy of DefaultIgnoreDirs so callers cannot mutate the global default.

func IsPermanentError

func IsPermanentError(err error) bool

IsPermanentError reports whether an error will not resolve on retry.

func IsTransientError

func IsTransientError(err error) bool

IsTransientError reports whether an error is potentially retryable.

Types

type Attribute added in v2.2.0

type Attribute struct {
	Key   string
	Value string
}

Attribute is a minimal key-value pair for span attributes.

type BatchError added in v2.1.0

type BatchError struct {
	Event Event
	Error error
}

BatchError represents an error paired with the event that caused it.

type CircuitState added in v2.1.0

type CircuitState int

CircuitState represents the state of a circuit breaker.

const (
	// CircuitClosed means the circuit is healthy and all events pass through.
	CircuitClosed CircuitState = iota
	// CircuitOpen means the circuit has tripped and all events are dropped.
	CircuitOpen
	// CircuitHalfOpen means the circuit is testing if the downstream is healthy.
	CircuitHalfOpen
)

func (CircuitState) String added in v2.1.0

func (s CircuitState) String() string

String returns a human-readable name for the circuit state.

type ContentCheckMode

type ContentCheckMode int

ContentCheckMode controls whether generated code detection reads file content. This eliminates boolean blindness at call sites.

const (
	// ContentCheckDisabled uses only filename-based detection (zero I/O).
	ContentCheckDisabled ContentCheckMode = iota
	// ContentCheckEnabled uses filename + content detection (may read files).
	ContentCheckEnabled
)

type CounterMetric added in v2.2.0

type CounterMetric struct {
	Name  string
	Help  string
	Value uint64
}

CounterMetric is a generic counter descriptor for a Prometheus metric. It carries the name, help text, and value extraction from Stats.

Users adapt this to their metrics library by iterating over the PrometheusCollector.Counters() method, which returns []CounterMetric.

type DebounceKey

type DebounceKey string

DebounceKey is a distinct type for debouncer keys (typically file paths). It ensures debounce keys are not mixed with other path-like strings.

func NewDebounceKey

func NewDebounceKey(key string) DebounceKey

NewDebounceKey creates a new DebounceKey from a string.

func (DebounceKey) Get

func (dk DebounceKey) Get() string

Get returns the underlying string value.

func (DebounceKey) IsZero

func (dk DebounceKey) IsZero() bool

IsZero returns true if the DebounceKey is empty.

func (DebounceKey) String

func (dk DebounceKey) String() string

String returns the string representation.

type Debouncer

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

Debouncer prevents rapid successive function executions by coalescing calls within a delay window. It supports per-key debouncing so that different keys (e.g., file paths) are debounced independently.

Example

ExampleDebouncer demonstrates using the debouncer directly.

package main

import (
	"fmt"
	"time"

	"github.com/larsartmann/go-filewatcher/v2"
)

func main() {
	// Create a debouncer with 100ms delay
	debouncer := filewatcher.NewDebouncer(100 * time.Millisecond)
	defer debouncer.Stop()

	// Schedule a function - it will run after 100ms unless reset
	debouncer.Debounce(filewatcher.NewDebounceKey("key"), func() {
		fmt.Println("Debounced function executed")
	})

	// Flush to execute immediately
	debouncer.Flush()

	fmt.Printf("Pending: %d\n", debouncer.Pending())
}
Output:
Debounced function executed
Pending: 0

func NewDebouncer

func NewDebouncer(delay time.Duration) *Debouncer

NewDebouncer creates a new Debouncer with the specified delay.

func (*Debouncer) Debounce

func (d *Debouncer) Debounce(key DebounceKey, callback func())

Debounce schedules fn to run after the delay, resetting any pending execution for the same key. This ensures callback runs only once for a burst of events sharing the same key.

func (*Debouncer) Flush

func (d *Debouncer) Flush()

Flush executes all pending functions immediately and clears all timers.

func (*Debouncer) Pending

func (d *Debouncer) Pending() int

Pending returns the number of keys with pending executions.

func (*Debouncer) Stop

func (d *Debouncer) Stop()

Stop cancels all pending executions without running them. Waits for any in-flight callbacks to complete before returning.

type DebouncerInterface

type DebouncerInterface interface {
	Debounce(key DebounceKey, fn func())
	Stop()
	Flush()
}

DebouncerInterface is the interface for debouncer implementations.

type ErrorCategory

type ErrorCategory int

ErrorCategory classifies errors as transient (retryable) or permanent.

const (
	// CategoryUnknown indicates the error category could not be determined.
	CategoryUnknown ErrorCategory = iota

	// CategoryTransient indicates a temporary error that may resolve on retry.
	// Examples: temporary filesystem issues, resource contention.
	CategoryTransient

	// CategoryPermanent indicates a persistent error that won't resolve on retry.
	// Examples: invalid paths, permission denied, watcher closed.
	CategoryPermanent
)

func (ErrorCategory) String added in v2.1.0

func (c ErrorCategory) String() string

String returns a human-readable name for the error category.

type ErrorCode added in v2.1.0

type ErrorCode string

ErrorCode is a string identifier for categorizing watcher errors. Use these constants for programmatic error matching instead of comparing error messages.

const (
	// ErrorCodeWatcherClosed indicates an operation on a closed watcher.
	ErrorCodeWatcherClosed ErrorCode = "WATCHER_CLOSED"
	// ErrorCodeNoPaths indicates no paths were provided.
	ErrorCodeNoPaths ErrorCode = "NO_PATHS"
	// ErrorCodePathNotFound indicates a specified path does not exist.
	ErrorCodePathNotFound ErrorCode = "PATH_NOT_FOUND"
	// ErrorCodePathNotDir indicates a path is not a directory.
	ErrorCodePathNotDir ErrorCode = "PATH_NOT_DIR"
	// ErrorCodeAlreadyRunning indicates Watch() was called while running.
	ErrorCodeAlreadyRunning ErrorCode = "ALREADY_RUNNING"
	// ErrorCodeFsnotify indicates an fsnotify-level failure.
	ErrorCodeFsnotify ErrorCode = "FSNOTIFY_ERROR"
	// ErrorCodeWalk indicates a directory walk failure.
	ErrorCodeWalk ErrorCode = "WALK_ERROR"
	// ErrorCodePathResolve indicates a path resolution failure.
	ErrorCodePathResolve ErrorCode = "PATH_RESOLVE_ERROR"
	// ErrorCodeEventProcessing indicates an event processing failure.
	ErrorCodeEventProcessing ErrorCode = "EVENT_PROCESSING_ERROR"
	// ErrorCodeMiddleware indicates a middleware execution failure.
	ErrorCodeMiddleware ErrorCode = "MIDDLEWARE_ERROR"
	// ErrorCodeUnknown indicates an uncategorized error.
	ErrorCodeUnknown ErrorCode = "UNKNOWN"
)

type ErrorContext

type ErrorContext struct {
	// Operation is the high-level operation being performed.
	Operation string

	// Path is the file path involved, if any.
	Path string

	// Event holds the event being processed, if applicable.
	Event *Event

	// Retryable indicates whether this error might resolve on retry.
	Retryable bool
}

ErrorContext provides context about what was happening when an error occurred. This is passed to error handlers for better observability.

type ErrorHandler

type ErrorHandler func(ctx ErrorContext, err error)

ErrorHandler is called when errors occur during watching. The context provides additional information about what was happening.

type Event

type Event struct {
	// Path is the absolute path of the file or directory that changed.
	Path string `json:"path"`
	// Op is the operation that occurred.
	Op Op `json:"op"`
	// Timestamp is when the event was detected.
	Timestamp time.Time `json:"timestamp"`
	// IsDir indicates whether the event is for a directory (true) or file (false).
	IsDir bool `json:"isDir"`
	// Size is the file size in bytes, if available. Zero if the information
	// could not be obtained (e.g., file already removed, or lazy mode enabled).
	Size int64 `json:"size"`
	// ModTime is the file modification time, if available. Zero time.Time if
	// the information could not be obtained.
	ModTime time.Time `json:"modTime,omitzero"`
	// Hash is the hex-encoded SHA-256 content hash, if available.
	// Only populated when the watcher is created with WithContentHashing().
	// Empty string if hashing is disabled or the file could not be read
	// (e.g., directory, removed file, or permission denied).
	Hash string `json:"hash,omitempty"`
}

Event represents a file system change event.

Example

ExampleEvent demonstrates event structure.

package main

import (
	"fmt"
	"time"

	"github.com/larsartmann/go-filewatcher/v2"
)

func main() {
	// Use a fixed timestamp for deterministic output
	fixedTime := time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)
	event := filewatcher.Event{
		Path:      "/path/to/file.go",
		Op:        filewatcher.Write,
		Timestamp: fixedTime,
		IsDir:     false,
		Size:      0,
		ModTime:   time.Time{},
	}

	fmt.Printf("Event: %s\n", event.String())
	fmt.Printf("Operation: %s\n", event.Op.String())
	fmt.Printf("Is directory: %v\n", event.IsDir)

}
Output:
Event: WRITE /path/to/file.go at 2006-01-02T15:04:05Z
Operation: WRITE
Is directory: false

func (Event) GetPath

func (e Event) GetPath() EventPath

GetPath returns the event path as a phantom type for type-safe usage.

func (Event) LogValue

func (e Event) LogValue() slog.Value

LogValue implements slog.LogValuer for structured logging.

func (Event) String

func (e Event) String() string

String returns a human-readable representation of the event.

type EventChannelMode added in v2.4.0

type EventChannelMode int

EventChannelMode controls the watcher's behavior when the event channel is full.

const (
	// EventChannelBlocking blocks the watch loop until the event can be sent.
	// This is the default and preserves all events at the cost of potential
	// kernel-level event drops if the consumer is consistently slow.
	EventChannelBlocking EventChannelMode = iota
	// EventChannelDropOnFull drops events when the channel is full, incrementing
	// Stats.EventsDroppedByBackpressure. Use this when you prefer losing events
	// over blocking the watcher.
	EventChannelDropOnFull
)

type EventPath

type EventPath string

EventPath is a distinct type for event file/directory paths. It prevents accidentally passing event paths where other path types are expected.

Example

ExampleEventPath demonstrates phantom type usage for type-safe paths.

package main

import (
	"fmt"

	"github.com/larsartmann/go-filewatcher/v2"
)

func main() {
	// Create an event and extract its path as a phantom type
	event := filewatcher.Event{
		Path: "/home/user/project/main.go",
		Op:   filewatcher.Write,
	}

	path := event.GetPath()
	fmt.Printf("Base: %s\n", path.Base())
	fmt.Printf("Extension: %s\n", path.Ext())
	fmt.Printf("Directory: %s\n", path.Dir())

}
Output:
Base: main.go
Extension: .go
Directory: /home/user/project

func NewEventPath

func NewEventPath(path string) EventPath

NewEventPath creates a new EventPath from a string.

func (EventPath) Base

func (ep EventPath) Base() string

Base returns the last element of the path. Example: EventPath("/home/user/file.go").Base() returns "file.go".

func (EventPath) Dir

func (ep EventPath) Dir() EventPath

Dir returns all but the last element of the path. Example: EventPath("/home/user/file.go").Dir() returns EventPath("/home/user").

func (EventPath) Ext

func (ep EventPath) Ext() string

Ext returns the file extension of the path. Example: EventPath("/home/user/file.go").Ext() returns ".go".

func (EventPath) Get

func (ep EventPath) Get() string

Get returns the underlying string value.

func (EventPath) IsZero

func (ep EventPath) IsZero() bool

IsZero returns true if the EventPath is empty.

func (EventPath) Join

func (ep EventPath) Join(elem ...string) EventPath

Join appends the given elements to the path. Example: EventPath("/home/user").Join("docs", "readme.md") returns EventPath("/home/user/docs/readme.md").

func (EventPath) String

func (ep EventPath) String() string

String returns the string representation.

type FilesystemCaseSensitivity added in v2.4.0

type FilesystemCaseSensitivity int

FilesystemCaseSensitivity controls whether path comparisons treat character case.

Different filesystems behave differently: NTFS (Windows) and APFS (macOS) are case-insensitive by default, while ext4/XFS/btrfs (Linux) are case-sensitive. Choosing the wrong mode can cause duplicate watches, missed Remove() calls, or inconsistent debouncing on paths that differ only in case.

const (
	// CaseSensitivityAuto selects the platform default: case-insensitive on
	// Windows and macOS, case-sensitive everywhere else.
	CaseSensitivityAuto FilesystemCaseSensitivity = iota
	// CaseSensitive treats paths that differ only in case as distinct.
	CaseSensitive
	// CaseInsensitive treats paths that differ only in case as identical.
	CaseInsensitive
)

func (FilesystemCaseSensitivity) GaugeValue added in v2.4.0

func (c FilesystemCaseSensitivity) GaugeValue() float64

GaugeValue returns the numeric encoding for the Prometheus filewatcher_case_sensitivity gauge (0=case-sensitive, 1=case-insensitive, 2=auto). This is the single source of truth for the gauge encoding.

func (FilesystemCaseSensitivity) String added in v2.4.0

func (c FilesystemCaseSensitivity) String() string

String returns a human-readable representation of the case-sensitivity mode.

type Filter

type Filter func(event Event) bool

Filter determines whether a file event should be processed. Return true to keep the event, false to discard it.

func FilterAnd

func FilterAnd(filters ...Filter) Filter

FilterAnd combines multiple filters with AND logic. All filters must return true for the event to pass.

Example

ExampleFilterAnd demonstrates combining filters with AND logic.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with combined filters created")
		},
		filewatcher.WithFilter(goExcludeDirsFilter("vendor")),
	)
}

func goExcludeDirsFilter(dirs ...string) filewatcher.Filter {
	return filewatcher.FilterAnd(
		filewatcher.FilterExtensions(".go"),
		filewatcher.FilterNot(filewatcher.FilterIgnoreDirs(dirs...)),
	)
}
Output:
Watcher with combined filters created

func FilterCaseInsensitive added in v2.4.0

func FilterCaseInsensitive(inner Filter) Filter

FilterCaseInsensitive wraps an inner Filter, normalizing the event path to lowercase + NFC before passing it to the inner filter. This makes path-based filters (FilterRegex, FilterGlob, FilterExcludePaths, etc.) match case-insensitively without modifying their implementation.

This is a standalone filter-level wrapper. For full filesystem-awareness (watch dedup, Remove matching, debounce keys, gitignore, poll loop), use WithCaseSensitivity(CaseInsensitive) instead — this wrapper only affects the specific filter it wraps.

Example

ExampleFilterCaseInsensitive demonstrates wrapping a filter so event paths are matched case-insensitively and NFC-normalized, regardless of the watcher-wide case-sensitivity mode.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with case-insensitive .go filter")
		},
		filewatcher.WithFilter(
			filewatcher.FilterCaseInsensitive(filewatcher.FilterExtensions(".go")),
		),
	)
}
Output:
Watcher with case-insensitive .go filter

func FilterCaseSensitive added in v2.4.0

func FilterCaseSensitive(inner Filter) Filter

FilterCaseSensitive wraps an inner Filter, NFC-normalizing the event path before passing it to the inner filter. Unlike FilterCaseInsensitive, it preserves character case. This is useful on macOS (where event paths may arrive as NFD) to ensure NFC-normalized matching without case-folding.

Example

ExampleFilterCaseSensitive demonstrates wrapping a filter so event paths are NFC-normalized (fixing macOS NFD paths) while preserving character case.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with case-sensitive NFC-normalized filter")
		},
		filewatcher.WithFilter(
			filewatcher.FilterCaseSensitive(filewatcher.FilterExtensions(".go")),
		),
	)
}
Output:
Watcher with case-sensitive NFC-normalized filter

func FilterContentHash added in v2.1.0

func FilterContentHash(expectedHex string) Filter

FilterContentHash creates a filter that only passes events for files whose SHA-256 content hash matches the expected hash. This is useful for detecting when a file's content has actually changed, ignoring touch-only modifications. Directory events always pass through. Files that cannot be read are filtered out.

func FilterExcludePaths

func FilterExcludePaths(paths ...string) Filter

FilterExcludePaths creates a filter that discards events for files matching any of the given exact paths. Paths are matched after normalization (absolute path conversion).

This differs from FilterIgnoreDirs which matches directory names anywhere in the path. FilterExcludePaths requires exact path matches.

Example:

// Exclude specific files
watcher, _ := filewatcher.New("./src",
    filewatcher.WithFilter(filewatcher.FilterExcludePaths(
        "/home/user/project/src/generated.go",
        "/home/user/project/src/vendor",
    )),
)

func FilterExtensions

func FilterExtensions(exts ...string) Filter

FilterExtensions creates a filter that only passes events for files matching one of the given extensions. Extensions should include the dot prefix (e.g., ".go", ".md").

Example

ExampleFilterExtensions demonstrates filtering by file extension.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher filtering .go and .md files")
		},
		filewatcher.WithExtensions(".go", ".md"),
	)
}
Output:
Watcher filtering .go and .md files

func FilterFromWithMeta added in v2.2.0

func FilterFromWithMeta(f FilterWithMeta) Filter

FilterFromWithMeta converts a FilterWithMeta to a plain Filter. The boolean result is preserved; metadata is discarded.

func FilterGeneratedCode

func FilterGeneratedCode(options ...gogenfilter.FilterOption) Filter

FilterGeneratedCode creates a filewatcher filter that excludes auto-generated Go code files detected by gogenfilter using filename-based detection only. This is zero-I/O and suitable for most file watching scenarios.

For content-based detection, use FilterGeneratedCodeFull with ContentCheckEnabled.

Example:

watcher, _ := filewatcher.New("./src",
    filewatcher.WithFilter(filewatcher.FilterGeneratedCode(
        gogenfilter.FilterSQLC,
        gogenfilter.FilterProtobuf,
    )),
)

This filter returns false for generated files (excluding them from events), and true for non-generated files (allowing them through).

func FilterGeneratedCodeFull

func FilterGeneratedCodeFull(mode ContentCheckMode, options ...gogenfilter.FilterOption) Filter

FilterGeneratedCodeFull creates a filter with configurable content checking.

Use ContentCheckDisabled for only filename-based detection (zero I/O). Use ContentCheckEnabled for filename + content detection (may read files).

Content checking is more accurate but requires file I/O. For file watching scenarios, filename-only detection is usually sufficient since generated files typically have distinctive naming patterns.

When content checking is enabled, files larger than 10 MiB skip the content check and fall back to filename-only detection to avoid blocking the event loop on large files.

The filter returns true to keep (non-generated) files, false to discard (generated) files.

func FilterGeneratedCodeWithFilter

func FilterGeneratedCodeWithFilter(genFilter *gogenfilter.Filter) Filter

FilterGeneratedCodeWithFilter creates a filter using an existing gogenfilter.Filter instance. This allows for more advanced configuration including custom filesystems and include/exclude patterns.

Example:

config, _ := gogenfilter.WithFilterOptions(gogenfilter.FilterAll)
genFilter, _ := gogenfilter.NewFilter(config)

watcher, _ := filewatcher.New("./src",
    filewatcher.WithFilter(filewatcher.FilterGeneratedCodeWithFilter(genFilter)),
)

func FilterGitignore added in v2.2.0

func FilterGitignore(repoRoot string) Filter

FilterGitignore creates a filter that discards events for paths matching .gitignore rules from the specified repo root directory. This provides event-level filtering to supplement the walk-time gitignore filtering. The .gitignore file is loaded from repoRoot at filter creation time. If the .gitignore file cannot be loaded, all events pass through.

func FilterGlob

func FilterGlob(pattern string) Filter

FilterGlob creates a filter that only passes events for files matching the given glob pattern.

func FilterIgnoreDirs

func FilterIgnoreDirs(dirs ...string) Filter

FilterIgnoreDirs creates a filter that discards events for files within directories matching any of the given directory names. Directory names are matched against path components (e.g., "vendor" matches both "vendor" and "pkg/vendor").

func FilterIgnoreDirsCaseInsensitive added in v2.4.0

func FilterIgnoreDirsCaseInsensitive(dirs ...string) Filter

FilterIgnoreDirsCaseInsensitive is like FilterIgnoreDirs but compares directory names case-insensitively. Use this on case-insensitive filesystems (NTFS, APFS) where "BUILD" and "build" refer to the same directory.

func FilterIgnoreExtensions

func FilterIgnoreExtensions(exts ...string) Filter

FilterIgnoreExtensions creates a filter that discards events for files matching one of the given extensions.

func FilterIgnoreGlobs

func FilterIgnoreGlobs(patterns ...string) Filter

FilterIgnoreGlobs creates a filter that discards events for files matching any of the given glob patterns. This is useful for excluding files by pattern (e.g., "*.log", "*.tmp", ".*") at the filter level.

func FilterIgnoreHidden

func FilterIgnoreHidden() Filter

FilterIgnoreHidden creates a filter that discards events for hidden files and directories (those starting with a dot).

func FilterMaxSize

func FilterMaxSize(maxSize int64) Filter

FilterMaxSize creates a filter that only passes events for files with size less than or equal to the given maximum size in bytes. Directory events are not filtered by size.

func FilterMinAge

func FilterMinAge(age time.Duration) Filter

FilterMinAge creates a filter that only passes events for files that are at least the given age old. Directory events are not filtered. Useful for ignoring recently modified files (e.g., during save operations).

func FilterMinSize

func FilterMinSize(minSize int64) Filter

FilterMinSize creates a filter that only passes events for files with size greater than or equal to the given minimum size in bytes. Directory events are not filtered by size.

Example

ExampleFilterMinSize demonstrates filtering by minimum file size.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with min size filter created")
		},
		filewatcher.WithFilter(filewatcher.FilterMinSize(1024)),
	)
}
Output:
Watcher with min size filter created

func FilterModifiedSince

func FilterModifiedSince(minTime time.Time) Filter

FilterModifiedSince creates a filter that only passes events for files modified after the given time. Directory events are not filtered by time. Useful for ignoring old files during initial scan.

Example

ExampleFilterModifiedSince demonstrates filtering by modification time.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	// Only process files modified in the last hour
	oneHourAgo := time.Now().Add(-time.Hour)

	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with time filter created")
		},
		filewatcher.WithFilter(filewatcher.FilterModifiedSince(oneHourAgo)),
	)
}
Output:
Watcher with time filter created

func FilterNot

func FilterNot(f Filter) Filter

FilterNot inverts a filter.

func FilterNotOperations

func FilterNotOperations(ops ...Op) Filter

FilterNotOperations creates a filter that discards events matching any of the given operations.

func FilterOperations

func FilterOperations(ops ...Op) Filter

FilterOperations creates a filter that only passes events matching one of the given operations.

func FilterOr

func FilterOr(filters ...Filter) Filter

FilterOr combines multiple filters with OR logic. At least one filter must return true for the event to pass.

Example

ExampleFilterOr demonstrates combining filters with OR logic.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with OR filter created")
		},
		filewatcher.WithFilter(filewatcher.FilterOr(
			filewatcher.FilterExtensions(".go"),
			filewatcher.FilterExtensions(".md"),
		)),
	)
}
Output:
Watcher with OR filter created

func FilterRegex

func FilterRegex(pattern string) Filter

FilterRegex creates a filter that only passes events for paths matching the given regular expression pattern. The pattern is pre-compiled at creation time for efficiency. Panics if the pattern is invalid (use regexp.Compile for runtime validation).

Example

ExampleFilterRegex demonstrates filtering with regex patterns.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher filtering with regex pattern")
		},
		filewatcher.WithFilter(filewatcher.FilterRegex(`_test\.go$`)),
	)
}
Output:
Watcher filtering with regex pattern

type FilterWithMeta added in v2.2.0

type FilterWithMeta func(event Event) MatchResult

FilterWithMeta is a filter that returns match metadata in addition to the boolean match result. This is useful when callers want to log why events were kept or dropped (e.g., for debugging, audit logs, or metrics on filter behavior).

Use FilterWithMetaAnd/FilterWithMetaOr to compose multiple metadata filters.

func FilterWithMetaAnd added in v2.2.0

func FilterWithMetaAnd(filters ...FilterWithMeta) FilterWithMeta

FilterWithMetaAnd combines multiple FilterWithMeta with AND logic. All filters must return Matched=true. The first filter that reports Matched=false short-circuits, returning its result.

func FilterWithMetaNot added in v2.2.0

func FilterWithMetaNot(f FilterWithMeta, name string) FilterWithMeta

FilterWithMetaNot inverts a FilterWithMeta.

func FilterWithMetaOr added in v2.2.0

func FilterWithMetaOr(filters ...FilterWithMeta) FilterWithMeta

FilterWithMetaOr combines multiple FilterWithMeta with OR logic. At least one filter must return Matched=true. The first matching filter's result is returned.

func WithMeta added in v2.2.0

func WithMeta(f Filter, name, reason string) FilterWithMeta

WithMeta wraps a plain Filter with a name and reason, returning a FilterWithMeta. The result is always Matched=true if the inner filter passes, with the given reason and name attached.

type GaugeMetric added in v2.2.0

type GaugeMetric struct {
	Name  string
	Help  string
	Value float64
}

GaugeMetric is a generic gauge descriptor for a Prometheus metric.

type GeneratedCodeDetector

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

GeneratedCodeDetector provides a reusable detector for generated code. Useful when you need to check files outside of the event filter context.

func NewGeneratedCodeDetector

func NewGeneratedCodeDetector(options ...gogenfilter.FilterOption) *GeneratedCodeDetector

NewGeneratedCodeDetector creates a new detector with the specified options.

func (*GeneratedCodeDetector) GetReason

func (d *GeneratedCodeDetector) GetReason(filePath string) gogenfilter.FilterReason

GetReason returns the specific reason why a file was detected as generated, or gogenfilter.ReasonNotFiltered if it's not generated.

func (*GeneratedCodeDetector) IsGenerated

func (d *GeneratedCodeDetector) IsGenerated(filePath string) bool

IsGenerated checks if a file path represents generated code using filename-based detection only (zero I/O).

func (*GeneratedCodeDetector) IsGeneratedWithContent

func (d *GeneratedCodeDetector) IsGeneratedWithContent(filePath, content string) bool

IsGeneratedWithContent checks if a file is generated using both filename and content detection.

type GlobalDebouncer

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

GlobalDebouncer coalesces all events into a single timer, regardless of key. Useful when you want to batch all file changes into one action.

func NewGlobalDebouncer

func NewGlobalDebouncer(delay time.Duration) *GlobalDebouncer

NewGlobalDebouncer creates a new GlobalDebouncer with the specified delay.

func (*GlobalDebouncer) Debounce

func (g *GlobalDebouncer) Debounce(_ DebounceKey, callback func())

Debounce resets the global timer. callback runs only once after the delay since the last call, regardless of how many times Debounce is called. The key parameter is intentionally ignored — GlobalDebouncer coalesces all events into a single timer regardless of their key.

Note: only the last callback is executed. If multiple events have different callbacks, earlier ones are discarded. This is by design: GlobalDebouncer coalesces all events into a single action.

func (*GlobalDebouncer) Flush

func (g *GlobalDebouncer) Flush()

Flush executes the pending function immediately and clears the timer.

func (*GlobalDebouncer) Pending

func (g *GlobalDebouncer) Pending() int

Pending returns whether there is a pending execution.

func (*GlobalDebouncer) Stop

func (g *GlobalDebouncer) Stop()

Stop cancels the pending execution. Waits for any in-flight callback to complete before returning.

type Handler

type Handler func(ctx context.Context, event Event) error

Handler processes a file event.

type LogSubstring

type LogSubstring string

LogSubstring is a distinct type for log substring assertions in tests.

func NewLogSubstring

func NewLogSubstring(s string) LogSubstring

NewLogSubstring creates a new LogSubstring from a string.

func (LogSubstring) Get

func (ls LogSubstring) Get() string

Get returns the underlying string value.

func (LogSubstring) String

func (ls LogSubstring) String() string

String returns the string representation of LogSubstring.

type MatchResult added in v2.2.0

type MatchResult struct {
	// Matched indicates whether the event passed the filter.
	Matched bool
	// Reason is a short, human-readable explanation of why the event
	// matched or didn't match (e.g., "extension .go", "in ignored dir").
	// May be empty.
	Reason string
	// FilterName is the name of the filter that produced this result.
	// Useful when composing multiple filters with FilterWithMetaAnd/Or.
	FilterName string
}

MatchResult is the result of a filter evaluation that returns metadata. It allows filters to communicate WHY an event matched or didn't match, which is useful for debugging, logging, and analytics.

type Middleware

type Middleware func(Handler) Handler

Middleware wraps an event handler for cross-cutting concerns. Middleware is applied in reverse order (last added runs first), matching the go-cqrs-lite convention.

func MiddlewareBatch

func MiddlewareBatch(window time.Duration, maxSize int, flush func([]Event) error) Middleware

func MiddlewareCircuitBreaker added in v2.1.0

func MiddlewareCircuitBreaker(maxFailures int, resetTimeout time.Duration) Middleware

func MiddlewareDeduplicate

func MiddlewareDeduplicate(window time.Duration) Middleware

MiddlewareDeduplicate returns a middleware that drops duplicate events for the same file path and operation within a time window. This is useful for reducing noise from rapid successive file operations.

Path normalization: NFC Unicode normalization is applied so that NFD (macOS decomposed) and NFC (composed) forms of the same path are treated as duplicates. For case-insensitive deduplication, use MiddlewareDeduplicateCaseInsensitive.

Example: A file saved twice in quick succession generates two events, but only the first is processed.

Example

ExampleMiddlewareDeduplicate demonstrates event deduplication.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with deduplication created")
		},
		filewatcher.WithMiddleware(
			filewatcher.MiddlewareDeduplicate(100*time.Millisecond),
		),
	)
}
Output:
Watcher with deduplication created

func MiddlewareDeduplicateCaseInsensitive added in v2.4.0

func MiddlewareDeduplicateCaseInsensitive(window time.Duration) Middleware

MiddlewareDeduplicateCaseInsensitive is like MiddlewareDeduplicate but also folds character case when comparing paths. Use this on case-insensitive filesystems (NTFS, APFS) where File.txt and file.txt refer to the same file.

func MiddlewareErrorBatch added in v2.1.0

func MiddlewareErrorBatch(window time.Duration, maxSize int, flush func([]BatchError)) Middleware

MiddlewareErrorBatch returns a middleware that collects errors and flushes them in batches. The flush function receives all collected errors when the window expires or the batch reaches maxSize. Events are always forwarded regardless of errors.

func MiddlewareErrorCorrelation added in v2.1.0

func MiddlewareErrorCorrelation(idGenerator func() string) Middleware

MiddlewareErrorCorrelation returns a middleware that attaches a unique correlation ID to errors. When the downstream handler returns an error, the middleware wraps it with a correlation ID for distributed tracing.

func MiddlewareErrorRateLimit added in v2.1.0

func MiddlewareErrorRateLimit(maxErrors int, window time.Duration) Middleware

MiddlewareErrorRateLimit returns a middleware that limits the rate of error dispatching. When the downstream handler returns errors, this middleware tracks the error rate and applies backoff when errors exceed maxErrors within the window duration. During backoff, events are still forwarded but errors are suppressed.

func MiddlewareErrorRecovery added in v2.1.0

func MiddlewareErrorRecovery(strategy func(event Event, err error) error) Middleware

MiddlewareErrorRecovery returns a middleware that applies a recovery strategy when the downstream handler returns an error. The strategy function receives the event and error, and returns a replacement error (or nil to suppress it).

func MiddlewareErrorSanitization added in v2.1.0

func MiddlewareErrorSanitization(sanitize func(string) string) Middleware

MiddlewareErrorSanitization returns a middleware that sanitizes errors by stripping sensitive file paths. The replacement function receives the error string and returns a sanitized version.

func MiddlewareExponentialBackoff added in v2.2.0

func MiddlewareExponentialBackoff(maxFailures int, initialBackoff, maxBackoff time.Duration) Middleware

func MiddlewareFilter

func MiddlewareFilter(filter Filter) Middleware

MiddlewareFilter returns a middleware that applies a Filter to events. Events that don't pass the filter are dropped.

func MiddlewareLogging

func MiddlewareLogging(logger *slog.Logger) Middleware

MiddlewareLogging returns a middleware that logs all events to the provided slog logger. If logger is nil, it uses slog.Default().

Example (Structured)

ExampleMiddlewareLogging_structured demonstrates structured logging with a custom logger.

package main

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

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	logger := slog.New(
		slog.NewTextHandler(
			os.Stdout,
			&slog.HandlerOptions{
				Level: slog.LevelInfo,
			},
		),
	)

	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with custom structured logger created")
		},
		filewatcher.WithMiddleware(
			filewatcher.MiddlewareLogging(logger),
		),
	)
}
Output:
Watcher with custom structured logger created

func MiddlewareMetrics

func MiddlewareMetrics(counter func(op Op)) Middleware

MiddlewareMetrics returns a middleware that counts processed events. The counter function is called with the event operation type after each successful event processing.

Example

ExampleMiddlewareMetrics demonstrates event metrics.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	// Count events by operation type
	eventCounts := make(map[filewatcher.Op]int)

	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with metrics created")
		},
		filewatcher.WithMiddleware(
			filewatcher.MiddlewareMetrics(func(op filewatcher.Op) {
				eventCounts[op]++
			}),
		),
	)
}
Output:
Watcher with metrics created

func MiddlewareOnError

func MiddlewareOnError(onError func(event Event, err error)) Middleware

MiddlewareOnError returns a middleware that calls the provided callback when an error occurs in downstream handlers.

func MiddlewareRateLimit deprecated

func MiddlewareRateLimit(maxEvents int) Middleware

MiddlewareRateLimit returns a middleware that limits the rate of events using a token bucket algorithm. It allows maxEvents events per second with burst=maxEvents. Events exceeding the limit are dropped.

Deprecated: Use MiddlewareThrottle instead, which exposes the burst parameter directly. MiddlewareRateLimit(maxEvents) is exactly equivalent to MiddlewareThrottle(maxEvents, maxEvents).

Example

ExampleMiddlewareRateLimit demonstrates rate limiting.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with rate limiting created")
		},
		filewatcher.WithMiddleware(
			filewatcher.MiddlewareRateLimit(10),
		),
	)
}
Output:
Watcher with rate limiting created

func MiddlewareRecovery

func MiddlewareRecovery() Middleware

MiddlewareRecovery returns a middleware that recovers from panics in downstream handlers, logging the panic value and stack trace.

func MiddlewareSlidingWindowRateLimit

func MiddlewareSlidingWindowRateLimit(maxEvents int, window time.Duration) Middleware

MiddlewareSlidingWindowRateLimit returns a middleware that limits events to maxEvents per window duration using a token bucket. This provides smooth rate limiting without the boundary-burst problem of fixed windows.

func MiddlewareThrottle

func MiddlewareThrottle(maxEvents, burst int) Middleware

MiddlewareThrottle returns a middleware that limits event processing to maxEvents per second with burst support. Up to burst events can be processed immediately; after that, only maxEvents per second are allowed. Excess events are dropped. Uses golang.org/x/time/rate for correctness.

func MiddlewareWriteFileLog

func MiddlewareWriteFileLog(filePath string) Middleware

MiddlewareWriteFileLog returns a middleware that appends event logs to a file at the given path. This is useful for audit trails. The file handle is cached for the lifetime of the middleware.

Note: the file handle is opened lazily on the first event and is NOT closed automatically. For long-lived watchers or watchers that use Reset() cycles, prefer NewFileLogMiddleware + WithCleanup to avoid fd leaks:

mw, closeLog := filewatcher.NewFileLogMiddleware("audit.log")
watcher, _ := filewatcher.New(paths,
    filewatcher.WithMiddleware(mw),
    filewatcher.WithCleanup(closeLog),
)

func NewFileLogMiddleware added in v2.3.0

func NewFileLogMiddleware(filePath string) (Middleware, func() error)

NewFileLogMiddleware returns a file-logging middleware and a cleanup function that closes the underlying file handle. The cleanup function is safe to call multiple times.

Pair with WithCleanup to ensure the file handle is released when the watcher closes:

mw, closeLog := filewatcher.NewFileLogMiddleware("audit.log")
watcher, _ := filewatcher.New(paths,
    filewatcher.WithMiddleware(mw),
    filewatcher.WithCleanup(closeLog),
)

func OTelMiddleware added in v2.2.0

func OTelMiddleware(startSpan func(path, op string) OTelSpan) Middleware

OTelMiddleware returns a Middleware that wraps each event in a span. The span is created via the provided startSpan function and ended when the downstream handler returns.

startSpan receives the event path and operation string. The returned span will have its status set to "ok" on success or "error" on failure, and a "filewatcher.error" attribute on error.

The function is nil-safe: if startSpan is nil, the middleware is a no-op (events pass through unchanged).

type OTelSpan added in v2.2.0

type OTelSpan interface {
	// End completes the span. Should be called exactly once.
	End()
	// SetStatus sets the span status (typically "ok" or "error").
	SetStatus(code, description string)
	// SetAttributes attaches string key-value pairs to the span.
	SetAttributes(attrs ...Attribute)
}

OTelSpan is a minimal interface representing an OpenTelemetry span. It abstracts over the actual OTel API so this library does not need to depend on go.opentelemetry.io/otel.

Implementations should call End() when the span is no longer needed.

The real go.opentelemetry.io/otel/trace.Span satisfies this interface (End, SetStatus, SetAttributes, RecordError). Users can wrap their span type with a small adapter:

func toOtelSpan(s trace.Span) filewatcher.OTelSpan {
    return &otelSpanAdapter{s: s}
}

type Op

type Op int
Example

ExampleOp demonstrates Op string conversion and JSON marshaling.

package main

import (
	"encoding/json/v2"
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

func main() {
	op := filewatcher.Write
	fmt.Println(op.String())

	data, err := json.Marshal(op)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(data))

}
Output:
WRITE
"WRITE"
const (
	// Create indicates a file or directory was created.
	Create Op = iota + 1
	// Write indicates a file was modified.
	Write
	// Remove indicates a file or directory was removed.
	Remove
	// Rename indicates a file or directory was renamed.
	Rename
)

func (Op) MarshalJSON

func (op Op) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

func (Op) MarshalText

func (op Op) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler.

func (Op) String

func (op Op) String() string

String returns a human-readable representation of the operation.

func (*Op) UnmarshalJSON

func (op *Op) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler.

func (*Op) UnmarshalText

func (op *Op) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler.

type OpString

type OpString string

OpString is a distinct type for operation names (e.g., "fsnotify", "middleware").

func NewOpString

func NewOpString(op string) OpString

NewOpString creates a new OpString from a string.

func (OpString) Get

func (o OpString) Get() string

Get returns the underlying string value.

func (OpString) String

func (o OpString) String() string

String returns the string representation.

type Option

type Option func(*Watcher)

Option configures a Watcher during creation.

func WithBuffer

func WithBuffer(size int) Option

WithBuffer sets the buffer size for the event channel. A larger buffer helps handle event bursts without dropping events. Default is 64. A value of 0 creates an unbuffered channel which may cause deadlocks if the consumer is slow; use with caution.

Example

ExampleWithBuffer demonstrates using a custom buffer size.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with custom buffer created")
		},
		filewatcher.WithBuffer(256),
	)
}
Output:
Watcher with custom buffer created

func WithCaseSensitivity added in v2.4.0

func WithCaseSensitivity(mode FilesystemCaseSensitivity) Option

WithCaseSensitivity configures how the watcher compares paths.

Different filesystems treat filename case differently: NTFS (Windows) and APFS (macOS) are case-insensitive by default, while ext4/XFS/btrfs (Linux) are case-sensitive. The watcher uses this setting when deduplicating watches, matching Remove() targets, comparing exclude paths, and generating debounce keys.

  • CaseSensitivityAuto (default): case-insensitive on Windows and macOS, case-sensitive everywhere else.
  • CaseSensitive: paths that differ only in case are treated as distinct.
  • CaseInsensitive: paths that differ only in case are treated as identical.

Event paths reported by the watcher preserve the case provided by the underlying backend; only internal comparisons are affected.

Example

ExampleWithCaseSensitivity demonstrates configuring filesystem case-sensitivity awareness for cross-platform compatibility.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(watcher *filewatcher.Watcher) {
			stats := watcher.Stats()
			fmt.Println(stats.CaseSensitivity)
		},
		filewatcher.WithCaseSensitivity(filewatcher.CaseInsensitive),
	)
}
Output:
case-insensitive

func WithCleanup added in v2.3.0

func WithCleanup(fn func() error) Option

WithCleanup registers a cleanup function that is called when the watcher's Close() method is invoked, after all goroutines and channels are torn down. Use this to release resources held by middleware (e.g., file handles from NewFileLogMiddleware). Cleanup functions are called in registration order and are cleared on Reset().

func WithContentHashMaxSize added in v2.4.0

func WithContentHashMaxSize(bytes int64) Option

WithContentHashMaxSize sets the maximum file size for content hashing. Files larger than this are skipped to avoid blocking the event loop on I/O. This option implicitly enables content hashing if not already enabled via WithContentHashing. Set to 0 to disable content hashing entirely.

Default is 10 MiB (10 * 1024 * 1024 bytes).

func WithContentHashing added in v2.2.0

func WithContentHashing() Option

WithContentHashing enables SHA-256 content hashing of file events. When enabled, Event.Hash is populated with the hex-encoded SHA-256 digest of the file content for Create and Write events on regular files. Hashing is skipped for directories, removed files, and files larger than the max size (default 10 MiB, configurable via WithContentHashMaxSize).

Performance: each hashed event requires reading the entire file. This adds filesystem I/O proportional to file size. Use this option when content verification is needed (e.g., cache invalidation, deduplication) and disable it for high-throughput scenarios.

func WithDebounce

func WithDebounce(delay time.Duration) Option

WithDebounce sets a global debounce delay. All events are coalesced into a single emission after the delay since the last event. Default is no debouncing. Panics if delay is negative.

func WithDebug added in v2.1.0

func WithDebug(logger *slog.Logger) Option

WithDebug enables verbose debug logging to the provided logger. If logger is nil, log/slog.Default() is used. When enabled, the watcher logs detailed information about event processing, filter decisions, debounce actions, and error handling.

Example

ExampleWithDebug demonstrates debug logging.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with debug logging enabled")
		},
		filewatcher.WithDebug(nil), // nil uses slog.Default()
	)
}
Output:
Watcher with debug logging enabled

func WithErrorBufferSize added in v2.4.0

func WithErrorBufferSize(size int) Option

WithErrorBufferSize sets the buffer size for the error channel (Errors()). This decouples the error channel capacity from the event channel, allowing a larger error buffer under high error load (e.g., many ENOSPC errors during a large tree walk). When not set or set to 0, the error channel uses the same buffer size as the event channel (WithBuffer).

func WithErrorHandler

func WithErrorHandler(handler ErrorHandler) Option

WithErrorHandler sets a callback for watcher errors that occur during the event loop. Errors are passed to this handler with context about what operation was being performed. If not set, errors are logged to stderr.

Example

ExampleWithErrorHandler demonstrates error handler callback.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with error handler created")
		},
		filewatcher.WithErrorHandler(func(_ filewatcher.ErrorContext, err error) {
			// Handle errors - in production, log to your error tracking system
			_ = err
		}),
	)
}
Output:
Watcher with error handler created

func WithEventChannelMode added in v2.4.0

func WithEventChannelMode(mode EventChannelMode) Option

WithEventChannelMode configures how the watcher behaves when the event channel is full and the consumer is not reading fast enough.

  • EventChannelBlocking (default): the watch loop blocks until the event can be sent. This preserves all events but can cause the kernel to drop fsnotify events if the consumer is consistently slow.
  • EventChannelDropOnFull: events are dropped (and counted in Stats.EventsDroppedByBackpressure) when the channel is full. This prevents the watch loop from blocking but loses events.

Use DropOnFull when you prefer losing events over blocking the watcher (e.g., in firehose scenarios where eventual consistency is acceptable).

func WithExcludePaths added in v2.2.0

func WithExcludePaths(paths ...string) Option

WithExcludePaths excludes specific absolute paths (and their subtrees) from being watched during directory walking. This is a walk-time exclusion — directories matching these paths are never added to inotify. Paths are normalized to absolute paths. Use this to exclude entire subtrees like "/home/user/projects/forks" without affecting directories with the same name elsewhere.

func WithExtensions

func WithExtensions(exts ...string) Option

WithExtensions filters events to only those matching the given file extensions. Extensions should include the dot prefix (e.g., ".go", ".md").

func WithFilter

func WithFilter(f Filter) Option

WithFilter adds an event filter. Only events that pass all registered filters are emitted. Multiple filters are ANDed together.

func WithFollowSymlinks(follow bool) Option

WithFollowSymlinks enables following symbolic links during directory walking. When enabled, symlinked directories are resolved to their targets and added to the watcher. This allows monitoring directories accessed through symlinks. Default is false (symlinks are not followed).

Cycle detection is built-in: if a symlink target was already visited (directly or via another symlink), it is skipped to prevent infinite recursion.

func WithGitignore added in v2.2.0

func WithGitignore(enabled bool) Option

WithGitignore enables or disables .gitignore-aware walk filtering. When enabled (default: true), the watcher reads .gitignore files during directory traversal and skips directories matching gitignore patterns. This significantly reduces the number of inotify watches needed for directories with large build artifacts, generated code, etc.

func WithIgnoreDirs

func WithIgnoreDirs(dirs ...string) Option

WithIgnoreDirs discards events for files within the given directory names. Common values: "vendor", "node_modules", ".git", "dist", "build", "bin". Also skips these directories during recursive walking.

func WithIgnoreHidden

func WithIgnoreHidden() Option

WithIgnoreHidden discards events for hidden files and directories (those starting with a dot).

func WithIgnorePatterns

func WithIgnorePatterns(patterns ...string) Option

WithIgnorePatterns adds glob patterns to ignore at the filter level. Files matching any of the patterns are filtered out. Patterns are applied to the filename only (not the full path). Example patterns: "*.log", "*.tmp", ".*".

func WithLazyIsDir

func WithLazyIsDir() Option

WithLazyIsDir skips the os.Stat call in convertEvent for better performance. When enabled, Event.IsDir will always be false. This is useful when you don't need directory information and want to minimize filesystem calls. Default is false (IsDir is populated accurately).

func WithMaxWatches added in v2.2.0

func WithMaxWatches(n int) Option

WithMaxWatches sets the maximum number of inotify watches the watcher will attempt to create. When this limit is reached, additional directories are skipped with a warning. If set to 0 (default), the limit is auto-detected from /proc/sys/fs/inotify/max_user_watches on Linux, or unlimited on other platforms.

func WithMaxWatchesSafetyFraction added in v2.4.0

func WithMaxWatchesSafetyFraction(fraction float64) Option

WithMaxWatchesSafetyFraction sets a safety fraction applied to the auto-detected inotify watch limit. This leaves headroom for other processes on shared or multi-tenant machines, preventing the watcher from exhausting the entire system-wide budget.

A value of 0.75 means the watcher uses at most 75% of the system limit. Values <= 0 or > 1 are clamped to the default (1.0 = no reduction). This only affects auto-detected limits (WithMaxWatches(0)); explicit limits set via WithMaxWatches(n) are used as-is.

Default: 1.0 (full system limit). Recommended for shared environments: 0.75.

func WithMiddleware

func WithMiddleware(m ...Middleware) Option

WithMiddleware adds middleware to the event processing pipeline. Middleware is applied in reverse order (last added runs first), matching the go-cqrs-lite convention.

Example

ExampleWithMiddleware demonstrates using middleware.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with middleware created")
		},
		filewatcher.WithMiddleware(
			filewatcher.MiddlewareRecovery(),
			filewatcher.MiddlewareLogging(nil),
		),
	)
}
Output:
Watcher with middleware created

func WithOnAdd

func WithOnAdd(fn func(path string)) Option

WithOnAdd sets a callback that is invoked whenever a new path is added to the watcher. This is useful for logging or tracking which directories are being watched.

func WithOnError deprecated

func WithOnError(fn func(error)) Option

WithOnError sets a simple callback for errors that occur during watching. This is a convenience wrapper around WithErrorHandler for simple use cases.

Deprecated: Use WithErrorHandler instead. It provides the full ErrorContext (operation, path, retryability) and is the canonical error-handling option. To preserve the simple behavior, wrap the callback:

WithErrorHandler(func(_ ErrorContext, err error) { fn(err) })

func WithPerPathDebounce

func WithPerPathDebounce(delay time.Duration) Option

WithPerPathDebounce sets a per-path debounce delay. Events for different file paths are debounced independently. This is useful when watching many files and changes to different files should trigger separate actions. Panics if delay is negative.

Example

ExampleWithPerPathDebounce demonstrates per-path debouncing.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher with per-path debounce created")
		},
		filewatcher.WithPerPathDebounce(500*time.Millisecond),
	)
}
Output:
Watcher with per-path debounce created

func WithPollInterval added in v2.1.0

func WithPollInterval(interval time.Duration) Option

WithPollInterval sets a polling interval for filesystem change detection. When set to a non-zero duration, the watcher will periodically poll watched paths instead of relying solely on OS-native file events. This is useful for NFS mounts, FUSE filesystems, and other environments where inotify/FSEvents are unreliable or unavailable.

A typical value is 1–5 seconds. Shorter intervals detect changes faster but consume more CPU. Set to 0 (default) to disable polling.

func WithPolling added in v2.1.0

func WithPolling(fallback bool) Option

WithPolling enables or disables polling-based filesystem watching as a complement to OS-native events. When enabled (fallback=true), the watcher supplements fsnotify with periodic polling at the configured poll interval (default 2s, customizable via WithPollInterval). This detects changes on NFS mounts, FUSE filesystems, Docker volumes, and other environments where inotify/FSEvents may not fire.

When fallback=false, polling is fully disabled (identical to not calling this option).

Limitation: when both backends are active, a single file change can produce two events (one from fsnotify, one from the poll loop). Polling is a fallback, not a duplicate-suppression layer. Consumers that need exactly-once semantics should use MiddlewareDeduplicate or rely on idempotent event handling. See the Troubleshooting guide for details.

func WithRecursive

func WithRecursive(b bool) Option

WithRecursive enables recursive directory watching. When enabled, subdirectories are added to the watcher automatically, and newly created directories are added dynamically. Default is true.

func WithSelfHeal added in v2.2.0

func WithSelfHeal(interval time.Duration) Option

WithSelfHeal enables automatic retry of failed watch registrations. When a path fails to be added to the underlying fsnotify watcher (e.g., due to inotify resource exhaustion / ENOSPC), the watcher will periodically retry the failed path at the given interval until it succeeds or the watcher is closed. The interval must be > 0; a value of 0 is ignored.

This is useful for long-running watchers that may encounter transient resource exhaustion when directories are created faster than watches can be added. Failed paths that succeed on retry are added to the watch list normally and will receive subsequent fsnotify events.

func WithSkipDotDirs

func WithSkipDotDirs(skip bool) Option

WithSkipDotDirs controls whether directories starting with a dot (. are skipped during recursive directory walking. Default is true. Set to false to watch dot-directories like .config, .vscode, etc.

func WithWatchFilteredDirectories added in v2.4.0

func WithWatchFilteredDirectories(watch bool) Option

WithWatchFilteredDirectories controls whether newly created directories are added to the watcher even when their Create event was filtered out.

When true (default), the watcher adds new directories to maintain a complete watch tree, even if events for files within them are filtered. This ensures that files created in those directories are still detected.

When false, directories whose Create event was filtered out are NOT added, saving inotify budget. Use this when you truly want to ignore certain subtrees (e.g., build output directories filtered by name).

func WithWatchedIgnoreDirs deprecated added in v2.1.0

func WithWatchedIgnoreDirs(dirs ...string) Option

WithWatchedIgnoreDirs adds directory names that are ignored at the event filtering level only, without affecting directory walking. This differs from WithIgnoreDirs which both filters events AND skips directories during recursive walking. Use this when you want to walk into a directory (to watch its subdirectories) but ignore file events within it.

Deprecated: Use WithFilter(FilterIgnoreDirs(dirs...)) instead for better composability.

type PrometheusCollector added in v2.2.0

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

PrometheusCollector is a Prometheus metrics collector for the watcher. It implements the prometheus.Collector interface:

type Collector interface {
    Describe(chan<- *Desc)
    Collect(chan<- Metric)
}

Users register it with their prometheus.Registry:

import "github.com/prometheus/client_golang/prometheus"

collector := filewatcher.NewPrometheusCollector(watcher.Stats)
prometheus.MustRegister(collector)

The collector does not depend on prometheus/client_golang — it is a standalone type that can be adapted by users to any metrics system (Prometheus, OpenTelemetry, Datadog, etc.) without adding a dependency to this library.

The StatsFunc is called on each Collect invocation to retrieve the current Stats snapshot. This allows the collector to be reused across watchers by passing different StatsFunc closures.

func NewPrometheusCollector added in v2.2.0

func NewPrometheusCollector(stats StatsFunc) *PrometheusCollector

NewPrometheusCollector returns a Prometheus collector for a watcher's stats. The provided function should return the current Stats (e.g., `watcher.Stats` or a closure that calls it).

func (*PrometheusCollector) Counters added in v2.2.0

func (c *PrometheusCollector) Counters() []CounterMetric

Counters returns the current counter values from the watcher stats. Callers can map these to their metrics library of choice.

func (*PrometheusCollector) Gauges added in v2.2.0

func (c *PrometheusCollector) Gauges() []GaugeMetric

Gauges returns the current gauge values from the watcher stats.

type RootPath

type RootPath string

RootPath is a distinct type for root directory paths during filesystem walking. It prevents accidentally passing event paths or other paths where root paths are expected.

func NewRootPath

func NewRootPath(path string) RootPath

NewRootPath creates a new RootPath from a string.

func (RootPath) Get

func (rp RootPath) Get() string

Get returns the underlying string value.

func (RootPath) IsZero

func (rp RootPath) IsZero() bool

IsZero returns true if the RootPath is empty.

func (RootPath) String

func (rp RootPath) String() string

String returns the string representation.

type Stats

type Stats struct {
	WatchCount                  int                       // Number of paths currently being watched
	IsWatching                  bool                      // Whether the watcher is running
	IsClosed                    bool                      // Whether the watcher is closed
	EventsProcessed             uint64                    // Total events that reached the event channel
	EventsFilteredOut           uint64                    // Events dropped by filters
	EventsDroppedByMiddleware   uint64                    // Events dropped by middleware (rate limit, dedup, etc.)
	EventsDroppedByBackpressure uint64                    // Events dropped because the event channel was full (DropOnFull mode)
	ErrorsEncountered           uint64                    // Total errors encountered
	ErrorsDropped               uint64                    // Errors dropped because the error channel was full
	WatchErrors                 uint64                    // Watch add failures (ENOSPC, permission denied, etc.)
	Uptime                      time.Duration             // Time since watcher was started
	WatchLimit                  int                       // System inotify limit before safety fraction (0 if unknown or explicit)
	WatchBudgetCap              int                       // Effective watch budget cap after safety fraction
	WatchBudgetUsed             float64                   // Percentage of budget used (0.0-1.0)
	CaseSensitivity             string                    // Resolved case-sensitivity mode ("case-sensitive", "case-insensitive", "auto")
	CaseSensitivityMode         FilesystemCaseSensitivity // Resolved case-sensitivity mode (enum, type-safe)
}

Stats provides observability metrics for the watcher.

type StatsFunc added in v2.2.0

type StatsFunc func() Stats

StatsFunc is a function that returns a Stats snapshot. It is called on each Prometheus scrape to get the latest values.

type TempDir

type TempDir string

TempDir is a distinct type for temporary directory paths in tests.

func NewTempDir

func NewTempDir(path string) TempDir

NewTempDir creates a new TempDir from a string.

func (TempDir) Get

func (td TempDir) Get() string

Get returns the underlying string value.

func (TempDir) String

func (td TempDir) String() string

String returns the string representation.

type Watcher

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

Watcher watches file system paths for changes and emits filtered, debounced events through a channel.

Thread-safety guarantees:

  • New() is not safe for concurrent use during creation
  • Watch() is safe to call concurrently with Close()
  • Add(), Remove(), WatchList(), Stats(), IsClosed(), Errors() are safe for concurrent use
  • Close() is safe to call multiple times and concurrently with other methods
  • The event channel returned by Watch() is closed when the watcher stops
  • The error channel returned by Errors() is closed when the watcher stops
  • All callbacks (errorHandler, onAdd) may be called concurrently

func New

func New(
	paths []string,
	opts ...Option,
) (*Watcher, error)

New creates a new Watcher for the given paths with the specified options. At least one path must be provided. Paths are validated to exist.

The watcher is not started until Watch() is called.

Example

ExampleNew demonstrates creating a basic watcher with options.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(_ *filewatcher.Watcher) {
			fmt.Println("Watcher created successfully")
		},
		filewatcher.WithExtensions(".go"),
		filewatcher.WithDebounce(500*time.Millisecond),
		filewatcher.WithIgnoreDirs("vendor", "node_modules"),
	)
}
Output:
Watcher created successfully

func (*Watcher) Add

func (w *Watcher) Add(path string) error

Add adds a new path to the watcher. The path must be an existing directory. This method is safe for concurrent use with other methods.

Example

ExampleWatcher_Add demonstrates adding a path to an existing watcher.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(func(watcher *filewatcher.Watcher) {
		// Add a path to the existing watcher. Errors are returned for
		// invalid paths, permission issues, or fsnotify resource limits.
		addErr := watcher.Add("./examples")
		fmt.Println(addErr == nil)
	}, filewatcher.WithRecursive(false))
}
Output:
true

func (*Watcher) AddRecursive added in v2.1.0

func (w *Watcher) AddRecursive(path string, maxDepth int) error

AddRecursive adds a path with selective recursive depth. If maxDepth is 0, only the immediate directory is watched (equivalent to Add). If maxDepth is -1, full recursion is used (equivalent to the default recursive behavior). If maxDepth is N > 0, directories up to N levels deep are watched. This method is safe for concurrent use with other methods.

func (*Watcher) Close

func (w *Watcher) Close() error

Close stops the watcher and releases all resources. It is safe to call Close multiple times.

func (*Watcher) EffectiveCaseSensitivity added in v2.4.0

func (w *Watcher) EffectiveCaseSensitivity() FilesystemCaseSensitivity

EffectiveCaseSensitivity returns the resolved case-sensitivity mode (auto-detection resolved to concrete CaseSensitive or CaseInsensitive). This is the type-safe enum equivalent of Stats().CaseSensitivity (string).

This method is safe for concurrent use with other methods.

Example

ExampleWatcher_EffectiveCaseSensitivity demonstrates querying the resolved case-sensitivity mode as a type-safe enum.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(
		func(watcher *filewatcher.Watcher) {
			mode := watcher.EffectiveCaseSensitivity()
			fmt.Println(mode)
		},
		filewatcher.WithCaseSensitivity(filewatcher.CaseSensitive),
	)
}
Output:
case-sensitive

func (*Watcher) Errors

func (w *Watcher) Errors() <-chan error

Errors returns a receive-only channel that receives errors from the watcher. This provides an alternative to the error handler callback. If both are configured, errors are sent to the channel AND passed to the error handler. The channel is closed when the watcher is closed.

This method is safe for concurrent use with other methods.

Example

ExampleWatcher_Errors demonstrates receiving errors via a channel.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(func(watcher *filewatcher.Watcher) {
		// Get the errors channel - closed when the watcher is closed.
		errCh := watcher.Errors()
		_ = errCh

		fmt.Println("errors channel initialized")
	})
}
Output:
errors channel initialized

func (*Watcher) IsClosed

func (w *Watcher) IsClosed() bool

IsClosed reports if the watcher has been closed. This is safe to call concurrently with other methods.

func (*Watcher) IsWatching

func (w *Watcher) IsWatching() bool

IsWatching reports if the watcher is currently running and watching for events. This is safe to call concurrently with other methods.

func (*Watcher) Remove

func (w *Watcher) Remove(path string) error

Remove removes a path from the watcher. The watcher stops monitoring this path and all its subdirectories (if recursive). This method is safe for concurrent use with other methods.

Example

ExampleWatcher_Remove demonstrates removing a watched path.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(func(watcher *filewatcher.Watcher) {
		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		_, err := watcher.Watch(ctx)
		if err != nil {
			log.Fatal(err)
		}

		// Later, stop watching a specific subdirectory
		// In real usage, this would be a subdirectory of the watched path
		// _ = watcher.Remove("./some-subdirectory")

		fmt.Println("Watcher with remove capability created")
	})
}
Output:
Watcher with remove capability created

func (*Watcher) Reset added in v2.2.0

func (w *Watcher) Reset() error

Reset clears the watcher's runtime state while preserving configuration. After calling Reset(), the watcher can be started again with Watch(). This is useful for restarting the watcher after Close() without losing filters, middleware, debounce settings, and other options.

The watcher must be closed before calling Reset. Returns an error if the watcher is still running.

func (*Watcher) Stats

func (w *Watcher) Stats() Stats

Stats returns current statistics about the watcher. This method is safe for concurrent use with other methods.

Example

ExampleWatcher_Stats demonstrates getting watcher statistics.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(func(watcher *filewatcher.Watcher) {
		// Get current statistics
		stats := watcher.Stats()
		fmt.Printf("Watch count: %d, Watching: %v, Closed: %v\n",
			stats.WatchCount, stats.IsWatching, stats.IsClosed)
	})
}
Output:
Watch count: 0, Watching: false, Closed: false

func (*Watcher) Watch

func (w *Watcher) Watch(ctx context.Context) (<-chan Event, error)

Watch starts watching the configured paths and returns a read-only channel of filtered, debounced events. The channel is closed when the context is cancelled or Close() is called.

Callers should range over the returned channel to process events:

events, err := watcher.Watch(ctx)
for event := range events {
    handleEvent(event)
}
Example

ExampleWatcher_Watch demonstrates watching for file events.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(func(watcher *filewatcher.Watcher) {
		ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
		defer cancel()

		events, err := watcher.Watch(ctx)
		if err != nil {
			log.Fatal(err)
		}

		// Process events until context is cancelled
		for event := range events {
			fmt.Printf("%s: %s\n", event.Op.String(), event.Path)
		}

		fmt.Println("Watcher created and started")
	})
}
Output:
Watcher created and started

func (*Watcher) WatchList

func (w *Watcher) WatchList() []string

WatchList returns a copy of the list of paths currently being watched. This method is safe for concurrent use with other methods.

Example

ExampleWatcher_WatchList demonstrates inspecting watched paths.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-filewatcher/v2"
)

// runExampleWatcher creates a watcher on the current directory with the given
// options, invokes fn with it, then closes it. Centralises the error-check and
// cleanup boilerplate shared by every Example function so each one can focus
// on the option or feature it demonstrates.
func runExampleWatcher(fn func(*filewatcher.Watcher), opts ...filewatcher.Option) {
	watcher, err := filewatcher.New([]string{"."}, opts...)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = watcher.Close() }()

	fn(watcher)
}

func main() {
	runExampleWatcher(func(watcher *filewatcher.Watcher) {
		// Get the list of paths currently being watched
		paths := watcher.WatchList()
		fmt.Printf("Watching %d paths.\n", len(paths))
	})
}
Output:
Watching 0 paths.

func (*Watcher) WatchOnce

func (w *Watcher) WatchOnce(ctx context.Context) (Event, error)

WatchOnce starts watching and returns the first matching event, then stops. This is a convenience method for one-shot file watching. The watcher is automatically closed after the event is received or the context is cancelled.

Returns the event and nil on success, or an empty Event and an error if the context is cancelled or the watcher encounters an error.

type WatcherError

type WatcherError struct {
	// Op is the operation being performed when the error occurred.
	Op OpString

	// Path is the file path involved, if any.
	Path string

	// Err is the underlying error.
	Err error

	// Category indicates whether this is a transient or permanent error.
	Category ErrorCategory

	// Stack holds the goroutine stack trace captured at the time the error
	// was wrapped by NewWatcherError — not at the original failure site.
	// This is useful for tracing the error-handling path. To capture the
	// stack at the failure site instead, use NewWatcherErrorWithStack.
	Stack []byte
}

WatcherError provides structured error information with context.

func NewWatcherError

func NewWatcherError(op OpString, path string, err error) *WatcherError

NewWatcherError creates a new WatcherError with the given parameters. It automatically categorizes common error types.

func NewWatcherErrorWithStack added in v2.4.0

func NewWatcherErrorWithStack(op OpString, path string, err error, stack []byte) *WatcherError

NewWatcherErrorWithStack creates a WatcherError using the provided stack trace instead of capturing one at the wrapper site. Callers should capture the stack at the original failure point using debug.Stack() and pass it here. This is useful when the underlying error originates in a different goroutine or deep in a syscall where NewWatcherError's stack would be misleading.

func (*WatcherError) Code added in v2.1.0

func (e *WatcherError) Code() ErrorCode

Code returns the ErrorCode for this error based on the underlying sentinel. This enables programmatic error matching without string comparisons.

func (*WatcherError) Error

func (e *WatcherError) Error() string

Error implements the error interface.

func (*WatcherError) IsPermanent

func (e *WatcherError) IsPermanent() bool

IsPermanent returns true if this error will not resolve on retry.

func (*WatcherError) IsTransient

func (e *WatcherError) IsTransient() bool

IsTransient returns true if this error is potentially retryable.

func (*WatcherError) Unwrap

func (e *WatcherError) Unwrap() error

Unwrap returns the underlying error for errors.Is/As support.

type WatcherStateFlags

type WatcherStateFlags byte

WatcherStateFlags holds state booleans as bit flags for memory efficiency. 4 bools (4 bytes) → 1 byte with 4 bit flags.

Directories

Path Synopsis
examples
basic command
Example: Basic file watching Simplest usage with extensions filter and global debounce.
Example: Basic file watching Simplest usage with extensions filter and global debounce.
demo
Package demo provides helper functions for examples.
Package demo provides helper functions for examples.
filter-generated command
filter-generated demonstrates how to use go-filewatcher with gogenfilter to automatically exclude auto-generated Go code files from file watching events.
filter-generated demonstrates how to use go-filewatcher with gogenfilter to automatically exclude auto-generated Go code files from file watching events.
middleware command
Example: Middleware chain Demonstrates logging, recovery, and metrics middleware.
Example: Middleware chain Demonstrates logging, recovery, and metrics middleware.
per-path-debounce command
Example: Per-path debouncing Each file path is debounced independently.
Example: Per-path debouncing Each file path is debounced independently.

Jump to

Keyboard shortcuts

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