diff --git a/.gitignore b/.gitignore index 21f99f37..b9c70563 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.exe -VERSION \ No newline at end of file +VERSION +*.swp +*.un~ diff --git a/README.md b/README.md index e6aff4be..f7462059 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ process | [Win32_PerfRawData_PerfProc_Process](https://msdn.microsoft.com/en-us/ service | [Win32_Service](https://msdn.microsoft.com/en-us/library/aa394418(v=vs.85).aspx) metrics (service states) | ✓ system | Win32_PerfRawData_PerfOS_System metrics (system calls) | ✓ tcp | [Win32_PerfRawData_Tcpip_TCPv4](https://msdn.microsoft.com/en-us/library/aa394341(v=vs.85).aspx) metrics (tcp connections) | +textfile | Read prometheus metrics from a text file | ✓ vmware | Performance counters installed by the Vmware Guest agent | The HELP texts shows the WMI data source, please see MSDN documentation for details. @@ -39,6 +40,7 @@ Name | Description `LISTEN_ADDR` | The IP address to bind to. Defaults to 0.0.0.0 `LISTEN_PORT` | The port to bind to. Defaults to 9182. `METRICS_PATH` | The path at which to serve metrics. Defaults to `/metrics` +`TEXTFILE_DIR` | As the `--collector.textfile.directory` flag, provide a directory to read text files with metrics from Parameters are sent to the installer via `msiexec`. Example invocation: diff --git a/collector/textfile.go b/collector/textfile.go new file mode 100644 index 00000000..2a9fd673 --- /dev/null +++ b/collector/textfile.go @@ -0,0 +1,255 @@ +// 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("C:\\Program Files\\wmi_exporter\\textfile_inputs").String() + + mtimeDesc = prometheus.NewDesc( + "wmi_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() { + Factories["textfile"] = NewTextFileCollector +} + +// NewTextFileCollector returns a new Collector exposing metrics read from files +// in the given textfile directory. +func NewTextFileCollector() (Collector, error) { + return &textFileCollector{ + path: *textFileDirectory, + }, 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 == false { + 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: + log.Errorf("unknown metric type for file") + continue + } + 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) Collect(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 + } + +fileLoop: + 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 + } + var parser expfmt.TextParser + parsedFamilies, err := parser.TextToMetricFamilies(file) + file.Close() + if err != nil { + log.Errorf("Error parsing %q: %v", path, err) + error = 1.0 + continue + } + for _, mf := range parsedFamilies { + for _, m := range mf.Metric { + if m.TimestampMs != nil { + log.Errorf("Textfile %q contains unsupported client-side timestamps, skipping entire file", path) + error = 1.0 + continue fileLoop + } + } + 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. + mtimes[f.Name()] = f.ModTime() + + for _, mf := range parsedFamilies { + convertMetricFamily(mf, ch) + } + } + + c.exportMTimes(mtimes, ch) + + // Export if there were errors. + ch <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + "wmi_textfile_scrape_error", + "1 if there was an error opening or reading a file, 0 otherwise", + nil, nil, + ), + prometheus.GaugeValue, error, + ) + return nil +} diff --git a/exporter.go b/exporter.go index 5909c36c..3a897820 100644 --- a/exporter.go +++ b/exporter.go @@ -26,7 +26,7 @@ type WmiCollector struct { } const ( - defaultCollectors = "cpu,cs,logical_disk,net,os,service,system" + defaultCollectors = "cpu,cs,logical_disk,net,os,service,system,textfile" defaultCollectorsPlaceholder = "[defaults]" serviceName = "wmi_exporter" ) diff --git a/installer/wmi_exporter.wxs b/installer/wmi_exporter.wxs index 29e1dde3..ca986cd9 100644 --- a/installer/wmi_exporter.wxs +++ b/installer/wmi_exporter.wxs @@ -31,12 +31,15 @@ + + TEXTFILE_DIR + - + @@ -45,4 +48,4 @@ - \ No newline at end of file +