diff --git a/README.md b/README.md index 09ca7c53..b2e0e265 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,10 @@ $ docker run --rm -it -p 9090:9090 -v $PWD/examples/prometheus.yml:/etc/promethe ``` Then head over to http://localhost:9090/graph?g0.range_input=1h&g0.expr=example_value_active&g0.tab=1 or http://localhost:9090/targets to check the scraped metrics or the targets. +## Using custom timestamps + +This exporter allows you to use a field of the metric as the (unix/epoch) timestamp for the data as an int64. However, this may lead to unexpected behaviour, as the prometheus implements a [Staleness](https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness) mechanism. Including timestamps in metrics disabled this staleness handling and can make data visible for longer than expected. + ## Exposing metrics through HTTPS TLS configuration supported by this exporter can be found at [exporter-toolkit/web](https://github.com/prometheus/exporter-toolkit/blob/v0.5.1/docs/web-configuration.md) diff --git a/config/config.go b/config/config.go index 319db9cf..8a5322de 100644 --- a/config/config.go +++ b/config/config.go @@ -22,13 +22,14 @@ import ( // Metric contains values that define a metric type Metric struct { - Name string - Path string - Labels map[string]string - Type ScrapeType - ValueType ValueType - Help string - Values map[string]string + Name string + Path string + Labels map[string]string + Type ScrapeType + ValueType ValueType + EpochTimestamp string + Help string + Values map[string]string } type ScrapeType string diff --git a/examples/config.yml b/examples/config.yml index 5db61ea7..70e43a75 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -8,7 +8,12 @@ modules: labels: environment: beta # static label location: "planet-{.location}" # dynamic label - + - name: example_timestamped_value + path: "{ .values[?(@.state == "INACTIVE")] }" + epochTimestamp: "{ .timestamp }" + help: Example of a timestamped value scrape in the json + labels: + environment: beta # static label - name: example_value type: object help: Example of sub-level value scrapes from a json diff --git a/examples/data.json b/examples/data.json index 93dea69a..2890657a 100644 --- a/examples/data.json +++ b/examples/data.json @@ -1,5 +1,6 @@ { "counter": 1234, + "timestamp": 1657568506, "values": [ { "id": "id-A", diff --git a/exporter/collector.go b/exporter/collector.go index d7ba92a2..4effc10f 100644 --- a/exporter/collector.go +++ b/exporter/collector.go @@ -16,6 +16,7 @@ package exporter import ( "bytes" "encoding/json" + "time" "github.com/go-kit/log" "github.com/go-kit/log/level" @@ -31,12 +32,13 @@ type JSONMetricCollector struct { } type JSONMetric struct { - Desc *prometheus.Desc - Type config.ScrapeType - KeyJSONPath string - ValueJSONPath string - LabelsJSONPaths []string - ValueType prometheus.ValueType + Desc *prometheus.Desc + Type config.ScrapeType + KeyJSONPath string + ValueJSONPath string + LabelsJSONPaths []string + ValueType prometheus.ValueType + EpochTimestampJSONPath string } func (mc JSONMetricCollector) Describe(ch chan<- *prometheus.Desc) { @@ -56,13 +58,13 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { } if floatValue, err := SanitizeValue(value); err == nil { - - ch <- prometheus.MustNewConstMetric( + metric := prometheus.MustNewConstMetric( m.Desc, m.ValueType, floatValue, extractLabels(mc.Logger, mc.Data, m.LabelsJSONPaths)..., ) + ch <- timestampMetric(mc.Logger, m, mc.Data, metric) } else { level.Error(mc.Logger).Log("msg", "Failed to convert extracted value to float64", "path", m.KeyJSONPath, "value", value, "err", err, "metric", m.Desc) continue @@ -90,12 +92,13 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { } if floatValue, err := SanitizeValue(value); err == nil { - ch <- prometheus.MustNewConstMetric( + metric := prometheus.MustNewConstMetric( m.Desc, m.ValueType, floatValue, extractLabels(mc.Logger, jdata, m.LabelsJSONPaths)..., ) + ch <- timestampMetric(mc.Logger, m, jdata, metric) } else { level.Error(mc.Logger).Log("msg", "Failed to convert extracted value to float64", "path", m.ValueJSONPath, "value", value, "err", err, "metric", m.Desc) continue @@ -157,3 +160,21 @@ func extractLabels(logger log.Logger, data []byte, paths []string) []string { } return labels } + +func timestampMetric(logger log.Logger, m JSONMetric, data []byte, pm prometheus.Metric) prometheus.Metric { + if m.EpochTimestampJSONPath == "" { + return pm + } + ts, err := extractValue(logger, data, m.EpochTimestampJSONPath, false) + if err != nil { + level.Error(logger).Log("msg", "Failed to extract timestamp for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc) + return pm + } + epochTime, err := SanitizeIntValue(ts) + if err != nil { + level.Error(logger).Log("msg", "Failed to parse timestamp for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc) + return pm + } + timestamp := time.UnixMilli(epochTime) + return prometheus.NewMetricWithTimestamp(timestamp, pm) +} diff --git a/exporter/util.go b/exporter/util.go index 1a5d8cd5..d684fac3 100644 --- a/exporter/util.go +++ b/exporter/util.go @@ -62,6 +62,19 @@ func SanitizeValue(s string) (float64, error) { return value, fmt.Errorf(resultErr) } +func SanitizeIntValue(s string) (int64, error) { + var err error + var value int64 + var resultErr string + + if value, err = strconv.ParseInt(s, 10, 64); err == nil { + return value, nil + } + resultErr = fmt.Sprintf("%s", err) + + return value, fmt.Errorf(resultErr) +} + func CreateMetricsList(c config.Module) ([]JSONMetric, error) { var ( metrics []JSONMetric @@ -91,9 +104,10 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) { variableLabels, nil, ), - KeyJSONPath: metric.Path, - LabelsJSONPaths: variableLabelsValues, - ValueType: valueType, + KeyJSONPath: metric.Path, + LabelsJSONPaths: variableLabelsValues, + ValueType: valueType, + EpochTimestampJSONPath: metric.EpochTimestamp, } metrics = append(metrics, jsonMetric) case config.ObjectScrape: @@ -112,10 +126,11 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) { variableLabels, nil, ), - KeyJSONPath: metric.Path, - ValueJSONPath: valuePath, - LabelsJSONPaths: variableLabelsValues, - ValueType: valueType, + KeyJSONPath: metric.Path, + ValueJSONPath: valuePath, + LabelsJSONPaths: variableLabelsValues, + ValueType: valueType, + EpochTimestampJSONPath: metric.EpochTimestamp, } metrics = append(metrics, jsonMetric) }