Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified demo/remetric.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 15 additions & 9 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ The terminal output looks like this:
▸ labelpattern... done (8ms)
▸ unusedmetrics... done (13ms)
▸ alerthygiene... done (7ms)
┌──────────┬────────────────────┬──────────┬────────┬────────┬────────────────┐
│ SEVERITY │ METRIC │ LABEL │ SERIES │ UNIQUE │ EST. REDUCTION │
├──────────┼────────────────────┼──────────┼────────┼────────┼────────────────┤
│ CRITICAL │ app_requests_total │ trace_id │ 500 │ 500 │ ~499 │
│ MEDIUM │ │ user_id │ 500 │ 500 │ ~499 │
│ MEDIUM │ │ trace_id │ 500 │ 500 │ ~499 │
│ MEDIUM │ │ │ 0 │ 0 │ ~0 │
└──────────┴────────────────────┴──────────┴────────┴────────┴────────────────┘
┌──────────┬───────────────────────────────┬────────────────────┬──────────┬────────┬────────┬────────────────┐
│ SEVERITY │ CLASS │ METRIC │ LABEL │ SERIES │ UNIQUE │ EST. REDUCTION │
├──────────┼───────────────────────────────┼────────────────────┼──────────┼────────┼────────┼────────────────┤
│ CRITICAL │ hot-label │ app_requests_total │ trace_id │ 500 │ 500 │ ~499 │
│ MEDIUM │ label-pattern-overly-granular │ │ user_id │ 500 │ 500 │ ~499 │
│ MEDIUM │ label-pattern-overly-granular │ │ trace_id │ 500 │ 500 │ ~499 │
│ MEDIUM │ never-firing-alert │ RemetricNeverFired │ │ 0 │ 0 │ ~0 │
└──────────┴───────────────────────────────┴────────────────────┴──────────┴────────┴────────┴────────────────┘

