...

Source file src/github.com/linkerd/linkerd2/viz/metrics-api/top_routes_test.go

Documentation: github.com/linkerd/linkerd2/viz/metrics-api

     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  // deployment/books
    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  // daemonset/books
    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  // job/books
    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  	// service/books
   124  	`apiVersion: v1
   125  kind: Service
   126  metadata:
   127    name: books
   128    namespace: default
   129  spec:
   130    selector:
   131      app: books`,
   132  
   133  	// po/books-64c68d6d46-jrmmx
   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  	// serviceprofile/books.default.svc.cluster.local
   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  // the request we would like to test
   173  	expectedResponse *pb.TopRoutesResponse // the routes response we expect
   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 // pin
   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