Skip to content

Commit d7df860

Browse files
Upgrade prometheus/common to v0.67.5 and use NewTextParser
In v0.67.5 the zero-value TextParser panics because the ValidationScheme enum was reordered: UnsetValidation=0 (new), LegacyValidation=1, UTF8Validation=2. The TextParser.scheme field defaults to zero which now maps to UnsetValidation and panics in IsValidMetricName. Use expfmt.NewTextParser(model.LegacyValidation) to explicitly initialize the parser with the correct validation scheme. Add comprehensive test coverage for all prometheus metric types (counter, gauge, histogram, summary), multiple metric families, malformed input, empty output, NaN handling, and special character labels. Signed-off-by: Sarvodaya Kumar <sarvodaya.kumar.ctr@sumologic.com>
1 parent 93cff7a commit d7df860

5 files changed

Lines changed: 225 additions & 9 deletions

File tree

agent/check_handler_internal_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,49 @@ func TestExtractMetricsPanicRecovery(t *testing.T) {
606606
})
607607
}
608608

609+
func TestExtractMetricsPrometheus(t *testing.T) {
610+
event := &corev2.Event{
611+
Check: &corev2.Check{
612+
ObjectMeta: corev2.ObjectMeta{
613+
Name: "disk-usage",
614+
Namespace: "default",
615+
},
616+
Output: "# HELP node_filesystem_avail_bytes Filesystem space available.\n# TYPE node_filesystem_avail_bytes gauge\nnode_filesystem_avail_bytes{device=\"/dev/sda1\",mountpoint=\"/\"} 5.3687091e+10\n",
617+
OutputMetricFormat: corev2.PrometheusOutputMetricFormat,
618+
},
619+
}
620+
621+
metrics := extractMetrics(event)
622+
require.NotNil(t, metrics)
623+
assert.Equal(t, 1, len(metrics))
624+
assert.Equal(t, "node_filesystem_avail_bytes", metrics[0].Name)
625+
assert.Equal(t, 5.3687091e+10, metrics[0].Value)
626+
}
627+
628+
func TestExtractMetricsPrometheusMultiFamily(t *testing.T) {
629+
event := &corev2.Event{
630+
Check: &corev2.Check{
631+
ObjectMeta: corev2.ObjectMeta{
632+
Name: "node-metrics",
633+
Namespace: "default",
634+
},
635+
Output: "# HELP node_cpu_seconds_total Seconds the CPUs spent in each mode.\n# TYPE node_cpu_seconds_total counter\nnode_cpu_seconds_total{cpu=\"0\",mode=\"idle\"} 123456.78\nnode_cpu_seconds_total{cpu=\"0\",mode=\"system\"} 4567.89\n# HELP node_load1 1m load average.\n# TYPE node_load1 gauge\nnode_load1 2.34\n",
636+
OutputMetricFormat: corev2.PrometheusOutputMetricFormat,
637+
},
638+
}
639+
640+
metrics := extractMetrics(event)
641+
require.NotNil(t, metrics)
642+
assert.Equal(t, 3, len(metrics))
643+
644+
nameSet := make(map[string]bool)
645+
for _, m := range metrics {
646+
nameSet[m.Name] = true
647+
}
648+
assert.True(t, nameSet["node_cpu_seconds_total"])
649+
assert.True(t, nameSet["node_load1"])
650+
}
651+
609652
func TestFailOnAssetCheckWithDisabledAssets(t *testing.T) {
610653
config, cleanup := FixtureConfig()
611654
defer cleanup()

agent/transformers/prometheus.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func ParseProm(event *types.Event) PromList {
6565
}
6666

6767
t := strings.NewReader(event.Check.Output)
68-
var parser expfmt.TextParser
68+
parser := expfmt.NewTextParser(model.LegacyValidation)
6969
metricFamilies, err := parser.TextToMetricFamilies(t)
7070

7171
if err != nil {

agent/transformers/prometheus_test.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,178 @@ func TestParsePromValidationScheme(t *testing.T) {
228228
})
229229
}
230230

