1
0
Fork 0
telegraf/plugins/parsers/openmetrics/textparse.go

395 lines
10 KiB
Go
Raw Normal View History

package openmetrics
import (
"bytes"
"errors"
"fmt"
"hash/maphash"
"io"
"math"
"slices"
"strconv"
"strings"
"time"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/textparse"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TextToMetricFamilies(data []byte) ([]*MetricFamily, error) {
var metrics []*MetricFamily
parser := textparse.NewOpenMetricsParser(data, nil)
seed := maphash.MakeSeed()
mf := &MetricFamily{}
mfMetric := &Metric{}
mfMetricKey := uint64(0)
mfMetricPoint := &MetricPoint{}
for {
entry, err := parser.Next()
if err != nil {
if errors.Is(err, io.EOF) {
if mf.Name != "" {
if mfMetricPoint.Value != nil {
mfMetric.MetricPoints = append(mfMetric.MetricPoints, mfMetricPoint)
}
if len(mfMetric.MetricPoints) > 0 {
mf.Metrics = append(mf.Metrics, mfMetric)
}
metrics = append(metrics, mf)
}
break
}
return nil, fmt.Errorf("parsing failed: %w", err)
}
switch entry {
case textparse.EntryInvalid:
continue
case textparse.EntryType:
name, mtype := parser.Type()
if len(name) == 0 {
return nil, errors.New("empty metric-family name")
}
if mf.Name == "" {
mf.Name = string(name)
} else if mf.Name != string(name) {
if mfMetricPoint.Value != nil {
mfMetric.MetricPoints = append(mfMetric.MetricPoints, mfMetricPoint)
}
if len(mfMetric.MetricPoints) > 0 {
mf.Metrics = append(mf.Metrics, mfMetric)
}
metrics = append(metrics, mf)
mf = &MetricFamily{Name: string(name)}
mfMetric = &Metric{}
mfMetricKey = 0
mfMetricPoint = &MetricPoint{}
}
switch mtype {
case model.MetricTypeCounter:
mf.Type = MetricType_COUNTER
case model.MetricTypeGauge:
mf.Type = MetricType_GAUGE
case model.MetricTypeHistogram:
mf.Type = MetricType_HISTOGRAM
case model.MetricTypeGaugeHistogram:
mf.Type = MetricType_GAUGE_HISTOGRAM
case model.MetricTypeSummary:
mf.Type = MetricType_SUMMARY
case model.MetricTypeInfo:
mf.Type = MetricType_INFO
case model.MetricTypeStateset:
mf.Type = MetricType_STATE_SET
case model.MetricTypeUnknown:
mf.Type = MetricType_UNKNOWN
}
case textparse.EntryHelp:
name, mhelp := parser.Help()
if len(name) == 0 {
return nil, errors.New("empty metric-family name")
}
if mf.Name == "" {
mf.Name = string(name)
} else if mf.Name != string(name) {
if mfMetricPoint.Value != nil {
mfMetric.MetricPoints = append(mfMetric.MetricPoints, mfMetricPoint)
}
if len(mfMetric.MetricPoints) > 0 {
mf.Metrics = append(mf.Metrics, mfMetric)
}
metrics = append(metrics, mf)
mf = &MetricFamily{Name: string(name)}
mfMetric = &Metric{}
mfMetricKey = 0
mfMetricPoint = &MetricPoint{}
}
mf.Help = string(mhelp)
case textparse.EntrySeries:
series, ts, value := parser.Series()
// Extract the metric name and labels
dn, _, _ := bytes.Cut(series, []byte("{"))
if len(dn) == 0 {
return nil, errors.New("empty metric name")
}
sampleName := string(dn)
var metricLabels labels.Labels
parser.Metric(&metricLabels)
// There might be metrics without meta-information, however in this
// case the metric is of type UNKNOWN according to the spec and do
// only contain a single metric. Therefore, we can use the metric
// name as metric-family name
if mf.Name == "" {
mf.Name = sampleName
}
// The name contained in the sample is constructed using the metric
// name and an optional sample-type suffix used for more complex
// types (e.g. histograms).
sampleType, seriesLabels := extractSampleType(sampleName, mf.Name, mf.Type, &metricLabels)
// Check if we are still in the same metric, if not, add the
// previous one to the metric family and create a new one...
key := getSeriesKey(seriesLabels, seed)
if mfMetricKey != key {
if mfMetricPoint.Value != nil {
mfMetric.MetricPoints = append(mfMetric.MetricPoints, mfMetricPoint)
}
if len(mfMetric.MetricPoints) > 0 {
mf.Metrics = append(mf.Metrics, mfMetric)
}
mfMetric = &Metric{}
mfMetricKey = key
mfMetricPoint = &MetricPoint{}
mfMetric.Labels = make([]*Label, 0, len(*seriesLabels))
for _, l := range *seriesLabels {
mfMetric.Labels = append(mfMetric.Labels, &Label{
Name: l.Name,
Value: l.Value,
})
}
}
// Check if we are still in the same metric-point
var mpTimestamp int64
if mfMetricPoint.Timestamp != nil {
mpTimestamp = mfMetricPoint.Timestamp.Seconds * int64(time.Second)
mpTimestamp += int64(mfMetricPoint.Timestamp.Nanos)
}
var timestamp int64
if ts != nil {
timestamp = *ts * int64(time.Millisecond)
}
if mpTimestamp != timestamp {
if mfMetricPoint.Value != nil {
mfMetric.MetricPoints = append(mfMetric.MetricPoints, mfMetricPoint)
}
mfMetricPoint = &MetricPoint{}
if ts != nil {
mfMetricPoint.Timestamp = timestamppb.New(time.Unix(0, timestamp))
}
}
// Fill in the metric-point
mfMetricPoint.set(mf.Name, mf.Type, sampleType, value, &metricLabels)
case textparse.EntryComment:
// ignore comments
case textparse.EntryUnit:
name, munit := parser.Unit()
if len(name) == 0 {
return nil, errors.New("empty metric-family name")
}
if mf.Name == "" {
mf.Name = string(name)
} else if mf.Name != string(name) {
if mfMetricPoint.Value != nil {
mfMetric.MetricPoints = append(mfMetric.MetricPoints, mfMetricPoint)
}
if len(mfMetric.MetricPoints) > 0 {
mf.Metrics = append(mf.Metrics, mfMetric)
}
metrics = append(metrics, mf)
mf = &MetricFamily{Name: string(name)}
mfMetric = &Metric{}
mfMetricKey = 0
mfMetricPoint = &MetricPoint{}
}
mf.Unit = string(munit)
case textparse.EntryHistogram:
// not supported yet
default:
return nil, fmt.Errorf("unknown entry type %v", entry)
}
}
return metrics, nil
}
func getSeriesKey(seriesLabels *labels.Labels, seed maphash.Seed) uint64 {
sorted := make([]string, 0, len(*seriesLabels))
for _, l := range *seriesLabels {
sorted = append(sorted, l.Name+"="+l.Value)
}
slices.Sort(sorted)
var h maphash.Hash
h.SetSeed(seed)
for _, p := range sorted {
h.WriteString(p)
h.WriteByte(0)
}
return h.Sum64()
}
func extractSampleType(raw, name string, mtype MetricType, metricLabels *labels.Labels) (string, *labels.Labels) {
suffix := strings.TrimLeft(strings.TrimPrefix(raw, name), "_")
var seriesLabels labels.Labels
for _, l := range *metricLabels {
// filter out special labels
switch {
case l.Name == "__name__":
case mtype == MetricType_STATE_SET && l.Name == name:
case mtype == MetricType_HISTOGRAM && l.Name == "le":
case mtype == MetricType_GAUGE_HISTOGRAM && l.Name == "le":
case mtype == MetricType_SUMMARY && l.Name == "quantile":
default:
seriesLabels = append(seriesLabels, labels.Label{Name: l.Name, Value: l.Value})
}
}
return suffix, &seriesLabels
}
func (mp *MetricPoint) set(mname string, mtype MetricType, stype string, value float64, mlabels *labels.Labels) {
switch mtype {
case MetricType_UNKNOWN:
mp.Value = &MetricPoint_UnknownValue{
UnknownValue: &UnknownValue{
Value: &UnknownValue_DoubleValue{DoubleValue: value},
},
}
case MetricType_GAUGE:
mp.Value = &MetricPoint_GaugeValue{
GaugeValue: &GaugeValue{
Value: &GaugeValue_DoubleValue{DoubleValue: value},
},
}
case MetricType_COUNTER:
var v *MetricPoint_CounterValue
if mp.Value != nil {
v = mp.Value.(*MetricPoint_CounterValue)
} else {
v = &MetricPoint_CounterValue{
CounterValue: &CounterValue{},
}
}
switch stype {
case "total":
v.CounterValue.Total = &CounterValue_DoubleValue{DoubleValue: value}
case "created":
t := time.Unix(0, int64(value*float64(time.Second)))
v.CounterValue.Created = timestamppb.New(t)
}
mp.Value = v
case MetricType_STATE_SET:
var v *MetricPoint_StateSetValue
if mp.Value != nil {
v = mp.Value.(*MetricPoint_StateSetValue)
} else {
v = &MetricPoint_StateSetValue{
StateSetValue: &StateSetValue{},
}
}
var name string
for _, l := range *mlabels {
if l.Name == mname {
name = l.Value
break
}
}
v.StateSetValue.States = append(v.StateSetValue.States, &StateSetValue_State{
Enabled: value > 0,
Name: name,
})
mp.Value = v
case MetricType_INFO:
mp.Value = &MetricPoint_InfoValue{
InfoValue: &InfoValue{},
}
case MetricType_HISTOGRAM, MetricType_GAUGE_HISTOGRAM:
var v *MetricPoint_HistogramValue
if mp.Value != nil {
v = mp.Value.(*MetricPoint_HistogramValue)
} else {
v = &MetricPoint_HistogramValue{
HistogramValue: &HistogramValue{},
}
}
switch stype {
case "sum", "gsum":
v.HistogramValue.Sum = &HistogramValue_DoubleValue{DoubleValue: value}
case "count", "gcount":
v.HistogramValue.Count = uint64(value)
case "created":
t := time.Unix(0, int64(value*float64(time.Second)))
v.HistogramValue.Created = timestamppb.New(t)
case "bucket":
var boundLabel string
for _, l := range *mlabels {
if l.Name == "le" {
boundLabel = l.Value
break
}
}
var bound float64
if boundLabel == "+Inf" {
bound = math.Inf(1)
} else {
var err error
if bound, err = strconv.ParseFloat(boundLabel, 64); err != nil {
bound = math.NaN()
}
}
v.HistogramValue.Buckets = append(v.HistogramValue.Buckets, &HistogramValue_Bucket{
Count: uint64(value),
UpperBound: bound,
})
}
mp.Value = v
case MetricType_SUMMARY:
var v *MetricPoint_SummaryValue
if mp.Value != nil {
v = mp.Value.(*MetricPoint_SummaryValue)
} else {
v = &MetricPoint_SummaryValue{
SummaryValue: &SummaryValue{},
}
}
switch stype {
case "sum":
v.SummaryValue.Sum = &SummaryValue_DoubleValue{DoubleValue: value}
case "count":
v.SummaryValue.Count = uint64(value)
case "created":
t := time.Unix(0, int64(value*float64(time.Second)))
v.SummaryValue.Created = timestamppb.New(t)
default:
var quantileLabel string
for _, l := range *mlabels {
if l.Name == "quantile" {
quantileLabel = l.Value
break
}
}
var quantile float64
if quantileLabel == "+Inf" {
quantile = math.MaxFloat64
} else {
var err error
if quantile, err = strconv.ParseFloat(quantileLabel, 64); err != nil {
quantile = math.NaN()
}
}
v.SummaryValue.Quantile = append(v.SummaryValue.Quantile, &SummaryValue_Quantile{
Quantile: quantile,
Value: value,
})
}
mp.Value = v
}
}