feat(metrics): Add EC rebuild/reconstruct Prometheus metrics (#10124)

* Review comment removed unnecessary success and failure count

* fix: use Gather.Gather() with seeded counter for EC rebuild registration test

- Restore Gather.Gather() to verify MustRegister calls as requested in review
- Seed VolumeServerECRebuildCounter before gathering because CounterVec
  only appears after at least one label value is observed
- Use correct fully-qualified metric names (SeaweedFS_volumeServer_*)

* fix: remove preflight checkEcVolumeStatus failure from ec_rebuild_total counter

ec_rebuild_total should only reflect actual rebuild execution failures
(from RebuildEcFiles / RebuildEcxFile), not scan/precheck failures in
the volume status loop. The error is still returned to the caller;
only the misleading counter increment was removed.

* Review comment removed unnecessary observe

* label EC rebuild duration histogram by result

Without a result label, fast failures pull down the success-latency
quantiles shown on the EC Rebuild Duration panel. Make the histogram a
HistogramVec keyed by result, record success/failure through one
recordEcRebuild helper, and split the Grafana quantiles by (le, result).

* reset EC rebuild metric vecs in registration test

The HistogramVec needs a child before Gather emits it, so the test must
observe once; reset both vecs in cleanup so that sample doesn't leak into
other tests.

---------

Co-authored-by: Ubuntu User <ubuntu@example.com>
Co-authored-by: Chris Lu <chris.lu@gmail.com>
This commit is contained in:
Rushikesh Deshpande
2026-06-27 22:01:36 -07:00
committed by GitHub
co-authored by Ubuntu User Chris Lu
parent 57ffef8543
commit d0db94c34a
4 changed files with 307 additions and 3 deletions
+214
View File
@@ -4362,6 +4362,220 @@
}
],
"pluginVersion": "10.3.1"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"title": "EC Rebuild Duration",
"description": "p50/p95/p99 duration of EC shard rebuild operations",
"type": "timeseries",
"id": 217,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 126
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 4,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"unit": "s",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.99, sum(rate(SeaweedFS_volumeServer_ec_rebuild_seconds_bucket{cluster=~\"$cluster\"}[$__rate_interval])) by (le, result))",
"range": true,
"refId": "A",
"legendFormat": "p99 {{result}}"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.95, sum(rate(SeaweedFS_volumeServer_ec_rebuild_seconds_bucket{cluster=~\"$cluster\"}[$__rate_interval])) by (le, result))",
"range": true,
"refId": "B",
"legendFormat": "p95 {{result}}"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.50, sum(rate(SeaweedFS_volumeServer_ec_rebuild_seconds_bucket{cluster=~\"$cluster\"}[$__rate_interval])) by (le, result))",
"range": true,
"refId": "C",
"legendFormat": "p50 {{result}}"
}
],
"pluginVersion": "10.3.1"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"title": "EC Rebuild Operations",
"description": "Rate of EC shard rebuild operations by result",
"type": "timeseries",
"id": 218,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 126
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 4,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"unit": "ops",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum by (result) (rate(SeaweedFS_volumeServer_ec_rebuild_total{cluster=~\"$cluster\"}[5m]))",
"range": true,
"refId": "A",
"legendFormat": "{{result}}"
}
],
"pluginVersion": "10.3.1"
}
]
},
+14 -3
View File
@@ -16,6 +16,7 @@ import (
"github.com/seaweedfs/seaweedfs/weed/operation"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
@@ -174,6 +175,11 @@ func (vs *VolumeServer) VolumeEcShardsGenerate(ctx context.Context, req *volume_
return &volume_server_pb.VolumeEcShardsGenerateResponse{}, nil
}
func recordEcRebuild(result string, d time.Duration) {
stats.VolumeServerECRebuildHistogram.WithLabelValues(result).Observe(d.Seconds())
stats.VolumeServerECRebuildCounter.WithLabelValues(result).Inc()
}
// VolumeEcShardsRebuild generates the any of the missing .ec00 ~ .ec13 files
func (vs *VolumeServer) VolumeEcShardsRebuild(ctx context.Context, req *volume_server_pb.VolumeEcShardsRebuildRequest) (*volume_server_pb.VolumeEcShardsRebuildResponse, error) {
if err := vs.CheckMaintenanceMode(); err != nil {
@@ -235,21 +241,26 @@ func (vs *VolumeServer) VolumeEcShardsRebuild(ctx context.Context, req *volume_s
// Rebuild missing EC files, searching all disk locations for input shards.
// Present input shards are verified against the bitrot sidecar (when present)
// and corrupt ones are regenerated; unsafe_ignore_sidecar bypasses the guard.
start := time.Now()
dataBaseFileName := path.Join(rebuildDataDir, baseFileName)
if generatedShardIds, err := erasure_coding.RebuildEcFiles(dataBaseFileName, erasure_coding.BackgroundECContext(), req.UnsafeIgnoreSidecar, additionalDirs...); err != nil {
generatedShardIds, err := erasure_coding.RebuildEcFiles(dataBaseFileName, erasure_coding.BackgroundECContext(), req.UnsafeIgnoreSidecar, additionalDirs...)
if err != nil {
recordEcRebuild("failure", time.Since(start))
return nil, fmt.Errorf("RebuildEcFiles %s: %v", dataBaseFileName, err)
} else {
rebuiltShardIds = generatedShardIds
}
rebuiltShardIds = generatedShardIds
indexBaseFileName := path.Join(rebuildLocation.IdxDirectory, baseFileName)
if !util.FileExists(indexBaseFileName+".ecx") && rebuildLocation.IdxDirectory != rebuildLocation.Directory {
indexBaseFileName = path.Join(rebuildLocation.Directory, baseFileName)
}
if err := erasure_coding.RebuildEcxFile(indexBaseFileName); err != nil {
recordEcRebuild("failure", time.Since(start))
return nil, fmt.Errorf("RebuildEcxFile %s: %v", indexBaseFileName, err)
}
recordEcRebuild("success", time.Since(start))
// Opportunistic bitrot backfill: if protection is enabled, no sidecar exists
// yet (a volume encoded before this feature), and this rebuilder can reach
// every shard, compute and write a generation-0 sidecar. The TOFU baseline
+21
View File
@@ -512,6 +512,25 @@ var (
Help: "Counter of replication failures by operation and reason (timeout, connection_refused, context_cancelled, server_error).",
}, []string{"operation", "reason"})
// VolumeServerECRebuildHistogram records the duration of EC shard rebuild operations by result (success, failure).
VolumeServerECRebuildHistogram = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: Namespace,
Subsystem: subsystemVolumeServer,
Name: "ec_rebuild_seconds",
Help: "Bucketed histogram of EC shard rebuild/reconstruct duration by result.",
Buckets: prometheus.ExponentialBuckets(0.01, 2, 20),
}, []string{"result"})
// VolumeServerECRebuildCounter counts EC shard rebuild operations by result (success, failure).
VolumeServerECRebuildCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: Namespace,
Subsystem: subsystemVolumeServer,
Name: "ec_rebuild_total",
Help: "Counter of EC shard rebuild operations by result.",
}, []string{"result"})
S3RequestCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: Namespace,
@@ -886,6 +905,8 @@ func init() {
Gather.MustRegister(VolumeServerReplicationHistogram)
Gather.MustRegister(VolumeServerReplicationTargets)
Gather.MustRegister(VolumeServerReplicationFailures)
Gather.MustRegister(VolumeServerECRebuildHistogram)
Gather.MustRegister(VolumeServerECRebuildCounter)
Gather.MustRegister(MasterUnderReplicatedVolumes)
Gather.MustRegister(MasterVolumeCreationCounter)
+58
View File
@@ -0,0 +1,58 @@
package stats
import (
"testing"
"github.com/prometheus/client_golang/prometheus/testutil"
)
func TestECRebuildMetricsRegistered(t *testing.T) {
t.Cleanup(VolumeServerECRebuildCounter.Reset)
t.Cleanup(VolumeServerECRebuildHistogram.Reset)
// A HistogramVec emits no metric family until it has a child, so observe once.
VolumeServerECRebuildCounter.WithLabelValues("success").Inc()
VolumeServerECRebuildHistogram.WithLabelValues("success").Observe(0.1)
metrics, err := Gather.Gather()
if err != nil {
t.Fatalf("failed to gather metrics: %v", err)
}
counterFound := false
histogramFound := false
for _, mf := range metrics {
if mf.GetName() == "SeaweedFS_volumeServer_ec_rebuild_total" {
counterFound = true
}
if mf.GetName() == "SeaweedFS_volumeServer_ec_rebuild_seconds" {
histogramFound = true
}
}
if !counterFound {
t.Errorf("VolumeServerECRebuildCounter: metric not registered with Gather")
}
if !histogramFound {
t.Errorf("VolumeServerECRebuildHistogram: metric not registered with Gather")
}
}
func TestECRebuildCounterIncrement(t *testing.T) {
VolumeServerECRebuildCounter.Reset()
t.Cleanup(func() {
VolumeServerECRebuildCounter.Reset()
})
VolumeServerECRebuildCounter.WithLabelValues("success").Inc()
VolumeServerECRebuildCounter.WithLabelValues("failure").Inc()
got := testutil.ToFloat64(VolumeServerECRebuildCounter.WithLabelValues("success"))
if got != 1 {
t.Errorf("expected 1.0, got %f", got)
}
got = testutil.ToFloat64(VolumeServerECRebuildCounter.WithLabelValues("failure"))
if got != 1 {
t.Errorf("expected 1.0, got %f", got)
}
}