// 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 collector import ( "fmt" "log/slog" "strconv" "strings" "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/procfs" ) var ( sysctlInclude = kingpin.Flag("collector.sysctl.include", "Select sysctl metrics to include").Strings() sysctlIncludeInfo = kingpin.Flag("collector.sysctl.include-info", "Select sysctl metrics to include as info metrics").Strings() sysctlInfoDesc = prometheus.NewDesc(prometheus.BuildFQName(namespace, "sysctl", "info"), "sysctl info", []string{"name", "value", "index"}, nil) ) type sysctlCollector struct { fs procfs.FS logger *slog.Logger sysctls []*sysctl } func init() { registerCollector("sysctl", defaultDisabled, NewSysctlCollector) } func NewSysctlCollector(logger *slog.Logger) (Collector, error) { fs, err := procfs.NewFS(*procPath) if err != nil { return nil, fmt.Errorf("failed to open sysfs: %w", err) } c := &sysctlCollector{ logger: logger, fs: fs, sysctls: []*sysctl{}, } for _, include := range *sysctlInclude { sysctl, err := newSysctl(include, true) if err != nil { return nil, err } c.sysctls = append(c.sysctls, sysctl) } for _, include := range *sysctlIncludeInfo { sysctl, err := newSysctl(include, false) if err != nil { return nil, err } c.sysctls = append(c.sysctls, sysctl) } return c, nil } func (c *sysctlCollector) Update(ch chan<- prometheus.Metric) error { for _, sysctl := range c.sysctls { metrics, err := c.newMetrics(sysctl) if err != nil { return err } for _, metric := range metrics { ch <- metric } } return nil } func (c *sysctlCollector) newMetrics(s *sysctl) ([]prometheus.Metric, error) { var ( values interface{} length int err error ) if s.numeric { values, err = c.fs.SysctlInts(s.name) if err != nil { return nil, fmt.Errorf("error obtaining sysctl info: %w", err) } length = len(values.([]int)) } else { values, err = c.fs.SysctlStrings(s.name) if err != nil { return nil, fmt.Errorf("error obtaining sysctl info: %w", err) } length = len(values.([]string)) } switch length { case 0: return nil, fmt.Errorf("sysctl %s has no values", s.name) case 1: if len(s.keys) > 0 { return nil, fmt.Errorf("sysctl %s has only one value, but expected %v", s.name, s.keys) } return []prometheus.Metric{s.newConstMetric(values)}, nil default: if len(s.keys) == 0 { return s.newIndexedMetrics(values), nil } if length != len(s.keys) { return nil, fmt.Errorf("sysctl %s has %d keys but only %d defined in f lag", s.name, length, len(s.keys)) } return s.newMappedMetrics(values) } } type sysctl struct { numeric bool name string keys []string } func newSysctl(include string, numeric bool) (*sysctl, error) { parts := strings.SplitN(include, ":", 2) s := &sysctl{ numeric: numeric, name: parts[0], } if len(parts) == 2 { s.keys = strings.Split(parts[1], ",") s.name = parts[0] } return s, nil } func (s *sysctl) metricName() string { return SanitizeMetricName(s.name) } func (s *sysctl) newConstMetric(v interface{}) prometheus.Metric { if s.numeric { return prometheus.MustNewConstMetric( prometheus.NewDesc( prometheus.BuildFQName(namespace, "sysctl", s.metricName()), fmt.Sprintf("sysctl %s", s.name), nil, nil), prometheus.UntypedValue, float64(v.([]int)[0])) } return prometheus.MustNewConstMetric( sysctlInfoDesc, prometheus.GaugeValue, 1.0, s.name, v.([]string)[0], "0", ) } func (s *sysctl) newIndexedMetrics(v interface{}) []prometheus.Metric { desc := prometheus.NewDesc( prometheus.BuildFQName(namespace, "sysctl", s.metricName()), fmt.Sprintf("sysctl %s", s.name), []string{"index"}, nil, ) switch values := v.(type) { case []int: metrics := make([]prometheus.Metric, len(values)) for i, n := range values { metrics[i] = prometheus.MustNewConstMetric(desc, prometheus.UntypedValue, float64(n), strconv.Itoa(i)) } return metrics case []string: metrics := make([]prometheus.Metric, len(values)) for i, str := range values { metrics[i] = prometheus.MustNewConstMetric(sysctlInfoDesc, prometheus.GaugeValue, 1.0, s.name, str, strconv.Itoa(i)) } return metrics default: panic(fmt.Sprintf("unexpected type %T", values)) } } func (s *sysctl) newMappedMetrics(v interface{}) ([]prometheus.Metric, error) { switch values := v.(type) { case []int: metrics := make([]prometheus.Metric, len(values)) for i, n := range values { key := s.keys[i] desc := prometheus.NewDesc( prometheus.BuildFQName(namespace, "sysctl", s.metricName()+"_"+key), fmt.Sprintf("sysctl %s, field %d", s.name, i), nil, nil, ) metrics[i] = prometheus.MustNewConstMetric(desc, prometheus.UntypedValue, float64(n)) } return metrics, nil case []string: return nil, fmt.Errorf("mapped sysctl string values not supported") default: return nil, fmt.Errorf("unexpected type %T", values) } }