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 ¶
- Variables
- func DefaultIgnoreDirsCopy() []string
- func IsPermanentError(err error) bool
- func IsTransientError(err error) bool
- type Attribute
- type BatchError
- type CircuitState
- type ContentCheckMode
- type CounterMetric
- type DebounceKey
- type Debouncer
- type DebouncerInterface
- type ErrorCategory
- type ErrorCode
- type ErrorContext
- type ErrorHandler
- type Event
- type EventChannelMode
- type EventPath
- type FilesystemCaseSensitivity
- type Filter
- func FilterAnd(filters ...Filter) Filter
- func FilterCaseInsensitive(inner Filter) Filter
- func FilterCaseSensitive(inner Filter) Filter
- func FilterContentHash(expectedHex string) Filter
- func FilterExcludePaths(paths ...string) Filter
- func FilterExtensions(exts ...string) Filter
- func FilterFromWithMeta(f FilterWithMeta) Filter
- func FilterGeneratedCode(options ...gogenfilter.FilterOption) Filter
- func FilterGeneratedCodeFull(mode ContentCheckMode, options ...gogenfilter.FilterOption) Filter
- func FilterGeneratedCodeWithFilter(genFilter *gogenfilter.Filter) Filter
- func FilterGitignore(repoRoot string) Filter
- func FilterGlob(pattern string) Filter
- func FilterIgnoreDirs(dirs ...string) Filter
- func FilterIgnoreDirsCaseInsensitive(dirs ...string) Filter
- func FilterIgnoreExtensions(exts ...string) Filter
- func FilterIgnoreGlobs(patterns ...string) Filter
- func FilterIgnoreHidden() Filter
- func FilterMaxSize(maxSize int64) Filter
- func FilterMinAge(age time.Duration) Filter
- func FilterMinSize(minSize int64) Filter
- func FilterModifiedSince(minTime time.Time) Filter
- func FilterNot(f Filter) Filter
- func FilterNotOperations(ops ...Op) Filter
- func FilterOperations(ops ...Op) Filter
- func FilterOr(filters ...Filter) Filter
- func FilterRegex(pattern string) Filter
- type FilterWithMeta
- type GaugeMetric
- type GeneratedCodeDetector
- type GlobalDebouncer
- type Handler
- type LogSubstring
- type MatchResult
- type Middleware
- func MiddlewareBatch(window time.Duration, maxSize int, flush func([]Event) error) Middleware
- func MiddlewareCircuitBreaker(maxFailures int, resetTimeout time.Duration) Middleware
- func MiddlewareDeduplicate(window time.Duration) Middleware
- func MiddlewareDeduplicateCaseInsensitive(window time.Duration) Middleware
- func MiddlewareErrorBatch(window time.Duration, maxSize int, flush func([]BatchError)) Middleware
- func MiddlewareErrorCorrelation(idGenerator func() string) Middleware
- func MiddlewareErrorRateLimit(maxErrors int, window time.Duration) Middleware
- func MiddlewareErrorRecovery(strategy func(event Event, err error) error) Middleware
- func MiddlewareErrorSanitization(sanitize func(string) string) Middleware
- func MiddlewareExponentialBackoff(maxFailures int, initialBackoff, maxBackoff time.Duration) Middleware
- func MiddlewareFilter(filter Filter) Middleware
- func MiddlewareLogging(logger *slog.Logger) Middleware
- func MiddlewareMetrics(counter func(op Op)) Middleware
- func MiddlewareOnError(onError func(event Event, err error)) Middleware
- func MiddlewareRateLimit(maxEvents int) Middlewaredeprecated
- func MiddlewareRecovery() Middleware
- func MiddlewareSlidingWindowRateLimit(maxEvents int, window time.Duration) Middleware
- func MiddlewareThrottle(maxEvents, burst int) Middleware
- func MiddlewareWriteFileLog(filePath string) Middleware
- func NewFileLogMiddleware(filePath string) (Middleware, func() error)
- func OTelMiddleware(startSpan func(path, op string) OTelSpan) Middleware
- type OTelSpan
- type Op
- type OpString
- type Option
- func WithBuffer(size int) Option
- func WithCaseSensitivity(mode FilesystemCaseSensitivity) Option
- func WithCleanup(fn func() error) Option
- func WithContentHashMaxSize(bytes int64) Option
- func WithContentHashing() Option
- func WithDebounce(delay time.Duration) Option
- func WithDebug(logger *slog.Logger) Option
- func WithErrorBufferSize(size int) Option
- func WithErrorHandler(handler ErrorHandler) Option
- func WithEventChannelMode(mode EventChannelMode) Option
- func WithExcludePaths(paths ...string) Option
- func WithExtensions(exts ...string) Option
- func WithFilter(f Filter) Option
- func WithFollowSymlinks(follow bool) Option
- func WithGitignore(enabled bool) Option
- func WithIgnoreDirs(dirs ...string) Option
- func WithIgnoreHidden() Option
- func WithIgnorePatterns(patterns ...string) Option
- func WithLazyIsDir() Option
- func WithMaxWatches(n int) Option
- func WithMaxWatchesSafetyFraction(fraction float64) Option
- func WithMiddleware(m ...Middleware) Option
- func WithOnAdd(fn func(path string)) Option
- func WithOnError(fn func(error)) Optiondeprecated
- func WithPerPathDebounce(delay time.Duration) Option
- func WithPollInterval(interval time.Duration) Option
- func WithPolling(fallback bool) Option
- func WithRecursive(b bool) Option
- func WithSelfHeal(interval time.Duration) Option
- func WithSkipDotDirs(skip bool) Option
- func WithWatchFilteredDirectories(watch bool) Option
- func WithWatchedIgnoreDirs(dirs ...string) Optiondeprecated
- type PrometheusCollector
- type RootPath
- type Stats
- type StatsFunc
- type TempDir
- type Watcher
- func (w *Watcher) Add(path string) error
- func (w *Watcher) AddRecursive(path string, maxDepth int) error
- func (w *Watcher) Close() error
- func (w *Watcher) EffectiveCaseSensitivity() FilesystemCaseSensitivity
- func (w *Watcher) Errors() <-chan error
- func (w *Watcher) IsClosed() bool
- func (w *Watcher) IsWatching() bool
- func (w *Watcher) Remove(path string) error
- func (w *Watcher) Reset() error
- func (w *Watcher) Stats() Stats
- func (w *Watcher) Watch(ctx context.Context) (<-chan Event, error)
- func (w *Watcher) WatchList() []string
- func (w *Watcher) WatchOnce(ctx context.Context) (Event, error)
- type WatcherError
- type WatcherStateFlags
Examples ¶
- Debouncer
- Event
- EventPath
- FilterAnd
- FilterCaseInsensitive
- FilterCaseSensitive
- FilterExtensions
- FilterMinSize
- FilterModifiedSince
- FilterOr
- FilterRegex
- MiddlewareDeduplicate
- MiddlewareLogging (Structured)
- MiddlewareMetrics
- MiddlewareRateLimit
- New
- Op
- Watcher.Add
- Watcher.EffectiveCaseSensitivity
- Watcher.Errors
- Watcher.Remove
- Watcher.Stats
- Watcher.Watch
- Watcher.WatchList
- WithBuffer
- WithCaseSensitivity
- WithDebug
- WithErrorHandler
- WithMiddleware
- WithPerPathDebounce
Constants ¶
This section is empty.
Variables ¶
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.
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 ¶
IsPermanentError reports whether an error will not resolve on retry.
func IsTransientError ¶
IsTransientError reports whether an error is potentially retryable.
Types ¶
type BatchError ¶ added in v2.1.0
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
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 ¶
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.
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
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 ¶
NewEventPath creates a new EventPath from a string.
func (EventPath) Base ¶
Base returns the last element of the path. Example: EventPath("/home/user/file.go").Base() returns "file.go".
func (EventPath) Dir ¶
Dir returns all but the last element of the path. Example: EventPath("/home/user/file.go").Dir() returns EventPath("/home/user").
func (EventPath) Ext ¶
Ext returns the file extension of the path. Example: EventPath("/home/user/file.go").Ext() returns ".go".
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 ¶
Filter determines whether a file event should be processed. Return true to keep the event, false to discard it.
func FilterAnd ¶
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
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
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
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 ¶
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 ¶
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
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 ¶
FilterGlob creates a filter that only passes events for files matching the given glob pattern.
func FilterIgnoreDirs ¶
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
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 ¶
FilterIgnoreExtensions creates a filter that discards events for files matching one of the given extensions.
func FilterIgnoreGlobs ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 FilterNotOperations ¶
FilterNotOperations creates a filter that discards events matching any of the given operations.
func FilterOperations ¶
FilterOperations creates a filter that only passes events matching one of the given operations.
func FilterOr ¶
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 ¶
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
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 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 ¶
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 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"
func (Op) MarshalJSON ¶
MarshalJSON implements json.Marshaler.
func (Op) MarshalText ¶
MarshalText implements encoding.TextMarshaler.
func (*Op) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler.
func (*Op) UnmarshalText ¶
UnmarshalText implements encoding.TextUnmarshaler.
type OpString ¶
type OpString string
OpString is a distinct type for operation names (e.g., "fsnotify", "middleware").
func NewOpString ¶
NewOpString creates a new OpString from a string.
type Option ¶
type Option func(*Watcher)
Option configures a Watcher during creation.
func WithBuffer ¶
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
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
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 ¶
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
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
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
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 ¶
WithExtensions filters events to only those matching the given file extensions. Extensions should include the dot prefix (e.g., ".go", ".md").
func WithFilter ¶
WithFilter adds an event filter. Only events that pass all registered filters are emitted. Multiple filters are ANDed together.
func WithFollowSymlinks ¶ added in v2.1.0
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
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 ¶
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 ¶
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
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
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 ¶
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
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 ¶
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
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
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 ¶
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
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 ¶
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
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
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 ¶
NewRootPath creates a new RootPath from a string.
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 ¶
NewTempDir creates a new TempDir from a string.
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 ¶
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 ¶
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
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 ¶
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 ¶
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 ¶
IsClosed reports if the watcher has been closed. This is safe to call concurrently with other methods.
func (*Watcher) IsWatching ¶
IsWatching reports if the watcher is currently running and watching for events. This is safe to call concurrently with other methods.
func (*Watcher) Remove ¶
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
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 ¶
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 ¶
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 ¶
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 ¶
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.
Source Files
¶
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. |