1 package api
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "sort"
8 "testing"
9
10 pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
11 pb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
12 "github.com/prometheus/common/model"
13 "google.golang.org/protobuf/proto"
14 )
15
16
17 var booksDeployConfig = []string{`kind: Deployment
18 apiVersion: apps/v1
19 metadata:
20 name: books
21 namespace: default
22 uid: a1b2c3
23 spec:
24 replicas: 1
25 selector:
26 matchLabels:
27 app: books
28 template:
29 metadata:
30 labels:
31 app: books
32 spec:
33 dnsPolicy: ClusterFirst
34 containers:
35 - image: buoyantio/booksapp:v0.0.2
36 `, `
37 apiVersion: apps/v1
38 kind: ReplicaSet
39 metadata:
40 uid: a1b2c3d4
41 name: books
42 namespace: default
43 labels:
44 app: books
45 ownerReferences:
46 - apiVersion: apps/v1
47 uid: a1b2c3
48 spec:
49 selector:
50 matchLabels:
51 app: books`,
52 }
53
54
55 var booksDaemonsetConfig = `kind: DaemonSet
56 apiVersion: apps/v1
57 metadata:
58 name: books
59 namespace: default
60 spec:
61 selector:
62 matchLabels:
63 app: books
64 template:
65 metadata:
66 labels:
67 app: books
68 spec:
69 dnsPolicy: ClusterFirst
70 containers:
71 - image: buoyantio/booksapp:v0.0.2`
72
73
74 var booksJobConfig = `kind: Job
75 apiVersion: batch/v1
76 metadata:
77 name: books
78 namespace: default
79 spec:
80 selector:
81 matchLabels:
82 app: books
83 template:
84 metadata:
85 labels:
86 app: books
87 spec:
88 dnsPolicy: ClusterFirst
89 containers:
90 - image: buoyantio/booksapp:v0.0.2`
91
92 var booksStatefulsetConfig = `kind: StatefulSet
93 apiVersion: apps/v1
94 metadata:
95 name: books
96 namespace: default
97 spec:
98 selector:
99 matchLabels:
100 app: books
101 template:
102 serviceName: books
103 metadata:
104 labels:
105 app: books
106 spec:
107 containers:
108 - image: buoyantio/booksapp:v0.0.2
109 volumes:
110 - name: data
111 mountPath: /usr/src/app
112 volumeClaimTemplates:
113 - metadata:
114 name: data
115 spec:
116 accessModes: ["ReadWriteOnce"]
117 resources:
118 requests:
119 storage: 10Gi
120 `
121
122 var booksServiceConfig = []string{
123
124 `apiVersion: v1
125 kind: Service
126 metadata:
127 name: books
128 namespace: default
129 spec:
130 selector:
131 app: books`,
132
133
134 `apiVersion: v1
135 kind: Pod
136 metadata:
137 labels:
138 app: books
139 ownerReferences:
140 - apiVersion: apps/v1
141 uid: a1b2c3d4
142 name: books-64c68d6d46-jrmmx
143 namespace: default
144 spec:
145 containers:
146 - image: buoyantio/booksapp:v0.0.2
147 status:
148 phase: Running`,
149
150
151 `apiVersion: linkerd.io/v1alpha2
152 kind: ServiceProfile
153 metadata:
154 name: books.default.svc.cluster.local
155 namespace: default
156 spec:
157 routes:
158 - condition:
159 method: GET
160 pathRegex: /a
161 name: /a
162 `,
163 }
164
165 var booksConfig = append(booksServiceConfig, booksDeployConfig...)
166 var booksDSConfig = append(booksServiceConfig, booksDaemonsetConfig)
167 var booksSSConfig = append(booksServiceConfig, booksStatefulsetConfig)
168 var booksJConfig = append(booksServiceConfig, booksJobConfig)
169
170 type topRoutesExpected struct {
171 expectedStatRPC
172 req *pb.TopRoutesRequest
173 expectedResponse *pb.TopRoutesResponse
174 }
175
176 func routesMetric(routes []string) model.Vector {
177 samples := make(model.Vector, 0)
178 for _, route := range routes {
179 samples = append(samples, genRouteSample(route))
180 }
181 samples = append(samples, genDefaultRouteSample())
182 return samples
183 }
184
185 func genRouteSample(route string) *model.Sample {
186 return &model.Sample{
187 Metric: model.Metric{
188 "rt_route": model.LabelValue(route),
189 "dst": "books.default.svc.cluster.local",
190 "classification": success,
191 },
192 Value: 123,
193 Timestamp: 456,
194 }
195 }
196
197 func genDefaultRouteSample() *model.Sample {
198 return &model.Sample{
199 Metric: model.Metric{
200 "dst": "books.default.svc.cluster.local",
201 "classification": success,
202 },
203 Value: 123,
204 Timestamp: 456,
205 }
206 }
207
208 func testTopRoutes(t *testing.T, expectations []topRoutesExpected) {
209 for id, exp := range expectations {
210 exp := exp
211 t.Run(fmt.Sprintf("%d", id), func(t *testing.T) {
212 mockProm, fakeGrpcServer, err := newMockGrpcServer(exp.expectedStatRPC)
213 if err != nil {
214 t.Fatalf("Error creating mock grpc server: %s", err)
215 }
216
217 rsp, err := fakeGrpcServer.TopRoutes(context.TODO(), exp.req)
218 if !errors.Is(err, exp.err) {
219 t.Fatalf("Expected error: %s, Got: %s", exp.err, err)
220 }
221
222 err = exp.verifyPromQueries(mockProm)
223 if err != nil {
224 t.Fatal(err)
225 }
226
227 rows := rsp.GetOk().GetRoutes()[0].Rows
228
229 if len(rows) != len(exp.expectedResponse.GetOk().GetRoutes()[0].Rows) {
230 t.Fatalf(
231 "Expected [%d] rows, got [%d].\nExpected:\n%s\nGot:\n%s",
232 len(exp.expectedResponse.GetOk().GetRoutes()[0].Rows),
233 len(rows),
234 exp.expectedResponse.GetOk().GetRoutes()[0].Rows,
235 rows,
236 )
237 }
238
239 sort.Slice(rows, func(i, j int) bool {
240 return rows[i].GetAuthority()+rows[i].GetRoute() < rows[j].GetAuthority()+rows[j].GetRoute()
241 })
242
243 for i, row := range rows {
244 expected := exp.expectedResponse.GetOk().GetRoutes()[0].Rows[i]
245 if !proto.Equal(row, expected) {
246 t.Fatalf("Expected: %+v\n Got: %+v", expected, row)
247 }
248 }
249 })
250 }
251 }
252
253 func TestTopRoutes(t *testing.T) {
254 t.Run("Successfully performs a routes query", func(t *testing.T) {
255 routes := []string{"/a"}
256 counts := []uint64{123}
257 expectations := []topRoutesExpected{
258 {
259 expectedStatRPC: expectedStatRPC{
260 err: nil,
261 mockPromResponse: routesMetric([]string{"/a"}),
262 expectedPrometheusQueries: []string{
263 `histogram_quantile(0.5, sum(irate(route_response_latency_ms_bucket{deployment="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
264 `histogram_quantile(0.95, sum(irate(route_response_latency_ms_bucket{deployment="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
265 `histogram_quantile(0.99, sum(irate(route_response_latency_ms_bucket{deployment="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
266 `sum(increase(route_response_total{deployment="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (rt_route, dst, classification)`,
267 },
268 k8sConfigs: booksConfig,
269 },
270 req: &pb.TopRoutesRequest{
271 Selector: &pb.ResourceSelection{
272 Resource: &pb.Resource{
273 Namespace: "default",
274 Type: pkgK8s.Deployment,
275 Name: "books",
276 },
277 },
278 TimeWindow: "1m",
279 Outbound: &pb.TopRoutesRequest_None{
280 None: &pb.Empty{},
281 },
282 },
283 expectedResponse: GenTopRoutesResponse(routes, counts, false, "books"),
284 },
285 }
286
287 testTopRoutes(t, expectations)
288 })
289
290 t.Run("Successfully performs a routes query for a service", func(t *testing.T) {
291 routes := []string{"/a"}
292 counts := []uint64{123}
293 expectations := []topRoutesExpected{
294 {
295 expectedStatRPC: expectedStatRPC{
296 err: nil,
297 mockPromResponse: routesMetric([]string{"/a"}),
298 expectedPrometheusQueries: []string{
299 `histogram_quantile(0.5, sum(irate(route_response_latency_ms_bucket{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
300 `histogram_quantile(0.95, sum(irate(route_response_latency_ms_bucket{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
301 `histogram_quantile(0.99, sum(irate(route_response_latency_ms_bucket{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
302 `sum(increase(route_response_total{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (rt_route, dst, classification)`,
303 },
304 k8sConfigs: booksConfig,
305 },
306 req: &pb.TopRoutesRequest{
307 Selector: &pb.ResourceSelection{
308 Resource: &pb.Resource{
309 Namespace: "default",
310 Type: pkgK8s.Service,
311 Name: "books",
312 },
313 },
314 TimeWindow: "1m",
315 Outbound: &pb.TopRoutesRequest_None{
316 None: &pb.Empty{},
317 },
318 },
319 expectedResponse: GenTopRoutesResponse(routes, counts, false, "books"),
320 },
321 }
322
323 testTopRoutes(t, expectations)
324 })
325
326 t.Run("Successfully performs a routes query for a daemonset", func(t *testing.T) {
327 routes := []string{"/a"}
328 counts := []uint64{123}
329 expectations := []topRoutesExpected{
330 {
331 expectedStatRPC: expectedStatRPC{
332 err: nil,
333 mockPromResponse: routesMetric([]string{"/a"}),
334 expectedPrometheusQueries: []string{
335 `histogram_quantile(0.5, sum(irate(route_response_latency_ms_bucket{daemonset="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
336 `histogram_quantile(0.95, sum(irate(route_response_latency_ms_bucket{daemonset="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
337 `histogram_quantile(0.99, sum(irate(route_response_latency_ms_bucket{daemonset="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
338 `sum(increase(route_response_total{daemonset="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (rt_route, dst, classification)`,
339 },
340 k8sConfigs: booksDSConfig,
341 },
342 req: &pb.TopRoutesRequest{
343 Selector: &pb.ResourceSelection{
344 Resource: &pb.Resource{
345 Namespace: "default",
346 Type: pkgK8s.DaemonSet,
347 Name: "books",
348 },
349 },
350 TimeWindow: "1m",
351 },
352 expectedResponse: GenTopRoutesResponse(routes, counts, false, "books"),
353 },
354 }
355
356 testTopRoutes(t, expectations)
357 })
358
359 t.Run("Successfully performs a routes query for a job", func(t *testing.T) {
360 routes := []string{"/a"}
361 counts := []uint64{123}
362 expectations := []topRoutesExpected{
363 {
364 expectedStatRPC: expectedStatRPC{
365 err: nil,
366 mockPromResponse: routesMetric([]string{"/a"}),
367 expectedPrometheusQueries: []string{
368 `histogram_quantile(0.5, sum(irate(route_response_latency_ms_bucket{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", k8s_job="books", namespace="default"}[1m])) by (le, dst, rt_route))`,
369 `histogram_quantile(0.95, sum(irate(route_response_latency_ms_bucket{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", k8s_job="books", namespace="default"}[1m])) by (le, dst, rt_route))`,
370 `histogram_quantile(0.99, sum(irate(route_response_latency_ms_bucket{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", k8s_job="books", namespace="default"}[1m])) by (le, dst, rt_route))`,
371 `sum(increase(route_response_total{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", k8s_job="books", namespace="default"}[1m])) by (rt_route, dst, classification)`,
372 },
373 k8sConfigs: booksJConfig,
374 },
375 req: &pb.TopRoutesRequest{
376 Selector: &pb.ResourceSelection{
377 Resource: &pb.Resource{
378 Namespace: "default",
379 Type: pkgK8s.Job,
380 Name: "books",
381 },
382 },
383 TimeWindow: "1m",
384 },
385 expectedResponse: GenTopRoutesResponse(routes, counts, false, "books"),
386 },
387 }
388
389 testTopRoutes(t, expectations)
390 })
391
392 t.Run("Successfully performs a routes query for a statefulset", func(t *testing.T) {
393 routes := []string{"/a"}
394 counts := []uint64{123}
395 expectations := []topRoutesExpected{
396 {
397 expectedStatRPC: expectedStatRPC{
398 err: nil,
399 mockPromResponse: routesMetric([]string{"/a"}),
400 expectedPrometheusQueries: []string{
401 `histogram_quantile(0.5, sum(irate(route_response_latency_ms_bucket{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default", statefulset="books"}[1m])) by (le, dst, rt_route))`,
402 `histogram_quantile(0.95, sum(irate(route_response_latency_ms_bucket{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default", statefulset="books"}[1m])) by (le, dst, rt_route))`,
403 `histogram_quantile(0.99, sum(irate(route_response_latency_ms_bucket{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default", statefulset="books"}[1m])) by (le, dst, rt_route))`,
404 `sum(increase(route_response_total{direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default", statefulset="books"}[1m])) by (rt_route, dst, classification)`,
405 },
406 k8sConfigs: booksSSConfig,
407 },
408 req: &pb.TopRoutesRequest{
409 Selector: &pb.ResourceSelection{
410 Resource: &pb.Resource{
411 Namespace: "default",
412 Type: pkgK8s.StatefulSet,
413 Name: "books",
414 },
415 },
416 TimeWindow: "1m",
417 },
418 expectedResponse: GenTopRoutesResponse(routes, counts, false, "books"),
419 },
420 }
421
422 testTopRoutes(t, expectations)
423 })
424
425 t.Run("Successfully performs an outbound routes query", func(t *testing.T) {
426 routes := []string{"/a"}
427 counts := []uint64{123}
428 expectations := []topRoutesExpected{
429 {
430 expectedStatRPC: expectedStatRPC{
431 err: nil,
432 mockPromResponse: routesMetric([]string{"/a"}),
433 expectedPrometheusQueries: []string{
434 `histogram_quantile(0.5, sum(irate(route_response_latency_ms_bucket{deployment="books", direction="outbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
435 `histogram_quantile(0.95, sum(irate(route_response_latency_ms_bucket{deployment="books", direction="outbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
436 `histogram_quantile(0.99, sum(irate(route_response_latency_ms_bucket{deployment="books", direction="outbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
437 `sum(increase(route_response_total{deployment="books", direction="outbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (rt_route, dst, classification)`,
438 `sum(increase(route_actual_response_total{deployment="books", direction="outbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (rt_route, dst, classification)`,
439 },
440 k8sConfigs: booksConfig,
441 },
442 req: &pb.TopRoutesRequest{
443 Selector: &pb.ResourceSelection{
444 Resource: &pb.Resource{
445 Namespace: "default",
446 Type: pkgK8s.Deployment,
447 Name: "books",
448 },
449 },
450 Outbound: &pb.TopRoutesRequest_ToResource{
451 ToResource: &pb.Resource{
452 Type: pkgK8s.Service,
453 },
454 },
455 TimeWindow: "1m",
456 },
457 expectedResponse: GenTopRoutesResponse(routes, counts, true, "books"),
458 },
459 }
460
461 testTopRoutes(t, expectations)
462 })
463
464 t.Run("Successfully performs an outbound authority query", func(t *testing.T) {
465 routes := []string{"/a"}
466 counts := []uint64{123}
467 expectations := []topRoutesExpected{
468 {
469 expectedStatRPC: expectedStatRPC{
470 err: nil,
471 mockPromResponse: routesMetric([]string{"/a"}),
472 expectedPrometheusQueries: []string{
473 `histogram_quantile(0.5, sum(irate(route_response_latency_ms_bucket{deployment="books", direction="outbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
474 `histogram_quantile(0.95, sum(irate(route_response_latency_ms_bucket{deployment="books", direction="outbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
475 `histogram_quantile(0.99, sum(irate(route_response_latency_ms_bucket{deployment="books", direction="outbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
476 `sum(increase(route_response_total{deployment="books", direction="outbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (rt_route, dst, classification)`,
477 `sum(increase(route_actual_response_total{deployment="books", direction="outbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (rt_route, dst, classification)`,
478 },
479 k8sConfigs: booksConfig,
480 },
481 req: &pb.TopRoutesRequest{
482 Selector: &pb.ResourceSelection{
483 Resource: &pb.Resource{
484 Namespace: "default",
485 Type: pkgK8s.Deployment,
486 Name: "books",
487 },
488 },
489 Outbound: &pb.TopRoutesRequest_ToResource{
490 ToResource: &pb.Resource{
491 Type: pkgK8s.Authority,
492 Name: "books.default.svc.cluster.local",
493 },
494 },
495 TimeWindow: "1m",
496 },
497 expectedResponse: GenTopRoutesResponse(routes, counts, true, "books.default.svc.cluster.local"),
498 },
499 }
500
501 testTopRoutes(t, expectations)
502 })
503 }
504
View as plain text