Repository: linkerd/linkerd2
Pull request #6631 (2021-08-09 16:43:03)

Collapse *.golden files in pull requests
Our *.golden files are effectively auto-generated. It's almost never
important to view them when reviewing pull requests.

This change sets `linguist-generated=true` for these files so that
GitHub collapses them by default.

<!--  Thanks for sending a pull request!

If you already have a well-structured git commit message, chances are GitHub
set the title and description of this PR to the git commit message subject and
body, respectively. If so, you may delete these instructions and submit your PR.

If this is your first time, please read our contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md

The title and description of your Pull Request should match the git commit
subject and body, respectively. Git commit messages are structured as follows:

```
Subject

Problem

Solution

Validation

Fixes #[GitHub issue ID]

DCO Sign off
```

Example git commit message:

```
Introduce Pull Request Template

GitHub's community guidelines recommend a pull request template, the repo was
lacking one.

Introduce a `PULL_REQUEST_TEMPLATE.md` file.

Once merged, the
[Community profile checklist](https://github.com/linkerd/linkerd2/community)
should indicate the repo now provides a pull request template.

Fixes #3321

Signed-off-by: Jane Smith <jane.smith@example.com>
```

Note the git commit message subject becomes the pull request title.

For more details around git commits, see the section on Committing in our
contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md#committing
-->


