snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 03:23:42 +05:00
commit 89f5be005d
3997 changed files with 961970 additions and 0 deletions

514
weed/stats/disk.go Normal file
View File

@@ -0,0 +1,514 @@
package stats
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
type DiskIOProbeConfig struct {
// enable/disable disk io probing
Enabled bool
// timeout for a single io probe
Timeout time.Duration
// probe interval
Interval time.Duration
// latency above this threshold is considered slow
SlowLatency time.Duration
// per disk type overrides for SlowLatency, keyed by the -disk tag
SlowLatencyByDiskType map[string]time.Duration
// rolling observation window
Window time.Duration
// minimum number of samples required before evaluating health
MinSamples int
// percentage of slow operations required to mark disk degraded
SlowPercent float64
// percentage of failed operations required to mark disk degraded
ErrorPercent float64
// recovery coef defines the hysteresis threshold for recovering from degraded state.
RecoveryCoef float64
// statfs failures before alerting
MaxStatFailures int
}
func DefaultDiskIOProbeConfig() DiskIOProbeConfig {
return DiskIOProbeConfig{
Enabled: false,
Timeout: 2 * time.Second,
Interval: 30 * time.Second,
SlowLatency: 100 * time.Millisecond,
Window: time.Minute,
MinSamples: 10,
SlowPercent: 20,
ErrorPercent: 10,
RecoveryCoef: 0.5,
MaxStatFailures: 3,
}
}
// SlowLatencyFor returns the slow threshold configured for a disk type,
// falling back to SlowLatency when the type has no entry of its own.
func (config DiskIOProbeConfig) SlowLatencyFor(diskType string) time.Duration {
if slowLatency, found := config.SlowLatencyByDiskType[diskType]; found {
return slowLatency
}
return config.SlowLatency
}
type ioSample struct {
ts time.Time
latency time.Duration
failed bool
slow bool
}
type diskState struct {
mu sync.Mutex
// statfs probe
isChecking bool
statFailureCount int
statSuccessCount int
lastStatErr error
lastGoodStatus volume_server_pb.DiskStatus
hasLastGood bool
samples []ioSample
// IO probe
isIOChecking bool
ioCheckID uint64
lastIOCheck time.Time
ioFailureCount int
lastIOErr error
ioDegraded bool
}
var diskRegistry sync.Map
const (
// statfs timeout
diskTimeout = 500 * time.Millisecond
)
func NewDiskStatus(path string) (disk *volume_server_pb.DiskStatus) {
disk = &volume_server_pb.DiskStatus{Dir: path}
fillInDiskStatus(disk)
if disk.PercentUsed > 95 {
glog.V(0).Infof("disk status: %v", disk)
}
return
}
func NewDiskStatusOnStart(path string, config DiskIOProbeConfig) (disk *volume_server_pb.DiskStatus) {
disk = &volume_server_pb.DiskStatus{Dir: path}
diskProbe(disk, config)
if disk.PercentUsed > 95 {
glog.V(0).Infof("disk status: %v", disk)
}
return
}
func diskProbe(disk *volume_server_pb.DiskStatus, config DiskIOProbeConfig) {
actual, _ := diskRegistry.LoadOrStore(disk.Dir, &diskState{})
state := actual.(*diskState)
state.mu.Lock()
if state.isChecking {
state.statFailureCount++
state.statSuccessCount = 0
state.lastStatErr = errors.New("statfs still in progress")
state.updateErrorLocked(disk, config)
state.applyLastGoodStatusLocked(disk)
state.mu.Unlock()
return
}
state.isChecking = true
state.mu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), diskTimeout)
defer cancel()
probe := &volume_server_pb.DiskStatus{Dir: disk.Dir}
ch := make(chan error, 1)
go func() {
err := fillInDiskStatus(probe)
// Clear the flag only once statfs has actually returned, so a stuck disk
// keeps a single outstanding probe instead of spawning a new one each tick.
state.mu.Lock()
state.isChecking = false
state.mu.Unlock()
ch <- err
}()
var probeErr error
select {
case probeErr = <-ch:
case <-ctx.Done():
// Leave isChecking set; the probe goroutine clears it when statfs returns.
state.mu.Lock()
state.statFailureCount++
state.statSuccessCount = 0
state.lastStatErr = errors.New("statfs timeout")
state.updateErrorLocked(disk, config)
state.applyLastGoodStatusLocked(disk)
state.mu.Unlock()
return
}
state.mu.Lock()
if probeErr != nil {
state.statFailureCount++
state.statSuccessCount = 0
state.lastStatErr = probeErr
state.updateErrorLocked(disk, config)
state.applyLastGoodStatusLocked(disk)
state.mu.Unlock()
return
}
state.observeStatSuccessLocked(config)
disk.All = probe.All
disk.Free = probe.Free
disk.Used = probe.Used
disk.PercentFree = probe.PercentFree
disk.PercentUsed = probe.PercentUsed
state.rememberGoodStatusLocked(disk)
if !config.Enabled {
state.ioFailureCount = 0
state.lastIOErr = nil
state.ioDegraded = false
state.updateErrorLocked(disk, config)
state.mu.Unlock()
return
}
shouldCheckIO := false
ioCheckID := uint64(0)
if time.Since(state.lastIOCheck) >= config.Interval {
if state.isIOChecking {
degraded := state.observeLatencyLocked(
config.Timeout,
true,
config,
)
state.ioDegraded = degraded
state.lastIOCheck = time.Now()
if degraded {
state.ioFailureCount++
state.lastIOErr = errors.New("disk io still in progress")
} else {
state.ioFailureCount = 0
state.lastIOErr = nil
}
} else {
state.isIOChecking = true
state.ioCheckID++
ioCheckID = state.ioCheckID
state.lastIOCheck = time.Now()
shouldCheckIO = true
}
}
state.updateErrorLocked(disk, config)
state.mu.Unlock()
if shouldCheckIO {
checkDiskIOLatency(state, disk, ioCheckID, config)
}
}
type ioProbeResult struct {
err error
latency time.Duration
}
func checkDiskIOLatency(state *diskState, disk *volume_server_pb.DiskStatus, checkID uint64, config DiskIOProbeConfig) {
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
defer cancel()
ch := make(chan ioProbeResult, 1)
go func() {
start := time.Now()
err := runIOProbe(disk.Dir)
ch <- ioProbeResult{
err: err,
latency: time.Since(start),
}
state.mu.Lock()
if state.ioCheckID == checkID {
state.isIOChecking = false
state.lastIOCheck = time.Now()
}
state.mu.Unlock()
}()
select {
case result := <-ch:
state.mu.Lock()
defer state.mu.Unlock()
if state.ioCheckID != checkID {
return
}
state.isIOChecking = false
state.lastIOCheck = time.Now()
if result.err != nil {
degraded := state.observeLatencyLocked(
config.Timeout,
true,
config,
)
state.ioDegraded = degraded
if degraded {
state.ioFailureCount++
state.lastIOErr = result.err
} else {
state.ioFailureCount = 0
state.lastIOErr = nil
}
state.updateErrorLocked(disk, config)
return
}
degraded := state.observeLatencyLocked(
result.latency,
false,
config,
)
state.ioDegraded = degraded
if degraded {
state.ioFailureCount++
state.lastIOErr = fmt.Errorf(
"disk io degradation detected latency=%v",
result.latency,
)
} else {
state.ioFailureCount = 0
state.lastIOErr = nil
}
state.updateErrorLocked(disk, config)
glog.V(1).Infof(
"disk io latency dir=%s latency=%v ioFailureCount=%d degraded=%v samples=%d error=%v",
disk.Dir,
result.latency,
state.ioFailureCount,
degraded,
len(state.samples),
state.lastIOErr,
)
case <-ctx.Done():
state.mu.Lock()
if state.ioCheckID == checkID {
degraded := state.observeLatencyLocked(
config.Timeout,
true,
config,
)
state.ioDegraded = degraded
if degraded {
state.ioFailureCount++
state.lastIOErr = errors.New("disk io timeout")
} else {
state.ioFailureCount = 0
state.lastIOErr = nil
}
state.lastIOCheck = time.Now()
state.updateErrorLocked(disk, config)
}
state.mu.Unlock()
}
}
func runIOProbe(dir string) error {
path := filepath.Join(dir, ".disk-health-check")
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteAt(make([]byte, 4096), 0); err != nil {
return err
}
return f.Sync()
}
func (s *diskState) observeLatencyLocked(latency time.Duration, failed bool, config DiskIOProbeConfig) bool {
now := time.Now()
sample := ioSample{
ts: now,
latency: latency,
failed: failed,
slow: latency >= config.SlowLatency,
}
s.samples = append(s.samples, sample)
cutoff := now.Add(-config.Window)
idx := 0
for _, sample := range s.samples {
if sample.ts.After(cutoff) {
s.samples[idx] = sample
idx++
}
}
s.samples = s.samples[:idx]
total := len(s.samples)
if total < config.MinSamples {
return s.ioDegraded
}
var slowCount int
var failedCount int
for _, sample := range s.samples {
if sample.slow {
slowCount++
}
if sample.failed {
failedCount++
}
}
slowPercent := float64(slowCount) / float64(total) * 100
errorPercent := float64(failedCount) / float64(total) * 100
if !s.ioDegraded {
return slowPercent >= config.SlowPercent ||
errorPercent >= config.ErrorPercent
}
recoverSlowPercent := config.SlowPercent * config.RecoveryCoef
recoverErrorPercent := config.ErrorPercent * config.RecoveryCoef
return slowPercent > recoverSlowPercent ||
errorPercent > recoverErrorPercent
}
func (s *diskState) observeStatSuccessLocked(config DiskIOProbeConfig) {
if s.statFailureCount < config.MaxStatFailures {
s.statFailureCount = 0
s.statSuccessCount = 0
s.lastStatErr = nil
return
}
s.statSuccessCount++
if s.statSuccessCount >= statRecoverySuccesses(config) {
s.statFailureCount = 0
s.statSuccessCount = 0
s.lastStatErr = nil
}
}
func statRecoverySuccesses(config DiskIOProbeConfig) int {
if config.MaxStatFailures <= 1 {
return 1
}
if config.RecoveryCoef <= 0 {
return config.MaxStatFailures * 2
}
successes := int(float64(config.MaxStatFailures) * (1/config.RecoveryCoef - 1))
if successes < 1 {
return 1
}
return successes
}
func (s *diskState) updateErrorLocked(disk *volume_server_pb.DiskStatus, config DiskIOProbeConfig) {
var problems []string
if s.statFailureCount >= config.MaxStatFailures &&
s.lastStatErr != nil {
problems = append(
problems,
fmt.Sprintf("statfs: %v", s.lastStatErr),
)
}
if config.Enabled && s.ioDegraded && s.lastIOErr != nil {
problems = append(
problems,
fmt.Sprintf("io: %v", s.lastIOErr),
)
}
if len(problems) == 0 {
disk.Error = ""
return
}
disk.Error =
"disk health check failed: " +
strings.Join(problems, "; ")
}
func (s *diskState) rememberGoodStatusLocked(disk *volume_server_pb.DiskStatus) {
s.lastGoodStatus = *disk
s.hasLastGood = true
}
func (s *diskState) applyLastGoodStatusLocked(disk *volume_server_pb.DiskStatus) {
if !s.hasLastGood || disk.Error != "" {
return
}
dir := disk.Dir
errorMessage := disk.Error
*disk = s.lastGoodStatus
disk.Dir = dir
disk.Error = errorMessage
}

