394 lines
10 KiB
Go
394 lines
10 KiB
Go
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
|
|
}
|
|
}
|