Skip to content

Commit c8a254b

Browse files
Add OpenMetrics parser
Picking up from #710 to add an OpenMetrics parser to work towards support for OpenMetrics in promtool. Contributes to prometheus/prometheus#8932. Co-Authored-By: Yi <38248129+jyz0309@users.noreply.github.com> Signed-off-by: martincostello <martin@martincostello.com>
1 parent ce9215c commit c8a254b

5 files changed

Lines changed: 2794 additions & 11 deletions

File tree

expfmt/decode.go

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ func ResponseFormat(h http.Header) Format {
6565
return FmtUnknown
6666
}
6767
return FmtText
68+
case OpenMetricsType:
69+
if c, ok := params["charset"]; ok && c != "utf-8" {
70+
return FmtUnknown
71+
}
72+
switch params["version"] {
73+
case "", OpenMetricsVersion_0_0_1:
74+
return FmtOpenMetrics_0_0_1
75+
case OpenMetricsVersion_1_0_0:
76+
return FmtOpenMetrics_1_0_0
77+
default:
78+
return FmtUnknown
79+
}
6880
}
6981

7082
return FmtUnknown
@@ -74,12 +86,11 @@ func ResponseFormat(h http.Header) Format {
7486
// names are validated based on the provided Format -- if the format requires
7587
// escaping, raditional Prometheues validity checking is used. Otherwise, names
7688
// are checked for UTF-8 validity. Supported formats include delimited protobuf
77-
// and Prometheus text format. For historical reasons, this decoder fallbacks
78-
// to classic text decoding for any other format. This decoder does not fully
79-
// support OpenMetrics although it may often succeed due to the similarities
80-
// between the formats. This decoder may not support the latest features of
81-
// Prometheus text format and is not intended for high-performance applications.
82-
// See: https://github.com/prometheus/common/issues/812
89+
// and the Prometheus/OpenMetrics text formats. For historical reasons, this
90+
// decoder fallbacks to classic text decoding for any other format. This decoder
91+
// may not support the latest features of the text formats and is not intended
92+
// for high-performance applications. See:
93+
// https://github.com/prometheus/common/issues/812
8394
func NewDecoder(r io.Reader, format Format) Decoder {
8495
scheme := model.LegacyValidation
8596
if format.ToEscapingScheme() == model.NoEscaping {
@@ -88,6 +99,8 @@ func NewDecoder(r io.Reader, format Format) Decoder {
8899
switch format.FormatType() {
89100
case TypeProtoDelim:
90101
return &protoDecoder{r: bufio.NewReader(r), s: scheme}
102+
case TypeOpenMetrics:
103+
return &openMetricsDecoder{r: r}
91104
case TypeProtoText, TypeProtoCompact:
92105
return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)}
93106
}
@@ -139,6 +152,37 @@ func (d *errDecoder) Decode(*dto.MetricFamily) error {
139152
return d.err
140153
}
141154

155+
// openMetricsDecoder implements the Decoder interface for the OpenMetrics text protocol.
156+
type openMetricsDecoder struct {
157+
r io.Reader
158+
fams map[string]*dto.MetricFamily
159+
err error
160+
}
161+
162+
// Decode implements the Decoder interface.
163+
func (d *openMetricsDecoder) Decode(mf *dto.MetricFamily) error {
164+
if d.err == nil {
165+
// Read all metrics in one shot.
166+
var p OpenMetricsParser
167+
d.fams, d.err = p.OpenMetricsToMetricFamilies(d.r)
168+
// If we don't get an error, store io.EOF for the end.
169+
if d.err == nil {
170+
d.err = io.EOF
171+
}
172+
}
173+
// Pick off one MetricFamily per Decode until there's nothing left.
174+
for key, fam := range d.fams {
175+
mf.Name = fam.Name
176+
mf.Help = fam.Help
177+
mf.Type = fam.Type
178+
mf.Unit = fam.Unit
179+
mf.Metric = fam.Metric
180+
delete(d.fams, key)
181+
return nil
182+
}
183+
return d.err
184+
}
185+
142186
// textDecoder implements the Decoder interface for the text protocol.
143187
type textDecoder struct {
144188
r io.Reader

expfmt/decode_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,74 @@ mf2 4
103103
require.Truef(t, reflect.DeepEqual(all, out), "output does not match")
104104
}
105105

106+
func TestOpenMetricsDecoder(t *testing.T) {
107+
var (
108+
ts = model.Now()
109+
in = `
110+
# Only a quite simple scenario with two metric families.
111+
# More complicated tests of the parser itself can be found in the OpenMetrics parser tests.
112+
# TYPE metric1 counter
113+
metric1_total 3
114+
mf1{label="value1"} -3.14 123456
115+
mf1{label="value2"} 42
116+
metric1_total 4
117+
# EOF
118+
`
119+
out = model.Vector{
120+
&model.Sample{
121+
Metric: model.Metric{
122+
model.MetricNameLabel: "mf1",
123+
"label": "value1",
124+
},
125+
Value: -3.14,
126+
Timestamp: 123456,
127+
},
128+
&model.Sample{
129+
Metric: model.Metric{
130+
model.MetricNameLabel: "mf1",
131+
"label": "value2",
132+
},
133+
Value: 42,
134+
Timestamp: ts,
135+
},
136+
&model.Sample{
137+
Metric: model.Metric{
138+
model.MetricNameLabel: "metric1",
139+
},
140+
Value: 3,
141+
Timestamp: ts,
142+
},
143+
&model.Sample{
144+
Metric: model.Metric{
145+
model.MetricNameLabel: "metric1",
146+
},
147+
Value: 4,
148+
Timestamp: ts,
149+
},
150+
}
151+
)
152+
153+
dec := &SampleDecoder{
154+
Dec: NewDecoder(strings.NewReader(in), FmtOpenMetrics_1_0_0),
155+
Opts: &DecodeOptions{
156+
Timestamp: ts,
157+
},
158+
}
159+
var all model.Vector
160+
for {
161+
var smpls model.Vector
162+
err := dec.Decode(&smpls)
163+
if err != nil && errors.Is(err, io.EOF) {
164+
break
165+
}
166+
require.NoError(t, err)
167+
all = append(all, smpls...)
168+
}
169+
sort.Sort(all)
170+
sort.Sort(out)
171+
require.Truef(t, reflect.DeepEqual(all, out), "output does not match")
172+
}
173+
106174
func TestProtoDecoder(t *testing.T) {
107175
testTime := model.Now()
108176

@@ -454,6 +522,22 @@ func testDiscriminatorHTTPHeader(t testing.TB) {
454522
input: map[string]string{"Content-Type": `text/plain; version=0.0.3`},
455523
output: FmtUnknown,
456524
},
525+
{
526+
input: map[string]string{"Content-Type": `application/openmetrics-text; version=1.0.0; charset=utf-8`},
527+
output: FmtOpenMetrics_1_0_0,
528+
},
529+
{
530+
input: map[string]string{"Content-Type": `application/openmetrics-text; version=0.0.1; charset=utf-8`},
531+
output: FmtOpenMetrics_0_0_1,
532+
},
533+
{
534+
input: map[string]string{"Content-Type": `application/openmetrics-text; charset=utf-8`},
535+
output: FmtOpenMetrics_0_0_1,
536+
},
537+
{
538+
input: map[string]string{"Content-Type": `application/openmetrics-text; version=1.0.0; charset=latin-1`},
539+
output: FmtUnknown,
540+
},
457541
}
458542

459543
for i, scenario := range scenarios {

0 commit comments

Comments
 (0)