Repository: linkerd/linkerd2
Pull request #6633 (2021-08-09 17:04:57)

Bump eslint-plugin-import from 2.23.4 to 2.24.0 in /web/app
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.23.4 to 2.24.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/import-js/eslint-plugin-import/blob/master/CHANGELOG.md">eslint-plugin-import's changelog</a>.</em></p>
<blockquote>
<h2>[2.24.0] - 2021-08-08</h2>
<h3>Added</h3>
<ul>
<li>[<code>no-dynamic-require</code>]: add option <code>esmodule</code> (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/1223">#1223</a>, thanks [<a href="https://github.com/vikr01"><code>@​vikr01</code></a>])</li>
<li>[<code>named</code>]: add <code>commonjs</code> option (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/1222">#1222</a>, thanks [<a href="https://github.com/vikr01"><code>@​vikr01</code></a>])</li>
<li>[<code>no-namespace</code>]: Add <code>ignore</code> option (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/2112">#2112</a>, thanks [<a href="https://github.com/aberezkin"><code>@​aberezkin</code></a>])</li>
<li>[<code>max-dependencies</code>]: add option <code>ignoreTypeImports</code> (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/1847">#1847</a>, thanks [<a href="https://github.com/rfermann"><code>@​rfermann</code></a>])</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>[<code>no-duplicates</code>]: ensure autofix avoids excessive newlines (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/2028">#2028</a>, thanks [<a href="https://github.com/ertrzyiks"><code>@​ertrzyiks</code></a>])</li>
<li>[<code>extensions</code>]: avoid crashing on partially typed import/export statements (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/2118">#2118</a>, thanks [<a href="https://github.com/ljharb"><code>@​ljharb</code></a>])</li>
<li>[<code>no-extraneous-dependencies</code>]: add ESM intermediate package.json support] (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/2121">#2121</a>, thanks [<a href="https://github.com/paztis"><code>@​paztis</code></a>])</li>
<li>Use <code>context.getPhysicalFilename()</code> when available (ESLint 7.28+) (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/2160">#2160</a>, thanks [<a href="https://github.com/pmcelhaney"><code>@​pmcelhaney</code></a>])</li>
<li>[<code>extensions</code>]/<code>importType</code>: fix isScoped treating @/abc as scoped module (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/2146">#2146</a>, thanks [<a href="https://github.com/rperello"><code>@​rperello</code></a>])</li>
</ul>
<h3>Changed</h3>
<ul>
<li>[Docs] [<code>extensions</code>]: removed incorrect cases (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/2138">#2138</a>, thanks [<a href="https://github.com/wenfangdu"><code>@​wenfangdu</code></a>])</li>
<li>[Tests] [<code>order</code>]: add tests for <code>pathGroupsExcludedImportTypes: ['type']</code> (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/2158">#2158</a>, thanks [<a href="https://github.com/atav32"><code>@​atav32</code></a>])</li>
<li>[Docs] [<code>order</code>]:  improve the documentation for the <code>pathGroupsExcludedImportTypes</code> option (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/2156">#2156</a>, thanks [<a href="https://github.com/liby"><code>@​liby</code></a>])</li>
<li>[Tests] [<code>no-cycle</code>]: Restructure test files (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/1517">#1517</a>, thanks [<a href="https://github.com/soryy708"><code>@​soryy708</code></a>])</li>
<li>[Docs] add description how to use plugin with yarn berry (<a href="https://github-redirect.dependabot.com/import-js/eslint-plugin-import/issues/2179">#2179</a>, thanks [<a href="https://github.com/KostyaZgara"><code>@​KostyaZgara</code></a>])</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/import-js/eslint-plugin-import/commit/202e5e02db46dfda897ec6952d0a73fea1f23bab"><code>202e5e0</code></a> Bump to v2.24.0</li>
<li><a href="https://github.com/import-js/eslint-plugin-import/commit/546f87f6c7d87a3cbdff7c8c6d0b36729ec9fa8d"><code>546f87f</code></a> utils: v2.6.2</li>
<li><a href="https://github.com/import-js/eslint-plugin-import/commit/74fa6c99cc12f79a25cc33e6819b6073344998f1"><code>74fa6c9</code></a> [resolvers/node] v0.3.5</li>
<li><a href="https://github.com/import-js/eslint-plugin-import/commit/32bf64556329dd61ef959fa7cc17e547d8031cf8"><code>32bf645</code></a> [Docs] add description how to use plugin with yarn berry</li>
<li><a href="https://github.com/import-js/eslint-plugin-import/commit/794e869757d047deafab35b38c9552bb42f9e835"><code>794e869</code></a> [meta] update repo URLs</li>
<li><a href="https://github.com/import-js/eslint-plugin-import/commit/429f3f60238356d1d6c1019be988bccad7def0e0"><code>429f3f6</code></a> [Tests] <code>no-cycle</code>: Restructure test files</li>
<li><a href="https://github.com/import-js/eslint-plugin-import/commit/b743a650e9d474fcd5a4252b01c522adcc11b65c"><code>b743a65</code></a> [New] <code>max-dependencies</code>: add option <code>ignoreTypeImports</code></li>
<li><a href="https://github.com/import-js/eslint-plugin-import/commit/bba59c4848cdd31461a9f45ebe79865d28cd3f5f"><code>bba59c4</code></a> [New] <code>no-namespace</code>: Add <code>ignore</code> option</li>
<li><a href="https://github.com/import-js/eslint-plugin-import/commit/54d86c8a64dc21c14088fcdd8fc3935206e8347a"><code>54d86c8</code></a> [New] <code>named</code>: add <code>commonjs</code> option</li>
<li><a href="https://github.com/import-js/eslint-plugin-import/commit/7626a148510cccedd1e4583111b92cd14bfcb630"><code>7626a14</code></a> [Refactor] <code>named</code>: clean up formatting</li>
<li>Additional commits viewable in <a href="https://github.com/import-js/eslint-plugin-import/compare/v2.23.4...v2.24.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=eslint-plugin-import&package-manager=npm_and_yarn&previous-version=2.23.4&new-version=2.24.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
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,
 						},
 					},
 				},