Skip to content

Commit 73828c2

Browse files
Merge pull request #1610 from benluddy/metrics-test-matcher
Add more precise matching to the metric end-to-end tests.
2 parents f378a05 + d8527b2 commit 73828c2

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)