Skip to content

Commit 50b5d21

Browse files
committed
expfmt: prevent st@ leaking to Gauge and Untyped samples in OpenMetrics 2.0
1 parent 5042671 commit 50b5d21

2 files changed

Lines changed: 106 additions & 28 deletions

File tree

expfmt/openmetrics_2_0_create.go

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,17 @@ func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ..
170170
if val < 0 {
171171
return written, fmt.Errorf("counter value cannot be negative (%g) in metric %s", val, name)
172172
}
173-
n, err = writeOpenMetrics20Sample(w, name, metric, val, 0, false, metric.Counter.Exemplar)
173+
n, err = writeOpenMetrics20Sample(w, name, metric, val, 0, false, metric.Counter.CreatedTimestamp, metric.Counter.Exemplar)
174174
case dto.MetricType_GAUGE:
175175
if metric.Gauge == nil {
176176
return written, fmt.Errorf("expected gauge in metric %s %s", name, metric)
177177
}
178-
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Gauge.GetValue(), 0, false, nil)
178+
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Gauge.GetValue(), 0, false, nil, nil)
179179
case dto.MetricType_UNTYPED:
180180
if metric.Untyped == nil {
181181
return written, fmt.Errorf("expected untyped in metric %s %s", name, metric)
182182
}
183-
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Untyped.GetValue(), 0, false, nil)
183+
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Untyped.GetValue(), 0, false, nil, nil)
184184
case dto.MetricType_SUMMARY:
185185
if metric.Summary == nil {
186186
return written, fmt.Errorf("expected summary in metric %s %s", name, metric)
@@ -203,7 +203,7 @@ func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ..
203203
}
204204

205205
// writeOpenMetrics20Sample writes a single sample for simple types (Counter, Gauge, Untyped).
206-
func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, floatValue float64, intValue uint64, useIntValue bool, exemplar *dto.Exemplar) (int, error) {
206+
func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, floatValue float64, intValue uint64, useIntValue bool, startTimestamp *timestamppb.Timestamp, exemplar *dto.Exemplar) (int, error) {
207207
if err := validateLabels20(metric.Label); err != nil {
208208
return 0, err
209209
}
@@ -242,18 +242,17 @@ func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric,
242242
}
243243
}
244244

