2015-09-26 08:36:40 -07:00
// 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.
2021-10-03 04:35:24 -07:00
//go:build !notextfile
2015-01-25 04:28:58 -08:00
// +build !notextfile
package collector
import (
"fmt"
2024-09-11 01:51:28 -07:00
"log/slog"
2015-01-25 04:28:58 -08:00
"os"
"path/filepath"
2015-09-03 07:59:56 -07:00
"sort"
2015-01-25 04:28:58 -08:00
"strings"
"time"
2023-03-07 00:25:05 -08:00
"github.com/alecthomas/kingpin/v2"
2015-09-17 05:05:56 -07:00
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
2015-10-30 13:20:06 -07:00
"github.com/prometheus/common/expfmt"
2015-01-25 04:28:58 -08:00
)
var (
2024-09-30 00:40:03 -07:00
textFileDirectories = kingpin . Flag ( "collector.textfile.directory" , "Directory to read text files with metrics from, supports glob matching. (repeatable)" ) . Default ( "" ) . Strings ( )
mtimeDesc = prometheus . NewDesc (
2018-01-22 05:02:19 -08:00
"node_textfile_mtime_seconds" ,
2017-12-23 11:21:58 -08:00
"Unixtime mtime of textfiles successfully read." ,
[ ] string { "file" } ,
nil ,
)
2015-01-25 04:28:58 -08:00
)
type textFileCollector struct {
2024-09-30 00:40:03 -07:00
paths [ ] string
2017-12-23 11:21:58 -08:00
// Only set for testing to get predictable output.
2019-12-31 08:19:37 -08:00
mtime * float64
2024-09-11 01:51:28 -07:00
logger * slog . Logger
2015-01-25 04:28:58 -08:00
}
func init ( ) {
2017-09-28 06:06:26 -07:00
registerCollector ( "textfile" , defaultEnabled , NewTextFileCollector )
2015-01-25 04:28:58 -08:00
}
2017-02-28 08:44:53 -08:00
// NewTextFileCollector returns a new Collector exposing metrics read from files
// in the given textfile directory.
2024-09-11 01:51:28 -07:00
func NewTextFileCollector ( logger * slog . Logger ) ( Collector , error ) {
2015-09-03 07:59:56 -07:00
c := & textFileCollector {
2024-09-30 00:40:03 -07:00
paths : * textFileDirectories ,
2019-12-31 08:19:37 -08:00
logger : logger ,
2015-09-03 07:59:56 -07:00
}
2017-12-23 11:21:58 -08:00
return c , nil
}
2015-09-03 07:59:56 -07:00
2024-09-11 01:51:28 -07:00
func convertMetricFamily ( metricFamily * dto . MetricFamily , ch chan <- prometheus . Metric , logger * slog . Logger ) {
2017-12-23 11:21:58 -08:00
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 { } { }
2017-12-06 08:05:40 -08:00
}
2017-12-23 11:21:58 -08:00
}
2015-01-25 04:28:58 -08:00
}
2017-12-23 11:21:58 -08:00
for _ , metric := range metricFamily . Metric {
2017-12-24 02:54:33 -08:00
if metric . TimestampMs != nil {
2024-09-11 01:51:28 -07:00
logger . Warn ( "Ignoring unsupported custom timestamp on textfile collector metric" , "metric" , metric )
2017-12-24 02:54:33 -08:00
}
2017-12-23 11:21:58 -08:00
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
}
}
2019-01-04 07:58:53 -08:00
if ! present {
2017-12-23 11:21:58 -08:00
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 ... ,
)
}
}
2015-01-25 04:28:58 -08:00
}
2017-12-23 11:21:58 -08:00
func ( c * textFileCollector ) exportMTimes ( mtimes map [ string ] time . Time , ch chan <- prometheus . Metric ) {
2019-11-22 10:20:52 -08:00
if len ( mtimes ) == 0 {
return
}
2017-12-23 11:21:58 -08:00
// Export the mtimes of the successful files.
2019-11-22 10:20:52 -08:00
// Sorting is needed for predictable output comparison in tests.
2021-10-18 05:05:21 -07:00
filepaths := make ( [ ] string , 0 , len ( mtimes ) )
for path := range mtimes {
filepaths = append ( filepaths , path )
2019-11-22 10:20:52 -08:00
}
2021-10-18 05:05:21 -07:00
sort . Strings ( filepaths )
2017-12-23 11:21:58 -08:00
2021-10-18 05:05:21 -07:00
for _ , path := range filepaths {
mtime := float64 ( mtimes [ path ] . UnixNano ( ) / 1e9 )
2019-11-22 10:20:52 -08:00
if c . mtime != nil {
mtime = * c . mtime
2017-12-23 11:21:58 -08:00
}
2021-10-18 05:05:21 -07:00
ch <- prometheus . MustNewConstMetric ( mtimeDesc , prometheus . GaugeValue , mtime , path )
2017-12-23 11:21:58 -08:00
}
2015-01-25 04:28:58 -08:00
}
2017-12-23 11:21:58 -08:00
// Update implements the Collector interface.
func ( c * textFileCollector ) Update ( ch chan <- prometheus . Metric ) error {
2019-11-22 10:20:52 -08:00
// Iterate over files and accumulate their metrics, but also track any
// parsing errors so an error metric can be reported.
var errored bool
2022-09-20 03:49:21 -07:00
var parsedFamilies [ ] * dto . MetricFamily
metricsNamesToFiles := map [ string ] [ ] string { }
2024-03-23 22:43:03 -07:00
metricsNamesToHelpTexts := map [ string ] [ 2 ] string { }
2018-02-27 10:43:38 -08:00
2024-09-30 00:40:03 -07:00
paths := [ ] string { }
for _ , glob := range c . paths {
ps , err := filepath . Glob ( glob )
if err != nil || len ( ps ) == 0 {
// not glob or not accessible path either way assume single
// directory and let os.ReadDir handle it
ps = [ ] string { glob }
}
paths = append ( paths , ps ... )
2021-10-18 05:05:21 -07:00
}
2018-10-05 04:20:30 -07:00
2021-10-18 05:05:21 -07:00
mtimes := make ( map [ string ] time . Time )
for _ , path := range paths {
2022-07-27 11:59:39 -07:00
files , err := os . ReadDir ( path )
2021-10-18 05:05:21 -07:00
if err != nil && path != "" {
2019-11-22 10:20:52 -08:00
errored = true
2024-09-11 01:51:28 -07:00
c . logger . Error ( "failed to read textfile collector directory" , "path" , path , "err" , err )
2019-04-18 08:47:04 -07:00
}
2018-02-27 10:43:38 -08:00
2021-10-18 05:05:21 -07:00
for _ , f := range files {
2022-09-20 03:49:21 -07:00
metricsFilePath := filepath . Join ( path , f . Name ( ) )
2021-10-18 05:05:21 -07:00
if ! strings . HasSuffix ( f . Name ( ) , ".prom" ) {
continue
}
2015-01-25 04:28:58 -08:00
2022-09-20 03:49:21 -07:00
mtime , families , err := c . processFile ( path , f . Name ( ) , ch )
for _ , mf := range families {
2024-03-23 22:43:03 -07:00
// Check for metrics with inconsistent help texts and take the first help text occurrence.
if helpTexts , seen := metricsNamesToHelpTexts [ * mf . Name ] ; seen {
if mf . Help != nil && helpTexts [ 0 ] != * mf . Help || helpTexts [ 1 ] != "" {
metricsNamesToHelpTexts [ * mf . Name ] = [ 2 ] string { helpTexts [ 0 ] , * mf . Help }
errored = true
2024-09-11 01:51:28 -07:00
c . logger . Error ( "inconsistent metric help text" ,
2024-03-23 22:43:03 -07:00
"metric" , * mf . Name ,
"original_help_text" , helpTexts [ 0 ] ,
"new_help_text" , * mf . Help ,
// Only the first file path will be recorded in case of two or more inconsistent help texts.
"file" , metricsNamesToFiles [ * mf . Name ] [ 0 ] )
continue
}
}
if mf . Help != nil {
metricsNamesToHelpTexts [ * mf . Name ] = [ 2 ] string { * mf . Help }
}
2022-09-20 03:49:21 -07:00
metricsNamesToFiles [ * mf . Name ] = append ( metricsNamesToFiles [ * mf . Name ] , metricsFilePath )
parsedFamilies = append ( parsedFamilies , mf )
}
2021-10-18 05:05:21 -07:00
if err != nil {
errored = true
2024-09-11 01:51:28 -07:00
c . logger . Error ( "failed to collect textfile data" , "file" , f . Name ( ) , "err" , err )
2021-10-18 05:05:21 -07:00
continue
}
2022-09-20 03:49:21 -07:00
mtimes [ metricsFilePath ] = * mtime
2021-10-18 05:05:21 -07:00
}
}
2022-09-20 03:49:21 -07:00
for _ , mf := range parsedFamilies {
if mf . Help == nil {
help := fmt . Sprintf ( "Metric read from %s" , strings . Join ( metricsNamesToFiles [ * mf . Name ] , ", " ) )
mf . Help = & help
}
}
for _ , mf := range parsedFamilies {
convertMetricFamily ( mf , ch , c . logger )
}
2017-12-23 11:21:58 -08:00
c . exportMTimes ( mtimes , ch )
2015-09-03 07:59:56 -07:00
2015-01-25 04:28:58 -08:00
// Export if there were errors.
2019-11-22 10:20:52 -08:00
var errVal float64
if errored {
errVal = 1.0
}
2017-12-23 11:21:58 -08:00
ch <- prometheus . MustNewConstMetric (
prometheus . NewDesc (
"node_textfile_scrape_error" ,
"1 if there was an error opening or reading a file, 0 otherwise" ,
nil , nil ,
) ,
2019-11-22 10:20:52 -08:00
prometheus . GaugeValue , errVal ,
2017-12-23 11:21:58 -08:00
)
2019-11-22 10:20:52 -08:00
2017-12-23 11:21:58 -08:00
return nil
2015-01-25 04:28:58 -08:00
}
2018-10-05 04:20:30 -07:00
2019-11-22 10:20:52 -08:00
// processFile processes a single file, returning its modification time on success.
2022-09-20 03:49:21 -07:00
func ( c * textFileCollector ) processFile ( dir , name string , ch chan <- prometheus . Metric ) ( * time . Time , map [ string ] * dto . MetricFamily , error ) {
2021-10-18 05:05:21 -07:00
path := filepath . Join ( dir , name )
2019-11-22 10:20:52 -08:00
f , err := os . Open ( path )
if err != nil {
2022-09-20 03:49:21 -07:00
return nil , nil , fmt . Errorf ( "failed to open textfile data file %q: %w" , path , err )
2019-11-22 10:20:52 -08:00
}
defer f . Close ( )
var parser expfmt . TextParser
families , err := parser . TextToMetricFamilies ( f )
if err != nil {
2022-09-20 03:49:21 -07:00
return nil , nil , fmt . Errorf ( "failed to parse textfile data from %q: %w" , path , err )
2019-11-22 10:20:52 -08:00
}
if hasTimestamps ( families ) {
2022-09-20 03:49:21 -07:00
return nil , nil , fmt . Errorf ( "textfile %q contains unsupported client-side timestamps, skipping entire file" , path )
2019-11-22 10:20:52 -08:00
}
// Only stat the file once it has been parsed and validated, so that
// a failure does not appear fresh.
stat , err := f . Stat ( )
if err != nil {
2022-09-20 03:49:21 -07:00
return nil , families , fmt . Errorf ( "failed to stat %q: %w" , path , err )
2019-11-22 10:20:52 -08:00
}
t := stat . ModTime ( )
2022-09-20 03:49:21 -07:00
return & t , families , nil
2019-11-22 10:20:52 -08:00
}
2018-10-05 04:20:30 -07:00
// 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
}