mirror of
https://github.com/prometheus-community/smartctl_exporter.git
synced 2024-11-16 01:33:07 +01:00
2c043b7fcb
The bulk of this change set was automated by the following script which is being used to aid in converting the various exporters/projects to use slog: https://gist.github.com/tjhop/49f96fb7ebbe55b12deee0b0312d8434 Other changes include: - bumping prometheus/{common,client_golang,exporter-toolkit} - bump minimum go version to go1.22 - remove old go-kit/log linter configs, add sloglint Signed-off-by: TJ Hoplock <t.hoplock@gmail.com>
224 lines
6.9 KiB
Go
224 lines
6.9 KiB
Go
// Copyright 2022 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.
|
|
|
|
package main
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
kingpin "github.com/alecthomas/kingpin/v2"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"github.com/prometheus/common/promslog"
|
|
"github.com/prometheus/common/promslog/flag"
|
|
"github.com/prometheus/common/version"
|
|
"github.com/prometheus/exporter-toolkit/web"
|
|
webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"
|
|
)
|
|
|
|
// Device
|
|
type Device struct {
|
|
Name string `json:"name"`
|
|
Info_Name string `json:"info_name"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// SMARTctlManagerCollector implements the Collector interface.
|
|
type SMARTctlManagerCollector struct {
|
|
CollectPeriod string
|
|
CollectPeriodDuration time.Duration
|
|
Devices []Device
|
|
|
|
logger *slog.Logger
|
|
mutex sync.Mutex
|
|
}
|
|
|
|
// Describe sends the super-set of all possible descriptors of metrics
|
|
func (i *SMARTctlManagerCollector) Describe(ch chan<- *prometheus.Desc) {
|
|
prometheus.DescribeByCollect(i, ch)
|
|
}
|
|
|
|
// Collect is called by the Prometheus registry when collecting metrics.
|
|
func (i *SMARTctlManagerCollector) Collect(ch chan<- prometheus.Metric) {
|
|
info := NewSMARTctlInfo(ch)
|
|
i.mutex.Lock()
|
|
for _, device := range i.Devices {
|
|
json := readData(i.logger, device)
|
|
if json.Exists() {
|
|
info.SetJSON(json)
|
|
smart := NewSMARTctl(i.logger, json, ch)
|
|
smart.Collect()
|
|
}
|
|
}
|
|
ch <- prometheus.MustNewConstMetric(
|
|
metricDeviceCount,
|
|
prometheus.GaugeValue,
|
|
float64(len(i.Devices)),
|
|
)
|
|
info.Collect()
|
|
i.mutex.Unlock()
|
|
}
|
|
|
|
func (i *SMARTctlManagerCollector) RescanForDevices() {
|
|
for {
|
|
time.Sleep(*smartctlRescanInterval)
|
|
i.logger.Info("Rescanning for devices")
|
|
devices := scanDevices(i.logger)
|
|
i.mutex.Lock()
|
|
i.Devices = devices
|
|
i.mutex.Unlock()
|
|
}
|
|
}
|
|
|
|
var (
|
|
smartctlPath = kingpin.Flag("smartctl.path",
|
|
"The path to the smartctl binary",
|
|
).Default("/usr/sbin/smartctl").String()
|
|
smartctlInterval = kingpin.Flag("smartctl.interval",
|
|
"The interval between smartctl polls",
|
|
).Default("60s").Duration()
|
|
smartctlRescanInterval = kingpin.Flag("smartctl.rescan",
|
|
"The interval between rescanning for new/disappeared devices. If the interval is smaller than 1s no rescanning takes place. If any devices are configured with smartctl.device also no rescanning takes place.",
|
|
).Default("10m").Duration()
|
|
smartctlDevices = kingpin.Flag("smartctl.device",
|
|
"The device to monitor (repeatable)",
|
|
).Strings()
|
|
smartctlDeviceExclude = kingpin.Flag(
|
|
"smartctl.device-exclude",
|
|
"Regexp of devices to exclude from automatic scanning. (mutually exclusive to device-include)",
|
|
).Default("").String()
|
|
smartctlDeviceInclude = kingpin.Flag(
|
|
"smartctl.device-include",
|
|
"Regexp of devices to exclude from automatic scanning. (mutually exclusive to device-exclude)",
|
|
).Default("").String()
|
|
smartctlFakeData = kingpin.Flag("smartctl.fake-data",
|
|
"The device to monitor (repeatable)",
|
|
).Default("false").Hidden().Bool()
|
|
)
|
|
|
|
// scanDevices uses smartctl to gather the list of available devices.
|
|
func scanDevices(logger *slog.Logger) []Device {
|
|
filter := newDeviceFilter(*smartctlDeviceExclude, *smartctlDeviceInclude)
|
|
|
|
json := readSMARTctlDevices(logger)
|
|
scanDevices := json.Get("devices").Array()
|
|
var scanDeviceResult []Device
|
|
for _, d := range scanDevices {
|
|
deviceName := extractDiskName(strings.TrimSpace(d.Get("info_name").String()))
|
|
if filter.ignored(deviceName) {
|
|
logger.Info("Ignoring device", "name", deviceName)
|
|
} else {
|
|
logger.Info("Found device", "name", deviceName)
|
|
device := Device{
|
|
Name: d.Get("name").String(),
|
|
Info_Name: deviceName,
|
|
Type: d.Get("type").String(),
|
|
}
|
|
scanDeviceResult = append(scanDeviceResult, device)
|
|
}
|
|
}
|
|
return scanDeviceResult
|
|
}
|
|
|
|
func filterDevices(logger *slog.Logger, devices []Device, filters []string) []Device {
|
|
var filtered []Device
|
|
for _, d := range devices {
|
|
for _, filter := range filters {
|
|
logger.Debug("filterDevices", "device", d.Info_Name, "filter", filter)
|
|
if strings.Contains(d.Info_Name, filter) {
|
|
filtered = append(filtered, d)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func main() {
|
|
metricsPath := kingpin.Flag(
|
|
"web.telemetry-path", "Path under which to expose metrics",
|
|
).Default("/metrics").String()
|
|
toolkitFlags := webflag.AddFlags(kingpin.CommandLine, ":9633")
|
|
|
|
promslogConfig := &promslog.Config{}
|
|
flag.AddFlags(kingpin.CommandLine, promslogConfig)
|
|
kingpin.Version(version.Print("smartctl_exporter"))
|
|
kingpin.HelpFlag.Short('h')
|
|
kingpin.Parse()
|
|
logger := promslog.New(promslogConfig)
|
|
|
|
logger.Info("Starting smartctl_exporter", "version", version.Info())
|
|
logger.Info("Build context", "build_context", version.BuildContext())
|
|
|
|
var devices []Device
|
|
devices = scanDevices(logger)
|
|
logger.Info("Number of devices found", "count", len(devices))
|
|
if len(*smartctlDevices) > 0 {
|
|
logger.Info("Devices specified", "devices", strings.Join(*smartctlDevices, ", "))
|
|
devices = filterDevices(logger, devices, *smartctlDevices)
|
|
logger.Info("Devices filtered", "count", len(devices))
|
|
}
|
|
|
|
collector := SMARTctlManagerCollector{
|
|
Devices: devices,
|
|
logger: logger,
|
|
}
|
|
|
|
if *smartctlRescanInterval >= 1*time.Second {
|
|
logger.Info("Start background scan process")
|
|
logger.Info("Rescanning for devices every", "rescanInterval", *smartctlRescanInterval)
|
|
go collector.RescanForDevices()
|
|
}
|
|
|
|
reg := prometheus.NewPedanticRegistry()
|
|
reg.MustRegister(
|
|
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
|
collectors.NewGoCollector(),
|
|
)
|
|
|
|
prometheus.WrapRegistererWithPrefix("", reg).MustRegister(&collector)
|
|
|
|
http.Handle(*metricsPath, promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
|
|
|
|
if *metricsPath != "/" && *metricsPath != "" {
|
|
landingConfig := web.LandingConfig{
|
|
Name: "smartctl_exporter",
|
|
Description: "Prometheus Exporter for S.M.A.R.T. devices",
|
|
Version: version.Info(),
|
|
Links: []web.LandingLinks{
|
|
{
|
|
Address: *metricsPath,
|
|
Text: "Metrics",
|
|
},
|
|
},
|
|
}
|
|
landingPage, err := web.NewLandingPage(landingConfig)
|
|
if err != nil {
|
|
logger.Error("error creating landing page", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
http.Handle("/", landingPage)
|
|
}
|
|
|
|
srv := &http.Server{}
|
|
if err := web.ListenAndServe(srv, toolkitFlags, logger); err != nil {
|
|
logger.Error("error running HTTP server", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|