393 lines
9.2 KiB
Go
393 lines
9.2 KiB
Go
package v1
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/influxdata/telegraf"
|
|
serializer "github.com/influxdata/telegraf/plugins/serializers/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
var (
|
|
invalidNameCharRE = regexp.MustCompile(`[^a-zA-Z0-9_:]`)
|
|
validNameCharRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*`)
|
|
)
|
|
|
|
// SampleID uniquely identifies a Sample
|
|
type SampleID string
|
|
|
|
// Sample represents the current value of a series.
|
|
type Sample struct {
|
|
// Labels are the Prometheus labels.
|
|
Labels map[string]string
|
|
// Value is the value in the Prometheus output. Only one of these will populated.
|
|
Value float64
|
|
HistogramValue map[float64]uint64
|
|
SummaryValue map[float64]float64
|
|
// Histograms and Summaries need a count and a sum
|
|
Count uint64
|
|
Sum float64
|
|
// Metric timestamp
|
|
Timestamp time.Time
|
|
// Expiration is the deadline that this Sample is valid until.
|
|
Expiration time.Time
|
|
}
|
|
|
|
// MetricFamily contains the data required to build valid prometheus Metrics.
|
|
type MetricFamily struct {
|
|
// Samples are the Sample belonging to this MetricFamily.
|
|
Samples map[SampleID]*Sample
|
|
// Need the telegraf ValueType because there isn't a Prometheus ValueType
|
|
// representing Histogram or Summary
|
|
TelegrafValueType telegraf.ValueType
|
|
// LabelSet is the label counts for all Samples.
|
|
LabelSet map[string]int
|
|
}
|
|
|
|
type Collector struct {
|
|
ExpirationInterval time.Duration
|
|
StringAsLabel bool
|
|
ExportTimestamp bool
|
|
Log telegraf.Logger
|
|
|
|
sync.Mutex
|
|
fam map[string]*MetricFamily
|
|
}
|
|
|
|
func NewCollector(expire time.Duration, stringsAsLabel bool, logger telegraf.Logger) *Collector {
|
|
return &Collector{
|
|
ExpirationInterval: expire,
|
|
StringAsLabel: stringsAsLabel,
|
|
Log: logger,
|
|
fam: make(map[string]*MetricFamily),
|
|
}
|
|
}
|
|
|
|
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
|
|
prometheus.NewGauge(prometheus.GaugeOpts{Name: "Dummy", Help: "Dummy"}).Describe(ch)
|
|
}
|
|
|
|
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
|
|
c.Expire(time.Now(), c.ExpirationInterval)
|
|
|
|
for name, family := range c.fam {
|
|
// Get list of all labels on MetricFamily
|
|
var labelNames []string
|
|
for k, v := range family.LabelSet {
|
|
if v > 0 {
|
|
labelNames = append(labelNames, k)
|
|
}
|
|
}
|
|
desc := prometheus.NewDesc(name, "Telegraf collected metric", labelNames, nil)
|
|
|
|
for _, sample := range family.Samples {
|
|
// Get labels for this sample; unset labels will be set to the
|
|
// empty string
|
|
var labels []string
|
|
for _, label := range labelNames {
|
|
v := sample.Labels[label]
|
|
labels = append(labels, v)
|
|
}
|
|
|
|
var metric prometheus.Metric
|
|
var err error
|
|
switch family.TelegrafValueType {
|
|
case telegraf.Summary:
|
|
metric, err = prometheus.NewConstSummary(desc, sample.Count, sample.Sum, sample.SummaryValue, labels...)
|
|
case telegraf.Histogram:
|
|
metric, err = prometheus.NewConstHistogram(desc, sample.Count, sample.Sum, sample.HistogramValue, labels...)
|
|
default:
|
|
metric, err = prometheus.NewConstMetric(desc, getPromValueType(family.TelegrafValueType), sample.Value, labels...)
|
|
}
|
|
if err != nil {
|
|
c.Log.Errorf("Error creating prometheus metric: "+
|
|
"key: %s, labels: %v, err: %v",
|
|
name, labels, err)
|
|
continue
|
|
}
|
|
|
|
if c.ExportTimestamp {
|
|
metric = prometheus.NewMetricWithTimestamp(sample.Timestamp, metric)
|
|
}
|
|
ch <- metric
|
|
}
|
|
}
|
|
}
|
|
|
|
func sanitize(value string) string {
|
|
return invalidNameCharRE.ReplaceAllString(value, "_")
|
|
}
|
|
|
|
func isValidTagName(tag string) bool {
|
|
return validNameCharRE.MatchString(tag)
|
|
}
|
|
|
|
func getPromValueType(tt telegraf.ValueType) prometheus.ValueType {
|
|
switch tt {
|
|
case telegraf.Counter:
|
|
return prometheus.CounterValue
|
|
case telegraf.Gauge:
|
|
return prometheus.GaugeValue
|
|
default:
|
|
return prometheus.UntypedValue
|
|
}
|
|
}
|
|
|
|
// CreateSampleID creates a SampleID based on the tags of a telegraf.Metric.
|
|
func CreateSampleID(tags map[string]string) SampleID {
|
|
pairs := make([]string, 0, len(tags))
|
|
for k, v := range tags {
|
|
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
sort.Strings(pairs)
|
|
return SampleID(strings.Join(pairs, ","))
|
|
}
|
|
|
|
func addSample(fam *MetricFamily, sample *Sample, sampleID SampleID) {
|
|
|
|
for k := range sample.Labels {
|
|
fam.LabelSet[k]++
|
|
}
|
|
|
|
fam.Samples[sampleID] = sample
|
|
}
|
|
|
|
func (c *Collector) addMetricFamily(point telegraf.Metric, sample *Sample, mname string, sampleID SampleID) {
|
|
var fam *MetricFamily
|
|
var ok bool
|
|
if fam, ok = c.fam[mname]; !ok {
|
|
fam = &MetricFamily{
|
|
Samples: make(map[SampleID]*Sample),
|
|
TelegrafValueType: point.Type(),
|
|
LabelSet: make(map[string]int),
|
|
}
|
|
c.fam[mname] = fam
|
|
}
|
|
|
|
addSample(fam, sample, sampleID)
|
|
}
|
|
|
|
// Sorted returns a copy of the metrics in time ascending order. A copy is
|
|
// made to avoid modifying the input metric slice since doing so is not
|
|
// allowed.
|
|
func sorted(metrics []telegraf.Metric) []telegraf.Metric {
|
|
batch := make([]telegraf.Metric, 0, len(metrics))
|
|
for i := len(metrics) - 1; i >= 0; i-- {
|
|
batch = append(batch, metrics[i])
|
|
}
|
|
sort.Slice(batch, func(i, j int) bool {
|
|
return batch[i].Time().Before(batch[j].Time())
|
|
})
|
|
return batch
|
|
}
|
|
|
|
func (c *Collector) Add(metrics []telegraf.Metric) error {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
|
|
now := time.Now()
|
|
|
|
for _, point := range sorted(metrics) {
|
|
tags := point.Tags()
|
|
sampleID := CreateSampleID(tags)
|
|
|
|
labels := make(map[string]string)
|
|
for k, v := range tags {
|
|
name, ok := serializer.SanitizeLabelName(k)
|
|
if !ok {
|
|
continue
|
|
}
|
|
labels[name] = v
|
|
}
|
|
|
|
// Prometheus doesn't have a string value type, so convert string
|
|
// fields to labels if enabled.
|
|
if c.StringAsLabel {
|
|
for fn, fv := range point.Fields() {
|
|
switch fv := fv.(type) {
|
|
case string:
|
|
name, ok := serializer.SanitizeLabelName(fn)
|
|
if !ok {
|
|
continue
|
|
}
|
|
labels[name] = fv
|
|
}
|
|
}
|
|
}
|
|
|
|
switch point.Type() {
|
|
case telegraf.Summary:
|
|
var mname string
|
|
var sum float64
|
|
var count uint64
|
|
summaryvalue := make(map[float64]float64)
|
|
for fn, fv := range point.Fields() {
|
|
var value float64
|
|
switch fv := fv.(type) {
|
|
case int64:
|
|
value = float64(fv)
|
|
case uint64:
|
|
value = float64(fv)
|
|
case float64:
|
|
value = fv
|
|
default:
|
|
continue
|
|
}
|
|
|
|
switch fn {
|
|
case "sum":
|
|
sum = value
|
|
case "count":
|
|
count = uint64(value)
|
|
default:
|
|
limit, err := strconv.ParseFloat(fn, 64)
|
|
if err == nil {
|
|
summaryvalue[limit] = value
|
|
}
|
|
}
|
|
}
|
|
sample := &Sample{
|
|
Labels: labels,
|
|
SummaryValue: summaryvalue,
|
|
Count: count,
|
|
Sum: sum,
|
|
Timestamp: point.Time(),
|
|
Expiration: now.Add(c.ExpirationInterval),
|
|
}
|
|
mname = sanitize(point.Name())
|
|
|
|
if !isValidTagName(mname) {
|
|
continue
|
|
}
|
|
|
|
c.addMetricFamily(point, sample, mname, sampleID)
|
|
|
|
case telegraf.Histogram:
|
|
var mname string
|
|
var sum float64
|
|
var count uint64
|
|
histogramvalue := make(map[float64]uint64)
|
|
for fn, fv := range point.Fields() {
|
|
var value float64
|
|
switch fv := fv.(type) {
|
|
case int64:
|
|
value = float64(fv)
|
|
case uint64:
|
|
value = float64(fv)
|
|
case float64:
|
|
value = fv
|
|
default:
|
|
continue
|
|
}
|
|
|
|
switch fn {
|
|
case "sum":
|
|
sum = value
|
|
case "count":
|
|
count = uint64(value)
|
|
default:
|
|
limit, err := strconv.ParseFloat(fn, 64)
|
|
if err == nil {
|
|
histogramvalue[limit] = uint64(value)
|
|
}
|
|
}
|
|
}
|
|
sample := &Sample{
|
|
Labels: labels,
|
|
HistogramValue: histogramvalue,
|
|
Count: count,
|
|
Sum: sum,
|
|
Timestamp: point.Time(),
|
|
Expiration: now.Add(c.ExpirationInterval),
|
|
}
|
|
mname = sanitize(point.Name())
|
|
|
|
if !isValidTagName(mname) {
|
|
continue
|
|
}
|
|
|
|
c.addMetricFamily(point, sample, mname, sampleID)
|
|
|
|
default:
|
|
for fn, fv := range point.Fields() {
|
|
// Ignore string and bool fields.
|
|
var value float64
|
|
switch fv := fv.(type) {
|
|
case int64:
|
|
value = float64(fv)
|
|
case uint64:
|
|
value = float64(fv)
|
|
case float64:
|
|
value = fv
|
|
default:
|
|
continue
|
|
}
|
|
|
|
sample := &Sample{
|
|
Labels: labels,
|
|
Value: value,
|
|
Timestamp: point.Time(),
|
|
Expiration: now.Add(c.ExpirationInterval),
|
|
}
|
|
|
|
// Special handling of value field; supports passthrough from
|
|
// the prometheus input.
|
|
var mname string
|
|
switch point.Type() {
|
|
case telegraf.Counter:
|
|
if fn == "counter" {
|
|
mname = sanitize(point.Name())
|
|
}
|
|
case telegraf.Gauge:
|
|
if fn == "gauge" {
|
|
mname = sanitize(point.Name())
|
|
}
|
|
}
|
|
if mname == "" {
|
|
if fn == "value" {
|
|
mname = sanitize(point.Name())
|
|
} else {
|
|
mname = sanitize(fmt.Sprintf("%s_%s", point.Name(), fn))
|
|
}
|
|
}
|
|
if !isValidTagName(mname) {
|
|
continue
|
|
}
|
|
c.addMetricFamily(point, sample, mname, sampleID)
|
|
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Collector) Expire(now time.Time, age time.Duration) {
|
|
if age == 0 {
|
|
return
|
|
}
|
|
|
|
for name, family := range c.fam {
|
|
for key, sample := range family.Samples {
|
|
if age != 0 && now.After(sample.Expiration) {
|
|
for k := range sample.Labels {
|
|
family.LabelSet[k]--
|
|
}
|
|
delete(family.Samples, key)
|
|
|
|
if len(family.Samples) == 0 {
|
|
delete(c.fam, name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|