mirror of
https://github.com/prometheus/node_exporter.git
synced 2024-12-28 23:19:46 -08:00
36e3b2a923
Previously, the node_textfile_mtime_seconds metric was based on the Fileinfo.ModTime() of the ioutil.ReadDir() return value. This is based on lstat() and therefore has unintended consequences for symlinks (modification time of the symlink instead of the symlink target is returned). It is also racy as the lstat() is performed before reading the file. This commit changes the node_textfile_mtime_seconds metric to be based on a fresh Stat() call on the open file. This eliminates the race and works as expected for symlinks. Fixes #1324. Signed-off-by: Christian Hoffmann <mail@hoffmann-christian.info>
268 lines
6.8 KiB
Go
268 lines
6.8 KiB
Go
// Copyright 2015 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.
|
|
|
|
// +build !notextfile
|
|
|
|
package collector
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
dto "github.com/prometheus/client_model/go"
|
|
"github.com/prometheus/common/expfmt"
|
|
"github.com/prometheus/common/log"
|
|
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
|
)
|
|
|
|
var (
|
|
textFileDirectory = kingpin.Flag("collector.textfile.directory", "Directory to read text files with metrics from.").Default("").String()
|
|
mtimeDesc = prometheus.NewDesc(
|
|
"node_textfile_mtime_seconds",
|
|
"Unixtime mtime of textfiles successfully read.",
|
|
[]string{"file"},
|
|
nil,
|
|
)
|
|
)
|
|
|
|
type textFileCollector struct {
|
|
path string
|
|
// Only set for testing to get predictable output.
|
|
mtime *float64
|
|
}
|
|
|
|
func init() {
|
|
registerCollector("textfile", defaultEnabled, NewTextFileCollector)
|
|
}
|
|
|
|
// NewTextFileCollector returns a new Collector exposing metrics read from files
|
|
// in the given textfile directory.
|
|
func NewTextFileCollector() (Collector, error) {
|
|
c := &textFileCollector{
|
|
path: *textFileDirectory,
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func convertMetricFamily(metricFamily *dto.MetricFamily, ch chan<- prometheus.Metric) {
|
|
var valType prometheus.ValueType
|
|
var val float64
|
|
|
|
allLabelNames := map[string]struct{}{}
|
|
for _, metric := range metricFamily.Metric {
|
|
labels := metric.GetLabel()
|
|
for _, label := range labels {
|
|
if _, ok := allLabelNames[label.GetName()]; !ok {
|
|
allLabelNames[label.GetName()] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, metric := range metricFamily.Metric {
|
|
if metric.TimestampMs != nil {
|
|
log.Warnf("Ignoring unsupported custom timestamp on textfile collector metric %v", metric)
|
|
}
|
|
|
|
labels := metric.GetLabel()
|
|
var names []string
|
|
var values []string
|
|
for _, label := range labels {
|
|
names = append(names, label.GetName())
|
|
values = append(values, label.GetValue())
|
|
}
|
|
|
|
for k := range allLabelNames {
|
|
present := false
|
|
for _, name := range names {
|
|
if k == name {
|
|
present = true
|
|
break
|
|
}
|
|
}
|
|
if !present {
|
|
names = append(names, k)
|
|
values = append(values, "")
|
|
}
|
|
}
|
|
|
|
metricType := metricFamily.GetType()
|
|
switch metricType {
|
|
case dto.MetricType_COUNTER:
|
|
valType = prometheus.CounterValue
|
|
val = metric.Counter.GetValue()
|
|
|
|
case dto.MetricType_GAUGE:
|
|
valType = prometheus.GaugeValue
|
|
val = metric.Gauge.GetValue()
|
|
|
|
case dto.MetricType_UNTYPED:
|
|
valType = prometheus.UntypedValue
|
|
val = metric.Untyped.GetValue()
|
|
|
|
case dto.MetricType_SUMMARY:
|
|
quantiles := map[float64]float64{}
|
|
for _, q := range metric.Summary.Quantile {
|
|
quantiles[q.GetQuantile()] = q.GetValue()
|
|
}
|
|
ch <- prometheus.MustNewConstSummary(
|
|
prometheus.NewDesc(
|
|
*metricFamily.Name,
|
|
metricFamily.GetHelp(),
|
|
names, nil,
|
|
),
|
|
metric.Summary.GetSampleCount(),
|
|
metric.Summary.GetSampleSum(),
|
|
quantiles, values...,
|
|
)
|
|
case dto.MetricType_HISTOGRAM:
|
|
buckets := map[float64]uint64{}
|
|
for _, b := range metric.Histogram.Bucket {
|
|
buckets[b.GetUpperBound()] = b.GetCumulativeCount()
|
|
}
|
|
ch <- prometheus.MustNewConstHistogram(
|
|
prometheus.NewDesc(
|
|
*metricFamily.Name,
|
|
metricFamily.GetHelp(),
|
|
names, nil,
|
|
),
|
|
metric.Histogram.GetSampleCount(),
|
|
metric.Histogram.GetSampleSum(),
|
|
buckets, values...,
|
|
)
|
|
default:
|
|
panic("unknown metric type")
|
|
}
|
|
if metricType == dto.MetricType_GAUGE || metricType == dto.MetricType_COUNTER || metricType == dto.MetricType_UNTYPED {
|
|
ch <- prometheus.MustNewConstMetric(
|
|
prometheus.NewDesc(
|
|
*metricFamily.Name,
|
|
metricFamily.GetHelp(),
|
|
names, nil,
|
|
),
|
|
valType, val, values...,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *textFileCollector) exportMTimes(mtimes map[string]time.Time, ch chan<- prometheus.Metric) {
|
|
// Export the mtimes of the successful files.
|
|
if len(mtimes) > 0 {
|
|
// Sorting is needed for predictable output comparison in tests.
|
|
filenames := make([]string, 0, len(mtimes))
|
|
for filename := range mtimes {
|
|
filenames = append(filenames, filename)
|
|
}
|
|
sort.Strings(filenames)
|
|
|
|
for _, filename := range filenames {
|
|
mtime := float64(mtimes[filename].UnixNano() / 1e9)
|
|
if c.mtime != nil {
|
|
mtime = *c.mtime
|
|
}
|
|
ch <- prometheus.MustNewConstMetric(mtimeDesc, prometheus.GaugeValue, mtime, filename)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update implements the Collector interface.
|
|
func (c *textFileCollector) Update(ch chan<- prometheus.Metric) error {
|
|
error := 0.0
|
|
mtimes := map[string]time.Time{}
|
|
|
|
// Iterate over files and accumulate their metrics.
|
|
files, err := ioutil.ReadDir(c.path)
|
|
if err != nil && c.path != "" {
|
|
log.Errorf("Error reading textfile collector directory %q: %s", c.path, err)
|
|
error = 1.0
|
|
}
|
|
|
|
for _, f := range files {
|
|
if !strings.HasSuffix(f.Name(), ".prom") {
|
|
continue
|
|
}
|
|
path := filepath.Join(c.path, f.Name())
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
log.Errorf("Error opening %q: %v", path, err)
|
|
error = 1.0
|
|
continue
|
|
}
|
|
defer file.Close()
|
|
var parser expfmt.TextParser
|
|
parsedFamilies, err := parser.TextToMetricFamilies(file)
|
|
if err != nil {
|
|
log.Errorf("Error parsing %q: %v", path, err)
|
|
error = 1.0
|
|
continue
|
|
}
|
|
if hasTimestamps(parsedFamilies) {
|
|
log.Errorf("Textfile %q contains unsupported client-side timestamps, skipping entire file", path)
|
|
error = 1.0
|
|
continue
|
|
}
|
|
|
|
for _, mf := range parsedFamilies {
|
|
if mf.Help == nil {
|
|
help := fmt.Sprintf("Metric read from %s", path)
|
|
mf.Help = &help
|
|
}
|
|
}
|
|
|
|
// Only set this once it has been parsed and validated, so that
|
|
// a failure does not appear fresh.
|
|
stat, err := file.Stat()
|
|
if err != nil {
|
|
log.Errorf("Error stat'ing %q: %v", path, err)
|
|
error = 1.0
|
|
continue
|
|
}
|
|
mtimes[f.Name()] = stat.ModTime()
|
|
|
|
for _, mf := range parsedFamilies {
|
|
convertMetricFamily(mf, ch)
|
|
}
|
|
}
|
|
|
|
c.exportMTimes(mtimes, ch)
|
|
|
|
// Export if there were errors.
|
|
ch <- prometheus.MustNewConstMetric(
|
|
prometheus.NewDesc(
|
|
"node_textfile_scrape_error",
|
|
"1 if there was an error opening or reading a file, 0 otherwise",
|
|
nil, nil,
|
|
),
|
|
prometheus.GaugeValue, error,
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// hasTimestamps returns true when metrics contain unsupported timestamps.
|
|
func hasTimestamps(parsedFamilies map[string]*dto.MetricFamily) bool {
|
|
for _, mf := range parsedFamilies {
|
|
for _, m := range mf.Metric {
|
|
if m.TimestampMs != nil {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|