codecov-commenter: # [Codecov](https://codecov.io/gh/linkerd/linkerd2/pull/6631?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd) Report
> Merging [#6631](https://codecov.io/gh/linkerd/linkerd2/pull/6631?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd) (4324b7e) into [main](https://codecov.io/gh/linkerd/linkerd2/commit/10a36b010a81916c80b5e7a4a37f7a84af8978d7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd) (10a36b0) will **increase** coverage by `21.41%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/linkerd/linkerd2/pull/6631/graphs/tree.svg?width=650&height=150&src=pr&token=FCBISvpqTv&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd)](https://codecov.io/gh/linkerd/linkerd2/pull/6631?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd)

```diff
@@             Coverage Diff             @@
##             main    #6631       +/-   ##
===========================================
+ Coverage   45.34%   66.76%   +21.41%     
===========================================
  Files         182       39      -143     
  Lines       24272     1396    -22876     
  Branches      290      290               
===========================================
- Hits        11006      932    -10074     
+ Misses      12488      460    -12028     
+ Partials      778        4      -774     
```

| Flag | Coverage Δ | |
|---|---|---|
| golang | `?` | |
| javascript | `66.76% <ø> (ø)` | |
| unittests | `66.76% <ø> (+21.41%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/linkerd/linkerd2/pull/6631?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd) | Coverage Δ | |
|---|---|---|
| [web/app/js/components/Navigation.jsx](https://codecov.io/gh/linkerd/linkerd2/pull/6631/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-d2ViL2FwcC9qcy9jb21wb25lbnRzL05hdmlnYXRpb24uanN4) | `66.26% <0.00%> (ø)` | |
| [web/app/js/components/BreadcrumbHeader.jsx](https://codecov.io/gh/linkerd/linkerd2/pull/6631/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-d2ViL2FwcC9qcy9jb21wb25lbnRzL0JyZWFkY3J1bWJIZWFkZXIuanN4) | `100.00% <0.00%> (ø)` | |
| [jaeger/cmd/list.go](https://codecov.io/gh/linkerd/linkerd2/pull/6631/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-amFlZ2VyL2NtZC9saXN0Lmdv) | | |
| [viz/cmd/dashboard.go](https://codecov.io/gh/linkerd/linkerd2/pull/6631/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-dml6L2NtZC9kYXNoYm9hcmQuZ28=) | | |
| [jaeger/cmd/uninstall.go](https://codecov.io/gh/linkerd/linkerd2/pull/6631/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-amFlZ2VyL2NtZC91bmluc3RhbGwuZ28=) | | |
| [pkg/tls/cred.go](https://codecov.io/gh/linkerd/linkerd2/pull/6631/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-cGtnL3Rscy9jcmVkLmdv) | | |
| [controller/webhook/util.go](https://codecov.io/gh/linkerd/linkerd2/pull/6631/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-Y29udHJvbGxlci93ZWJob29rL3V0aWwuZ28=) | | |
| [pkg/tls/ca.go](https://codecov.io/gh/linkerd/linkerd2/pull/6631/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-cGtnL3Rscy9jYS5nbw==) | | |
| [pkg/profiles/profiles.go](https://codecov.io/gh/linkerd/linkerd2/pull/6631/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-cGtnL3Byb2ZpbGVzL3Byb2ZpbGVzLmdv) | | |
| [viz/metrics-api/prometheus.go](https://codecov.io/gh/linkerd/linkerd2/pull/6631/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-dml6L21ldHJpY3MtYXBpL3Byb21ldGhldXMuZ28=) | | |
| ... and [135 more](https://codecov.io/gh/linkerd/linkerd2/pull/6631/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/linkerd/linkerd2/pull/6631?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/linkerd/linkerd2/pull/6631?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd). Last update [c20c4a7...4324b7e](https://codecov.io/gh/linkerd/linkerd2/pull/6631?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd).


Patch:
diff --git a/.gitattributes b/.gitattributes
index d51cb17c8..b8c97f717 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,4 @@
 # By default, collapse for these files in GitHub reviews.
 go.sum linguist-generated=false
 controller/gen/**/*.pb.go linguist-generated=true
+**/*.golden linguist-generated=true
diff --git a/viz/cmd/stat.go b/viz/cmd/stat.go
index d549d61a7..c00e580c9 100644
--- a/viz/cmd/stat.go
+++ b/viz/cmd/stat.go
@@ -310,6 +310,7 @@ type row struct {
 	status string
 	*rowStats
 	*tsStats
+	*dstStats
 }
 
 type tsStats struct {
@@ -318,6 +319,11 @@ type tsStats struct {
 	weight string
 }
 
+type dstStats struct {
+	dst    string
+	weight string
+}
+
 var (
 	nameHeader      = "NAME"
 	namespaceHeader = "NAMESPACE"
@@ -331,7 +337,7 @@ func statHasRequestData(stat *pb.BasicStats) bool {
 }
 
 func isPodOwnerResource(typ string) bool {
-	return typ != k8s.TrafficSplit && typ != k8s.Authority
+	return typ != k8s.TrafficSplit && typ != k8s.Authority && typ != k8s.Service
 }
 
 func writeStatsToBuffer(rows []*pb.StatTable_PodGroup_Row, w *tabwriter.Writer, options *statOptions) {
@@ -339,6 +345,7 @@ func writeStatsToBuffer(rows []*pb.StatTable_PodGroup_Row, w *tabwriter.Writer,
 	maxNamespaceLength := len(namespaceHeader)
 	maxApexLength := len(apexHeader)
 	maxLeafLength := len(leafHeader)
+	maxDstLength := len(dstHeader)
 	maxWeightLength := len(weightHeader)
 
 	statTables := make(map[string]map[string]*row)
@@ -373,7 +380,7 @@ func writeStatsToBuffer(rows []*pb.StatTable_PodGroup_Row, w *tabwriter.Writer,
 
 		namespace := r.Resource.Namespace
 		key := fmt.Sprintf("%s/%s", namespace, name)
-		if r.Resource.Type == k8s.TrafficSplit {
+		if r.Resource.Type == k8s.TrafficSplit || (r.Resource.Type == k8s.Service && r.TsStats != nil) {
 			key = fmt.Sprintf("%s/%s/%s", namespace, name, r.TsStats.Leaf)
 		}
 		resourceKey := r.Resource.Type
@@ -391,7 +398,7 @@ func writeStatsToBuffer(rows []*pb.StatTable_PodGroup_Row, w *tabwriter.Writer,
 		}
 
 		meshedCount := fmt.Sprintf("%d/%d", r.MeshedPodCount, r.RunningPodCount)
-		if resourceKey == k8s.Authority {
+		if resourceKey == k8s.Authority || resourceKey == k8s.Service {
 			meshedCount = "-"
 		}
 		statTables[resourceKey][key] = &row{
@@ -412,26 +419,40 @@ func writeStatsToBuffer(rows []*pb.StatTable_PodGroup_Row, w *tabwriter.Writer,
 			}
 		}
 		if r.TsStats != nil {
-			leaf := r.TsStats.Leaf
-			apex := r.TsStats.Apex
-			weight := r.TsStats.Weight
+			if r.GetResource().GetType() == k8s.TrafficSplit {
+				leaf := r.TsStats.Leaf
+				apex := r.TsStats.Apex
+				weight := r.TsStats.Weight
 
-			if len(leaf) > maxLeafLength {
-				maxLeafLength = len(leaf)
-			}
+				if len(leaf) > maxLeafLength {
+					maxLeafLength = len(leaf)
+				}
 
-			if len(apex) > maxApexLength {
-				maxApexLength = len(apex)
-			}
+				if len(apex) > maxApexLength {
+					maxApexLength = len(apex)
+				}
 
-			if len(weight) > maxWeightLength {
-				maxWeightLength = len(weight)
-			}
+				statTables[resourceKey][key].tsStats = &tsStats{
+					apex:   apex,
+					leaf:   leaf,
+					weight: weight,
+				}
+			} else {
+				dst := r.TsStats.Leaf
+				weight := r.TsStats.Weight
+
+				if len(dst) > maxDstLength {
+					maxDstLength = len(dst)
+				}
+
+				if len(weight) > maxWeightLength {
+					maxWeightLength = len(weight)
+				}
 
-			statTables[resourceKey][key].tsStats = &tsStats{
-				apex:   apex,
-				leaf:   leaf,
-				weight: weight,
+				statTables[resourceKey][key].dstStats = &dstStats{
+					dst:    dst,
+					weight: weight,
+				}
 			}
 		}
 	}
@@ -442,13 +463,13 @@ func writeStatsToBuffer(rows []*pb.StatTable_PodGroup_Row, w *tabwriter.Writer,
 			fmt.Fprintln(os.Stderr, "No traffic found.")
 			return
 		}
-		printStatTables(statTables, w, maxNameLength, maxNamespaceLength, maxLeafLength, maxApexLength, maxWeightLength, options)
+		printStatTables(statTables, w, maxNameLength, maxNamespaceLength, maxLeafLength, maxApexLength, maxDstLength, maxWeightLength, options)
 	case jsonOutput:
 		printStatJSON(statTables, w)
 	}
 }
 
-func printStatTables(statTables map[string]map[string]*row, w *tabwriter.Writer, maxNameLength, maxNamespaceLength, maxLeafLength, maxApexLength, maxWeightLength int, options *statOptions) {
+func printStatTables(statTables map[string]map[string]*row, w *tabwriter.Writer, maxNameLength, maxNamespaceLength, maxLeafLength, maxApexLength, maxDstLength, maxWeightLength int, options *statOptions) {
 	usePrefix := false
 	if len(statTables) > 1 {
 		usePrefix = true
@@ -465,7 +486,7 @@ func printStatTables(statTables map[string]map[string]*row, w *tabwriter.Writer,
 			if !usePrefix {
 				resourceTypeLabel = ""
 			}
-			printSingleStatTable(stats, resourceTypeLabel, resourceType, w, maxNameLength, maxNamespaceLength, maxLeafLength, maxApexLength, maxWeightLength, options)
+			printSingleStatTable(stats, resourceTypeLabel, resourceType, w, maxNameLength, maxNamespaceLength, maxLeafLength, maxApexLength, maxDstLength, maxWeightLength, options)
 		}
 	}
 }
@@ -479,14 +500,29 @@ func showTCPConns(resourceType string) bool {
 	return resourceType != k8s.Authority && resourceType != k8s.TrafficSplit
 }
 
-func printSingleStatTable(stats map[string]*row, resourceTypeLabel, resourceType string, w *tabwriter.Writer, maxNameLength, maxNamespaceLength, maxLeafLength, maxApexLength, maxWeightLength int, options *statOptions) {
+func printSingleStatTable(stats map[string]*row, resourceTypeLabel, resourceType string, w *tabwriter.Writer, maxNameLength, maxNamespaceLength, maxLeafLength, maxApexLength, maxDstLength, maxWeightLength int, options *statOptions) {
 	headers := make([]string, 0)
 	nameTemplate := fmt.Sprintf("%%-%ds", maxNameLength)
 	namespaceTemplate := fmt.Sprintf("%%-%ds", maxNamespaceLength)
 	apexTemplate := fmt.Sprintf("%%-%ds", maxApexLength)
 	leafTemplate := fmt.Sprintf("%%-%ds", maxLeafLength)
+	dstTemplate := fmt.Sprintf("%%-%ds", maxDstLength)
 	weightTemplate := fmt.Sprintf("%%-%ds", maxWeightLength)
 
+	hasDstStats := false
+	for _, r := range stats {
+		if r.dstStats != nil {
+			hasDstStats = true
+		}
+	}
+
+	hasTsStats := false
+	for _, r := range stats {
+		if r.tsStats != nil {
+			hasTsStats = true
+		}
+	}
+
 	if options.allNamespaces {
 		headers = append(headers,
 			fmt.Sprintf(namespaceTemplate, namespaceHeader))
@@ -499,7 +535,11 @@ func printSingleStatTable(stats map[string]*row, resourceTypeLabel, resourceType
 		headers = append(headers, "STATUS")
 	}
 
-	if resourceType == k8s.TrafficSplit {
+	if hasDstStats {
+		headers = append(headers,
+			fmt.Sprintf(dstTemplate, dstHeader),
+			fmt.Sprintf(weightTemplate, weightHeader))
+	} else if hasTsStats {
 		headers = append(headers,
 			fmt.Sprintf(apexTemplate, apexHeader),
 			fmt.Sprintf(leafTemplate, leafHeader),
@@ -542,9 +582,12 @@ func printSingleStatTable(stats map[string]*row, resourceTypeLabel, resourceType
 			templateStringEmpty = "%s\t" + templateStringEmpty
 		}
 
-		if resourceType == k8s.TrafficSplit {
+		if hasTsStats {
 			templateString = "%s\t%s\t%s\t%s\t%.2f%%\t%.1frps\t%dms\t%dms\t%dms\t"
 			templateStringEmpty = "%s\t%s\t%s\t%s\t-\t-\t-\t-\t-\t"
+		} else if hasDstStats {
+			templateString = "%s\t%s\t%s\t%.2f%%\t%.1frps\t%dms\t%dms\t%dms\t"
+			templateStringEmpty = "%s\t%s\t%s\t-\t-\t-\t-\t-\t"
 		}
 
 		if !showTCPConns(resourceType) {
@@ -578,6 +621,7 @@ func printSingleStatTable(stats map[string]*row, resourceTypeLabel, resourceType
 
 		apexPadding := 0
 		leafPadding := 0
+		dstPadding := 0
 
 		if stats[key].tsStats != nil {
 			if maxApexLength > len(stats[key].tsStats.apex) {
@@ -586,6 +630,10 @@ func printSingleStatTable(stats map[string]*row, resourceTypeLabel, resourceType
 			if maxLeafLength > len(stats[key].tsStats.leaf) {
 				leafPadding = maxLeafLength - len(stats[key].tsStats.leaf)
 			}
+		} else if stats[key].dstStats != nil {
+			if maxDstLength > len(stats[key].dstStats.dst) {
+				dstPadding = maxDstLength - len(stats[key].dstStats.dst)
+			}
 		}
 
 		values = append(values, name+strings.Repeat(" ", padding))
@@ -593,12 +641,17 @@ func printSingleStatTable(stats map[string]*row, resourceTypeLabel, resourceType
 			values = append(values, stats[key].status)
 		}
 
-		if resourceType == k8s.TrafficSplit {
+		if hasTsStats {
 			values = append(values,
 				stats[key].tsStats.apex+strings.Repeat(" ", apexPadding),
 				stats[key].tsStats.leaf+strings.Repeat(" ", leafPadding),
 				stats[key].tsStats.weight,
 			)
+		} else if hasDstStats {
+			values = append(values,
+				stats[key].dstStats.dst+strings.Repeat(" ", dstPadding),
+				stats[key].dstStats.weight,
+			)
 		} else {
 			values = append(values, []interface{}{
 				stats[key].meshed,
@@ -656,6 +709,7 @@ type jsonStats struct {
 	TCPWriteBytes  *float64 `json:"tcp_write_bytes_rate,omitempty"`
 	Apex           string   `json:"apex,omitempty"`
 	Leaf           string   `json:"leaf,omitempty"`
+	Dst            string   `json:"dst,omitempty"`
 	Weight         string   `json:"weight,omitempty"`
 }
 
@@ -692,7 +746,10 @@ func printStatJSON(statTables map[string]map[string]*row, w *tabwriter.Writer) {
 				if stats[key].tsStats != nil {
 					entry.Apex = stats[key].apex
 					entry.Leaf = stats[key].leaf
-					entry.Weight = stats[key].weight
+					entry.Weight = stats[key].tsStats.weight
+				} else if stats[key].dstStats != nil {
+					entry.Dst = stats[key].dstStats.dst
+					entry.Weight = stats[key].dstStats.weight
 				}
 				entries = append(entries, entry)
 			}
diff --git a/viz/metrics-api/prometheus.go b/viz/metrics-api/prometheus.go
index bdacb3f97..1eb039640 100644
--- a/viz/metrics-api/prometheus.go
+++ b/viz/metrics-api/prometheus.go
@@ -39,6 +39,7 @@ const (
 	gatewayNameLabel       = model.LabelName("gateway_name")
 	gatewayNamespaceLabel  = model.LabelName("gateway_namespace")
 	remoteClusterNameLabel = model.LabelName("target_cluster_name")
+	authorityLabel         = model.LabelName("authority")
 )
 
 var (
@@ -163,7 +164,7 @@ func generateLabelStringWithRegex(l model.LabelSet, labelName string, stringToMa
 	for l, v := range l {
 		lstrs = append(lstrs, fmt.Sprintf("%s=%q", l, v))
 	}
-	lstrs = append(lstrs, fmt.Sprintf(`%s=~"^%s.+"`, labelName, stringToMatch))
+	lstrs = append(lstrs, fmt.Sprintf(`%s=~"^%s.*"`, labelName, stringToMatch))
 
 	sort.Strings(lstrs)
 	return fmt.Sprintf("{%s}", strings.Join(lstrs, ", "))
diff --git a/viz/metrics-api/stat_summary.go b/viz/metrics-api/stat_summary.go
index d0f18ff50..c53e9d3fe 100644
--- a/viz/metrics-api/stat_summary.go
+++ b/viz/metrics-api/stat_summary.go
@@ -5,14 +5,17 @@ import (
 	"fmt"
 	"reflect"
 	"sort"
+	"strings"
 
 	proto "github.com/golang/protobuf/proto"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	pb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
-	"github.com/linkerd/linkerd2/viz/pkg/util"
+	vizutil "github.com/linkerd/linkerd2/viz/pkg/util"
 	"github.com/prometheus/common/model"
 	"github.com/servicemeshinterface/smi-sdk-go/pkg/apis/split/v1alpha1"
+	log "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
@@ -23,7 +26,6 @@ type resourceResult struct {
 	res *pb.StatTable
 	err error
 }
-
 type k8sStat struct {
 	object   metav1.Object
 	podStats *podStats
@@ -44,6 +46,13 @@ type tsKey struct {
 	Weight    string
 }
 
+type dstKey struct {
+	Namespace string
+	Service   string
+	Dst       string
+	Weight    string
+}
+
 const (
 	success = "success"
 	failure = "failure"
@@ -53,6 +62,8 @@ const (
 	tcpConnectionsQuery  = "sum(tcp_open_connections%s) by (%s)"
 	tcpReadBytesQuery    = "sum(increase(tcp_read_bytes_total%s[%s])) by (%s)"
 	tcpWriteBytesQuery   = "sum(increase(tcp_write_bytes_total%s[%s])) by (%s)"
+
+	regexAny = ".+"
 )
 
 type podStats struct {
@@ -76,9 +87,9 @@ func (s *grpcServer) StatSummary(ctx context.Context, req *pb.StatSummaryRequest
 		return statSummaryError(req, "StatSummary request missing Selector Resource"), nil
 	}
 
-	// special case to check for services as outbound only
-	if isInvalidServiceRequest(req.Selector, req.GetFromResource()) {
-		return statSummaryError(req, "service only supported as a target on 'from' queries, or as a destination on 'to' queries"), nil
+	// err if --from is a service
+	if req.GetFromResource() != nil && req.GetFromResource().GetType() == k8s.Service {
+		return statSummaryError(req, "service is not supported as a target on 'from' queries, or as a target with 'to' queries"), nil
 	}
 
 	switch req.Outbound.(type) {
@@ -111,6 +122,8 @@ func (s *grpcServer) StatSummary(ctx context.Context, req *pb.StatSummaryRequest
 		go func() {
 			if isNonK8sResourceQuery(statReq.GetSelector().GetResource().GetType()) {
 				resultChan <- s.nonK8sResourceQuery(ctx, statReq)
+			} else if statReq.GetSelector().GetResource().GetType() == k8s.Service {
+				resultChan <- s.serviceResourceQuery(ctx, statReq)
 			} else if isTrafficSplitQuery(statReq.GetSelector().GetResource().GetType()) {
 				resultChan <- s.trafficSplitResourceQuery(ctx, statReq)
 			} else {
@@ -122,7 +135,7 @@ func (s *grpcServer) StatSummary(ctx context.Context, req *pb.StatSummaryRequest
 	for i := 0; i < len(resourcesToQuery); i++ {
 		result := <-resultChan
 		if result.err != nil {
-			return nil, util.GRPCError(result.err)
+			return nil, vizutil.GRPCError(result.err)
 		}
 		statTables = append(statTables, result.res)
 	}
@@ -135,17 +148,10 @@ func (s *grpcServer) StatSummary(ctx context.Context, req *pb.StatSummaryRequest
 		},
 	}
 
+	log.Debugf("Sent response as %+v\n", statTables)
 	return &rsp, nil
 }
 
-func isInvalidServiceRequest(selector *pb.ResourceSelection, fromResource *pb.Resource) bool {
-	if fromResource != nil {
-		return fromResource.Type == k8s.Service
-	}
-
-	return selector.Resource.Type == k8s.Service
-}
-
 func statSummaryError(req *pb.StatSummaryRequest, message string) *pb.StatSummaryResponse {
 	return &pb.StatSummaryResponse{
 		Response: &pb.StatSummaryResponse_Error{
@@ -198,6 +204,7 @@ func (s *grpcServer) getKubernetesObjectStats(req *pb.StatSummaryRequest) (map[r
 }
 
 func (s *grpcServer) k8sResourceQuery(ctx context.Context, req *pb.StatSummaryRequest) resourceResult {
+
 	k8sObjects, err := s.getKubernetesObjectStats(req)
 	if err != nil {
 		return resourceResult{res: nil, err: err}
@@ -288,8 +295,8 @@ func (s *grpcServer) getTrafficSplits(req *pb.StatSummaryRequest) ([]*v1alpha1.T
 }
 
 func (s *grpcServer) trafficSplitResourceQuery(ctx context.Context, req *pb.StatSummaryRequest) resourceResult {
-	tss, err := s.getTrafficSplits(req)
 
+	tss, err := s.getTrafficSplits(req)
 	if err != nil {
 		return resourceResult{res: nil, err: err}
 	}
@@ -359,11 +366,88 @@ func (s *grpcServer) trafficSplitResourceQuery(ctx context.Context, req *pb.Stat
 	return resourceResult{res: &rsp, err: nil}
 }
 
+func (s *grpcServer) serviceResourceQuery(ctx context.Context, req *pb.StatSummaryRequest) resourceResult {
+
+	rows := make([]*pb.StatTable_PodGroup_Row, 0)
+	dstBasicStats := make(map[dstKey]*pb.BasicStats)
+	dstTCPStats := make(map[dstKey]*pb.TcpStats)
+
+	if !req.SkipStats {
+		var err error
+		dstBasicStats, dstTCPStats, err = s.getServiceMetrics(ctx, req, req.TimeWindow)
+		if err != nil {
+			return resourceResult{res: nil, err: err}
+		}
+	}
+
+	weights := make(map[dstKey]string)
+	for k := range dstBasicStats {
+		weights[k] = ""
+	}
+
+	name := req.GetSelector().GetResource().GetName()
+	namespace := req.GetSelector().GetResource().GetNamespace()
+
+	// Check if a ServiceProfile exists for the Service
+	spName := fmt.Sprintf("%s.%s.svc.%s", name, namespace, s.clusterDomain)
+	sp, err := s.k8sAPI.SP().Lister().ServiceProfiles(namespace).Get(spName)
+	if err == nil {
+		for _, weightedDst := range sp.Spec.DstOverrides {
+			weights[dstKey{
+				Namespace: namespace,
+				Service:   name,
+				Dst:       dstFromAuthority(weightedDst.Authority),
+			}] = weightedDst.Weight.String()
+		}
+	} else if !kerrors.IsNotFound(err) {
+		log.Errorf("Failed to get weights from ServiceProfile %s: %v", spName, err)
+	}
+
+	for k, weight := range weights {
+		row := pb.StatTable_PodGroup_Row{
+			Resource: &pb.Resource{
+				Name:      k.Service,
+				Namespace: k.Namespace,
+				Type:      req.GetSelector().GetResource().GetType(),
+			},
+			TimeWindow: req.TimeWindow,
+			Stats:      dstBasicStats[k],
+			TcpStats:   dstTCPStats[k],
+		}
+
+		// Set TrafficSplitStats only when weight is not empty
+		if weight != "" {
+			row.TsStats = &pb.TrafficSplitStats{
+				Apex:   k.Service,
+				Leaf:   k.Dst,
+				Weight: weight,
+			}
+		}
+		rows = append(rows, &row)
+	}
+
+	// sort rows before returning in order to have a consistent order for tests
+	rows = sortTrafficSplitRows(rows)
+
+	rsp := pb.StatTable{
+		Table: &pb.StatTable_PodGroup_{
+			PodGroup: &pb.StatTable_PodGroup{
+				Rows: rows,
+			},
+		},
+	}
+
+	return resourceResult{res: &rsp, err: nil}
+}
+
 func sortTrafficSplitRows(rows []*pb.StatTable_PodGroup_Row) []*pb.StatTable_PodGroup_Row {
 	sort.Slice(rows, func(i, j int) bool {
-		key1 := rows[i].TsStats.Apex + rows[i].TsStats.Leaf
-		key2 := rows[j].TsStats.Apex + rows[j].TsStats.Leaf
-		return key1 < key2
+		if rows[i].TsStats != nil && rows[j].TsStats != nil {
+			key1 := rows[i].TsStats.Apex + rows[i].TsStats.Leaf
+			key2 := rows[j].TsStats.Apex + rows[j].TsStats.Leaf
+			return key1 < key2
+		}
+		return false
 	})
 	return rows
 }
@@ -464,28 +548,30 @@ func buildRequestLabels(req *pb.StatSummaryRequest) (labels model.LabelSet, labe
 	return
 }
 
-func buildTrafficSplitRequestLabels(req *pb.StatSummaryRequest) (labels model.LabelSet, labelNames model.LabelNames) {
-	// Trafficsplit labels are always direction="outbound". If the --from or --to flags were used,
-	// we merge an additional ToResource or FromResource label. Trafficsplit metrics results are
-	// always grouped by dst_service.
-	// N.b. requests to a traffic split may come from any namespace so we do not do any filtering
-	// by namespace.
+func buildServiceRequestLabels(req *pb.StatSummaryRequest) (labels model.LabelSet, labelNames model.LabelNames) {
+	// Service Request labels are always direction="outbound". If the --from or --to flags were used,
+	// we merge an additional ToResource or FromResource label. Service metrics results are
+	// always grouped by dst_service, and dst_namespace (to avoid conflicts) .
 	labels = model.LabelSet{
 		"direction": model.LabelValue("outbound"),
 	}
 
 	switch out := req.Outbound.(type) {
 	case *pb.StatSummaryRequest_ToResource:
+		// if --to flag is passed, Calculate traffic sent to the service
+		// with additional filtering narrowing down to the workload
+		// it is sent to.
 		labels = labels.Merge(promDstQueryLabels(out.ToResource))
 
 	case *pb.StatSummaryRequest_FromResource:
+		// if --from flag is passed, FromResource is never a service here
 		labels = labels.Merge(promQueryLabels(out.FromResource))
 
 	default:
 		// no extra labels needed
 	}
 
-	groupBy := model.LabelNames{model.LabelName("dst_service")}
+	groupBy := model.LabelNames{model.LabelName("dst_namespace"), model.LabelName("dst_service")}
 
 	return labels, groupBy
 }
@@ -530,26 +616,23 @@ func (s *grpcServer) getStatMetrics(ctx context.Context, req *pb.StatSummaryRequ
 
 func (s *grpcServer) getTrafficSplitMetrics(ctx context.Context, req *pb.StatSummaryRequest, tsStats *trafficSplitStats, timeWindow string) (map[tsKey]*pb.BasicStats, error) {
 	tsBasicStats := make(map[tsKey]*pb.BasicStats)
-	labels, groupBy := buildTrafficSplitRequestLabels(req)
+	labels, groupBy := buildServiceRequestLabels(req)
 
 	apex := tsStats.apex
 	namespace := tsStats.namespace
 	// TODO: add cluster domain to stringToMatch
 	stringToMatch := fmt.Sprintf("%s.%s.svc", apex, namespace)
 
-	reqLabels := generateLabelStringWithRegex(labels, "authority", stringToMatch)
+	reqLabels := generateLabelStringWithRegex(labels, string(authorityLabel), stringToMatch)
 
 	promQueries := map[promType]string{
 		promRequests: fmt.Sprintf(reqQuery, reqLabels, timeWindow, groupBy.String()),
 	}
-
 	quantileQueries := generateQuantileQueries(latencyQuantileQuery, reqLabels, timeWindow, groupBy.String())
 	results, err := s.getPrometheusMetrics(ctx, promQueries, quantileQueries)
-
 	if err != nil {
 		return nil, err
 	}
-
 	basicStats, _ := processPrometheusMetrics(req, results, groupBy) // we don't need tcpStat info for traffic split
 
 	for rKey, basicStatsVal := range basicStats {
@@ -564,6 +647,75 @@ func (s *grpcServer) getTrafficSplitMetrics(ctx context.Context, req *pb.StatSum
 	return tsBasicStats, nil
 }
 
+func (s *grpcServer) getServiceMetrics(ctx context.Context, req *pb.StatSummaryRequest, timeWindow string) (map[dstKey]*pb.BasicStats, map[dstKey]*pb.TcpStats, error) {
+	dstBasicStats := make(map[dstKey]*pb.BasicStats)
+	dstTCPStats := make(map[dstKey]*pb.TcpStats)
+	labels, groupBy := buildServiceRequestLabels(req)
+
+	service := req.GetSelector().GetResource().GetName()
+	namespace := req.GetSelector().GetResource().GetNamespace()
+
+	if service == "" {
+		service = regexAny
+	}
+	authority := fmt.Sprintf("%s.%s.svc.%s", service, namespace, s.clusterDomain)
+
+	reqLabels := generateLabelStringWithRegex(labels, string(authorityLabel), authority)
+
+	promQueries := map[promType]string{
+		promRequests: fmt.Sprintf(reqQuery, reqLabels, timeWindow, groupBy.String()),
+	}
+
+	if req.TcpStats {
+		// Service stats always need to have `peer=dst`, cuz there is no `src` with `authority` label
+		tcpLabels := labels.Merge(promPeerLabel("dst"))
+		tcpLabelString := generateLabelStringWithRegex(tcpLabels, string(authorityLabel), authority)
+		promQueries[promTCPConnections] = fmt.Sprintf(tcpConnectionsQuery, tcpLabelString, groupBy.String())
+		promQueries[promTCPReadBytes] = fmt.Sprintf(tcpReadBytesQuery, tcpLabelString, timeWindow, groupBy.String())
+		promQueries[promTCPWriteBytes] = fmt.Sprintf(tcpWriteBytesQuery, tcpLabelString, timeWindow, groupBy.String())
+	}
+
+	quantileQueries := generateQuantileQueries(latencyQuantileQuery, reqLabels, timeWindow, groupBy.String())
+	results, err := s.getPrometheusMetrics(ctx, promQueries, quantileQueries)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	basicStats, tcpStats := processPrometheusMetrics(req, results, groupBy)
+
+	for rKey, basicStatsVal := range basicStats {
+
+		// Use the returned `dst_service` in the `all` svc case
+		svcName := service
+		if svcName == regexAny {
+			svcName = rKey.Name
+		}
+
+		dstBasicStats[dstKey{
+			Namespace: rKey.Namespace,
+			Service:   svcName,
+			Dst:       rKey.Name,
+		}] = basicStatsVal
+	}
+
+	for rKey, tcpStatsVal := range tcpStats {
+
+		// Use the returned `dst_service` in the `all` svc case
+		svcName := service
+		if svcName == regexAny {
+			svcName = rKey.Name
+		}
+
+		dstTCPStats[dstKey{
+			Namespace: rKey.Namespace,
+			Service:   svcName,
+			Dst:       rKey.Name,
+		}] = tcpStatsVal
+	}
+
+	return dstBasicStats, dstTCPStats, nil
+}
+
 func processPrometheusMetrics(req *pb.StatSummaryRequest, results []promResult, groupBy model.LabelNames) (map[rKey]*pb.BasicStats, map[rKey]*pb.TcpStats) {
 	basicStats := make(map[rKey]*pb.BasicStats)
 	tcpStats := make(map[rKey]*pb.TcpStats)
@@ -717,3 +869,13 @@ func getLabelSelector(req *pb.StatSummaryRequest) (labels.Selector, error) {
 	}
 	return labelSelector, nil
 }
+
+func dstFromAuthority(authority string) string {
+	// name.namespace.svc.suffix
+	labels := strings.Split(authority, ".")
+	if len(labels) >= 3 && labels[2] == "svc" {
+		// name
+		return labels[0]
+	}
+	return authority
+}
diff --git a/web/app/package.json b/web/app/package.json
index 2fba2fb94..ca761c877 100644
--- a/web/app/package.json
+++ b/web/app/package.json
@@ -16,7 +16,7 @@
     "@material-ui/icons": "4.11.2",
     "@material-ui/lab": "^4.0.0-alpha.60",
     "classnames": "2.3.1",
-    "core-js": "^3.16.0",
+    "core-js": "^3.16.1",
     "css-mediaquery": "^0.1.2",
     "d3-drag": "3.0.0",
     "d3-force": "3.0.0",
@@ -42,7 +42,7 @@
   },
   "devDependencies": {
     "@babel/core": "^7.15.0",
-    "@babel/eslint-parser": "^7.14.9",
+    "@babel/eslint-parser": "^7.15.0",
     "@babel/plugin-proposal-class-properties": "^7.13.0",
     "@babel/preset-env": "^7.15.0",
     "@babel/preset-react": "^7.14.5",
@@ -61,7 +61,7 @@
     "enzyme-adapter-react-16": "^1.15.6",
     "eslint": "^7.32.0",
     "eslint-config-airbnb": "^18.2.1",
-    "eslint-plugin-import": "^2.23.4",
+    "eslint-plugin-import": "^2.24.0",
     "eslint-plugin-jsx-a11y": "^6.4.1",
     "eslint-plugin-promise": "^5.1.0",
     "eslint-plugin-react": "^7.23.1",
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index be0ce9c5b..ba48615cf 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -42,10 +42,10 @@
     semver "^6.3.0"
     source-map "^0.5.0"
 
-"@babel/eslint-parser@^7.14.9":
-  version "7.14.9"
-  resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.14.9.tgz#b2cfaf67cc7f7a9b475d3ee9b166755ffd44e086"
-  integrity sha512-W3En2CT6iOZJ+jEujLlwL2h/Jraj6S/IHrh63nV+TfX83DYe9pI4pcu0Luj6/hJ2kIr/3hOttxzaEsSQnUKx+w==
+"@babel/eslint-parser@^7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.15.0.tgz#b54f06e04d0e93aebcba99f89251e3bf0ee39f21"
+  integrity sha512-+gSPtjSBxOZz4Uh8Ggqu7HbfpB8cT1LwW0DnVVLZEJvzXauiD0Di3zszcBkRmfGGrLdYeHUwcflG7i3tr9kQlw==
   dependencies:
     eslint-scope "^5.1.1"
     eslint-visitor-keys "^2.1.0"
@@ -3142,10 +3142,10 @@ core-js-pure@^3.15.0:
   resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.15.1.tgz#8356f6576fa2aa8e54ca6fe02620968ff010eed7"
   integrity sha512-OZuWHDlYcIda8sJLY4Ec6nWq2hRjlyCqCZ+jCflyleMkVt3tPedDVErvHslyS2nbO+SlBFMSBJYvtLMwxnrzjA==
 
-core-js@^3.16.0:
-  version "3.16.0"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.16.0.tgz#1d46fb33720bc1fa7f90d20431f36a5540858986"
-  integrity sha512-5+5VxRFmSf97nM8Jr2wzOwLqRo6zphH2aX+7KsAUONObyzakDNq2G/bgbhinxB4PoV9L3aXQYhiDKyIKWd2c8g==
+core-js@^3.16.1:
+  version "3.16.1"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.16.1.tgz#f4485ce5c9f3c6a7cb18fa80488e08d362097249"
+  integrity sha512-AAkP8i35EbefU+JddyWi12AWE9f2N/qr/pwnDtWz4nyUIBGMJPX99ANFFRSw6FefM374lDujdtLDyhN2A/btHw==
 
 core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
@@ -3914,33 +3914,33 @@ eslint-config-airbnb@^18.2.1:
     object.assign "^4.1.2"
     object.entries "^1.1.2"
 
-eslint-import-resolver-node@^0.3.4:
-  version "0.3.4"
-  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
-  integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==
+eslint-import-resolver-node@^0.3.5:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.5.tgz#939bbb0f74e179e757ca87f7a4a890dabed18ac4"
+  integrity sha512-XMoPKjSpXbkeJ7ZZ9icLnJMTY5Mc1kZbCakHquaFsXPpyWOwK0TK6CODO+0ca54UoM9LKOxyUNnoVZRl8TeaAg==
   dependencies:
-    debug "^2.6.9"
-    resolve "^1.13.1"
+    debug "^3.2.7"
+    resolve "^1.20.0"
 
-eslint-module-utils@^2.6.1:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.1.tgz#b51be1e473dd0de1c5ea638e22429c2490ea8233"
-  integrity sha512-ZXI9B8cxAJIH4nfkhTwcRTEAnrVfobYqwjWy/QMCZ8rHkZHFjf9yO4BzpiF9kCSfNlMG54eKigISHpX0+AaT4A==
+eslint-module-utils@^2.6.2:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz#94e5540dd15fe1522e8ffa3ec8db3b7fa7e7a534"
+  integrity sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q==
   dependencies:
     debug "^3.2.7"
     pkg-dir "^2.0.0"
 
-eslint-plugin-import@^2.23.4:
-  version "2.23.4"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.23.4.tgz#8dceb1ed6b73e46e50ec9a5bb2411b645e7d3d97"
-  integrity sha512-6/wP8zZRsnQFiR3iaPFgh5ImVRM1WN5NUWfTIRqwOdeiGJlBcSk82o1FEVq8yXmy4lkIzTo7YhHCIxlU/2HyEQ==
+eslint-plugin-import@^2.24.0:
+  version "2.24.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.24.0.tgz#697ffd263e24da5e84e03b282f5fb62251777177"
+  integrity sha512-Kc6xqT9hiYi2cgybOc0I2vC9OgAYga5o/rAFinam/yF/t5uBqxQbauNPMC6fgb640T/89P0gFoO27FOilJ/Cqg==
   dependencies:
     array-includes "^3.1.3"
     array.prototype.flat "^1.2.4"
     debug "^2.6.9"
     doctrine "^2.1.0"
-    eslint-import-resolver-node "^0.3.4"
-    eslint-module-utils "^2.6.1"
+    eslint-import-resolver-node "^0.3.5"
+    eslint-module-utils "^2.6.2"
     find-up "^2.0.0"
     has "^1.0.3"
     is-core-module "^2.4.0"
@@ -8113,7 +8113,7 @@ resolve-url@^0.2.1:
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
   integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
 
-resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.9.0:
+resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.9.0:
   version "1.20.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
   integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==


Test patch:
diff --git a/test/integration/stat/stat_test.go b/test/integration/stat/stat_test.go
index 8beb724e0..9dea7f5c8 100644
--- a/test/integration/stat/stat_test.go
+++ b/test/integration/stat/stat_test.go
@@ -118,6 +118,12 @@ func TestCliStatForLinkerdNamespace(t *testing.T) {
 				prometheusAuthority: "-",
 			},
 		},
+		{
+			args: []string{"viz", "stat", "au", "-n", TestHelper.GetVizNamespace(), "--to", fmt.Sprintf("po/%s", prometheusPod), "--to-namespace", prometheusNamespace},
+			expectedRows: map[string]string{
+				prometheusAuthority: "-",
+			},
+		},
 	}
 
 	if !TestHelper.ExternalPrometheus() {
@@ -146,7 +152,7 @@ func TestCliStatForLinkerdNamespace(t *testing.T) {
 			{
 				args: []string{"viz", "stat", "svc", "prometheus", "-n", TestHelper.GetVizNamespace(), "--from", "deploy/metrics-api", "--from-namespace", TestHelper.GetVizNamespace()},
 				expectedRows: map[string]string{
-					"prometheus": "1/1",
+					"prometheus": "-",
 				},
 			},
 		}...,
@@ -177,42 +183,87 @@ func TestCliStatForLinkerdNamespace(t *testing.T) {
 		)
 	}
 
-	for _, tt := range testCases {
-		tt := tt // pin
-		timeout := 20 * time.Second
-		t.Run("linkerd "+strings.Join(tt.args, " "), func(t *testing.T) {
-			err := TestHelper.RetryFor(timeout, func() error {
-				// Use a short time window so that transient errors at startup
-				// fall out of the window.
-				tt.args = append(tt.args, "-t", "30s")
-				out, err := TestHelper.LinkerdRun(tt.args...)
-				if err != nil {
-					testutil.AnnotatedFatalf(t, "unexpected stat error",
-						"unexpected stat error: %s\n%s", err, out)
-				}
+	// Apply a sample application
+	TestHelper.WithDataPlaneNamespace(ctx, "stat-test", map[string]string{}, t, func(t *testing.T, prefixedNs string) {
+		out, err := TestHelper.LinkerdRun("inject", "--manual", "../trafficsplit/testdata/application.yaml")
+		if err != nil {
+			testutil.AnnotatedFatal(t, "'linkerd inject' command failed", err)
+		}
 
-				expectedColumnCount := 8
-				if tt.status != "" {
-					expectedColumnCount++
-				}
-				rowStats, err := testutil.ParseRows(out, len(tt.expectedRows), expectedColumnCount)
-				if err != nil {
-					return err
+		out, err = TestHelper.KubectlApply(out, prefixedNs)
+		if err != nil {
+			testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+				"'kubectl apply' command failed\n%s", out)
+		}
+
+		// wait for deployments to start
+		for _, deploy := range []string{"backend", "failing", "slow-cooker"} {
+			if err := TestHelper.CheckPods(ctx, prefixedNs, deploy, 1); err != nil {
+				if rce, ok := err.(*testutil.RestartCountError); ok {
+					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
+				} else {
+					testutil.AnnotatedError(t, "CheckPods timed-out", err)
 				}
+			}
+		}
+
+		testCases = append(testCases, []struct {
+			args         []string
+			expectedRows map[string]string
+			status       string
+		}{
+			{
+				args: []string{"viz", "stat", "svc", "-n", prefixedNs},
+				expectedRows: map[string]string{
+					"backend-svc": "-",
+				},
+			},
+			{
+				args: []string{"viz", "stat", "svc/backend-svc", "-n", prefixedNs, "--from", "deploy/slow-cooker"},
+				expectedRows: map[string]string{
+					"backend-svc": "-",
+				},
+			},
+		}...,
+		)
 
-				for name, meshed := range tt.expectedRows {
-					if err := validateRowStats(name, meshed, tt.status, rowStats); err != nil {
+		for _, tt := range testCases {
+			tt := tt // pin
+			timeout := 20 * time.Second
+			t.Run("linkerd "+strings.Join(tt.args, " "), func(t *testing.T) {
+				err := TestHelper.RetryFor(timeout, func() error {
+					// Use a short time window so that transient errors at startup
+					// fall out of the window.
+					tt.args = append(tt.args, "-t", "30s")
+					out, err := TestHelper.LinkerdRun(tt.args...)
+					if err != nil {
+						testutil.AnnotatedFatalf(t, "unexpected stat error",
+							"unexpected stat error: %s\n%s", err, out)
+					}
+
+					expectedColumnCount := 8
+					if tt.status != "" {
+						expectedColumnCount++
+					}
+					rowStats, err := testutil.ParseRows(out, len(tt.expectedRows), expectedColumnCount)
+					if err != nil {
 						return err
 					}
-				}
 
-				return nil
+					for name, meshed := range tt.expectedRows {
+						if err := validateRowStats(name, meshed, tt.status, rowStats); err != nil {
+							return err
+						}
+					}
+
+					return nil
+				})
+				if err != nil {
+					testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out checking stats (%s)", timeout), err)
+				}
 			})
-			if err != nil {
-				testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out checking stats (%s)", timeout), err)
-			}
-		})
-	}
+		}
+	})
 }
 
 func validateRowStats(name, expectedMeshCount, expectedStatus string, rowStats map[string]*testutil.RowStat) error {
diff --git a/viz/metrics-api/prometheus_test.go b/viz/metrics-api/prometheus_test.go
new file mode 100644
index 000000000..ec4388479
--- /dev/null
+++ b/viz/metrics-api/prometheus_test.go
@@ -0,0 +1,14 @@
+package api
+
+import (
+	"testing"
+
+	"github.com/prometheus/common/model"
+)
+
+func TestGenerateLabelStringWithRegex(t *testing.T) {
+	query := generateLabelStringWithRegex(model.LabelSet{}, "key", "value")
+	if query != "{key=~\"^value.*\"}" {
+		t.Errorf("Expected 'key=~\"^value.+\"', got '%s'", query)
+	}
+}
diff --git a/viz/metrics-api/stat_summary_test.go b/viz/metrics-api/stat_summary_test.go
index 755e04a79..a0b93a17e 100644
--- a/viz/metrics-api/stat_summary_test.go
+++ b/viz/metrics-api/stat_summary_test.go
@@ -1406,13 +1406,16 @@ status:
 											Rows: []*pb.StatTable_PodGroup_Row{
 												{
 													Resource: &pb.Resource{
-														Namespace: "emojivoto",
-														Type:      pkgK8s.Service,
-														Name:      "emoji-svc",
+														Type: pkgK8s.Service,
+													},
+													TimeWindow: "1m",
+													Stats: &pb.BasicStats{
+														SuccessCount: 123,
+														FailureCount: 0,
+														LatencyMsP50: 123,
+														LatencyMsP95: 123,
+														LatencyMsP99: 123,
 													},
-													TimeWindow:      "1m",
-													MeshedPodCount:  1,
-													RunningPodCount: 1,
 												},
 											},
 										},
@@ -1514,21 +1517,37 @@ status:
 				req: &pb.StatSummaryRequest{
 					Selector: &pb.ResourceSelection{
 						Resource: &pb.Resource{
+							Type: pkgK8s.Pod,
+						},
+					},
+					Outbound: &pb.StatSummaryRequest_FromResource{
+						FromResource: &pb.Resource{
 							Type: pkgK8s.Service,
 						},
 					},
 				},
 			},
+		}
+
+		for _, invalid := range invalidRequests {
+			rsp, err := fakeGrpcServer.StatSummary(context.TODO(), invalid.req)
+
+			if err != nil || rsp.GetError() == nil {
+				t.Fatalf("Expected validation error on StatSummaryResponse, got %v, %v", rsp, err)
+			}
+		}
+
+		validRequests := []statSumExpected{
 			{
 				req: &pb.StatSummaryRequest{
 					Selector: &pb.ResourceSelection{
 						Resource: &pb.Resource{
-							Type: pkgK8s.Service,
+							Type: pkgK8s.Pod,
 						},
 					},
 					Outbound: &pb.StatSummaryRequest_ToResource{
 						ToResource: &pb.Resource{
-							Type: pkgK8s.Pod,
+							Type: pkgK8s.Service,
 						},
 					},
 				},
@@ -1537,37 +1556,21 @@ status:
 				req: &pb.StatSummaryRequest{
 					Selector: &pb.ResourceSelection{
 						Resource: &pb.Resource{
-							Type: pkgK8s.Pod,
-						},
-					},
-					Outbound: &pb.StatSummaryRequest_FromResource{
-						FromResource: &pb.Resource{
 							Type: pkgK8s.Service,
 						},
 					},
 				},
 			},
-		}
-
-		for _, invalid := range invalidRequests {
-			rsp, err := fakeGrpcServer.StatSummary(context.TODO(), invalid.req)
-
-			if err != nil || rsp.GetError() == nil {
-				t.Fatalf("Expected validation error on StatSummaryResponse, got %v, %v", rsp, err)
-			}
-		}
-
-		validRequests := []statSumExpected{
 			{
 				req: &pb.StatSummaryRequest{
 					Selector: &pb.ResourceSelection{
 						Resource: &pb.Resource{
-							Type: pkgK8s.Pod,
+							Type: pkgK8s.Service,
 						},
 					},
 					Outbound: &pb.StatSummaryRequest_ToResource{
 						ToResource: &pb.Resource{
-							Type: pkgK8s.Service,
+							Type: pkgK8s.Pod,
 						},
 					},
 				},