Skip to content

Commit d8527b2

Browse files
committed
Add more precise matching to the metric end-to-end tests.
The metric tests scrape metrics in the Prometheus text format and have been mainly asserting based on the presence or absence of substrings within the entire response body. Metrics are now parsed into a slice of structs, and there is a new Gomega matcher that can reference metric family, labels, and value (for untyped metrics, gauges, and counters).
1 parent 07ca9ad commit d8527b2

File tree

4 files changed

+221
-56
lines changed

4 files changed

+221
-56
lines changed

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ require (
2727
github.com/otiai10/copy v1.0.2
2828
github.com/pkg/errors v0.9.1
2929
github.com/prometheus/client_golang v1.2.1
30+
github.com/prometheus/client_model v0.2.0
31+
github.com/prometheus/common v0.4.1
3032
github.com/sirupsen/logrus v1.4.2
3133
github.com/spf13/cobra v1.0.0
3234
github.com/spf13/pflag v1.0.5

test/e2e/like_metric_matcher_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package e2e
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/onsi/gomega/format"
7+
"github.com/onsi/gomega/types"
8+
)
9+
10+
type Metric struct {
11+
Family string
12+
Labels map[string][]string
13+
Value float64 // Zero unless type is Untypted, Gauge, or Counter!
14+
}
15+
16+
type MetricPredicate struct {
17+
f func(m Metric) bool
18+
name string
19+
}
20+
21+
func (mp MetricPredicate) String() string {
22+
return mp.name
23+
}
24+
25+
func WithFamily(f string) MetricPredicate {
26+
return MetricPredicate{
27+
name: fmt.Sprintf("WithFamily(%s)", f),
28+
f: func(m Metric) bool {
29+
return m.Family == f
30+
},
31+
}
32+
}
33+
34+
func WithLabel(n, v string) MetricPredicate {
35+
return MetricPredicate{
36+
name: fmt.Sprintf("WithLabel(%s=%s)", n, v),
37+
f: func(m Metric) bool {
38+
for name, values := range m.Labels {
39+
for _, value := range values {
40+
if name == n && value == v {
41+
return true
42+
}
43+
}
44+
}
45+
return false
46+
},
47+
}
48+
}
49+
50+
func WithValue(v float64) MetricPredicate {
51+
return MetricPredicate{
52+
name: fmt.Sprintf("WithValue(%g)", v),
53+
f: func(m Metric) bool {
54+
return m.Value == v
55+
},
56+
}
57+
}
58+
59+
type LikeMetricMatcher struct {
60+
Predicates []MetricPredicate
61+
}
62+
63+
func (matcher *LikeMetricMatcher) Match(actual interface{}) (bool, error) {
64+
metric, ok := actual.(Metric)
65+
if !ok {
66+
return false, fmt.Errorf("LikeMetric matcher expects Metric (got %T)", actual)
67+
}
68+
for _, predicate := range matcher.Predicates {
69+
if !predicate.f(metric) {
70+
return false, nil
71+
}
72+
}
73+
return true, nil
74+
}
75+
76+
func (matcher *LikeMetricMatcher) FailureMessage(actual interface{}) string {
77+
return format.Message(actual, "to satisfy", matcher.Predicates)
78+
}
79+
80+
func (matcher *LikeMetricMatcher) NegatedFailureMessage(actual interface{}) string {
81+
return format.Message(actual, "not to satisfy", matcher.Predicates)
82+
}
83+
84+
func LikeMetric(preds ...MetricPredicate) types.GomegaMatcher {
85+
return &LikeMetricMatcher{
86+
Predicates: preds,
87+
}
88+
}

test/e2e/metrics_e2e_test.go

+131-51
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,22 @@
33
package e2e
44

55
import (
6+
"bytes"
67
"context"
7-
"fmt"
88
"regexp"
9-
"time"
9+
"strings"
1010

1111
. "github.com/onsi/ginkgo"
1212
. "github.com/onsi/gomega"
13+
io_prometheus_client "github.com/prometheus/client_model/go"
14+
"github.com/prometheus/common/expfmt"
1315
corev1 "k8s.io/api/core/v1"
1416
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1517
"k8s.io/apimachinery/pkg/util/net"
1618

1719
"github.com/operator-framework/api/pkg/operators/v1alpha1"
1820
"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
1921
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
20-
"github.com/operator-framework/operator-lifecycle-manager/pkg/metrics"
2122
"github.com/operator-framework/operator-lifecycle-manager/test/e2e/ctx"
2223
)
2324

