diff --git a/collector/scheduled_task.go b/collector/scheduled_task.go new file mode 100644 index 00000000..82210694 --- /dev/null +++ b/collector/scheduled_task.go @@ -0,0 +1,283 @@ +// +build windows + +package collector + +import ( + "fmt" + "regexp" + "strings" + + ole "github.com/go-ole/go-ole" + "github.com/go-ole/go-ole/oleutil" + "github.com/prometheus-community/windows_exporter/log" + "github.com/prometheus/client_golang/prometheus" + "gopkg.in/alecthomas/kingpin.v2" +) + +var ( + taskWhitelist = kingpin.Flag( + "collector.scheduled_task.whitelist", + "Regexp of tasks to whitelist. Task path must both match whitelist and not match blacklist to be included.", + ).Default(".+").String() + taskBlacklist = kingpin.Flag( + "collector.scheduled_task.blacklist", + "Regexp of tasks to blacklist. Task path must both match whitelist and not match blacklist to be included.", + ).String() +) + +type ScheduledTaskCollector struct { + LastResult *prometheus.Desc + MissedRuns *prometheus.Desc + State *prometheus.Desc + + taskWhitelistPattern *regexp.Regexp + taskBlacklistPattern *regexp.Regexp +} + +// TaskState ... +// https://docs.microsoft.com/en-us/windows/desktop/api/taskschd/ne-taskschd-task_state +type TaskState uint + +type TaskResult uint + +const ( + TASK_STATE_UNKNOWN TaskState = iota + TASK_STATE_DISABLED + TASK_STATE_QUEUED + TASK_STATE_READY + TASK_STATE_RUNNING + TASK_RESULT_SUCCESS TaskResult = 0x0 +) + +// RegisteredTask ... +type ScheduledTask struct { + Name string + Path string + Enabled bool + State TaskState + MissedRunsCount float64 + LastTaskResult TaskResult +} + +func init() { + registerCollector("scheduled_task", NewScheduledTask) +} + +// NewScheduledTask ... +func NewScheduledTask() (Collector, error) { + const subsystem = "scheduled_task" + + return &ScheduledTaskCollector{ + LastResult: prometheus.NewDesc( + prometheus.BuildFQName(Namespace, subsystem, "last_result"), + "The result that was returned the last time the registered task was run", + []string{"task"}, + nil, + ), + + MissedRuns: prometheus.NewDesc( + prometheus.BuildFQName(Namespace, subsystem, "missed_runs"), + "The number of times the registered task missed a scheduled run", + []string{"task"}, + nil, + ), + + State: prometheus.NewDesc( + prometheus.BuildFQName(Namespace, subsystem, "state"), + "The current state of a scheduled task", + []string{"task", "state"}, + nil, + ), + + taskWhitelistPattern: regexp.MustCompile(fmt.Sprintf("^(?:%s)$", *taskWhitelist)), + taskBlacklistPattern: regexp.MustCompile(fmt.Sprintf("^(?:%s)$", *taskBlacklist)), + }, nil +} + +func (c *ScheduledTaskCollector) Collect(ctx *ScrapeContext, ch chan<- prometheus.Metric) error { + if desc, err := c.collect(ch); err != nil { + log.Error("failed collecting user metrics:", desc, err) + return err + } + + return nil +} + +var TASK_STATES = []string{"disabled", "queued", "ready", "running", "unknown"} + +func (c *ScheduledTaskCollector) collect(ch chan<- prometheus.Metric) (*prometheus.Desc, error) { + scheduledTasks, err := getScheduledTasks() + if err != nil { + return nil, err + } + + for _, task := range scheduledTasks { + if c.taskBlacklistPattern.MatchString(task.Path) || + !c.taskWhitelistPattern.MatchString(task.Path) { + continue + } + + lastResult := 0.0 + if task.LastTaskResult == TASK_RESULT_SUCCESS { + lastResult = 1.0 + } + + ch <- prometheus.MustNewConstMetric( + c.LastResult, + prometheus.GaugeValue, + lastResult, + task.Path, + ) + + ch <- prometheus.MustNewConstMetric( + c.MissedRuns, + prometheus.GaugeValue, + task.MissedRunsCount, + task.Path, + ) + + for _, state := range TASK_STATES { + var stateValue float64 = 0.0 + + if strings.ToLower(task.State.String()) == state { + stateValue = 1.0 + } + + ch <- prometheus.MustNewConstMetric( + c.State, + prometheus.GaugeValue, + stateValue, + task.Path, + state, + ) + } + } + + return nil, nil +} + +const SCHEDULED_TASK_PROGRAM_ID = "Schedule.Service.1" + +// S_FALSE is returned by CoInitialize if it was already called on this thread. +const S_FALSE = 0x00000001 + +func getScheduledTasks() ([]ScheduledTask, error) { + var err error + scheduledTasks := []ScheduledTask{} + + err = ole.CoInitialize(0) + if err != nil { + code := err.(*ole.OleError).Code() + if code != ole.S_OK && code != S_FALSE { + return scheduledTasks, err + } + } + defer ole.CoUninitialize() + + schedClassID, err := ole.ClassIDFrom(SCHEDULED_TASK_PROGRAM_ID) + if err != nil { + ole.CoUninitialize() + return scheduledTasks, err + } + + taskSchedulerObj, err := ole.CreateInstance(schedClassID, nil) + if err != nil || taskSchedulerObj == nil { + ole.CoUninitialize() + return scheduledTasks, err + } + defer taskSchedulerObj.Release() + + taskServiceObj := taskSchedulerObj.MustQueryInterface(ole.IID_IDispatch) + _, err = oleutil.CallMethod(taskServiceObj, "Connect") + if err != nil { + return scheduledTasks, err + } + defer taskServiceObj.Release() + + res, err := oleutil.CallMethod(taskServiceObj, "GetFolder", `\`) + if err != nil { + return scheduledTasks, err + } + + rootFolderObj := res.ToIDispatch() + defer rootFolderObj.Release() + + var fetchTasksInFolder func(*ole.IDispatch) error + var fetchTasksRecursively func(*ole.IDispatch) error + + fetchTasksInFolder = func(folder *ole.IDispatch) error { + res, err := oleutil.CallMethod(folder, "GetTasks", 1) + if err != nil { + return err + } + + tasks := res.ToIDispatch() + defer tasks.Release() + + err = oleutil.ForEach(tasks, func(v *ole.VARIANT) error { + task := v.ToIDispatch() + + parsedTask := parseTask(task) + scheduledTasks = append(scheduledTasks, parsedTask) + + return nil + }) + + return err + } + + fetchTasksRecursively = func(folder *ole.IDispatch) error { + if err := fetchTasksInFolder(folder); err != nil { + return err + } + + res, err := oleutil.CallMethod(folder, "GetFolders", 1) + if err != nil { + return err + } + + subFolders := res.ToIDispatch() + defer subFolders.Release() + + err = oleutil.ForEach(subFolders, func(v *ole.VARIANT) error { + subFolder := v.ToIDispatch() + return fetchTasksRecursively(subFolder) + }) + + return err + } + + fetchTasksRecursively(rootFolderObj) + + return scheduledTasks, nil +} + +func parseTask(task *ole.IDispatch) ScheduledTask { + scheduledTask := ScheduledTask{} + + scheduledTask.Name = oleutil.MustGetProperty(task, "Name").ToString() + scheduledTask.Path = strings.ReplaceAll(oleutil.MustGetProperty(task, "Path").ToString(), "\\", "/") + scheduledTask.Enabled = oleutil.MustGetProperty(task, "Enabled").Value().(bool) + scheduledTask.State = TaskState(oleutil.MustGetProperty(task, "State").Val) + scheduledTask.MissedRunsCount = float64(oleutil.MustGetProperty(task, "NumberOfMissedRuns").Val) + scheduledTask.LastTaskResult = TaskResult(oleutil.MustGetProperty(task, "LastTaskResult").Val) + + return scheduledTask +} + +func (t TaskState) String() string { + switch t { + case TASK_STATE_UNKNOWN: + return "Unknown" + case TASK_STATE_DISABLED: + return "Disabled" + case TASK_STATE_QUEUED: + return "Queued" + case TASK_STATE_READY: + return "Ready" + case TASK_STATE_RUNNING: + return "Running" + default: + return "" + } +} diff --git a/go.mod b/go.mod index 85300f77..d3d4316d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f github.com/dimchansky/utfbom v1.1.1 github.com/go-kit/log v0.2.0 - github.com/go-ole/go-ole v1.2.1 // indirect + github.com/go-ole/go-ole v1.2.5 github.com/leoluk/perflib_exporter v0.1.1-0.20211204221052-9e3696429c20 github.com/prometheus/client_golang v1.12.1 github.com/prometheus/client_model v0.2.0 diff --git a/go.sum b/go.sum index 07954b0f..b44c8c78 100644 --- a/go.sum +++ b/go.sum @@ -297,8 +297,8 @@ github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNV github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= -github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=