mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 18:19:04 +02:00
Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
371 lines
10 KiB
Go
371 lines
10 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// Copyright The Prometheus Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//go:build windows
|
|
|
|
package printer
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/alecthomas/kingpin/v2"
|
|
"github.com/prometheus-community/windows_exporter/internal/headers/winspool"
|
|
"github.com/prometheus-community/windows_exporter/internal/metriccache"
|
|
"github.com/prometheus-community/windows_exporter/internal/mi"
|
|
"github.com/prometheus-community/windows_exporter/internal/types"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
const Name = "printer"
|
|
|
|
// Win32_Printer PrinterStatus values.
|
|
// https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-printer
|
|
const (
|
|
printerStatusOther uint16 = 1
|
|
printerStatusUnknown uint16 = 2
|
|
printerStatusIdle uint16 = 3
|
|
printerStatusPrinting uint16 = 4
|
|
printerStatusWarmup uint16 = 5
|
|
)
|
|
|
|
// printerStatusMap source: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-printer#:~:text=Power%20Save-,PrinterStatus,Offline%20(7),-PrintJobDataType
|
|
//
|
|
//nolint:gochecknoglobals
|
|
var printerStatusMap = map[uint16]string{
|
|
1: "Other",
|
|
2: "Unknown",
|
|
3: "Idle",
|
|
4: "Printing",
|
|
5: "Warmup",
|
|
6: "Stopped Printing",
|
|
7: "Offline",
|
|
}
|
|
|
|
type Config struct {
|
|
PrinterInclude *regexp.Regexp `yaml:"include"`
|
|
PrinterExclude *regexp.Regexp `yaml:"exclude"`
|
|
}
|
|
|
|
//nolint:gochecknoglobals
|
|
var ConfigDefaults = Config{
|
|
PrinterInclude: types.RegExpAny,
|
|
PrinterExclude: types.RegExpEmpty,
|
|
}
|
|
|
|
type Collector struct {
|
|
config Config
|
|
|
|
printerStatus *prometheus.Desc
|
|
printerJobStatus *prometheus.Desc
|
|
printerJobCount *prometheus.Desc
|
|
|
|
// metricCache reuses the printer status series of printers whose status did
|
|
// not change since the last scrape. Each printer publishes one series per
|
|
// printerStatusMap entry. Job counts change and are built on every scrape.
|
|
metricCache metriccache.Cache[string, printerValues]
|
|
}
|
|
|
|
// printerValues holds the inputs of the cached metrics of a printer besides its name.
|
|
type printerValues struct {
|
|
status uint16
|
|
}
|
|
|
|
func New(config *Config) *Collector {
|
|
if config == nil {
|
|
config = &ConfigDefaults
|
|
}
|
|
|
|
if config.PrinterExclude == nil {
|
|
config.PrinterExclude = ConfigDefaults.PrinterExclude
|
|
}
|
|
|
|
if config.PrinterInclude == nil {
|
|
config.PrinterInclude = ConfigDefaults.PrinterInclude
|
|
}
|
|
|
|
c := &Collector{
|
|
config: *config,
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
func NewWithFlags(app *kingpin.Application) *Collector {
|
|
c := &Collector{
|
|
config: ConfigDefaults,
|
|
}
|
|
|
|
var printerInclude, printerExclude string
|
|
|
|
app.Flag(
|
|
"collector.printer.include",
|
|
"Regular expression to match printers to collect metrics for",
|
|
).Default(".+").StringVar(&printerInclude)
|
|
|
|
app.Flag(
|
|
"collector.printer.exclude",
|
|
"Regular expression to match printers to exclude",
|
|
).Default("").StringVar(&printerExclude)
|
|
|
|
app.Action(func(*kingpin.ParseContext) error {
|
|
var err error
|
|
|
|
c.config.PrinterInclude, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", printerInclude))
|
|
if err != nil {
|
|
return fmt.Errorf("collector.printer.include: %w", err)
|
|
}
|
|
|
|
c.config.PrinterExclude, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", printerExclude))
|
|
if err != nil {
|
|
return fmt.Errorf("collector.printer.exclude: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return c
|
|
}
|
|
|
|
func (c *Collector) Close() error {
|
|
c.metricCache.Reset()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) 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,
|
|
)
|
|
|
|
// Cached metrics refer to the descriptors of an earlier Build.
|
|
c.metricCache.Reset()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Collector) GetName() string { return Name }
|
|
|
|
// Collect reads printers and jobs from the print spooler.
|
|
// Win32_Printer is not used because the CIMWin32 provider calls DeviceCapabilities
|
|
// for every printer, which blocks for about a minute on an unreachable network printer.
|
|
func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
|
|
// CIMWin32 enumerates the same printers for Win32_Printer.
|
|
printers, err := winspool.EnumPrinters(winspool.PRINTER_ENUM_LOCAL | winspool.PRINTER_ENUM_CONNECTIONS)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to enumerate printers: %w", err)
|
|
}
|
|
|
|
scrape := c.metricCache.Begin()
|
|
|
|
var errs []error
|
|
|
|
for _, printer := range printers {
|
|
if c.config.PrinterExclude.MatchString(printer.Name) ||
|
|
!c.config.PrinterInclude.MatchString(printer.Name) {
|
|
continue
|
|
}
|
|
|
|
// Win32_Printer JobCountSinceLastReset is PRINTER_INFO_2.cJobs, the number of queued jobs.
|
|
ch <- prometheus.MustNewConstMetric(
|
|
c.printerJobCount,
|
|
prometheus.CounterValue,
|
|
float64(printer.Jobs),
|
|
printer.Name,
|
|
)
|
|
|
|
jobs, err := printerJobs(printer)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to enumerate jobs of printer %s: %w", printer.Name, err))
|
|
|
|
continue
|
|
}
|
|
|
|
values := printerValues{
|
|
status: printerStatus(printer.Status, jobs),
|
|
}
|
|
|
|
for _, metric := range scrape.Get(printer.Name, values, func() []prometheus.Metric {
|
|
return c.printerStatusMetrics(printer.Name, values)
|
|
}) {
|
|
ch <- metric
|
|
}
|
|
|
|
for status, count := range groupJobsByStatus(jobs) {
|
|
ch <- prometheus.MustNewConstMetric(
|
|
c.printerJobStatus,
|
|
prometheus.GaugeValue,
|
|
float64(count),
|
|
printer.Name,
|
|
status,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Printers that were not collected in this scrape are dropped from the cache.
|
|
scrape.Commit()
|
|
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// printerStatusMetrics builds the status series of a printer.
|
|
func (c *Collector) printerStatusMetrics(printerName string, values printerValues) []prometheus.Metric {
|
|
metrics := make([]prometheus.Metric, 0, len(printerStatusMap))
|
|
|
|
for status, statusName := range printerStatusMap {
|
|
isCurrentStatus := 0.0
|
|
if status == values.status {
|
|
isCurrentStatus = 1.0
|
|
}
|
|
|
|
metrics = append(metrics, prometheus.MustNewConstMetric(
|
|
c.printerStatus,
|
|
prometheus.GaugeValue,
|
|
isCurrentStatus,
|
|
printerName,
|
|
statusName,
|
|
))
|
|
}
|
|
|
|
return metrics
|
|
}
|
|
|
|
// printerJobs returns the jobs of the printer in queue order.
|
|
func printerJobs(printer winspool.Printer) (_ []winspool.Job, err error) {
|
|
// Skip OpenPrinter for an empty queue. For a printer connection, it can contact the print server.
|
|
if printer.Jobs == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
handle, err := winspool.OpenPrinter(printer.Name)
|
|
if err != nil {
|
|
// The printer was deleted after EnumPrinters.
|
|
if errors.Is(err, windows.ERROR_INVALID_PRINTER_NAME) {
|
|
return nil, nil
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
err = errors.Join(err, winspool.ClosePrinter(handle))
|
|
}()
|
|
|
|
return winspool.EnumJobs(handle)
|
|
}
|
|
|
|
// printerStatus returns the Win32_Printer PrinterStatus value for PRINTER_INFO_2.Status,
|
|
// mapped the same way as the CIMWin32 provider (cimwin32.dll).
|
|
//
|
|
// CIMWin32 compares the whole Status value with single PRINTER_STATUS_* flags.
|
|
// For 0 and for combinations of flags, it uses the first job in the queue instead.
|
|
// It never reports Stopped Printing (6) or Offline (7); an offline printer is Other (1).
|
|
func printerStatus(status uint32, jobs []winspool.Job) uint16 {
|
|
switch status {
|
|
case winspool.PRINTER_STATUS_PAUSED,
|
|
winspool.PRINTER_STATUS_ERROR,
|
|
winspool.PRINTER_STATUS_PENDING_DELETION,
|
|
winspool.PRINTER_STATUS_PAPER_JAM,
|
|
winspool.PRINTER_STATUS_PAPER_OUT,
|
|
winspool.PRINTER_STATUS_MANUAL_FEED,
|
|
winspool.PRINTER_STATUS_PAPER_PROBLEM,
|
|
winspool.PRINTER_STATUS_OFFLINE,
|
|
winspool.PRINTER_STATUS_OUTPUT_BIN_FULL,
|
|
winspool.PRINTER_STATUS_TONER_LOW,
|
|
winspool.PRINTER_STATUS_NO_TONER,
|
|
winspool.PRINTER_STATUS_PAGE_PUNT,
|
|
winspool.PRINTER_STATUS_USER_INTERVENTION,
|
|
winspool.PRINTER_STATUS_OUT_OF_MEMORY,
|
|
winspool.PRINTER_STATUS_DOOR_OPEN:
|
|
return printerStatusOther
|
|
case winspool.PRINTER_STATUS_IO_ACTIVE,
|
|
winspool.PRINTER_STATUS_BUSY,
|
|
winspool.PRINTER_STATUS_PRINTING,
|
|
winspool.PRINTER_STATUS_PROCESSING:
|
|
return printerStatusPrinting
|
|
case winspool.PRINTER_STATUS_NOT_AVAILABLE:
|
|
return printerStatusUnknown
|
|
case winspool.PRINTER_STATUS_WAITING,
|
|
winspool.PRINTER_STATUS_SERVER_UNKNOWN,
|
|
winspool.PRINTER_STATUS_POWER_SAVE:
|
|
return printerStatusIdle
|
|
case winspool.PRINTER_STATUS_INITIALIZING,
|
|
winspool.PRINTER_STATUS_WARMING_UP:
|
|
return printerStatusWarmup
|
|
}
|
|
|
|
if len(jobs) == 0 {
|
|
return printerStatusIdle
|
|
}
|
|
|
|
job := jobs[0]
|
|
|
|
switch {
|
|
case job.StatusText != "":
|
|
return printerStatusUnknown
|
|
case job.Status&(winspool.JOB_STATUS_PAUSED|winspool.JOB_STATUS_ERROR|winspool.JOB_STATUS_DELETING|
|
|
winspool.JOB_STATUS_OFFLINE|winspool.JOB_STATUS_PAPEROUT|winspool.JOB_STATUS_PRINTED) != 0:
|
|
return printerStatusOther
|
|
case job.Status&(winspool.JOB_STATUS_SPOOLING|winspool.JOB_STATUS_PRINTING) != 0:
|
|
return printerStatusPrinting
|
|
default:
|
|
return printerStatusUnknown
|
|
}
|
|
}
|
|
|
|
// jobStatus returns the Win32_PrintJob Status value for JOB_INFO_1.Status,
|
|
// mapped the same way as the CIMWin32 provider (cimwin32.dll).
|
|
func jobStatus(status uint32) string {
|
|
switch {
|
|
case status&winspool.JOB_STATUS_ERROR != 0:
|
|
return "Error"
|
|
case status&(winspool.JOB_STATUS_PAUSED|winspool.JOB_STATUS_OFFLINE|winspool.JOB_STATUS_PAPEROUT) != 0:
|
|
return "Degraded"
|
|
case status&(winspool.JOB_STATUS_DELETING|winspool.JOB_STATUS_SPOOLING|winspool.JOB_STATUS_PRINTING|winspool.JOB_STATUS_PRINTED) != 0:
|
|
return "OK"
|
|
default:
|
|
return "UNKNOWN"
|
|
}
|
|
}
|
|
|
|
func groupJobsByStatus(jobs []winspool.Job) map[string]int {
|
|
groupedJobs := make(map[string]int)
|
|
|
|
for _, job := range jobs {
|
|
groupedJobs[jobStatus(job.Status)]++
|
|
}
|
|
|
|
return groupedJobs
|
|
}
|