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(&timestamp) } func (m *TimestampedMetric) Add(value float64, timestamp time.Time) { m.value += value m.ensureTimestamp(&timestamp) } 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[:]) }