Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Introduce FlatMetricFamily struct to flatten prometheus metric output #1538

Merged
merged 11 commits into from
Oct 22, 2024
13 changes: 7 additions & 6 deletions test/e2e/logs_self_monitor_healthy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,18 @@ var _ = Describe(suite.ID(), Label(suite.LabelSelfMonitoringLogsHealthy), Ordere
// Pushing metrics to the metric gateway triggers an alert.
// It makes the self-monitor call the webhook, which in turn increases the counter.
assert.ManagerEmitsMetric(proxyClient,
Equal("controller_runtime_webhook_requests_total"),
HaveName(Equal("controller_runtime_webhook_requests_total")),
SatisfyAll(
WithLabels(HaveKeyWithValue("webhook", "/api/v2/alerts")),
WithValue(BeNumerically(">", 0)),
HaveLabels(HaveKeyWithValue("webhook", "/api/v2/alerts")),
HaveMetricValue(BeNumerically(">", 0)),
))
})

It("Ensures that telemetry_self_monitor_prober_requests_total is emitted", func() {
assert.ManagerEmitsMetric(proxyClient,
Equal("telemetry_self_monitor_prober_requests_total"),
WithValue(BeNumerically(">", 0)),
assert.ManagerEmitsMetric(
proxyClient,
HaveName(Equal("telemetry_self_monitor_prober_requests_total")),
HaveMetricValue(BeNumerically(">", 0)),
)
})
})
Expand Down
10 changes: 5 additions & 5 deletions test/e2e/metrics_self_monitor_healthy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,17 @@ var _ = Describe(suite.ID(), Label(suite.LabelSelfMonitoringMetricsHealthy), Ord
// Pushing metrics to the metric gateway triggers an alert.
// It makes the self-monitor call the webhook, which in turn increases the counter.
assert.ManagerEmitsMetric(proxyClient,
Equal("controller_runtime_webhook_requests_total"),
HaveName(Equal("controller_runtime_webhook_requests_total")),
SatisfyAll(
WithLabels(HaveKeyWithValue("webhook", "/api/v2/alerts")),
WithValue(BeNumerically(">", 0)),
HaveLabels(HaveKeyWithValue("webhook", "/api/v2/alerts")),
HaveMetricValue(BeNumerically(">", 0)),
))
})

It("Ensures that telemetry_self_monitor_prober_requests_total is emitted", func() {
assert.ManagerEmitsMetric(proxyClient,
Equal("telemetry_self_monitor_prober_requests_total"),
WithValue(BeNumerically(">", 0)),
HaveName(Equal("telemetry_self_monitor_prober_requests_total")),
HaveMetricValue(BeNumerically(">", 0)),
)
})
})
Expand Down
10 changes: 5 additions & 5 deletions test/e2e/traces_self_monitor_healthy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,17 @@ var _ = Describe(suite.ID(), Label(suite.LabelSelfMonitoringTracesHealthy), Orde
// Pushing metrics to the metric gateway triggers an alert.
// It makes the self-monitor call the webhook, which in turn increases the counter.
assert.ManagerEmitsMetric(proxyClient,
Equal("controller_runtime_webhook_requests_total"),
HaveName(Equal("controller_runtime_webhook_requests_total")),
SatisfyAll(
WithLabels(HaveKeyWithValue("webhook", "/api/v2/alerts")),
WithValue(BeNumerically(">", 0)),
HaveLabels(HaveKeyWithValue("webhook", "/api/v2/alerts")),
HaveMetricValue(BeNumerically(">", 0)),
))
})

