Skip to content

Commit ce10d25

Browse files
committed
feat: implement gauge and counter support for OpenMetrics 2.0
Signed-off-by: David Ashpole <dashpole@google.com>
1 parent 0dfcdfb commit ce10d25

5 files changed

Lines changed: 437 additions & 1 deletion

File tree

expfmt/encode.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"fmt"
1818
"io"
1919
"net/http"
20+
"strings"
2021

2122
"github.com/munnerz/goautoneg"
2223
dto "github.com/prometheus/client_model/go"
@@ -118,8 +119,10 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format {
118119
if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
119120
return FmtText + escapingScheme
120121
}
121-
if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion_0_0_1 || ver == OpenMetricsVersion_1_0_0 || ver == "") {
122+
if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion_0_0_1 || ver == OpenMetricsVersion_1_0_0 || ver == OpenMetricsVersion_2_0_0 || ver == "") {
122123
switch ver {
124+
case OpenMetricsVersion_2_0_0:
125+
return FmtOpenMetrics_2_0_0 + escapingScheme
123126
case OpenMetricsVersion_1_0_0:
124127
return FmtOpenMetrics_1_0_0 + escapingScheme
125128
default:
@@ -181,6 +184,18 @@ func NewEncoder(w io.Writer, format Format, options ...EncoderOption) Encoder {
181184
close: func() error { return nil },
182185
}
183186
case TypeOpenMetrics:
187+
if strings.Contains(string(format), "version="+OpenMetricsVersion_2_0_0) {
188+
return encoderCloser{
189+
encode: func(v *dto.MetricFamily) error {
190+
_, err := MetricFamilyToOpenMetrics20(w, model.EscapeMetricFamily(v, escapingScheme), options...)
191+
return err
192+
},
193+
close: func() error {
194+
_, err := FinalizeOpenMetrics(w)
195+
return err
196+
},
197+
}
198+
}
184199
return encoderCloser{
185200
encode: func(v *dto.MetricFamily) error {
186201
_, err := MetricFamilyToOpenMetrics(w, model.EscapeMetricFamily(v, escapingScheme), options...)

expfmt/encode_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ func TestNegotiateIncludingOpenMetrics(t *testing.T) {
120120
acceptHeaderValue: "application/openmetrics-text;version=1.0.0",
121121
expectedFmt: "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=values",
122122
},
123+
{
124+
name: "OM format, 2.0.0 version",
125+
acceptHeaderValue: "application/openmetrics-text;version=2.0.0",
126+
expectedFmt: "application/openmetrics-text; version=2.0.0; charset=utf-8; escaping=values",
127+
},
123128
{
124129
name: "OM format, 0.0.1 version with utf-8 is not valid, falls back",
125130
acceptHeaderValue: "application/openmetrics-text;version=0.0.1",
@@ -268,6 +273,15 @@ foo_metric 1.234
268273
expOut: `# TYPE foo_metric unknown
269274
# UNIT foo_metric seconds
270275
foo_metric 1.234
276+
`,
277+
},
278+
// 8: Untyped FmtOpenMetrics_2_0_0
279+
{
280+
metric: metric1,
281+
format: FmtOpenMetrics_2_0_0,
282+
expOut: `# TYPE foo_metric unknown
283+
# UNIT foo_metric seconds
284+
foo_metric 1.234
271285
`,
272286
},
273287
}

expfmt/expfmt.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ const (
4242
OpenMetricsVersion_0_0_1 = "0.0.1"
4343
//nolint:revive // Allow for underscores.
4444
OpenMetricsVersion_1_0_0 = "1.0.0"
45+
//nolint:revive // Allow for underscores.
46+
OpenMetricsVersion_2_0_0 = "2.0.0"
4547

4648
// The Content-Type values for the different wire protocols. Do not do direct
4749
// comparisons to these constants, instead use the comparison functions.
@@ -59,6 +61,8 @@ const (
5961
// Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead.
6062
//nolint:revive // Allow for underscores.
6163
FmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8`
64+
//nolint:revive // Allow for underscores.
65+
FmtOpenMetrics_2_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_2_0_0 + `; charset=utf-8`
6266
// Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead.
6367
//nolint:revive // Allow for underscores.
6468
FmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8`
@@ -114,6 +118,9 @@ func NewOpenMetricsFormat(version string) (Format, error) {
114118
if version == OpenMetricsVersion_1_0_0 {
115119
return FmtOpenMetrics_1_0_0, nil
116120
}
121+
if version == OpenMetricsVersion_2_0_0 {
122+
return FmtOpenMetrics_2_0_0, nil
123+
}
117124
return FmtUnknown, errors.New("unknown open metrics version string")
118125
}
119126

expfmt/openmetrics_2_0_create.go

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
// Copyright The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package expfmt
15+
16+
import (
17+
"bufio"
18+
"errors"
19+
"fmt"
20+
"io"
21+
"math"
22+
"strconv"
23+
24+
dto "github.com/prometheus/client_model/go"
25+
)
26+
27+
// MetricFamilyToOpenMetrics20 converts a MetricFamily proto message into the
28+
// OpenMetrics text format version 2.0.0 and writes the resulting lines to 'out'.
29+
// It returns the number of bytes written and any error encountered.
30+
func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ...EncoderOption) (written int, err error) {
31+
_ = options
32+
name := in.GetName()
33+
if name == "" {
34+
return 0, fmt.Errorf("MetricFamily has no name: %s", in)
35+
}
36+
37+
// Try the interface upgrade. If it doesn't work, we'll use a
38+
// bufio.Writer from the sync.Pool.
39+
w, ok := out.(enhancedWriter)
40+
if !ok {
41+
b := bufPool.Get().(*bufio.Writer)
42+
b.Reset(out)
43+
w = b
44+
defer func() {
45+
bErr := b.Flush()
46+
if err == nil {
47+
err = bErr
48+
}
49+
bufPool.Put(b)
50+
}()
51+
}
52+
53+
var (
54+
n int
55+
metricType = in.GetType()
56+
)
57+
58+
// Comments, first HELP, then TYPE.
59+
if in.Help != nil {
60+
n, err = w.WriteString("# HELP ")
61+
written += n
62+
if err != nil {
63+
return written, err
64+
}
65+
n, err = writeName(w, name)
66+
written += n
67+
if err != nil {
68+
return written, err
69+
}
70+
err = w.WriteByte(' ')
71+
written++
72+
if err != nil {
73+
return written, err
74+
}
75+
n, err = writeEscapedString(w, *in.Help, true)
76+
written += n
77+
if err != nil {
78+
return written, err
79+
}
80+
err = w.WriteByte('\n')
81+
written++
82+
if err != nil {
83+
return written, err
84+
}
85+
}
86+
n, err = w.WriteString("# TYPE ")
87+
written += n
88+
if err != nil {
89+
return written, err
90+
}
91+
n, err = writeName(w, name)
92+
written += n
93+
if err != nil {
94+
return written, err
95+
}
96+
switch metricType {
97+
case dto.MetricType_COUNTER:
98+
n, err = w.WriteString(" counter\n")
99+
case dto.MetricType_GAUGE:
100+
n, err = w.WriteString(" gauge\n")
101+
case dto.MetricType_SUMMARY:
102+
n, err = w.WriteString(" summary\n")
103+
case dto.MetricType_UNTYPED:
104+
n, err = w.WriteString(" unknown\n")
105+
case dto.MetricType_HISTOGRAM:
106+
n, err = w.WriteString(" histogram\n")
107+
case dto.MetricType_GAUGE_HISTOGRAM:
108+
n, err = w.WriteString(" gaugehistogram\n")
109+
default:
110+
return written, fmt.Errorf("unknown metric type %s", metricType.String())
111+
}
112+
written += n
113+
if err != nil {
114+
return written, err
115+
}
116+
if in.Unit != nil {
117+
n, err = w.WriteString("# UNIT ")
118+
written += n
119+
if err != nil {
120+
return written, err
121+
}
122+
n, err = writeName(w, name)
123+
written += n
124+
if err != nil {
125+
return written, err
126+
}
127+
128+
err = w.WriteByte(' ')
129+
written++
130+
if err != nil {
131+
return written, err
132+
}
133+
n, err = writeEscapedString(w, *in.Unit, true)
134+
written += n
135+
if err != nil {
136+
return written, err
137+
}
138+
err = w.WriteByte('\n')
139+
written++
140+
if err != nil {
141+
return written, err
142+
}
143+
}
144+
145+
// Finally the samples, one line for each.
146+
for _, metric := range in.Metric {
147+
switch metricType {
148+
case dto.MetricType_COUNTER:
149+
if metric.Counter == nil {
150+
return written, fmt.Errorf("expected counter in metric %s %s", name, metric)
151+
}
152+
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Counter.GetValue(), 0, false, metric.Counter.Exemplar)
153+
case dto.MetricType_GAUGE:
154+
if metric.Gauge == nil {
155+
return written, fmt.Errorf("expected gauge in metric %s %s", name, metric)
156+
}
157+
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Gauge.GetValue(), 0, false, nil)
158+
case dto.MetricType_UNTYPED:
159+
if metric.Untyped == nil {
160+
return written, fmt.Errorf("expected untyped in metric %s %s", name, metric)
161+
}
162+
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Untyped.GetValue(), 0, false, nil)
163+
case dto.MetricType_SUMMARY:
164+
if metric.Summary == nil {
165+
return written, fmt.Errorf("expected summary in metric %s %s", name, metric)
166+
}
167+
n, err = writeCompositeSummary(w, name, metric)
168+
case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM:
169+
if metric.Histogram == nil {
170+
return written, fmt.Errorf("expected histogram in metric %s %s", name, metric)
171+
}
172+
n, err = writeCompositeHistogram(w, name, metric, metricType == dto.MetricType_GAUGE_HISTOGRAM)
173+
default:
174+
return written, fmt.Errorf("unexpected type in metric %s %s", name, metric)
175+
}
176+
written += n
177+
if err != nil {
178+
return written, err
179+
}
180+
}
181+
return written, nil
182+
}
183+
184+
// writeOpenMetrics20Sample writes a single sample for simple types (Counter, Gauge, Untyped).
185+
func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, floatValue float64, intValue uint64, useIntValue bool, exemplar *dto.Exemplar) (int, error) {
186+
written := 0
187+
n, err := writeOpenMetricsNameAndLabelPairs(w, name, metric.Label, "", 0)
188+
written += n
189+
if err != nil {
190+
return written, err
191+
}
192+
err = w.WriteByte(' ')
193+
written++
194+
if err != nil {
195+
return written, err
196+
}
197+
198+
if useIntValue {
199+
n, err = writeUint(w, intValue)
200+
} else {
201+
n, err = writeFloat(w, floatValue)
202+
}
203+
written += n
204+
if err != nil {
205+
return written, err
206+
}
207+
208+
if metric.TimestampMs != nil {
209+
err = w.WriteByte(' ')
210+
written++
211+
if err != nil {
212+
return written, err
213+
}
214+
n, err = writeOpenMetrics20Timestamp(w, float64(*metric.TimestampMs)/1000)
215+
written += n
216+
if err != nil {
217+
return written, err
218+
}
219+
}
220+
221+
// Start Timestamp for Counter
222+
if metric.Counter != nil && metric.Counter.CreatedTimestamp != nil {
223+
n, err = w.WriteString(" st@")
224+
written += n
225+
if err != nil {
226+
return written, err
227+
}
228+
ts := metric.Counter.CreatedTimestamp
229+
n, err = writeOpenMetrics20Timestamp(w, float64(ts.GetSeconds())+float64(ts.GetNanos())/1e9)
230+
written += n
231+
if err != nil {
232+
return written, err
233+
}
234+
}
235+
236+
if exemplar != nil && len(exemplar.Label) > 0 {
237+
n, err = writeExemplar(w, exemplar)
238+
written += n
239+
if err != nil {
240+
return written, err
241+
}
242+
}
243+
244+
err = w.WriteByte('\n')
245+
written++
246+
if err != nil {
247+
return written, err
248+
}
249+
return written, nil
250+
}
251+
252+
// writeOpenMetrics20Timestamp writes a float64 as a timestamp without scientific notation.
253+
func writeOpenMetrics20Timestamp(w enhancedWriter, f float64) (int, error) {
254+
switch {
255+
case math.IsNaN(f):
256+
return w.WriteString("NaN")
257+
case math.IsInf(f, +1):
258+
return w.WriteString("+Inf")
259+
case math.IsInf(f, -1):
260+
return w.WriteString("-Inf")
261+
default:
262+
bp := numBufPool.Get().(*[]byte)
263+
*bp = strconv.AppendFloat((*bp)[:0], f, 'f', -1, 64)
264+
written, err := w.Write(*bp)
265+
numBufPool.Put(bp)
266+
return written, err
267+
}
268+
}
269+
270+
// Stubs for Summary and Histogram
271+
272+
func writeCompositeSummary(w enhancedWriter, name string, metric *dto.Metric) (int, error) {
273+
_ = w
274+
_ = name
275+
_ = metric
276+
return 0, errors.New("summary not implemented yet")
277+
}
278+
279+
func writeCompositeHistogram(w enhancedWriter, name string, metric *dto.Metric, isGauge bool) (int, error) {
280+
_ = w
281+
_ = name
282+
_ = metric
283+
_ = isGauge
284+
return 0, errors.New("histogram not implemented yet")
285+
}

0 commit comments

Comments
 (0)