View File

@@ -0,0 +1,9 @@
//go:build netbsd || plan9
package stats
import "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
func fillInDiskStatus(status *volume_server_pb.DiskStatus) error {
return nil
}

View File

@@ -0,0 +1,24 @@
//go:build openbsd
package stats
import (
"syscall"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
func fillInDiskStatus(disk *volume_server_pb.DiskStatus) error {
fs := syscall.Statfs_t{}
if err := syscall.Statfs(disk.Dir, &fs); err != nil {
return err
}
disk.All = fs.F_blocks * uint64(fs.F_bsize)
disk.Free = fs.F_bfree * uint64(fs.F_bsize)
disk.Used = disk.All - disk.Free
disk.PercentFree = float32((float64(disk.Free) / float64(disk.All)) * 100)
disk.PercentUsed = float32((float64(disk.Used) / float64(disk.All)) * 100)
return nil
}

View File

@@ -0,0 +1,24 @@
//go:build solaris
package stats
import (
"golang.org/x/sys/unix"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
func fillInDiskStatus(disk *volume_server_pb.DiskStatus) error {
var stat unix.Statvfs_t
if err := unix.Statvfs(disk.Dir, &stat); err != nil {
return err
}
disk.All = stat.Blocks * uint64(stat.Bsize)
disk.Free = stat.Bfree * uint64(stat.Bsize)
disk.Used = disk.All - disk.Free
disk.PercentFree = float32((float64(disk.Free) / float64(disk.All)) * 100)
disk.PercentUsed = float32((float64(disk.Used) / float64(disk.All)) * 100)
return nil
}

View File

@@ -0,0 +1,25 @@
//go:build !windows && !openbsd && !netbsd && !plan9 && !solaris
package stats
import (
"syscall"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
func fillInDiskStatus(disk *volume_server_pb.DiskStatus) error {
fs := syscall.Statfs_t{}
if err := syscall.Statfs(disk.Dir, &fs); err != nil {
return err
}
disk.All = fs.Blocks * uint64(fs.Bsize)
// https://man7.org/linux/man-pages/man3/statvfs.3.html
// fs.Bfree: Number of free blocks
// fs.Bavail: Number of free blocks for unprivileged users
disk.Free = uint64(fs.Bavail) * uint64(fs.Bsize)
disk.Used = disk.All - disk.Free
disk.PercentFree = float32(float64(disk.Free) / float64(disk.All) * 100)
disk.PercentUsed = float32(float64(disk.Used) / float64(disk.All) * 100)
return nil
}

96
weed/stats/disk_test.go Normal file
View File

@@ -0,0 +1,96 @@
//go:build !netbsd && !plan9
package stats
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
func TestSetDiskStatusSuccess(t *testing.T) {
disk := &volume_server_pb.DiskStatus{Dir: t.TempDir()}
diskProbe(disk, DefaultDiskIOProbeConfig())
if disk.Error != "" {
t.Fatalf("unexpected disk error: %s", disk.Error)
}
if disk.All == 0 {
t.Fatalf("expected non-zero capacity for a real directory")
}
}
func TestSetDiskStatusReportsRepeatedFailures(t *testing.T) {
missing := filepath.Join(t.TempDir(), "does-not-exist")
config := DefaultDiskIOProbeConfig()
var disk *volume_server_pb.DiskStatus
for i := 0; i < config.MaxStatFailures; i++ {
disk = &volume_server_pb.DiskStatus{Dir: missing}
diskProbe(disk, config)
}
if disk.Error == "" {
t.Fatalf("expected disk error after %d failed checks", config.MaxStatFailures)
}
}
func TestSetDiskStatusRequiresRepeatedSuccessesToRecover(t *testing.T) {
missing := filepath.Join(t.TempDir(), "temporarily-missing")
config := DefaultDiskIOProbeConfig()
var disk *volume_server_pb.DiskStatus
for i := 0; i < config.MaxStatFailures; i++ {
disk = &volume_server_pb.DiskStatus{Dir: missing}
diskProbe(disk, config)
}
if disk.Error == "" {
t.Fatalf("expected disk error after %d failed checks", config.MaxStatFailures)
}
if err := os.Mkdir(missing, 0755); err != nil {
t.Fatalf("mkdir %s: %v", missing, err)
}
recoveryChecks := statRecoverySuccesses(config)
for i := 1; i < recoveryChecks; i++ {
disk = &volume_server_pb.DiskStatus{Dir: missing}
diskProbe(disk, config)
if disk.Error == "" {
t.Fatalf("expected disk error to persist after %d successful checks", i)
}
}
disk = &volume_server_pb.DiskStatus{Dir: missing}
diskProbe(disk, config)
if disk.Error != "" {
t.Fatalf("expected disk error to recover after %d successful checks: %s", recoveryChecks, disk.Error)
}
}
func TestSlowLatencyForFallsBackToDefault(t *testing.T) {
config := DiskIOProbeConfig{
SlowLatency: 500 * time.Millisecond,
SlowLatencyByDiskType: map[string]time.Duration{
"hdd": 500 * time.Millisecond,
"nvme": 50 * time.Millisecond,
},
}
for diskType, want := range map[string]time.Duration{
"hdd": 500 * time.Millisecond,
"nvme": 50 * time.Millisecond,
"ssd": 500 * time.Millisecond,
"nvme-gen5": 500 * time.Millisecond,
} {
if got := config.SlowLatencyFor(diskType); got != want {
t.Errorf("disk type %q: got %v, want %v", diskType, got, want)
}
}
empty := DiskIOProbeConfig{SlowLatency: 100 * time.Millisecond}
if got := empty.SlowLatencyFor("nvme"); got != 100*time.Millisecond {
t.Errorf("unconfigured table: got %v, want %v", got, 100*time.Millisecond)
}
}

View File

@@ -0,0 +1,48 @@
package stats
import (
"fmt"
"syscall"
"unsafe"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"golang.org/x/sys/windows"
)
var (
kernel32 = windows.NewLazySystemDLL("Kernel32.dll")
getDiskFreeSpaceEx = kernel32.NewProc("GetDiskFreeSpaceExW")
)
func fillInDiskStatus(disk *volume_server_pb.DiskStatus) error {
ptr, err := syscall.UTF16PtrFromString(disk.Dir)
if err != nil {
return err
}
var _temp uint64
/* #nosec */
r, _, e := syscall.Syscall6(
getDiskFreeSpaceEx.Addr(),
4,
uintptr(unsafe.Pointer(ptr)),
uintptr(unsafe.Pointer(&disk.Free)),
uintptr(unsafe.Pointer(&disk.All)),
uintptr(unsafe.Pointer(&_temp)),
0,
0,
)
if r == 0 {
if e != 0 {
return e
}
return fmt.Errorf("GetDiskFreeSpaceExW failed for %s", disk.Dir)
}
disk.Used = disk.All - disk.Free
disk.PercentFree = float32((float64(disk.Free) / float64(disk.All)) * 100)
disk.PercentUsed = float32((float64(disk.Used) / float64(disk.All)) * 100)
return nil
}

View File

@@ -0,0 +1,94 @@
package stats
import (
"time"
)
type TimedValue struct {
t time.Time
val int64
}
func NewTimedValue(t time.Time, val int64) *TimedValue {
return &TimedValue{t: t, val: val}
}
type RoundRobinCounter struct {
LastIndex int
Values []int64
Counts []int64
}
func NewRoundRobinCounter(slots int) *RoundRobinCounter {
return &RoundRobinCounter{LastIndex: -1, Values: make([]int64, slots), Counts: make([]int64, slots)}
}
func (rrc *RoundRobinCounter) Add(index int, val int64) {
if index >= len(rrc.Values) {
return
}
for rrc.LastIndex != index {
rrc.LastIndex = (rrc.LastIndex + 1) % len(rrc.Values)
rrc.Values[rrc.LastIndex] = 0
rrc.Counts[rrc.LastIndex] = 0
}
rrc.Values[index] += val
rrc.Counts[index]++
}
func (rrc *RoundRobinCounter) Max() (max int64) {
for _, val := range rrc.Values {
if max < val {
max = val
}
}
return
}
func (rrc *RoundRobinCounter) Count() (cnt int64) {
for _, c := range rrc.Counts {
cnt += c
}
return
}
func (rrc *RoundRobinCounter) Sum() (sum int64) {
for _, val := range rrc.Values {
sum += val
}
return
}
func (rrc *RoundRobinCounter) ToList() (ret []int64) {
index := rrc.LastIndex
step := len(rrc.Values)
for step > 0 {
step--
index++
if index >= len(rrc.Values) {
index = 0
}
ret = append(ret, rrc.Values[index])
}
return
}
type DurationCounter struct {
MinuteCounter *RoundRobinCounter
HourCounter *RoundRobinCounter
DayCounter *RoundRobinCounter
WeekCounter *RoundRobinCounter
}
func NewDurationCounter() *DurationCounter {
return &DurationCounter{
MinuteCounter: NewRoundRobinCounter(60),
HourCounter: NewRoundRobinCounter(60),
DayCounter: NewRoundRobinCounter(24),
WeekCounter: NewRoundRobinCounter(7),
}
}
// Add is for cumulative counts
func (sc *DurationCounter) Add(tv *TimedValue) {
sc.MinuteCounter.Add(tv.t.Second(), tv.val)
sc.HourCounter.Add(tv.t.Minute(), tv.val)
sc.DayCounter.Add(tv.t.Hour(), tv.val)
sc.WeekCounter.Add(int(tv.t.Weekday()), tv.val)
}

View File

@@ -0,0 +1,19 @@
package stats
import "testing"
func TestRobinCounter(t *testing.T) {
rrc := NewRoundRobinCounter(60)
rrc.Add(0, 1)
rrc.Add(50, 2)
if rrc.Count() != 2 {
t.Fatal()
}
if rrc.Sum() != 3 {
t.Fatal()
}
/*
index out of range
*/
rrc.Add(61, 1)
}

View File

@@ -0,0 +1,21 @@
package stats
import "net/http"
type StatusRecorder struct {
http.ResponseWriter
Status int
}
func NewStatusResponseWriter(w http.ResponseWriter) *StatusRecorder {
return &StatusRecorder{w, http.StatusOK}
}
func (r *StatusRecorder) WriteHeader(status int) {
r.Status = status
r.ResponseWriter.WriteHeader(status)
}
func (r *StatusRecorder) Flush() {
r.ResponseWriter.(http.Flusher).Flush()
}

20
weed/stats/memory.go Normal file
View File

@@ -0,0 +1,20 @@
package stats
import (
"runtime"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
func MemStat() *volume_server_pb.MemStatus {
mem := &volume_server_pb.MemStatus{}
mem.Goroutines = int32(runtime.NumGoroutine())
memStat := new(runtime.MemStats)
runtime.ReadMemStats(memStat)
mem.Self = memStat.Alloc
mem.Heap = memStat.HeapAlloc
mem.Stack = memStat.StackInuse
fillInMemStatus(mem)
return mem
}

View File

@@ -0,0 +1,9 @@
//go:build !linux
package stats
import "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
func fillInMemStatus(status *volume_server_pb.MemStatus) {
return
}

View File

@@ -0,0 +1,20 @@
//go:build linux
package stats
import (
"syscall"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
func fillInMemStatus(mem *volume_server_pb.MemStatus) {
//system memory usage
sysInfo := new(syscall.Sysinfo_t)
err := syscall.Sysinfo(sysInfo)
if err == nil {
mem.All = uint64(sysInfo.Totalram) //* uint64(syscall.Getpagesize())
mem.Free = uint64(sysInfo.Freeram) //* uint64(syscall.Getpagesize())
mem.Used = mem.All - mem.Free
}
}

1276
weed/stats/metrics.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
package stats
import (
"testing"
"github.com/prometheus/client_golang/prometheus/testutil"
)
func TestUpdateBucketQuotaMetrics(t *testing.T) {
bucket := "quota-test-bucket"
UpdateBucketQuotaMetrics(bucket, 1024, true)
if got := testutil.ToFloat64(S3BucketQuotaBytesGauge.WithLabelValues(bucket)); got != 1024 {
t.Errorf("quota gauge: got %.0f, want 1024", got)
}
if got := testutil.ToFloat64(S3BucketReadOnlyGauge.WithLabelValues(bucket)); got != 1 {
t.Errorf("read-only gauge: got %.0f, want 1", got)
}
UpdateBucketQuotaMetrics(bucket, 1024, false)
if got := testutil.ToFloat64(S3BucketReadOnlyGauge.WithLabelValues(bucket)); got != 0 {
t.Errorf("read-only gauge after clearing: got %.0f, want 0", got)
}
// disabled quota (negative) removes the quota series but keeps reporting read-only state
UpdateBucketQuotaMetrics(bucket, -1024, true)
if n := testutil.CollectAndCount(S3BucketQuotaBytesGauge); n != 0 {
t.Errorf("quota series after disabling: got %d, want 0", n)
}
if got := testutil.ToFloat64(S3BucketReadOnlyGauge.WithLabelValues(bucket)); got != 1 {
t.Errorf("read-only gauge with disabled quota: got %.0f, want 1", got)
}
UpdateBucketQuotaMetrics(bucket, 2048, true)
DeleteBucketMetrics(bucket)
if n := testutil.CollectAndCount(S3BucketQuotaBytesGauge); n != 0 {
t.Errorf("quota series after bucket deletion: got %d, want 0", n)
}
if n := testutil.CollectAndCount(S3BucketReadOnlyGauge); n != 0 {
t.Errorf("read-only series after bucket deletion: got %d, want 0", n)
}
}

View File

@@ -0,0 +1,81 @@
package stats_test
import (
"runtime"
"strings"
"testing"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/seaweedfs/seaweedfs/weed/stats"
_ "github.com/seaweedfs/seaweedfs/weed/util/version" // Import to trigger version init
)
func TestBuildInfo(t *testing.T) {
// Verify that BuildInfo metric is registered and has the expected value
count := testutil.CollectAndCount(stats.BuildInfo)
if count != 1 {
t.Errorf("Expected 1 BuildInfo metric, got %d", count)
}
// Verify the metric can be gathered
metrics, err := stats.Gather.Gather()
if err != nil {
t.Fatalf("Failed to gather metrics: %v", err)
}
// Find the build_info metric
found := false
for _, mf := range metrics {
if mf.GetName() == "SeaweedFS_build_info" {
found = true
metric := mf.GetMetric()[0]
// Verify the metric value is 1
if metric.GetGauge().GetValue() != 1 {
t.Errorf("Expected BuildInfo value to be 1, got %f", metric.GetGauge().GetValue())
}
// Verify labels exist
labels := metric.GetLabel()
labelMap := make(map[string]string)
for _, label := range labels {
labelMap[label.GetName()] = label.GetValue()
}
// Check required labels
if _, ok := labelMap["version"]; !ok {
t.Error("Missing 'version' label")
}
if _, ok := labelMap["commit"]; !ok {
t.Error("Missing 'commit' label")
}
if _, ok := labelMap["sizelimit"]; !ok {
t.Error("Missing 'sizelimit' label")
}
if labelMap["goos"] != runtime.GOOS {
t.Errorf("Expected goos='%s', got '%s'", runtime.GOOS, labelMap["goos"])
}
if labelMap["goarch"] != runtime.GOARCH {
t.Errorf("Expected goarch='%s', got '%s'", runtime.GOARCH, labelMap["goarch"])
}
// Verify version format
if !strings.Contains(labelMap["version"], ".") {
t.Errorf("Version should contain a dot: %s", labelMap["version"])
}
// Verify sizelimit is either 30GB or 8000GB
if labelMap["sizelimit"] != "30GB" && labelMap["sizelimit"] != "8000GB" {
t.Errorf("Expected sizelimit to be '30GB' or '8000GB', got '%s'", labelMap["sizelimit"])
}
t.Logf("BuildInfo metric: version=%s, commit=%s, sizelimit=%s, goos=%s, goarch=%s",
labelMap["version"], labelMap["commit"], labelMap["sizelimit"],
labelMap["goos"], labelMap["goarch"])
}
}
if !found {
t.Error("BuildInfo metric not found in gathered metrics")
}
}

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)
}
}

View File

@@ -0,0 +1,90 @@
package stats
// This file contains metric names for all errors
// The naming convention is ErrorSomeThing = "error.some.thing"
const (
// volume server
WriteToLocalDisk = "writeToLocalDisk"
WriteToReplicas = "writeToReplicas"
DownloadLimitCond = "downloadLimitCondition"
UploadLimitCond = "uploadLimitCondition"
ReadProxyReq = "readProxyRequest"
ReadRedirectReq = "readRedirectRequest"
EmptyReadProxyLoc = "emptyReadProxyLocaction"
FailedReadProxyReq = "failedReadProxyRequest"
ErrorSizeMismatchOffsetSize = "errorSizeMismatchOffsetSize"
ErrorSizeMismatch = "errorSizeMismatch"
ErrorCRC = "errorCRC"
ErrorIndexOutOfRange = "errorIndexOutOfRange"
ErrorGetNotFound = "errorGetNotFound"
ErrorGetInternal = "errorGetInternal"
// master topology
ErrorWriteToLocalDisk = "errorWriteToLocalDisk"
ErrorUnmarshalPairs = "errorUnmarshalPairs"
ErrorWriteToReplicas = "errorWriteToReplicas"
// master client
FailedToKeepConnected = "failedToKeepConnected"
FailedToSend = "failedToSend"
FailedToReceive = "failedToReceive"
RedirectedToLeader = "redirectedToLeader"
OnPeerUpdate = "onPeerUpdate"
Failed = "failed"
// filer handler
DirList = "dirList"
ContentSaveToFiler = "contentSaveToFiler"
AutoChunk = "autoChunk"
ChunkProxy = "chunkProxy"
ChunkAssign = "chunkAssign"
ChunkUpload = "chunkUpload"
ChunkMerge = "chunkMerge"
ChunkDoUploadRetry = "chunkDoUploadRetry"
ChunkUploadRetry = "chunkUploadRetry"
ChunkAssignRetry = "chunkAssignRetry"
ErrorReadNotFound = "read.notfound"
ErrorReadInternal = "read.internal.error"
ErrorWriteEntry = "write.entry.failed"
RepeatErrorUploadContent = "upload.content.repeat.failed"
ErrorChunkAssign = "chunkAssign.failed"
ErrorReadChunk = "read.chunk.failed"
ErrorReadCache = "read.cache.failed"
ErrorReadStream = "read.stream.failed"
// s3 handler
ErrorCompletedNoSuchUpload = "errorCompletedNoSuchUpload"
ErrorCompleteEntityTooSmall = "errorCompleteEntityTooSmall"
ErrorCompletedPartEmpty = "errorCompletedPartEmpty"
ErrorCompletedPartNumber = "errorCompletedPartNumber"
ErrorCompletedPartNotFound = "errorCompletedPartNotFound"
ErrorCompletedEtagInvalid = "errorCompletedEtagInvalid"
ErrorCompletedEtagMismatch = "errorCompletedEtagMismatch"
ErrorCompletedPartEntryMismatch = "errorCompletedPartEntryMismatch"
// ReplicationOpWrite is the label value for write replication operations.
ReplicationOpWrite = "write"
// ReplicationOpDelete is the label value for delete replication operations.
ReplicationOpDelete = "delete"
// ReplicationSuccess is the label value for successful replication operations.
ReplicationSuccess = "success"
// ReplicationFailure is the label value for failed replication operations.
ReplicationFailure = "failure"
// FailureTimeout is the failure reason label for deadline-exceeded errors.
FailureTimeout = "timeout"
// FailureConnectionRefused is the failure reason label for connection-refused errors.
FailureConnectionRefused = "connection_refused"
// FailureContextCancelled is the failure reason label for context-cancelled errors.
FailureContextCancelled = "context_cancelled"
// FailureServerError is the failure reason label for generic server-side errors.
FailureServerError = "server_error"
// remote-mount cache read labels
RemoteCacheSourceFiler = "filer"
RemoteCacheSourceS3 = "s3"
RemoteCacheResultHit = "hit"
RemoteCacheResultMiss = "miss"
)

View File

@@ -0,0 +1,123 @@
package stats
import (
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
)
func TestReplicationMetricsRegistered(t *testing.T) {
VolumeServerReplicationCounter.Reset()
VolumeServerReplicationHistogram.Reset()
VolumeServerReplicationFailures.Reset()
MasterUnderReplicatedVolumes.Reset()
t.Cleanup(func() {
VolumeServerReplicationCounter.Reset()
VolumeServerReplicationHistogram.Reset()
VolumeServerReplicationFailures.Reset()
MasterUnderReplicatedVolumes.Reset()
})
// Seed CounterVec and HistogramVec with a value so collection returns them
VolumeServerReplicationCounter.WithLabelValues(ReplicationOpWrite, ReplicationSuccess).Inc()
VolumeServerReplicationHistogram.WithLabelValues(ReplicationOpWrite).Observe(0.1)
VolumeServerReplicationFailures.WithLabelValues(ReplicationOpWrite, FailureTimeout).Inc()
VolumeServerReplicationTargets.Observe(1)
MasterUnderReplicatedVolumes.WithLabelValues("default", "ssd", "1", "").Set(1)
metrics := []struct {
name string
c prometheus.Collector
}{
{"VolumeServerReplicationCounter", VolumeServerReplicationCounter},
{"VolumeServerReplicationHistogram", VolumeServerReplicationHistogram},
{"VolumeServerReplicationTargets", VolumeServerReplicationTargets},
{"VolumeServerReplicationFailures", VolumeServerReplicationFailures},
{"MasterUnderReplicatedVolumes", MasterUnderReplicatedVolumes},
}
for _, m := range metrics {
count := testutil.CollectAndCount(m.c)
if count < 1 {
t.Errorf("%s: expected at least 1 collection, got %d", m.name, count)
}
}
}
func TestReplicationCounterIncrement(t *testing.T) {
VolumeServerReplicationCounter.Reset()
VolumeServerReplicationCounter.WithLabelValues(ReplicationOpWrite, ReplicationSuccess).Inc()
VolumeServerReplicationCounter.WithLabelValues(ReplicationOpWrite, ReplicationFailure).Inc()
VolumeServerReplicationCounter.WithLabelValues(ReplicationOpDelete, ReplicationSuccess).Inc()
got := testutil.ToFloat64(VolumeServerReplicationCounter.WithLabelValues(ReplicationOpWrite, ReplicationSuccess))
if got != 1 {
t.Errorf("expected 1.0, got %f", got)
}
got = testutil.ToFloat64(VolumeServerReplicationCounter.WithLabelValues(ReplicationOpDelete, ReplicationSuccess))
if got != 1 {
t.Errorf("expected 1.0, got %f", got)
}
got = testutil.ToFloat64(VolumeServerReplicationCounter.WithLabelValues(ReplicationOpWrite, ReplicationFailure))
if got != 1 {
t.Errorf("expected 1.0, got %f", got)
}
}
func TestReplicationTargetsHistogram(t *testing.T) {
// VolumeServerReplicationTargets is a plain prometheus.Histogram
// (not a HistogramVec) and does not expose a Reset() method, so the
// histogram state is cumulative across tests that share the Gather
// registry. We check >= to tolerate prior observations.
VolumeServerReplicationTargets.Observe(3)
metrics, err := Gather.Gather()
if err != nil {
t.Fatalf("Failed to gather metrics: %v", err)
}
found := false
for _, mf := range metrics {
if mf.GetName() == "SeaweedFS_volumeServer_replication_targets" {
found = true
h := mf.GetMetric()[0].GetHistogram()
if h.GetSampleCount() < 1 {
t.Errorf("expected histogram_count>=1, got %d", h.GetSampleCount())
}
if h.GetSampleSum() < 3 {
t.Errorf("expected histogram_sum>=3, got %f", h.GetSampleSum())
}
}
}
if !found {
t.Error("SeaweedFS_volumeServer_replication_targets not found in gathered metrics")
}
}
func TestUnderReplicatedVolumesGauge(t *testing.T) {
MasterUnderReplicatedVolumes.WithLabelValues("default", "ssd", "1", "").Set(5)
got := testutil.ToFloat64(MasterUnderReplicatedVolumes.WithLabelValues("default", "ssd", "1", ""))
if got != 5 {
t.Errorf("expected 5.0, got %f", got)
}
}
func TestReplicationFailuresCounter(t *testing.T) {
VolumeServerReplicationFailures.Reset()
VolumeServerReplicationFailures.WithLabelValues(ReplicationOpWrite, FailureTimeout).Inc()
VolumeServerReplicationFailures.WithLabelValues(ReplicationOpDelete, FailureConnectionRefused).Inc()
got := testutil.ToFloat64(VolumeServerReplicationFailures.WithLabelValues(ReplicationOpWrite, FailureTimeout))
if got != 1 {
t.Errorf("expected 1.0, got %f", got)
}
got = testutil.ToFloat64(VolumeServerReplicationFailures.WithLabelValues(ReplicationOpDelete, FailureConnectionRefused))
if got != 1 {
t.Errorf("expected 1.0, got %f", got)
}
}

105
weed/stats/stats.go Normal file
View File

@@ -0,0 +1,105 @@
package stats
import (
"time"
)
type ServerStats struct {
Requests *DurationCounter
Connections *DurationCounter
AssignRequests *DurationCounter
ReadRequests *DurationCounter
WriteRequests *DurationCounter
DeleteRequests *DurationCounter
BytesIn *DurationCounter
BytesOut *DurationCounter
}
type Channels struct {
Connections chan *TimedValue
Requests chan *TimedValue
AssignRequests chan *TimedValue
ReadRequests chan *TimedValue
WriteRequests chan *TimedValue
DeleteRequests chan *TimedValue
BytesIn chan *TimedValue
BytesOut chan *TimedValue
}
var (
Chan *Channels
)
func init() {
Chan = &Channels{
Connections: make(chan *TimedValue, 100),
Requests: make(chan *TimedValue, 100),
AssignRequests: make(chan *TimedValue, 100),
ReadRequests: make(chan *TimedValue, 100),
WriteRequests: make(chan *TimedValue, 100),
DeleteRequests: make(chan *TimedValue, 100),
BytesIn: make(chan *TimedValue, 100),
BytesOut: make(chan *TimedValue, 100),
}
}
func NewServerStats() *ServerStats {
return &ServerStats{
Requests: NewDurationCounter(),
Connections: NewDurationCounter(),
AssignRequests: NewDurationCounter(),
ReadRequests: NewDurationCounter(),
WriteRequests: NewDurationCounter(),
DeleteRequests: NewDurationCounter(),
BytesIn: NewDurationCounter(),
BytesOut: NewDurationCounter(),
}
}
func ConnectionOpen() {
Chan.Connections <- NewTimedValue(time.Now(), 1)
}
func ConnectionClose() {
Chan.Connections <- NewTimedValue(time.Now(), -1)
}
func AssignRequest() {
Chan.AssignRequests <- NewTimedValue(time.Now(), 1)
}
func ReadRequest() {
Chan.ReadRequests <- NewTimedValue(time.Now(), 1)
}
func WriteRequest() {
Chan.WriteRequests <- NewTimedValue(time.Now(), 1)
}
func DeleteRequest() {
Chan.DeleteRequests <- NewTimedValue(time.Now(), 1)
}
func BytesIn(val int64) {
Chan.BytesIn <- NewTimedValue(time.Now(), val)
}
func BytesOut(val int64) {
Chan.BytesOut <- NewTimedValue(time.Now(), val)
}
func (ss *ServerStats) Start() {
for {
select {
case tv := <-Chan.Connections:
ss.Connections.Add(tv)
case tv := <-Chan.Requests:
ss.Requests.Add(tv)
case tv := <-Chan.AssignRequests:
ss.AssignRequests.Add(tv)
case tv := <-Chan.ReadRequests:
ss.ReadRequests.Add(tv)
case tv := <-Chan.WriteRequests:
ss.WriteRequests.Add(tv)
case tv := <-Chan.DeleteRequests:
ss.DeleteRequests.Add(tv)
case tv := <-Chan.BytesIn:
ss.BytesIn.Add(tv)
case tv := <-Chan.BytesOut:
ss.BytesOut.Add(tv)
}
}
}