It("Ensures that telemetry_self_monitor_prober_requests_total is emitted", func() {
assert.ManagerEmitsMetric(proxyClient,
Equal("telemetry_self_monitor_prober_requests_total"),
WithValue(BeNumerically(">", 0)),
HaveName(Equal("telemetry_self_monitor_prober_requests_total")),
HaveMetricValue(BeNumerically(">", 0)),
)
})
})
Expand Down
10 changes: 3 additions & 7 deletions test/testkit/assert/monitoring.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func EmitsOTelCollectorMetrics(proxyClient *apiserverproxy.Client, metricsURL st
resp, err := proxyClient.Get(metricsURL)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(resp).To(HaveHTTPStatus(http.StatusOK))
g.Expect(resp).To(HaveHTTPBody(ContainMetricFamily(WithName(ContainSubstring("otelcol")))))
g.Expect(resp).To(HaveHTTPBody(HaveFlatMetricFamilies(ContainElement(HaveName(ContainSubstring("otelcol"))))))

err = resp.Body.Close()
g.Expect(err).NotTo(HaveOccurred())
Expand All @@ -27,8 +27,7 @@ func EmitsOTelCollectorMetrics(proxyClient *apiserverproxy.Client, metricsURL st

func ManagerEmitsMetric(
proxyClient *apiserverproxy.Client,
nameMatcher types.GomegaMatcher,
metricMatcher types.GomegaMatcher) {
matchers ...types.GomegaMatcher) {
Eventually(func(g Gomega) {
telemetryManagerMetricsURL := proxyClient.ProxyURLForService(
kitkyma.TelemetryManagerMetricsServiceName.Namespace,
Expand All @@ -39,10 +38,7 @@ func ManagerEmitsMetric(
g.Expect(err).NotTo(HaveOccurred())
g.Expect(resp).To(HaveHTTPStatus(http.StatusOK))

g.Expect(resp).To(HaveHTTPBody(ContainMetricFamily(SatisfyAll(
WithName(nameMatcher),
ContainMetric(metricMatcher),
))))
g.Expect(resp).To(HaveHTTPBody(HaveFlatMetricFamilies(ContainElement(SatisfyAll(matchers...)))))

err = resp.Body.Close()
g.Expect(err).NotTo(HaveOccurred())
Expand Down
62 changes: 13 additions & 49 deletions test/testkit/matchers/prometheus/prometheus_matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,70 +5,34 @@ import (

"github.com/onsi/gomega"
"github.com/onsi/gomega/types"
prommodel "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
)

func WithMetricFamilies(matcher types.GomegaMatcher) types.GomegaMatcher {
return gomega.WithTransform(func(responseBody []byte) ([]*prommodel.MetricFamily, error) {
func HaveFlatMetricFamilies(matcher types.GomegaMatcher) types.GomegaMatcher {
return gomega.WithTransform(func(responseBody []byte) ([]FlatMetricFamily, error) {
var parser expfmt.TextParser
mfs, _ := parser.TextToMetricFamilies(bytes.NewReader(responseBody)) //nolint:errcheck // ignore duplicate metrics parsing error and try extract metric

var result []*prommodel.MetricFamily
fmfs := flattenAllMetricFamily(mfs)

for _, mf := range mfs {
result = append(result, mf)
}

return result, nil
}, matcher)
}

func ContainMetricFamily(matcher types.GomegaMatcher) types.GomegaMatcher {
return WithMetricFamilies(gomega.ContainElement(matcher))
}

func WithName(matcher types.GomegaMatcher) types.GomegaMatcher {
return gomega.WithTransform(func(mf *prommodel.MetricFamily) string {
return mf.GetName()
return fmfs, nil
}, matcher)
}

func WithMetrics(matcher types.GomegaMatcher) types.GomegaMatcher {
return gomega.WithTransform(func(mf *prommodel.MetricFamily) []*prommodel.Metric {
return mf.GetMetric()
func HaveName(matcher types.GomegaMatcher) types.GomegaMatcher {
return gomega.WithTransform(func(fmf FlatMetricFamily) string {
return fmf.Name
}, matcher)
}

func ContainMetric(matcher types.GomegaMatcher) types.GomegaMatcher {
return WithMetrics(gomega.ContainElement(matcher))
}

func WithValue(matcher types.GomegaMatcher) types.GomegaMatcher {
return gomega.WithTransform(func(m *prommodel.Metric) (float64, error) {
if m.Gauge != nil {
return m.Gauge.GetValue(), nil
}

if m.Counter != nil {
return m.Counter.GetValue(), nil
}

if m.Untyped != nil {
return m.Untyped.GetValue(), nil
}

return 0, nil
func HaveMetricValue(matcher types.GomegaMatcher) types.GomegaMatcher {
return gomega.WithTransform(func(fmf FlatMetricFamily) float64 {
return fmf.MetricValues
}, matcher)
}

func WithLabels(matcher types.GomegaMatcher) types.GomegaMatcher {
return gomega.WithTransform(func(m *prommodel.Metric) (map[string]string, error) {
labels := make(map[string]string)
for _, l := range m.Label {
labels[l.GetName()] = l.GetValue()
}

return labels, nil
func HaveLabels(matcher types.GomegaMatcher) types.GomegaMatcher {
return gomega.WithTransform(func(fmf FlatMetricFamily) map[string]string {
return fmf.Labels
}, matcher)
}
40 changes: 19 additions & 21 deletions test/testkit/matchers/prometheus/prometheus_matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,46 @@ import (
. "github.com/onsi/gomega"
)

var _ = Describe("ContainMetric", Label("metrics"), func() {
var _ = Describe("HaveFlatMetricFamilies", Label("metrics"), func() {
Context("with nil input", func() {
It("should fail", func() {
success, err := ContainMetricFamily(WithName(Equal("foo_metric"))).Match(nil)
It("should error", func() {
success, err := HaveFlatMetricFamilies(ContainElement(HaveName(Equal("foo_metric")))).Match(nil)
Expect(err).Should(HaveOccurred())
Expect(success).Should(BeFalse())
})
})

Context("with empty input", func() {
It("should fail", func() {
success, err := ContainMetricFamily(WithName(Equal("foo_metric"))).Match([]byte{})
success, err := HaveFlatMetricFamilies(ContainElement(HaveName(Equal("foo_metric")))).Match([]byte{})
Expect(err).ShouldNot(HaveOccurred())
Expect(success).Should(BeFalse())
})
})

Context("with invalid input", func() {
It("should fail", func() {
success, err := ContainMetricFamily(WithName(Equal("foo_metric"))).Match([]byte{1, 2, 3})
success, err := HaveFlatMetricFamilies(ContainElement(HaveName(Equal("foo_metric")))).Match([]byte{1, 2, 3})
Expect(err).ShouldNot(HaveOccurred())
Expect(success).Should(BeFalse())
})
})

Context("with having metrics", func() {
It("should succeed", func() {
Context("with HaveName", func() {
It("should apply matcher", func() {
fileBytes := `
# HELP fluentbit_uptime Number of seconds that Fluent Bit has been running.
# TYPE fluentbit_uptime counter
fluentbit_uptime{hostname="telemetry-fluent-bit-dglkf"} 5489
# HELP fluentbit_input_bytes_total Number of input bytes.
# TYPE fluentbit_input_bytes_total counter
fluentbit_input_bytes_total{name="tele-tail"} 5217998`
Expect([]byte(fileBytes)).Should(ContainMetricFamily(WithName(Equal("fluentbit_uptime"))))
Expect([]byte(fileBytes)).Should(HaveFlatMetricFamilies(ContainElement(HaveName(Equal("fluentbit_uptime")))))
})
})
})

var _ = Describe("WithLabels", func() {
var _ = Describe("with HaveLabels", func() {
It("should apply matcher", func() {
fileBytes := `
# HELP fluentbit_uptime Number of seconds that Fluent Bit has been running.
Expand All @@ -54,14 +54,14 @@ fluentbit_uptime{hostname="telemetry-fluent-bit-dglkf"} 5489
# TYPE fluentbit_input_bytes_total counter
fluentbit_input_bytes_total{name="tele-tail"} 5000
`
Expect([]byte(fileBytes)).Should(ContainMetricFamily(SatisfyAll(
WithName(Equal("fluentbit_input_bytes_total")),
ContainMetric(WithLabels(HaveKeyWithValue("name", "tele-tail"))),
)))
Expect([]byte(fileBytes)).Should(HaveFlatMetricFamilies(ContainElement(SatisfyAll(
HaveName(Equal("fluentbit_input_bytes_total")),
HaveLabels(HaveKeyWithValue("name", "tele-tail")),
))))
})
})

var _ = Describe("WithValue", func() {
var _ = Describe("with HaveValue", func() {
It("should apply matcher", func() {
fileBytes := `
# HELP fluentbit_uptime Number of seconds that Fluent Bit has been running.
Expand All @@ -71,12 +71,10 @@ fluentbit_uptime{hostname="telemetry-fluent-bit-dglkf"} 5489
# TYPE fluentbit_input_bytes_total counter
fluentbit_input_bytes_total{name="tele-tail"} 5000
`
Expect([]byte(fileBytes)).Should(ContainMetricFamily(SatisfyAll(
WithName(Equal("fluentbit_input_bytes_total")),
ContainMetric(SatisfyAll(
WithLabels(HaveKeyWithValue("name", "tele-tail")),
WithValue(BeNumerically(">=", 0)),
)),
)))
Expect([]byte(fileBytes)).Should(HaveFlatMetricFamilies(ContainElement(SatisfyAll(
HaveName(Equal("fluentbit_input_bytes_total")),
HaveLabels(HaveKeyWithValue("name", "tele-tail")),
HaveMetricValue(BeNumerically(">=", 0)),
))))
})
})
68 changes: 68 additions & 0 deletions test/testkit/matchers/prometheus/prommetric_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package prometheus

import prommodel "github.com/prometheus/client_model/go"

// FlatMetricFamily holds all necessary information about a prometheus MetricFamily.
// Gomega doesn't handle deeply nested data structure very well and generates large, unreadable diffs when paired with
// the deeply nested structure of pmetrics.
jeffreylimnardy marked this conversation as resolved.
Show resolved Hide resolved
//
// Introducing a go struct with a flat data structure by extracting necessary information from different levels of
// metricfamily makes accessing the information easier and improves readability of the output.
jeffreylimnardy marked this conversation as resolved.
Show resolved Hide resolved
type FlatMetricFamily struct {
skhalash marked this conversation as resolved.
Show resolved Hide resolved
Name string
MetricValues float64
jeffreylimnardy marked this conversation as resolved.
Show resolved Hide resolved
Labels map[string]string
}

// flattenAllMetricFamily flattens an array of prometheus MetricFamily to a slice of FlatMetricFamily.
// It converts the deeply nested MetricFamily to a flat struct, making it more readable in the test output.
func flattenAllMetricFamily(mfs map[string]*prommodel.MetricFamily) []FlatMetricFamily {
jeffreylimnardy marked this conversation as resolved.
Show resolved Hide resolved
var fmf []FlatMetricFamily
for _, mf := range mfs {
fmf = append(fmf, flattenMetricFamily(mf)...)
}

return fmf
}

// flattenMetricFamily converts a single MetricFamily into a slice of FlatMetricFamily
// It loops through all the metrics in a MetricFamily and appends it to the FlatMetricFamily slice.
func flattenMetricFamily(mf *prommodel.MetricFamily) []FlatMetricFamily {
var fmf []FlatMetricFamily

for _, m := range mf.Metric {
v := getValuePerMetric(m)
fmf = append(fmf, FlatMetricFamily{
Name: mf.GetName(),
MetricValues: v,
Labels: labelsToMap(m.GetLabel()),
})
}

return fmf
}

func labelsToMap(l []*prommodel.LabelPair) map[string]string {
labels := make(map[string]string)
for _, l := range l {
labels[l.GetName()] = l.GetValue()
}

return labels
}

func getValuePerMetric(m *prommodel.Metric) float64 {
if m.Gauge != nil {
return m.Gauge.GetValue()
}

if m.Counter != nil {
return m.Counter.GetValue()
}

if m.Untyped != nil {
return m.Untyped.GetValue()
}

return 0
}
Loading