[CRITICAL] app_requests_total · trace_id has 500 unique values
Sample: trace-001f71cef44a4aca, trace-006ce2eaa75c9f65, trace-007fff8ea657221b, trace-0102b61d60431cf5, trace-0116b64ca9b86791
Expand All @@ -73,7 +73,13 @@ The output has three layers:
2. **Severity table** - at-a-glance ranking. Columns:
- `SEVERITY` - `CRITICAL` / `HIGH` / `MEDIUM` / `LOW`, computed from
observed series counts, uniqueness ratios, and lookback windows.
- `METRIC` / `LABEL` - the entity that triggered the finding.
- `CLASS` - finding class slug (e.g. `hot-label`, `never-firing-alert`);
each class has a dedicated documentation page at
`remetric.dev/findings/<class>`.
- `METRIC` - the metric the finding is about. For label-only findings
this is empty (the issue is the label across many metrics). For alert
findings the alert rule name appears here.
- `LABEL` - the offending label (when applicable).
- `SERIES` - total series in the metric.
- `UNIQUE` - unique values seen on the offending label.
- `EST. REDUCTION` - upper-bound series saved if you apply the fix.
Expand Down
12 changes: 10 additions & 2 deletions internal/output/terminal/findings.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,20 @@ func (r *Renderer) RenderFindings(fs []findings.Finding) error {
tbl.SetOutputMirror(r.w)
tbl.SetStyle(table.StyleLight)
tbl.Style().Format.Header = text.FormatUpper
tbl.AppendHeader(table.Row{"Severity", "Metric", "Label", "Series", "Unique", "Est. Reduction"})
tbl.AppendHeader(table.Row{"Severity", "Class", "Metric", "Label", "Series", "Unique", "Est. Reduction"})
for _, f := range sorted {
sev := r.st.severity(f.Severity).Render(f.Severity.String())
// Alert findings have no metric, so we display the alert rule name
// in the Metric column. The Class column already disambiguates the
// finding type, so the column header stays "Metric".
metric := f.Metric
if metric == "" && f.Alert != "" {
metric = f.Alert
}
tbl.AppendRow(table.Row{
sev,
truncate(f.Metric, 28),
truncate(f.Class, 30),
truncate(metric, 28),
truncate(f.Evidence.Label, 24),
fmtInt(f.Evidence.SeriesCount),
fmtInt(int64(f.Evidence.UniqueValues)),
Expand Down
50 changes: 50 additions & 0 deletions internal/output/terminal/findings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func sampleFindings() []findings.Finding {
{
ID: "card-istio_requests_total-destination_principal",
Severity: findings.SeverityCritical, Category: findings.CategoryCardinality,
Class: findings.ClassHotLabel,
Title: "high cardinality in istio_requests_total due to label \"destination_principal\"",
Metric: "istio_requests_total",
Evidence: findings.Evidence{
Expand All @@ -53,6 +54,7 @@ func sampleFindings() []findings.Finding {
{
ID: "card-etcd_request_duration_seconds_bucket-grpc_method",
Severity: findings.SeverityHigh, Category: findings.CategoryCardinality,
Class: findings.ClassHotLabel,
Title: "high cardinality in etcd_request_duration_seconds_bucket due to label \"grpc_method\"",
Metric: "etcd_request_duration_seconds_bucket",
Evidence: findings.Evidence{
Expand All @@ -65,6 +67,7 @@ func sampleFindings() []findings.Finding {
{
ID: "card-node_filesystem_size_bytes-device",
Severity: findings.SeverityMedium, Category: findings.CategoryCardinality,
Class: findings.ClassHotLabel,
Title: "high cardinality in node_filesystem_size_bytes due to label \"device\"",
Metric: "node_filesystem_size_bytes",
Evidence: findings.Evidence{
Expand Down Expand Up @@ -111,6 +114,53 @@ func TestRenderFindings_FixBlockOnlyForCritHigh(t *testing.T) {
}
}

func TestRenderFindings_ClassColumnAndAlertFallback(t *testing.T) {
fs := []findings.Finding{
{
ID: "card-app_requests_total-trace_id",
Severity: findings.SeverityCritical,
Category: findings.CategoryCardinality,
Class: findings.ClassHotLabel,
Metric: "app_requests_total",
Evidence: findings.Evidence{Label: "trace_id", UniqueValues: 500, SeriesCount: 500},
Impact: findings.Impact{SeriesReduction: 499},
},
{
ID: "alert_hygiene/never_fired",
Severity: findings.SeverityMedium,
Category: findings.CategoryAlertHygiene,
Class: findings.ClassNeverFiringAlert,
Alert: "HighRequestLatencyP99",
// Metric and Evidence.Label intentionally empty - alert
// findings live in their own entity space.
},
}

var buf bytes.Buffer
r := New(&buf, WithColor(false))
if err := r.RenderFindings(fs); err != nil {
t.Fatalf("RenderFindings: %v", err)
}
out := buf.String()

// Class column header rendered.
if !strings.Contains(out, "CLASS") {
t.Errorf("expected CLASS column header in output:\n%s", out)
}
// Class slug appears in the cardinality row.
if !strings.Contains(out, "hot-label") {
t.Errorf("expected 'hot-label' class slug in row:\n%s", out)
}
// Class slug appears in the alert row.
if !strings.Contains(out, "never-firing-alert") {
t.Errorf("expected 'never-firing-alert' class slug in row:\n%s", out)
}
// Alert name renders in the Metric column (fallback).
if !strings.Contains(out, "HighRequestLatencyP99") {
t.Errorf("expected alert name in Metric column (fallback when Metric empty):\n%s", out)
}
}

func TestRenderFindings_LabelPatternFixBlock(t *testing.T) {
f := findings.Finding{
ID: "label-user_id",
Expand Down
14 changes: 7 additions & 7 deletions internal/output/terminal/testdata/findings_all_severities.golden
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
┌──────────┬──────────────────────────────┬───────────────────────┬─────────┬────────┬────────────────┐
│ SEVERITY │ METRIC │ LABEL │ SERIES │ UNIQUE │ EST. REDUCTION │
├──────────┼──────────────────────────────┼───────────────────────┼─────────┼────────┼────────────────┤
│ CRITICAL │ istio_requests_total │ destination_principal │ 487,234 │ 5,234 │ ~487,141 │
│ HIGH │ etcd_request_duration_secon… │ grpc_method │ 124,540 │ 148 │ ~123,699 │
│ MEDIUM │ node_filesystem_size_bytes │ device │ 31,200 │ 52 │ ~30,600 │
└──────────┴──────────────────────────────┴───────────────────────┴─────────┴────────┴────────────────┘
┌──────────┬───────────┬──────────────────────────────┬───────────────────────┬─────────┬────────┬────────────────┐
│ SEVERITY │ CLASS │ METRIC │ LABEL │ SERIES │ UNIQUE │ EST. REDUCTION │
├──────────┼───────────┼──────────────────────────────┼───────────────────────┼─────────┼────────┼────────────────┤
│ CRITICAL │ hot-label │ istio_requests_total │ destination_principal │ 487,234 │ 5,234 │ ~487,141 │
│ HIGH │ hot-label │ etcd_request_duration_secon… │ grpc_method │ 124,540 │ 148 │ ~123,699 │
│ MEDIUM │ hot-label │ node_filesystem_size_bytes │ device │ 31,200 │ 52 │ ~30,600 │
└──────────┴───────────┴──────────────────────────────┴───────────────────────┴─────────┴────────┴────────────────┘

[CRITICAL] istio_requests_total · destination_principal has 5,234 unique values
Sample: spiffe://orders, spiffe://payments
Expand Down