diff --git a/README.md b/README.md index 245cb46b..c548efad 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Name | Description | Enabled by default [dhcp](docs/collector.dhcp.md) | DHCP Server | [dns](docs/collector.dns.md) | DNS Server | [exchange](docs/collector.exchange.md) | Exchange metrics | +[filetime](docs/collector.filetime.md) | FileTime metrics | [fsrmquota](docs/collector.fsrmquota.md) | Microsoft File Server Resource Manager (FSRM) Quotas collector | [hyperv](docs/collector.hyperv.md) | Hyper-V hosts | [iis](docs/collector.iis.md) | IIS sites and applications | diff --git a/docs/collector.filetime.md b/docs/collector.filetime.md new file mode 100644 index 00000000..17b8369e --- /dev/null +++ b/docs/collector.filetime.md @@ -0,0 +1,36 @@ +# filetime collector + +The filetime collector exposes modified timestamps of files in the filesystem. + +The collector + +||| +-|- +Metric name prefix | `filetime` +Enabled by default? | No + +## Flags + +### `--collectors.filetime.file-patterns` +Comma-separated list of file patterns. Each pattern is a glob pattern that can contain `*`, `?`, and `**` (recursive). +See https://github.com/bmatcuk/doublestar#patterns for an extended description of the pattern syntax. + +## Metrics + +Name | Description | Type | Labels +-----|-------------|------|------- +`windows_filetime_mtime_timestamp_seconds` | File modification time | gauge | `file` + +### Example metric + +``` +# HELP windows_filetime_mtime_timestamp_seconds File modification time +# TYPE windows_filetime_mtime_timestamp_seconds gauge +windows_filetime_mtime_timestamp_seconds{file="C:\\Users\\admin\\Desktop\\Dashboard.lnk"} 1.726434517e+09 +``` + +## Useful queries +_This collector does not yet have any useful queries added, we would appreciate your help adding them!_ + +## Alerting examples +_This collector does not yet have alerting examples, we would appreciate your help adding them!_ diff --git a/go.mod b/go.mod index 0b7e5f56..af950a93 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23 require ( github.com/Microsoft/hcsshim v0.12.6 github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/dimchansky/utfbom v1.1.1 github.com/go-ole/go-ole v1.3.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index ea93a193..669f3ebf 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 h1:t3eaIm0rUkzbrI github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= diff --git a/pkg/collector/collector.go b/pkg/collector/collector.go index 6eb6121f..0e883871 100644 --- a/pkg/collector/collector.go +++ b/pkg/collector/collector.go @@ -23,6 +23,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/collector/diskdrive" "github.com/prometheus-community/windows_exporter/pkg/collector/dns" "github.com/prometheus-community/windows_exporter/pkg/collector/exchange" + "github.com/prometheus-community/windows_exporter/pkg/collector/filetime" "github.com/prometheus-community/windows_exporter/pkg/collector/fsrmquota" "github.com/prometheus-community/windows_exporter/pkg/collector/hyperv" "github.com/prometheus-community/windows_exporter/pkg/collector/iis" @@ -90,6 +91,7 @@ func NewWithConfig(config Config) *MetricCollectors { collectors[diskdrive.Name] = diskdrive.New(&config.DiskDrive) collectors[dns.Name] = dns.New(&config.DNS) collectors[exchange.Name] = exchange.New(&config.Exchange) + collectors[filetime.Name] = filetime.New(&config.Filetime) collectors[fsrmquota.Name] = fsrmquota.New(&config.Fsrmquota) collectors[hyperv.Name] = hyperv.New(&config.Hyperv) collectors[iis.Name] = iis.New(&config.IIS) diff --git a/pkg/collector/config.go b/pkg/collector/config.go index 54337815..dda4d826 100644 --- a/pkg/collector/config.go +++ b/pkg/collector/config.go @@ -14,6 +14,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/collector/diskdrive" "github.com/prometheus-community/windows_exporter/pkg/collector/dns" "github.com/prometheus-community/windows_exporter/pkg/collector/exchange" + "github.com/prometheus-community/windows_exporter/pkg/collector/filetime" "github.com/prometheus-community/windows_exporter/pkg/collector/fsrmquota" "github.com/prometheus-community/windows_exporter/pkg/collector/hyperv" "github.com/prometheus-community/windows_exporter/pkg/collector/iis" @@ -63,6 +64,7 @@ type Config struct { DiskDrive diskdrive.Config `yaml:"diskdrive"` //nolint:tagliatelle DNS dns.Config `yaml:"dns"` Exchange exchange.Config `yaml:"exchange"` + Filetime filetime.Config `yaml:"filetime"` Fsrmquota fsrmquota.Config `yaml:"fsrmquota"` Hyperv hyperv.Config `yaml:"hyperv"` IIS iis.Config `yaml:"iis"` @@ -115,6 +117,7 @@ var ConfigDefaults = Config{ DiskDrive: diskdrive.ConfigDefaults, DNS: dns.ConfigDefaults, Exchange: exchange.ConfigDefaults, + Filetime: filetime.ConfigDefaults, Fsrmquota: fsrmquota.ConfigDefaults, Hyperv: hyperv.ConfigDefaults, IIS: iis.ConfigDefaults, diff --git a/pkg/collector/filetime/filetime.go b/pkg/collector/filetime/filetime.go new file mode 100644 index 00000000..cbde9048 --- /dev/null +++ b/pkg/collector/filetime/filetime.go @@ -0,0 +1,175 @@ +//go:build windows + +package filetime + +import ( + "fmt" + "log/slog" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/alecthomas/kingpin/v2" + "github.com/bmatcuk/doublestar/v4" + "github.com/prometheus-community/windows_exporter/pkg/types" + "github.com/prometheus/client_golang/prometheus" + "github.com/yusufpapurcu/wmi" +) + +const Name = "filetime" + +type Config struct { + filePatterns []string +} + +var ConfigDefaults = Config{ + filePatterns: []string{}, +} + +// A Collector is a Prometheus Collector for collecting file times. +type Collector struct { + config Config + + fileMTime *prometheus.Desc +} + +func New(config *Config) *Collector { + if config == nil { + config = &ConfigDefaults + } + + if config.filePatterns == nil { + config.filePatterns = ConfigDefaults.filePatterns + } + + c := &Collector{ + config: *config, + } + + return c +} + +func NewWithFlags(app *kingpin.Application) *Collector { + c := &Collector{ + config: ConfigDefaults, + } + c.config.filePatterns = make([]string, 0) + + var filePatterns string + + app.Flag( + "collectors.filetime.file-patterns", + "Comma-separated list of file patterns. Each pattern is a glob pattern that can contain `*`, `?`, and `**` (recursive). See https://github.com/bmatcuk/doublestar#patterns", + ).Default(strings.Join(ConfigDefaults.filePatterns, ",")).StringVar(&filePatterns) + + app.Action(func(*kingpin.ParseContext) error { + // doublestar.Glob() requires forward slashes + c.config.filePatterns = strings.Split(filepath.ToSlash(filePatterns), ",") + + return nil + }) + + return c +} + +func (c *Collector) GetName() string { + return Name +} + +func (c *Collector) GetPerfCounter(_ *slog.Logger) ([]string, error) { + return []string{}, nil +} + +func (c *Collector) Close(_ *slog.Logger) error { + return nil +} + +func (c *Collector) Build(logger *slog.Logger, _ *wmi.Client) error { + logger.Info("filetime collector is in an experimental state! It may subject to change.", + slog.String("collector", Name), + ) + + c.fileMTime = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "mtime_timestamp_seconds"), + "File modification time", + []string{"file"}, + nil, + ) + + for _, filePattern := range c.config.filePatterns { + basePath, pattern := doublestar.SplitPattern(filePattern) + + _, err := doublestar.Glob(os.DirFS(basePath), pattern, doublestar.WithFilesOnly()) + if err != nil { + return fmt.Errorf("invalid glob pattern: %w", err) + } + } + + return nil +} + +// Collect sends the metric values for each metric +// to the provided prometheus Metric channel. +func (c *Collector) Collect(_ *types.ScrapeContext, logger *slog.Logger, ch chan<- prometheus.Metric) error { + logger = logger.With(slog.String("collector", Name)) + + return c.collectGlob(logger, ch) +} + +// collectWin32 collects file times for each file path in the config. It using Win32 FindFirstFile and FindNextFile. +func (c *Collector) collectGlob(logger *slog.Logger, ch chan<- prometheus.Metric) error { + wg := sync.WaitGroup{} + + for _, filePattern := range c.config.filePatterns { + wg.Add(1) + + go func(filePattern string) { + defer wg.Done() + + if err := c.collectGlobFilePath(logger, ch, filePattern); err != nil { + logger.Error("failed collecting metrics for filepath", + slog.String("filepath", filePattern), + slog.Any("err", err), + ) + } + }(filePattern) + } + + wg.Wait() + + return nil +} + +func (c *Collector) collectGlobFilePath(logger *slog.Logger, ch chan<- prometheus.Metric, filePattern string) error { + basePath, pattern := doublestar.SplitPattern(filePattern) + basePathFS := os.DirFS(basePath) + + matches, err := doublestar.Glob(basePathFS, pattern, doublestar.WithFilesOnly()) + if err != nil { + return fmt.Errorf("failed to glob: %w", err) + } + + for _, match := range matches { + filePath := filepath.Join(basePath, match) + + fileInfo, err := os.Stat(filePath) + if err != nil { + logger.Warn("failed to state file", + slog.String("file", filePath), + slog.Any("err", err), + ) + + continue + } + + ch <- prometheus.MustNewConstMetric( + c.fileMTime, + prometheus.GaugeValue, + float64(fileInfo.ModTime().UTC().Unix()), + filePath, + ) + } + + return nil +} diff --git a/pkg/collector/filetime/filetime_test.go b/pkg/collector/filetime/filetime_test.go new file mode 100644 index 00000000..a7b01adb --- /dev/null +++ b/pkg/collector/filetime/filetime_test.go @@ -0,0 +1,12 @@ +package filetime_test + +import ( + "testing" + + "github.com/prometheus-community/windows_exporter/pkg/collector/filetime" + "github.com/prometheus-community/windows_exporter/pkg/testutils" +) + +func BenchmarkCollector(b *testing.B) { + testutils.FuncBenchmarkCollector(b, filetime.Name, filetime.NewWithFlags) +} diff --git a/pkg/collector/map.go b/pkg/collector/map.go index 72f208e1..2a43e633 100644 --- a/pkg/collector/map.go +++ b/pkg/collector/map.go @@ -18,6 +18,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/collector/diskdrive" "github.com/prometheus-community/windows_exporter/pkg/collector/dns" "github.com/prometheus-community/windows_exporter/pkg/collector/exchange" + "github.com/prometheus-community/windows_exporter/pkg/collector/filetime" "github.com/prometheus-community/windows_exporter/pkg/collector/fsrmquota" "github.com/prometheus-community/windows_exporter/pkg/collector/hyperv" "github.com/prometheus-community/windows_exporter/pkg/collector/iis" @@ -73,6 +74,7 @@ var BuildersWithFlags = map[string]BuilderWithFlags[Collector]{ diskdrive.Name: NewBuilderWithFlags(diskdrive.NewWithFlags), dns.Name: NewBuilderWithFlags(dns.NewWithFlags), exchange.Name: NewBuilderWithFlags(exchange.NewWithFlags), + filetime.Name: NewBuilderWithFlags(filetime.NewWithFlags), fsrmquota.Name: NewBuilderWithFlags(fsrmquota.NewWithFlags), hyperv.Name: NewBuilderWithFlags(hyperv.NewWithFlags), iis.Name: NewBuilderWithFlags(iis.NewWithFlags),