231+
func TestParsePromCounter(t *testing.T) {
232+
assert := assert.New(t)
233+
234+
event := types.FixtureEvent("test", "test")
235+
event.Check.Output = "# HELP http_requests_total The total number of HTTP requests.\n# TYPE http_requests_total counter\nhttp_requests_total{method=\"post\",code=\"200\"} 1027\nhttp_requests_total{method=\"post\",code=\"400\"} 3\n"
236+
237+
prom := ParseProm(event)
238+
assert.NotEmpty(prom)
239+
points := prom.Transform()
240+
assert.Equal(2, len(points))
241+
assert.Equal("http_requests_total", points[0].Name)
242+
assert.Equal("http_requests_total", points[1].Name)
243+
}
244+
245+
func TestParsePromGauge(t *testing.T) {
246+
assert := assert.New(t)
247+
248+
event := types.FixtureEvent("test", "test")
249+
event.Check.Output = "# HELP node_memory_MemAvailable_bytes Memory information field MemAvailable_bytes.\n# TYPE node_memory_MemAvailable_bytes gauge\nnode_memory_MemAvailable_bytes 5.361664e+09\n"
250+
251+
prom := ParseProm(event)
252+
assert.NotEmpty(prom)
253+
points := prom.Transform()
254+
assert.Equal(1, len(points))
255+
assert.Equal("node_memory_MemAvailable_bytes", points[0].Name)
256+
assert.Equal(5.361664e+09, points[0].Value)
257+
}
258+
259+
func TestParsePromHistogram(t *testing.T) {
260+
assert := assert.New(t)
261+
262+
event := types.FixtureEvent("test", "test")
263+
event.Check.Output = "# HELP http_request_duration_seconds A histogram of the request duration.\n# TYPE http_request_duration_seconds histogram\nhttp_request_duration_seconds_bucket{le=\"0.05\"} 24054\nhttp_request_duration_seconds_bucket{le=\"0.1\"} 33444\nhttp_request_duration_seconds_bucket{le=\"0.2\"} 100392\nhttp_request_duration_seconds_bucket{le=\"+Inf\"} 144320\nhttp_request_duration_seconds_sum 53423\nhttp_request_duration_seconds_count 144320\n"
264+
265+
prom := ParseProm(event)
266+
assert.NotEmpty(prom)
267+
points := prom.Transform()
268+
assert.True(len(points) >= 6)
269+
270+
nameSet := make(map[string]bool)
271+
for _, p := range points {
272+
nameSet[p.Name] = true
273+
}
274+
assert.True(nameSet["http_request_duration_seconds_bucket"])
275+
assert.True(nameSet["http_request_duration_seconds_sum"])
276+
assert.True(nameSet["http_request_duration_seconds_count"])
277+
}
278+
279+
func TestParsePromSummary(t *testing.T) {
280+
assert := assert.New(t)
281+
282+
event := types.FixtureEvent("test", "test")
283+
event.Check.Output = "# HELP go_gc_duration_seconds A summary of pause duration of garbage collection cycles.\n# TYPE go_gc_duration_seconds summary\ngo_gc_duration_seconds{quantile=\"0\"} 3.3722e-05\ngo_gc_duration_seconds{quantile=\"0.25\"} 5.0129e-05\ngo_gc_duration_seconds{quantile=\"0.5\"} 0.000126547\ngo_gc_duration_seconds{quantile=\"1\"} 0.009688629\ngo_gc_duration_seconds_sum 17.391350544\ngo_gc_duration_seconds_count 12345\n"
284+
285+
prom := ParseProm(event)
286+
assert.NotEmpty(prom)
287+
points := prom.Transform()
288+
assert.True(len(points) >= 6)
289+
290+
nameSet := make(map[string]bool)
291+
for _, p := range points {
292+
nameSet[p.Name] = true
293+
}
294+
assert.True(nameSet["go_gc_duration_seconds"])
295+
assert.True(nameSet["go_gc_duration_seconds_sum"])
296+
assert.True(nameSet["go_gc_duration_seconds_count"])
297+
}
298+
299+
func TestParsePromMultipleMetricFamilies(t *testing.T) {
300+
assert := assert.New(t)
301+
302+
event := types.FixtureEvent("test", "test")
303+
event.Check.Output = "# HELP node_filesystem_avail_bytes Filesystem space available.\n# TYPE node_filesystem_avail_bytes gauge\nnode_filesystem_avail_bytes{device=\"/dev/sda1\",mountpoint=\"/\"} 5.3687091e+10\n# HELP node_filesystem_size_bytes Filesystem size in bytes.\n# TYPE node_filesystem_size_bytes gauge\nnode_filesystem_size_bytes{device=\"/dev/sda1\",mountpoint=\"/\"} 1.03079215104e+11\n# HELP node_cpu_seconds_total Seconds the CPUs spent in each mode.\n# TYPE node_cpu_seconds_total counter\nnode_cpu_seconds_total{cpu=\"0\",mode=\"idle\"} 123456.78\nnode_cpu_seconds_total{cpu=\"0\",mode=\"system\"} 4567.89\n"
304+
305+
prom := ParseProm(event)
306+
assert.NotEmpty(prom)
307+
points := prom.Transform()
308+
assert.Equal(4, len(points))
309+
310+
nameSet := make(map[string]bool)
311+
for _, p := range points {
312+
nameSet[p.Name] = true
313+
}
314+
assert.True(nameSet["node_filesystem_avail_bytes"])
315+
assert.True(nameSet["node_filesystem_size_bytes"])
316+
assert.True(nameSet["node_cpu_seconds_total"])
317+
}
318+
319+
func TestParsePromMalformedInput(t *testing.T) {
320+
assert := assert.New(t)
321+
322+
testCases := []struct {
323+
name string
324+
output string
325+
}{
326+
{"completely invalid", "this is not prometheus text at all"},
327+
{"partial metric line", "http_requests_total{method=\"get\"}\n"},
328+
{"missing value", "# TYPE foo counter\nfoo\n"},
329+
{"truncated output", "# HELP foo A help message.\n# TYPE foo counter\n"},
330+
}
331+
332+
for _, tc := range testCases {
333+
t.Run(tc.name, func(t *testing.T) {
334+
event := types.FixtureEvent("test", "test")
335+
event.Check.Output = tc.output
336+
337+
assert.NotPanics(func() {
338+
_ = ParseProm(event)
339+
})
340+
})
341+
}
342+
}
343+
344+
func TestParsePromEmptyOutput(t *testing.T) {
345+
assert := assert.New(t)
346+
347+
event := types.FixtureEvent("test", "test")
348+
event.Check.Output = ""
349+
350+
prom := ParseProm(event)
351+
assert.Empty(prom)
352+
points := prom.Transform()
353+
assert.Nil(points)
354+
}
355+
356+
func TestParsePromNaN(t *testing.T) {
357+
assert := assert.New(t)
358+
359+
nanList := PromList{
360+
&model.Sample{
361+
Metric: model.Metric{
362+
model.MetricNameLabel: "stale_metric",
363+
},
364+
Value: model.SampleValue(math.NaN()),
365+
Timestamp: model.TimeFromUnix(time.Now().Unix()),
366+
},
367+
&model.Sample{
368+
Metric: model.Metric{
369+
model.MetricNameLabel: "valid_metric",
370+
},
371+
Value: 42.0,
372+
Timestamp: model.TimeFromUnix(time.Now().Unix()),
373+
},
374+
}
375+
376+
points := nanList.Transform()
377+
assert.Equal(1, len(points))
378+
assert.Equal("valid_metric", points[0].Name)
379+
assert.Equal(42.0, points[0].Value)
380+
}
381+
382+
func TestParsePromSpecialCharLabels(t *testing.T) {
383+
assert := assert.New(t)
384+
385+
event := types.FixtureEvent("test", "test")
386+
event.Check.Output = "# TYPE http_requests_total counter\nhttp_requests_total{method=\"GET\",path=\"/api/v1/users\",status_code=\"200\"} 1500\n"
387+
388+
prom := ParseProm(event)
389+
assert.NotEmpty(prom)
390+
points := prom.Transform()
391+
assert.Equal(1, len(points))
392+
assert.Equal("http_requests_total", points[0].Name)
393+
394+
tagMap := make(map[string]string)
395+
for _, tag := range points[0].Tags {
396+
tagMap[tag.Name] = tag.Value
397+
}
398+
assert.Equal("GET", tagMap["method"])
399+
assert.Equal("/api/v1/users", tagMap["path"])
400+
assert.Equal("200", tagMap["status_code"])
401+
}
402+
231403
func TestTransformProm(t *testing.T) {
232404
assert := assert.New(t)
233405
ts := time.Now().Unix()

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ require (
5151
github.com/mitchellh/mapstructure v1.5.0
5252
github.com/olekukonko/tablewriter v0.0.5
5353
github.com/prometheus/client_golang v1.20.5
54-
github.com/prometheus/client_model v0.6.1
55-
github.com/prometheus/common v0.62.0
54+
github.com/prometheus/client_model v0.6.2
55+
github.com/prometheus/common v0.67.5
5656
github.com/robertkrimen/otto v0.2.1
5757
github.com/robfig/cron/v3 v3.0.1
5858
github.com/sensu/lasr v1.2.1
@@ -117,7 +117,7 @@ require (
117117
github.com/go-logr/stdr v1.2.2 // indirect
118118
github.com/go-ole/go-ole v1.2.6 // indirect
119119
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
120-
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
120+
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
121121
github.com/google/go-cmp v0.7.0 // indirect
122122
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
123123
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect

go.sum

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,8 +1514,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
15141514
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
15151515
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
15161516
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
1517-
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
1518-
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
1517+
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
1518+
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
15191519
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
15201520
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
15211521
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
@@ -1806,10 +1806,11 @@ github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3d
18061806
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
18071807
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
18081808
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
1809-
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
18101809
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
1811-
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
1812-
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
1810+
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
1811+
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
1812+
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
1813+
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
18131814
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
18141815
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
18151816
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=

0 commit comments

Comments
 (0)