metricbeat/helper/prometheus/textparse.go (631 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package prometheus
import (
"errors"
"io"
"math"
"mime"
"net/http"
"strconv"
"strings"
"time"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/textparse"
"github.com/prometheus/prometheus/model/timestamp"
"github.com/elastic/elastic-agent-libs/logp"
)
const (
// The Content-Type values for the different wire protocols
hdrContentType = "Content-Type"
TextVersion = "0.0.4"
OpenMetricsType = `application/openmetrics-text`
ContentTypeTextFormat string = `text/plain; version=` + TextVersion + `; charset=utf-8`
)
type Gauge struct {
Value *float64
}
func (m *Gauge) GetValue() float64 {
if m != nil && m.Value != nil {
return *m.Value
}
return 0
}
type Info struct {
Value *int64
}
func (m *Info) GetValue() int64 {
if m != nil && m.Value != nil {
return *m.Value
}
return 0
}
func (m *Info) HasValidValue() bool {
return m != nil && *m.Value == 1
}
type Stateset struct {
Value *int64
}
func (m *Stateset) GetValue() int64 {
if m != nil && m.Value != nil {
return *m.Value
}
return 0
}
func (m *Stateset) HasValidValue() bool {
return m != nil && (*m.Value == 0 || *m.Value == 1)
}
type Counter struct {
Value *float64
}
func (m *Counter) GetValue() float64 {
if m != nil && m.Value != nil {
return *m.Value
}
return 0
}
type Quantile struct {
Quantile *float64
Value *float64
Exemplar *exemplar.Exemplar
}
func (m *Quantile) GetQuantile() float64 {
if m != nil && m.Quantile != nil {
return *m.Quantile
}
return 0
}
func (m *Quantile) GetValue() float64 {
if m != nil && m.Value != nil {
return *m.Value
}
return 0
}
type Summary struct {
SampleCount *uint64
SampleSum *float64
Quantile []*Quantile
}
func (m *Summary) GetSampleCount() uint64 {
if m != nil && m.SampleCount != nil {
return *m.SampleCount
}
return 0
}
func (m *Summary) GetSampleSum() float64 {
if m != nil && m.SampleSum != nil {
return *m.SampleSum
}
return 0
}
func (m *Summary) GetQuantile() []*Quantile {
if m != nil {
return m.Quantile
}
return nil
}
type Unknown struct {
Value *float64
}
func (m *Unknown) GetValue() float64 {
if m != nil && m.Value != nil {
return *m.Value
}
return 0
}
type Bucket struct {
CumulativeCount *uint64
UpperBound *float64
Exemplar *exemplar.Exemplar
}
func (m *Bucket) GetCumulativeCount() uint64 {
if m != nil && m.CumulativeCount != nil {
return *m.CumulativeCount
}
return 0
}
func (m *Bucket) GetUpperBound() float64 {
if m != nil && m.UpperBound != nil {
return *m.UpperBound
}
return 0
}
type Histogram struct {
SampleCount *uint64
SampleSum *float64
Bucket []*Bucket
IsGaugeHistogram bool
}
func (m *Histogram) GetSampleCount() uint64 {
if m != nil && m.SampleCount != nil {
return *m.SampleCount
}
return 0
}
func (m *Histogram) GetSampleSum() float64 {
if m != nil && m.SampleSum != nil {
return *m.SampleSum
}
return 0
}
func (m *Histogram) GetBucket() []*Bucket {
if m != nil {
return m.Bucket
}
return nil
}
type OpenMetric struct {
Label []*labels.Label
Exemplar *exemplar.Exemplar
Name *string
Gauge *Gauge
Counter *Counter
Info *Info
Stateset *Stateset
Summary *Summary
Unknown *Unknown
Histogram *Histogram
TimestampMs *int64
}
func (m *OpenMetric) GetName() *string {
if m != nil {
return m.Name
}
return nil
}
func (m *OpenMetric) GetLabel() []*labels.Label {
if m != nil {
return m.Label
}
return nil
}
func (m *OpenMetric) GetGauge() *Gauge {
if m != nil {
return m.Gauge
}
return nil
}
func (m *OpenMetric) GetCounter() *Counter {
if m != nil {
return m.Counter
}
return nil
}
func (m *OpenMetric) GetInfo() *Info {
if m != nil {
return m.Info
}
return nil
}
func (m *OpenMetric) GetStateset() *Stateset {
if m != nil {
return m.Stateset
}
return nil
}
func (m *OpenMetric) GetSummary() *Summary {
if m != nil {
return m.Summary
}
return nil
}
func (m *OpenMetric) GetUnknown() *Unknown {
if m != nil {
return m.Unknown
}
return nil
}
func (m *OpenMetric) GetHistogram() *Histogram {
if m != nil && m.Histogram != nil && !m.Histogram.IsGaugeHistogram {
return m.Histogram
}
return nil
}
func (m *OpenMetric) GetGaugeHistogram() *Histogram {
if m != nil && m.Histogram != nil && m.Histogram.IsGaugeHistogram {
return m.Histogram
}
return nil
}
func (m *OpenMetric) GetTimestampMs() int64 {
if m != nil && m.TimestampMs != nil {
return *m.TimestampMs
}
return 0
}
type MetricFamily struct {
Name *string
Help *string
Type model.MetricType
Unit *string
Metric []*OpenMetric
}
func (m *MetricFamily) GetName() string {
if m != nil && m.Name != nil {
return *m.Name
}
return ""
}
func (m *MetricFamily) GetUnit() string {
if m != nil && *m.Unit != "" {
return *m.Unit
}
return ""
}
func (m *MetricFamily) GetMetric() []*OpenMetric {
if m != nil {
return m.Metric
}
return nil
}
const (
suffixTotal = "_total"
suffixGCount = "_gcount"
suffixGSum = "_gsum"
suffixCount = "_count"
suffixSum = "_sum"
suffixBucket = "_bucket"
suffixCreated = "_created"
suffixInfo = "_info"
)
// Counters have _total suffix
func isTotal(name string) bool {
return strings.HasSuffix(name, suffixTotal)
}
func isCreated(name string) bool {
return strings.HasSuffix(name, suffixCreated)
}
func isGCount(name string) bool {
return strings.HasSuffix(name, suffixGCount)
}
func isGSum(name string) bool {
return strings.HasSuffix(name, suffixGSum)
}
func isCount(name string) bool {
return strings.HasSuffix(name, suffixCount)
}
func isSum(name string) bool {
return strings.HasSuffix(name, suffixSum)
}
func isBucket(name string) bool {
return strings.HasSuffix(name, suffixBucket)
}
func isInfo(name string) bool {
return strings.HasSuffix(name, suffixInfo)
}
func summaryMetricName(name string, s float64, qv string, lbls string, summariesByName map[string]map[string]*OpenMetric) (string, *OpenMetric) {
var summary = &Summary{}
var quantile = []*Quantile{}
var quant = &Quantile{}
switch {
case isCount(name):
u := uint64(s)
summary.SampleCount = &u
name = strings.TrimSuffix(name, suffixCount)
case isSum(name):
summary.SampleSum = &s
name = strings.TrimSuffix(name, suffixSum)
default:
f, err := strconv.ParseFloat(qv, 64)
if err != nil {
f = -1
}
quant.Quantile = &f
quant.Value = &s
}
_, ok := summariesByName[name]
if !ok {
summariesByName[name] = make(map[string]*OpenMetric)
}
metric, ok := summariesByName[name][lbls]
if !ok {
metric = &OpenMetric{}
metric.Name = &name
metric.Summary = summary
metric.Summary.Quantile = quantile
summariesByName[name][lbls] = metric
}
if metric.Summary.SampleSum == nil && summary.SampleSum != nil {
metric.Summary.SampleSum = summary.SampleSum
} else if metric.Summary.SampleCount == nil && summary.SampleCount != nil {
metric.Summary.SampleCount = summary.SampleCount
} else if quant.Quantile != nil {
metric.Summary.Quantile = append(metric.Summary.Quantile, quant)
}
return name, metric
}
/*
histogramMetricName returns the metric name without the suffix and its histogram.
OpenMetric suffixes: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#suffixes.
Prometheus histogram suffixes: https://prometheus.io/docs/concepts/metric_types/#histogram.
OpenMetric includes the extra suffix _created, that falls under default in this function.
OpenMetric also includes _g* suffixes that are not present for Prometheus metrics and are taken care of separately in
this function.
*/
func histogramMetricName(name string, s float64, qv string, lbls string, t *int64, isGaugeHistogram bool, e *exemplar.Exemplar, histogramsByName map[string]map[string]*OpenMetric) (string, *OpenMetric) {
var histogram = &Histogram{}
var bucket = []*Bucket{}
var bkt = &Bucket{}
switch {
case isCount(name):
u := uint64(s)
histogram.SampleCount = &u
name = strings.TrimSuffix(name, suffixCount)
case isSum(name):
histogram.SampleSum = &s
name = strings.TrimSuffix(name, suffixSum)
case isGaugeHistogram && isGCount(name):
u := uint64(s)
histogram.SampleCount = &u
name = strings.TrimSuffix(name, suffixGCount)
case isGaugeHistogram && isGSum(name):
histogram.SampleSum = &s
name = strings.TrimSuffix(name, suffixGSum)
case isBucket(name):
f, err := strconv.ParseFloat(qv, 64)
if err != nil {
f = math.MaxUint64
}
cnt := uint64(s)
bkt.UpperBound = &f
bkt.CumulativeCount = &cnt
if e != nil {
if !e.HasTs {
e.Ts = *t
}
bkt.Exemplar = e
}
name = strings.TrimSuffix(name, suffixBucket)
default:
return "", nil
}
_, k := histogramsByName[name]
if !k {
histogramsByName[name] = make(map[string]*OpenMetric)
}
metric, ok := histogramsByName[name][lbls]
if !ok {
metric = &OpenMetric{}
metric.Name = &name
metric.Histogram = histogram
metric.Histogram.Bucket = bucket
histogramsByName[name][lbls] = metric
}
if metric.Histogram.SampleSum == nil && histogram.SampleSum != nil {
metric.Histogram.SampleSum = histogram.SampleSum
} else if metric.Histogram.SampleCount == nil && histogram.SampleCount != nil {
metric.Histogram.SampleCount = histogram.SampleCount
} else if bkt.UpperBound != nil {
metric.Histogram.Bucket = append(metric.Histogram.Bucket, bkt)
}
return name, metric
}
func ParseMetricFamilies(b []byte, contentType string, ts time.Time, logger *logp.Logger) ([]*MetricFamily, error) {
parser, err := textparse.New(b, contentType, ContentTypeTextFormat, false, false, labels.NewSymbolTable()) // Fallback protocol set to ContentTypeTextFormat
if err != nil {
return nil, err
}
var (
defTime = timestamp.FromTime(ts)
metricFamiliesByName = map[string]*MetricFamily{}
summariesByName = map[string]map[string]*OpenMetric{}
histogramsByName = map[string]map[string]*OpenMetric{}
fam *MetricFamily
// metricTypes stores the metric type for each metric name.
metricTypes = make(map[string]model.MetricType)
)
for {
var (
et textparse.Entry
ok bool
e exemplar.Exemplar
)
if et, err = parser.Next(); err != nil {
if strings.HasPrefix(err.Error(), "invalid metric type") {
logger.Debugf("Ignored invalid metric type : %v ", err)
// NOTE: ignore any errors that are not EOF. This is to avoid breaking the parsing.
// if acceptHeader in the prometheus client is `Accept: text/plain; version=0.0.4` (like it is now)
// any `info` metrics are not supported, and then there will be ignored here.
// if acceptHeader in the prometheus client `Accept: application/openmetrics-text; version=0.0.1`
// any `info` metrics are supported, and then there will be parsed here.
continue
}
if errors.Is(err, io.EOF) {
break
}
if strings.HasPrefix(err.Error(), "data does not end with # EOF") {
break
}
logger.Debugf("Error while parsing metrics: %v ", err)
break
}
switch et {
case textparse.EntryType:
buf, t := parser.Type()
s := string(buf)
fam, ok = metricFamiliesByName[s]
if !ok {
fam = &MetricFamily{Name: &s, Type: t}
metricFamiliesByName[s] = fam
} else {
fam.Type = t
}
// Store the metric type for each base metric name.
metricTypes[s] = t
continue
case textparse.EntryHelp:
buf, t := parser.Help()
s := string(buf)
h := string(t)
_, ok = metricFamiliesByName[s]
if !ok {
fam = &MetricFamily{Name: &s, Help: &h}
metricFamiliesByName[s] = fam
} else {
fam.Help = &h
}
continue
case textparse.EntryUnit:
buf, t := parser.Unit()
s := string(buf)
u := string(t)
_, ok = metricFamiliesByName[s]
if !ok {
fam = &MetricFamily{Name: &s, Unit: &u}
metricFamiliesByName[string(buf)] = fam
} else {
fam.Unit = &u
}
continue
case textparse.EntryComment:
continue
default:
}
t := defTime
_, tp, v := parser.Series()
var (
lset labels.Labels
mets string
)
mets = parser.Metric(&lset)
if !lset.Has(labels.MetricName) {
// missing metric name from labels.MetricName, skip.
break
}
var lbls strings.Builder
lbls.Grow(len(mets))
var labelPairs = []*labels.Label{}
var qv string // value of le or quantile label
for _, l := range lset.Copy() {
if l.Name == labels.MetricName {
continue
}
switch l.Name {
case model.QuantileLabel:
qv = lset.Get(model.QuantileLabel)
case labels.BucketLabel:
qv = lset.Get(labels.BucketLabel)
default:
lbls.WriteString(l.Name)
lbls.WriteString(l.Value)
}
n := l.Name
v := l.Value
labelPairs = append(labelPairs, &labels.Label{
Name: n,
Value: v,
})
}
var metric *OpenMetric
metricName := lset.Get(labels.MetricName)
// lookupMetricName will have the suffixes removed
lookupMetricName := metricName
var exm *exemplar.Exemplar
mt, ok := metricTypes[metricName]
if !ok {
// Splitting is necessary to find the base metric name type in the metricTypes map.
// This allows us to group related metrics together under the same base metric name.
// For example, the metric family `summary_metric` can have the metrics
// `summary_metric_count` and `summary_metric_sum`, all having the same metric type.
parts := strings.Split(metricName, "_")
baseMetricNamekey := strings.Join(parts[:len(parts)-1], "_")
// If the metric type is not found, default to unknown
if metricTypeFound, ok := metricTypes[baseMetricNamekey]; ok {
mt = metricTypeFound
} else {
mt = model.MetricTypeUnknown
}
}
// Suffixes - https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#suffixes
switch mt {
case model.MetricTypeCounter:
if contentType == OpenMetricsType && !isTotal(metricName) && !isCreated(metricName) {
// Possible suffixes for counter in Open metrics are _created and _total.
// Otherwise, ignore.
// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#counter-1
continue
}
var counter = &Counter{Value: &v}
mn := lset.Get(labels.MetricName)
metric = &OpenMetric{Name: &mn, Counter: counter, Label: labelPairs}
if contentType == OpenMetricsType {
// Remove the two possible suffixes, _created and _total
if isTotal(metricName) {
lookupMetricName = strings.TrimSuffix(metricName, suffixTotal)
} else {
lookupMetricName = strings.TrimSuffix(metricName, suffixCreated)
}
} else {
lookupMetricName = metricName
}
case model.MetricTypeGauge:
var gauge = &Gauge{Value: &v}
metric = &OpenMetric{Name: &metricName, Gauge: gauge, Label: labelPairs}
//lookupMetricName = metricName
case model.MetricTypeInfo:
// Info only exists for Openmetrics. It must have the suffix _info
if !isInfo(metricName) {
continue
}
lookupMetricName = strings.TrimSuffix(metricName, suffixInfo)
value := int64(v)
var info = &Info{Value: &value}
metric = &OpenMetric{Name: &metricName, Info: info, Label: labelPairs}
case model.MetricTypeSummary:
lookupMetricName, metric = summaryMetricName(metricName, v, qv, lbls.String(), summariesByName)
metric.Label = labelPairs
if !isSum(metricName) {
// Avoid registering the metric multiple times.
continue
}
case model.MetricTypeHistogram:
if hasExemplar := parser.Exemplar(&e); hasExemplar {
exm = &e
}
lookupMetricName, metric = histogramMetricName(metricName, v, qv, lbls.String(), &t, false, exm, histogramsByName)
if metric == nil {
continue
}
metric.Label = labelPairs
if !isSum(metricName) {
// Avoid registering the metric multiple times.
continue
}
case model.MetricTypeGaugeHistogram:
if hasExemplar := parser.Exemplar(&e); hasExemplar {
exm = &e
}
lookupMetricName, metric = histogramMetricName(metricName, v, qv, lbls.String(), &t, true, exm, histogramsByName)
if metric == nil { // metric name does not have a suffix supported for the type gauge histogram
continue
}
metric.Label = labelPairs
metric.Histogram.IsGaugeHistogram = true
if !isGSum(metricName) {
// Avoid registering the metric multiple times.
continue
}
case model.MetricTypeStateset:
value := int64(v)
var stateset = &Stateset{Value: &value}
metric = &OpenMetric{Name: &metricName, Stateset: stateset, Label: labelPairs}
case model.MetricTypeUnknown:
var unknown = &Unknown{Value: &v}
metric = &OpenMetric{Name: &metricName, Unknown: unknown, Label: labelPairs}
default:
}
fam, ok = metricFamiliesByName[lookupMetricName]
if !ok {
// If the lookupMetricName is not in metricFamiliesByName, we check for metric name, in case
// the removed suffix is part of the name.
fam, ok = metricFamiliesByName[metricName]
if !ok {
// There is not any metadata for this metric. In this case, the name of the metric
// will remain metricName instead of the possible modified lookupMetricName
fam = &MetricFamily{Name: &metricName, Type: mt}
metricFamiliesByName[metricName] = fam
}
}
if hasExemplar := parser.Exemplar(&e); hasExemplar && mt != model.MetricTypeHistogram && metric != nil {
if !e.HasTs {
e.Ts = t
}
metric.Exemplar = &e
}
if tp != nil && metric != nil {
t = *tp
metric.TimestampMs = &t
}
fam.Metric = append(fam.Metric, metric)
}
families := make([]*MetricFamily, 0, len(metricFamiliesByName))
for _, v := range metricFamiliesByName {
if v.Metric != nil {
families = append(families, v)
}
}
return families, nil
}
func GetContentType(h http.Header) string {
ct := h.Get(hdrContentType)
mediatype, params, err := mime.ParseMediaType(ct)
if err != nil {
return ""
}
const textType = "text/plain"
switch mediatype {
case OpenMetricsType:
if e, ok := params["encoding"]; ok && e != "delimited" {
return ""
}
return OpenMetricsType
case textType:
if v, ok := params["version"]; ok && v != TextVersion {
return ""
}
return ContentTypeTextFormat
}
return ""
}