diff --git a/Makefile b/Makefile index 30ba07d54..d4b50c90e 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ test: go test -v ./... bench: - go test -v -bench='benchmarkcollector' ./pkg/collector/{cpu,logical_disk,physical_disk,logon,memory,net,process,service,system,tcp,time} + go test -v -bench='benchmarkcollector' ./pkg/collector/{cpu,logical_disk,physical_disk,logon,memory,net,printer,process,service,system,tcp,time} lint: golangci-lint -c .golangci.yaml run diff --git a/README.md b/README.md index db5410c81..4e547bb65 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Name | Description | Enabled by default [netframework_clrsecurity](docs/collector.netframework_clrsecurity.md) | .NET Framework Security Check metrics | [net](docs/collector.net.md) | Network interface I/O | ✓ [os](docs/collector.os.md) | OS metrics (memory, processes, users) | ✓ +[printer](docs/collector.printer.md) | Printer metrics | [process](docs/collector.process.md) | Per-process metrics | [remote_fx](docs/collector.remote_fx.md) | RemoteFX protocol (RDP) metrics | [scheduled_task](docs/collector.scheduled_task.md) | Scheduled Tasks metrics | diff --git a/docs/collector.printer.md b/docs/collector.printer.md new file mode 100644 index 000000000..32d631671 --- /dev/null +++ b/docs/collector.printer.md @@ -0,0 +1,28 @@ +# printer collector + +The printer collector exposes metrics about printers and their jobs. + +| | | +|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Metric name prefix | `printer` | +| Data source | WMI | +| Classes | [Win32_Printer](https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-printer)
[Win32_PrintJob](https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-printjob) | +| Enabled by default? | false | + +## Flags + +### `--collector.printer.include` + +If given, a printer needs to match the include regexp in order for the corresponding printer metrics to be reported + +### `--collector.printer.exclude` + +If given, a printer needs to *not* match the exclude regexp in order for the corresponding printer metrics to be reported + +## Metrics + +Name | Description | Type | Labels +-----|-------------|---------|------- +`windows_printer_status` | Status of the printer at the time the performance data is collected | counter | `printer`, `status` +`windows_printer_job_count` | Number of jobs processed by the printer since the last reset | gauge | `printer` +`windows_printer_job_status` | A counter of printer jobs by status | gauge | `printer`, `status` diff --git a/pkg/collector/collector.go b/pkg/collector/collector.go index 1a7d355bc..35ca12e7d 100644 --- a/pkg/collector/collector.go +++ b/pkg/collector/collector.go @@ -45,6 +45,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/collector/nps" "github.com/prometheus-community/windows_exporter/pkg/collector/os" "github.com/prometheus-community/windows_exporter/pkg/collector/physical_disk" + "github.com/prometheus-community/windows_exporter/pkg/collector/printer" "github.com/prometheus-community/windows_exporter/pkg/collector/process" "github.com/prometheus-community/windows_exporter/pkg/collector/remote_fx" "github.com/prometheus-community/windows_exporter/pkg/collector/scheduled_task" @@ -124,6 +125,7 @@ func NewWithConfig(logger log.Logger, config Config) Collectors { collectors[nps.Name] = nps.New(logger, &config.Nps) collectors[os.Name] = os.New(logger, &config.Os) collectors[physical_disk.Name] = physical_disk.New(logger, &config.PhysicalDisk) + collectors[printer.Name] = printer.New(logger, &config.Printer) collectors[process.Name] = process.New(logger, &config.Process) collectors[remote_fx.Name] = remote_fx.New(logger, &config.RemoteFx) collectors[scheduled_task.Name] = scheduled_task.New(logger, &config.ScheduledTask) @@ -140,7 +142,6 @@ func NewWithConfig(logger log.Logger, config Config) Collectors { collectors[time.Name] = time.New(logger, &config.Time) collectors[vmware.Name] = vmware.New(logger, &config.Vmware) collectors[vmware_blast.Name] = vmware_blast.New(logger, &config.VmwareBlast) - return New(collectors) } diff --git a/pkg/collector/config.go b/pkg/collector/config.go index 9ea051445..ecf10f77d 100644 --- a/pkg/collector/config.go +++ b/pkg/collector/config.go @@ -38,6 +38,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/collector/nps" "github.com/prometheus-community/windows_exporter/pkg/collector/os" "github.com/prometheus-community/windows_exporter/pkg/collector/physical_disk" + "github.com/prometheus-community/windows_exporter/pkg/collector/printer" "github.com/prometheus-community/windows_exporter/pkg/collector/process" "github.com/prometheus-community/windows_exporter/pkg/collector/remote_fx" "github.com/prometheus-community/windows_exporter/pkg/collector/scheduled_task" @@ -95,6 +96,7 @@ type Config struct { Nps nps.Config `yaml:"nps"` Os os.Config `yaml:"os"` PhysicalDisk physical_disk.Config `yaml:"physical_disk"` + Printer printer.Config `yaml:"printer"` Process process.Config `yaml:"process"` RemoteFx remote_fx.Config `yaml:"remote_fx"` ScheduledTask scheduled_task.Config `yaml:"scheduled_task"` @@ -153,6 +155,7 @@ var ConfigDefaults = Config{ Nps: nps.ConfigDefaults, Os: os.ConfigDefaults, PhysicalDisk: physical_disk.ConfigDefaults, + Printer: printer.ConfigDefaults, Process: process.ConfigDefaults, RemoteFx: remote_fx.ConfigDefaults, ScheduledTask: scheduled_task.ConfigDefaults, diff --git a/pkg/collector/map.go b/pkg/collector/map.go index a5ddf7062..7c3099182 100644 --- a/pkg/collector/map.go +++ b/pkg/collector/map.go @@ -39,6 +39,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/collector/nps" "github.com/prometheus-community/windows_exporter/pkg/collector/os" "github.com/prometheus-community/windows_exporter/pkg/collector/physical_disk" + "github.com/prometheus-community/windows_exporter/pkg/collector/printer" "github.com/prometheus-community/windows_exporter/pkg/collector/process" "github.com/prometheus-community/windows_exporter/pkg/collector/remote_fx" "github.com/prometheus-community/windows_exporter/pkg/collector/scheduled_task" @@ -99,6 +100,7 @@ var Map = map[string]types.CollectorBuilderWithFlags{ nps.Name: nps.NewWithFlags, os.Name: os.NewWithFlags, physical_disk.Name: physical_disk.NewWithFlags, + printer.Name: printer.NewWithFlags, process.Name: process.NewWithFlags, remote_fx.Name: remote_fx.NewWithFlags, scheduled_task.Name: scheduled_task.NewWithFlags, diff --git a/pkg/collector/printer/printer.go b/pkg/collector/printer/printer.go new file mode 100644 index 000000000..14ccdb160 --- /dev/null +++ b/pkg/collector/printer/printer.go @@ -0,0 +1,223 @@ +//go:build windows + +package printer + +import ( + "fmt" + "regexp" + "strings" + + "github.com/alecthomas/kingpin/v2" + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/client_golang/prometheus" + + "github.com/prometheus-community/windows_exporter/pkg/types" + "github.com/prometheus-community/windows_exporter/pkg/wmi" +) + +const ( + Name = "printer" + + FlagPrinterInclude = "collector.printer.include" + FlagPrinterExclude = "collector.printer.exclude" +) + +// printerStatusMap source: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-printer#:~:text=Power%20Save-,PrinterStatus,Offline%20(7),-PrintJobDataType +var printerStatusMap = map[uint16]string{ + 1: "Other", + 2: "Unknown", + 3: "Idle", + 4: "Printing", + 5: "Warmup", + 6: "Stopped Printing", + 7: "Offline", +} + +type Config struct { + printerInclude string `yaml:"printer_include"` + printerExclude string `yaml:"printer_exclude"` +} + +var ConfigDefaults = Config{ + printerInclude: ".+", + printerExclude: "", +} + +type collector struct { + logger log.Logger + + printerInclude *string + printerExclude *string + + printerStatus *prometheus.Desc + printerJobStatus *prometheus.Desc + printerJobCount *prometheus.Desc + + printerIncludePattern *regexp.Regexp + printerExcludePattern *regexp.Regexp +} + +func New(logger log.Logger, config *Config) types.Collector { + if config == nil { + config = &ConfigDefaults + } + c := &collector{ + printerInclude: &config.printerInclude, + printerExclude: &config.printerExclude, + } + c.SetLogger(logger) + return c +} + +func NewWithFlags(app *kingpin.Application) types.Collector { + c := &collector{ + printerInclude: app.Flag( + FlagPrinterInclude, + "Regular expression to match printers to collect metrics for", + ).Default(ConfigDefaults.printerInclude).String(), + printerExclude: app.Flag( + FlagPrinterExclude, + "Regular expression to match printers to exclude", + ).Default(ConfigDefaults.printerExclude).String(), + } + return c +} + +func (c *collector) SetLogger(logger log.Logger) { + c.logger = log.With(logger, "collector", Name) +} + +func (c *collector) Build() error { + c.printerJobStatus = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "job_status"), + "A counter of printer jobs by status", + []string{"printer", "status"}, + nil, + ) + c.printerStatus = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "status"), + "Printer status", + []string{"printer", "status"}, + nil, + ) + c.printerJobCount = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "job_count"), + "Number of jobs processed by the printer since the last reset", + []string{"printer"}, + nil, + ) + + var err error + c.printerIncludePattern, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", *c.printerInclude)) + if err != nil { + return err + } + c.printerExcludePattern, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", *c.printerExclude)) + return err +} + +func (c *collector) GetName() string { return Name } + +func (c *collector) GetPerfCounter() ([]string, error) { return []string{"Printer"}, nil } + +type win32_Printer struct { + Name string + Default bool + PrinterStatus uint16 + JobCountSinceLastReset uint32 +} + +type win32_PrintJob struct { + Name string + Status string +} + +func (c *collector) Collect(_ *types.ScrapeContext, ch chan<- prometheus.Metric) error { + if err := c.collectPrinterStatus(ch); err != nil { + _ = level.Error(c.logger).Log("msg", "failed to collect printer status metrics", "err", err) + return err + } + if err := c.collectPrinterJobStatus(ch); err != nil { + _ = level.Error(c.logger).Log("msg", "failed to collect printer job status metrics", "err", err) + return err + } + return nil +} + +func (c *collector) collectPrinterStatus(ch chan<- prometheus.Metric) error { + var printers []win32_Printer + q := wmi.QueryAll(&printers, c.logger) + if err := wmi.Query(q, &printers); err != nil { + return err + } + + for _, printer := range printers { + if c.printerExcludePattern.MatchString(printer.Name) || + !c.printerIncludePattern.MatchString(printer.Name) { + continue + } + + printerStatus, ok := printerStatusMap[printer.PrinterStatus] + if !ok { + printerStatus = "Unknown" + } + ch <- prometheus.MustNewConstMetric( + c.printerStatus, + prometheus.GaugeValue, + 1, + printer.Name, + printerStatus, + ) + ch <- prometheus.MustNewConstMetric( + c.printerJobCount, + prometheus.CounterValue, + float64(printer.JobCountSinceLastReset), + printer.Name, + ) + } + + return nil +} + +func (c *collector) collectPrinterJobStatus(ch chan<- prometheus.Metric) error { + var printJobs []win32_PrintJob + q := wmi.QueryAll(&printJobs, c.logger) + if err := wmi.Query(q, &printJobs); err != nil { + return err + } + + groupedPrintJobs := c.groupPrintJobs(printJobs) + for group, count := range groupedPrintJobs { + ch <- prometheus.MustNewConstMetric( + c.printerJobStatus, + prometheus.GaugeValue, + float64(count), + group.printerName, + group.status, + ) + } + return nil +} + +type PrintJobStatusGroup struct { + printerName string + status string +} + +func (c *collector) groupPrintJobs(printJobs []win32_PrintJob) map[PrintJobStatusGroup]int { + groupedPrintJobs := make(map[PrintJobStatusGroup]int) + for _, printJob := range printJobs { + printerName := strings.Split(printJob.Name, ",")[0] + + if c.printerExcludePattern.MatchString(printerName) || + !c.printerIncludePattern.MatchString(printerName) { + continue + } + groupedPrintJobs[PrintJobStatusGroup{ + printerName: printerName, + status: printJob.Status, + }]++ + } + return groupedPrintJobs +} diff --git a/pkg/collector/printer/printer_test.go b/pkg/collector/printer/printer_test.go new file mode 100644 index 000000000..35441510c --- /dev/null +++ b/pkg/collector/printer/printer_test.go @@ -0,0 +1,17 @@ +package printer_test + +import ( + "testing" + + "github.com/alecthomas/kingpin/v2" + + "github.com/prometheus-community/windows_exporter/pkg/collector/printer" + "github.com/prometheus-community/windows_exporter/pkg/testutils" +) + +func BenchmarkCollector(b *testing.B) { + // Whitelist is not set in testing context (kingpin flags not parsed), causing the collector to skip all printers. + printersInclude := ".+" + kingpin.CommandLine.GetArg(printer.FlagPrinterInclude).StringVar(&printersInclude) + testutils.FuncBenchmarkCollector(b, "printer", printer.NewWithFlags) +}