@@ -71,14 +72,19 @@ var _ = Describe("Metrics are generated for OLM managed resources", func() {
7172

7273
It("generates csv_abnormal metric for OLM pod", func() {
7374

74-
// Verify metrics have been emitted for packageserver csv
7575
Expect(getMetricsFromPod(c, getPodWithLabel(c, "app=olm-operator"), "8081")).To(And(
76-
ContainSubstring("csv_abnormal"),
77-
ContainSubstring(fmt.Sprintf("name=\"%s\"", failingCSV.Name)),
78-
ContainSubstring("phase=\"Failed\""),
79-
ContainSubstring("reason=\"UnsupportedOperatorGroup\""),
80-
ContainSubstring("version=\"0.0.0\""),
81-
ContainSubstring("csv_succeeded"),
76+
ContainElement(LikeMetric(
77+
WithFamily("csv_abnormal"),
78+
WithLabel("name", failingCSV.Name),
79+
WithLabel("phase", "Failed"),
80+
WithLabel("reason", "UnsupportedOperatorGroup"),
81+
WithLabel("version", "0.0.0"),
82+
)),
83+
ContainElement(LikeMetric(
84+
WithFamily("csv_succeeded"),
85+
WithValue(0),
86+
WithLabel("name", failingCSV.Name),
87+
)),
8288
))
8389

8490
cleanupCSV()
@@ -95,8 +101,8 @@ var _ = Describe("Metrics are generated for OLM managed resources", func() {
95101
It("deletes its associated CSV metrics", func() {
96102
// Verify that when the csv has been deleted, it deletes the corresponding CSV metrics
97103
Expect(getMetricsFromPod(c, getPodWithLabel(c, "app=olm-operator"), "8081")).ToNot(And(
98-
ContainSubstring("csv_abnormal{name=\"%s\"", failingCSV.Name),
99-
ContainSubstring("csv_succeeded{name=\"%s\"", failingCSV.Name),
104+
ContainElement(LikeMetric(WithFamily("csv_abnormal"), WithLabel("name", failingCSV.Name))),
105+
ContainElement(LikeMetric(WithFamily("csv_succeeded"), WithLabel("name", failingCSV.Name))),
100106
))
101107
})
102108
})
@@ -108,68 +114,98 @@ var _ = Describe("Metrics are generated for OLM managed resources", func() {
108114
subscriptionCleanup cleanupFunc
109115
subscription *v1alpha1.Subscription
110116
)
111-
When("A subscription object is created", func() {
112117

118+
When("A subscription object is created", func() {
113119
BeforeEach(func() {
114120
subscriptionCleanup, _ = createSubscription(GinkgoT(), crc, testNamespace, "metric-subscription-for-create", testPackageName, stableChannel, v1alpha1.ApprovalManual)
115121
})
116122

123+
AfterEach(func() {
124+
if subscriptionCleanup != nil {
125+
subscriptionCleanup()
126+
}
127+
})
128+
117129
It("generates subscription_sync_total metric", func() {
118130

119131
// Verify metrics have been emitted for subscription
120-
Eventually(func() string {
132+
Eventually(func() []Metric {
121133
return getMetricsFromPod(c, getPodWithLabel(c, "app=catalog-operator"), "8081")
122-
}, time.Minute, 5*time.Second).Should(And(
123-
ContainSubstring("subscription_sync_total"),
124-
ContainSubstring(fmt.Sprintf("%s=\"%s\"", metrics.NAME_LABEL, "metric-subscription-for-create")),
125-
ContainSubstring(fmt.Sprintf("%s=\"%s\"", metrics.CHANNEL_LABEL, stableChannel)),
126-
ContainSubstring(fmt.Sprintf("%s=\"%s\"", metrics.PACKAGE_LABEL, testPackageName))))
134+
}).Should(ContainElement(LikeMetric(
135+
WithFamily("subscription_sync_total"),
136+
WithLabel("name", "metric-subscription-for-create"),
137+
WithLabel("channel", stableChannel),
138+
WithLabel("package", testPackageName),
139+
)))
127140
})
128-
if subscriptionCleanup != nil {
129-
subscriptionCleanup()
130-
}
131141
})
132-
When("A subscription object is updated", func() {
142+
143+
When("A subscription object is updated after emitting metrics", func() {
133144

134145
BeforeEach(func() {
135146
subscriptionCleanup, subscription = createSubscription(GinkgoT(), crc, testNamespace, "metric-subscription-for-update", testPackageName, stableChannel, v1alpha1.ApprovalManual)
136-
subscription.Spec.Channel = "beta"
137-
updateSubscription(GinkgoT(), crc, subscription)
147+
Eventually(func() []Metric {
148+
return getMetricsFromPod(c, getPodWithLabel(c, "app=catalog-operator"), "8081")
149+
}).Should(ContainElement(LikeMetric(WithFamily("subscription_sync_total"), WithLabel("name", "metric-subscription-for-update"))))
150+
Eventually(func() error {
151+
s, err := crc.OperatorsV1alpha1().Subscriptions(subscription.GetNamespace()).Get(context.TODO(), subscription.GetName(), metav1.GetOptions{})
152+
if err != nil {
153+
return err
154+
}
155+
s.Spec.Channel = "beta"
156+
_, err = crc.OperatorsV1alpha1().Subscriptions(s.GetNamespace()).Update(context.TODO(), s, metav1.UpdateOptions{})
157+
return err
158+
}).Should(Succeed())
138159
})
139160

140-
It("deletes the old Subscription metric and emits the new metric", func() {
141-
Eventually(func() string {
142-
return getMetricsFromPod(c, getPodWithLabel(c, "app=catalog-operator"), "8081")
143-
}, time.Minute, 5*time.Second).ShouldNot(And(
144-
ContainSubstring("subscription_sync_total{name=\"metric-subscription-for-update\""),
145-
ContainSubstring(fmt.Sprintf("%s=\"%s\"", metrics.CHANNEL_LABEL, stableChannel))))
161+
AfterEach(func() {
162+
if subscriptionCleanup != nil {
163+
subscriptionCleanup()
164+
}
165+
})
146166

147-
Eventually(func() string {
167+
It("deletes the old Subscription metric and emits the new metric", func() {
168+
Eventually(func() []Metric {
148169
return getMetricsFromPod(c, getPodWithLabel(c, "app=catalog-operator"), "8081")
149-
}, time.Minute, 5*time.Second).Should(And(
150-
ContainSubstring("subscription_sync_total"),
151-
ContainSubstring(fmt.Sprintf("%s=\"%s\"", metrics.NAME_LABEL, "metric-subscription-for-update")),
152-
ContainSubstring(fmt.Sprintf("%s=\"%s\"", metrics.CHANNEL_LABEL, "beta")),
153-
ContainSubstring(fmt.Sprintf("%s=\"%s\"", metrics.PACKAGE_LABEL, testPackageName))))
170+
}).Should(And(
171+
Not(ContainElement(LikeMetric(
172+
WithFamily("subscription_sync_total"),
173+
WithLabel("name", "metric-subscription-for-update"),
174+
WithLabel("channel", stableChannel),
175+
))),
176+
ContainElement(LikeMetric(
177+
WithFamily("subscription_sync_total"),
178+
WithLabel("name", "metric-subscription-for-update"),
179+
WithLabel("channel", "beta"),
180+
WithLabel("package", testPackageName),
181+
)),
182+
))
154183
})
155-
if subscriptionCleanup != nil {
156-
subscriptionCleanup()
157-
}
158184
})
159185

160-
When("A subscription object is deleted", func() {
186+
When("A subscription object is deleted after emitting metrics", func() {
161187

162188
BeforeEach(func() {
163189
subscriptionCleanup, subscription = createSubscription(GinkgoT(), crc, testNamespace, "metric-subscription-for-delete", testPackageName, stableChannel, v1alpha1.ApprovalManual)
190+
Eventually(func() []Metric {
191+
return getMetricsFromPod(c, getPodWithLabel(c, "app=catalog-operator"), "8081")
192+
}).Should(ContainElement(LikeMetric(WithFamily("subscription_sync_total"), WithLabel("name", "metric-subscription-for-delete"))))
193+
if subscriptionCleanup != nil {
194+
subscriptionCleanup()
195+
subscriptionCleanup = nil
196+
}
197+
})
198+
199+
AfterEach(func() {
164200
if subscriptionCleanup != nil {
165201
subscriptionCleanup()
166202
}
167203
})
168204

169205
It("deletes the Subscription metric", func() {
170-
Eventually(func() string {
206+
Eventually(func() []Metric {
171207
return getMetricsFromPod(c, getPodWithLabel(c, "app=catalog-operator"), "8081")
172-
}, time.Minute, 5*time.Second).ShouldNot(ContainSubstring("subscription_sync_total{name=\"metric-subscription-for-update\""))
208+
}).ShouldNot(ContainElement(LikeMetric(WithFamily("subscription_sync_total"), WithLabel("name", "metric-subscription-for-delete"))))
173209
})
174210
})
175211
})
@@ -178,7 +214,7 @@ var _ = Describe("Metrics are generated for OLM managed resources", func() {
178214
func getPodWithLabel(client operatorclient.ClientInterface, label string) *corev1.Pod {
179215
listOptions := metav1.ListOptions{LabelSelector: label}
180216
var podList *corev1.PodList
181-
Eventually(func() (err error) {
217+
EventuallyWithOffset(1, func() (err error) {
182218
podList, err = client.KubernetesInterface().CoreV1().Pods(operatorNamespace).List(context.TODO(), listOptions)
183219
return
184220
}).Should(Succeed(), "Failed to list OLM pods")
@@ -187,8 +223,8 @@ func getPodWithLabel(client operatorclient.ClientInterface, label string) *corev
187223
return &podList.Items[0]
188224
}
189225

190-
func getMetricsFromPod(client operatorclient.ClientInterface, pod *corev1.Pod, port string) string {
191-
By(fmt.Sprintf("querying pod %s/%s", pod.GetNamespace(), pod.GetName()))
226+
func getMetricsFromPod(client operatorclient.ClientInterface, pod *corev1.Pod, port string) []Metric {
227+
ctx.Ctx().Logf("querying pod %s/%s\n", pod.GetNamespace(), pod.GetName())
192228

193229
// assuming -tls-cert and -tls-key aren't used anywhere else as a parameter value
194230
var foundCert, foundKey bool
@@ -210,17 +246,61 @@ func getMetricsFromPod(client operatorclient.ClientInterface, pod *corev1.Pod, p
210246
}
211247
ctx.Ctx().Logf("Retrieving metrics using scheme %v\n", scheme)
212248

213-
var raw []byte
214-
Eventually(func() (err error) {
215-
raw, err = client.KubernetesInterface().CoreV1().RESTClient().Get().
249+
mfs := make(map[string]*io_prometheus_client.MetricFamily)
250+
EventuallyWithOffset(1, func() error {
251+
raw, err := client.KubernetesInterface().CoreV1().RESTClient().Get().
216252
Namespace(pod.GetNamespace()).
217253
Resource("pods").
218254
SubResource("proxy").
219255
Name(net.JoinSchemeNamePort(scheme, pod.GetName(), port)).
220256
Suffix("metrics").
221257
Do(context.Background()).Raw()
222-
return
258+
if err != nil {
259+
return err
260+
}
261+
var p expfmt.TextParser
262+
mfs, err = p.TextToMetricFamilies(bytes.NewReader(raw))
263+
if err != nil {
264+
return err
265+
}
266+
return nil
223267
}).Should(Succeed())
224268

225-
return string(raw)
269+
var metrics []Metric
270+
for family, mf := range mfs {
271+
var ignore bool
272+
for _, ignoredPrefix := range []string{"go_", "process_", "promhttp_"} {
273+
ignore = ignore || strings.HasPrefix(family, ignoredPrefix)
274+
}
275+
if ignore {
276+
// Metrics with these prefixes shouldn't be
277+
// relevant to these tests, so they can be
278+
// stripped out to make test failures easier
279+
// to understand.
280+
continue
281+
}
282+
283+
for _, metric := range mf.Metric {
284+
m := Metric{
285+
Family: family,
286+
}
287+
if len(metric.GetLabel()) > 0 {
288+
m.Labels = make(map[string][]string)
289+
}
290+
for _, pair := range metric.GetLabel() {
291+
m.Labels[pair.GetName()] = append(m.Labels[pair.GetName()], pair.GetValue())
292+
}
293+
if u := metric.GetUntyped(); u != nil {
294+
m.Value = u.GetValue()
295+
}
296+
if g := metric.GetGauge(); g != nil {
297+
m.Value = g.GetValue()
298+
}
299+
if c := metric.GetCounter(); c != nil {
300+
m.Value = c.GetValue()
301+
}
302+
metrics = append(metrics, m)
303+
}
304+
}
305+
return metrics
226306
}

test/e2e/subscription_e2e_test.go

-5
Original file line numberDiff line numberDiff line change
@@ -1813,11 +1813,6 @@ func createSubscription(t GinkgoTInterface, crc versioned.Interface, namespace,
18131813
return buildSubscriptionCleanupFunc(crc, subscription), subscription
18141814
}
18151815

1816-
func updateSubscription(t GinkgoTInterface, crc versioned.Interface, subscription *v1alpha1.Subscription) {
1817-
_, err := crc.OperatorsV1alpha1().Subscriptions(subscription.GetNamespace()).Update(context.TODO(), subscription, metav1.UpdateOptions{})
1818-
Expect(err).ToNot(HaveOccurred())
1819-
}
1820-
18211816
func createSubscriptionForCatalog(crc versioned.Interface, namespace, name, catalog, packageName, channel, startingCSV string, approval v1alpha1.Approval) cleanupFunc {
18221817
subscription := &v1alpha1.Subscription{
18231818
TypeMeta: metav1.TypeMeta{

0 commit comments

Comments
 (0)