245-
// Start Timestamp for Counter
246-
if metric.Counter != nil && metric.Counter.CreatedTimestamp != nil {
247-
ts := metric.Counter.CreatedTimestamp
248-
if err := ts.CheckValid(); err != nil {
245+
// Start Timestamp
246+
if startTimestamp != nil {
247+
if err := startTimestamp.CheckValid(); err != nil {
249248
return written, fmt.Errorf("invalid created timestamp in metric %s: %w", name, err)
250249
}
251250
n, err = w.WriteString(" st@")
252251
written += n
253252
if err != nil {
254253
return written, err
255254
}
256-
n, err = writeProtoTimestamp(w, ts)
255+
n, err = writeProtoTimestamp(w, startTimestamp)
257256
written += n
258257
if err != nil {
259258
return written, err
@@ -330,20 +329,11 @@ func writeExemplar20(w enhancedWriter, e *dto.Exemplar) (int, error) {
330329

331330
// writeOpenMetrics20Timestamp writes a float64 as a timestamp without scientific notation.
332331
func writeOpenMetrics20Timestamp(w enhancedWriter, f float64) (int, error) {
333-
switch {
334-
case math.IsNaN(f):
335-
return w.WriteString("NaN")
336-
case math.IsInf(f, +1):
337-
return w.WriteString("+Inf")
338-
case math.IsInf(f, -1):
339-
return w.WriteString("-Inf")
340-
default:
341-
bp := numBufPool.Get().(*[]byte)
342-
*bp = strconv.AppendFloat((*bp)[:0], f, 'f', -1, 64)
343-
written, err := w.Write(*bp)
344-
numBufPool.Put(bp)
345-
return written, err
346-
}
332+
bp := numBufPool.Get().(*[]byte)
333+
*bp = strconv.AppendFloat((*bp)[:0], f, 'f', -1, 64)
334+
written, err := w.Write(*bp)
335+
numBufPool.Put(bp)
336+
return written, err
347337
}
348338

349339
// Stubs for Summary and Histogram

expfmt/openmetrics_2_0_create_test.go

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,48 @@ test_metric 1.23
263263
},
264264
out: `# TYPE http_requests_total counter
265265
http_requests_total 1027
266+
`,
267+
},
268+
{
269+
name: "GaugeWithAccidentalCounterCreatedTimestamp",
270+
in: &dto.MetricFamily{
271+
Name: proto.String("node_memory_active_bytes"),
272+
Help: proto.String("Active memory in bytes."),
273+
Type: dto.MetricType_GAUGE.Enum(),
274+
Metric: []*dto.Metric{
275+
{
276+
Gauge: &dto.Gauge{
277+
Value: proto.Float64(1.2345e+09),
278+
},
279+
Counter: &dto.Counter{
280+
CreatedTimestamp: &timestamppb.Timestamp{Seconds: 1234567890},
281+
},
282+
},
283+
},
284+
},
285+
out: `# HELP node_memory_active_bytes Active memory in bytes.
286+
# TYPE node_memory_active_bytes gauge
287+
node_memory_active_bytes 1.2345e+09
288+
`,
289+
},
290+
{
291+
name: "UntypedWithAccidentalCounterCreatedTimestamp",
292+
in: &dto.MetricFamily{
293+
Name: proto.String("test_metric"),
294+
Type: dto.MetricType_UNTYPED.Enum(),
295+
Metric: []*dto.Metric{
296+
{
297+
Untyped: &dto.Untyped{
298+
Value: proto.Float64(1.23),
299+
},
300+
Counter: &dto.Counter{
301+
CreatedTimestamp: &timestamppb.Timestamp{Seconds: 1234567890},
302+
},
303+
},
304+
},
305+
},
306+
out: `# TYPE test_metric unknown
307+
test_metric 1.23
266308
`,
267309
},
268310
{
@@ -304,15 +346,15 @@ http_requests_total 1027
304346
}
305347
}
306348

307-
func TestWriteOpenMetrics20Timestamp_SpecialValues(t *testing.T) {
349+
func TestWriteOpenMetrics20Timestamp(t *testing.T) {
308350
tests := []struct {
309351
name string
310352
val float64
311353
out string
312354
}{
313-
{"NaN", math.NaN(), "NaN"},
314-
{"+Inf", math.Inf(+1), "+Inf"},
315-
{"-Inf", math.Inf(-1), "-Inf"},
355+
{"Integer", 1234567890, "1234567890"},
356+
{"Subsecond", 1234567890.123, "1234567890.123"},
357+
{"Zero", 0, "0"},
316358
}
317359

318360
for _, tc := range tests {
@@ -576,7 +618,7 @@ func TestWriteOpenMetrics20Sample_UseIntValue(t *testing.T) {
576618
var buf bytes.Buffer
577619
w := enhancedWriter(&buf)
578620
metric := &dto.Metric{}
579-
n, err := writeOpenMetrics20Sample(w, "test_metric", metric, 0, 123, true, nil)
621+
n, err := writeOpenMetrics20Sample(w, "test_metric", metric, 0, 123, true, nil, nil)
580622
if err != nil {
581623
t.Fatal(err)
582624
}
@@ -589,6 +631,52 @@ func TestWriteOpenMetrics20Sample_UseIntValue(t *testing.T) {
589631
}
590632
}
591633

634+
func TestWriteProtoTimestamp(t *testing.T) {
635+
tests := []struct {
636+
name string
637+
ts *timestamppb.Timestamp
638+
out string
639+
}{
640+
{
641+
name: "WholeSecondsPositive",
642+
ts: &timestamppb.Timestamp{Seconds: 1234567890},
643+
out: "1234567890",
644+
},
645+
{
646+
name: "SubsecondPositive",
647+
ts: &timestamppb.Timestamp{Seconds: 1234567890, Nanos: 500000000},
648+
out: "1234567890.5",
649+
},
650+
{
651+
name: "SubsecondPositiveFullPrecision",
652+
ts: &timestamppb.Timestamp{Seconds: 1234567890, Nanos: 987654321},
653+
out: "1234567890.987654321",
654+
},
655+
{
656+
name: "ZeroSecondsSubsecond",
657+
ts: &timestamppb.Timestamp{Seconds: 0, Nanos: 500000000},
658+
out: "0.5",
659+
},
660+
}
661+
662+
for _, tc := range tests {
663+
t.Run(tc.name, func(t *testing.T) {
664+
var buf bytes.Buffer
665+
w := enhancedWriter(&buf)
666+
n, err := writeProtoTimestamp(w, tc.ts)
667+
if err != nil {
668+
t.Fatal(err)
669+
}
670+
if buf.String() != tc.out {
671+
t.Errorf("expected %q, got %q", tc.out, buf.String())
672+
}
673+
if n != len(tc.out) {
674+
t.Errorf("expected %d bytes written, got %d", len(tc.out), n)
675+
}
676+
})
677+
}
678+
}
679+
592680
func TestCreateOpenMetrics20_SimpleWriter(t *testing.T) {
593681
in := &dto.MetricFamily{
594682
Name: proto.String("http_requests_total"),

0 commit comments

Comments
 (0)