timestamped_metric.go (111 lines of code) (raw):
package main
import (
"crypto/sha1"
"encoding/hex"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
)
var (
// Do not report timestamped metrics older than 15m
// Prometheus complains about "Error on ingesting samples that are too old or
// are too far into the future"
// The prometheus code indicates that the max age of a metric is related to
// when the last WAL block was written:
// https://github.com/prometheus/prometheus/blob/master/tsdb/db.go#L616
// If this is correct, any value is arbitrary, and we might have to make this
// configurable in the future.
metricsMaxAge = 15 * time.Minute
)
func NewTimestampedMetric(
valueType prometheus.ValueType, opts prometheus.Opts,
) *TimestampedMetric {
fqName := strings.Join(
[]string{opts.Namespace, opts.Subsystem, opts.Name}, "_",
)
return &TimestampedMetric{
desc: prometheus.NewDesc(fqName, opts.Help, nil, opts.ConstLabels),
valueType: valueType,
}
}
type TimestampedMetric struct {
desc *prometheus.Desc
valueType prometheus.ValueType
value float64
labelValues []string
vec *TimestampedMetricVec
}
func (m *TimestampedMetric) ensureTimestamp(timestamp *time.Time) time.Time {
if m.vec == nil {
// back-fill a pseudo vector if needed
m.vec = &TimestampedMetricVec{}
}
if m.vec.timestamp == nil {
m.vec.timestamp = new(time.Time)
if timestamp != nil {
*(m.vec.timestamp) = *timestamp
}
return *(m.vec.timestamp)
}
if timestamp == nil {
return *(m.vec.timestamp)
}
if m.vec.timestamp.Before(*timestamp) {
*(m.vec.timestamp) = *timestamp
}
return *(m.vec.timestamp)
}
func (m *TimestampedMetric) Set(value float64, timestamp time.Time) {
m.value = value
m.ensureTimestamp(×tamp)
}
func (m *TimestampedMetric) Add(value float64, timestamp time.Time) {
m.value += value
m.ensureTimestamp(×tamp)
}
func (m *TimestampedMetric) Describe(descs chan<- *prometheus.Desc) {
descs <- m.desc
}
func (m *TimestampedMetric) Collect(metrics chan<- prometheus.Metric) {
timestamp := m.ensureTimestamp(nil)
if timestamp.Add(metricsMaxAge).Before(time.Now()) {
return
}
metrics <- prometheus.NewMetricWithTimestamp(timestamp, prometheus.MustNewConstMetric(
m.desc, m.valueType, m.value, m.labelValues...,
))
}
func NewTimestampedMetricVec(
valueType prometheus.ValueType, opts prometheus.Opts, variableLabels []string,
) *TimestampedMetricVec {
fqName := strings.Join(
[]string{opts.Namespace, opts.Subsystem, opts.Name}, "_",
)
return &TimestampedMetricVec{
desc: prometheus.NewDesc(fqName, opts.Help, variableLabels, opts.ConstLabels),
valueType: valueType,
metrics: map[string]*TimestampedMetric{},
}
}
type TimestampedMetricVec struct {
desc *prometheus.Desc
valueType prometheus.ValueType
metrics map[string]*TimestampedMetric
timestamp *time.Time
}
func (m *TimestampedMetricVec) WithLabelValues(labelValues ...string) *TimestampedMetric {
labelHash := hashLabels(labelValues)
if m.metrics[labelHash] == nil {
metric := &TimestampedMetric{
desc: m.desc,
valueType: m.valueType,
labelValues: labelValues,
vec: m,
}
m.metrics[labelHash] = metric
}
return m.metrics[labelHash]
}
func (m *TimestampedMetricVec) Describe(descs chan<- *prometheus.Desc) {
descs <- m.desc
}
func (m *TimestampedMetricVec) Collect(metrics chan<- prometheus.Metric) {
for _, metric := range m.metrics {
metric.Collect(metrics)
}
}
func hashLabels(labels []string) string {
hash := sha1.Sum([]byte(strings.Join(labels, "!!!")))
return hex.EncodeToString(hash[:])
}