Skip to content

Commit 7281bd8

Browse files
authored
feat: configurable disk alert threshold (#1318)
* feat: configurable disk alert threshold * fix flag name * improve alerting heuristic and rename config option
1 parent f9f7e33 commit 7281bd8

File tree

9 files changed

+188
-26
lines changed

9 files changed

+188
-26
lines changed

pkg/cli/flags_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ var _ = Describe("flags", func() {
214214
BaseURL: "",
215215
CacheEvictThreshold: 0.25,
216216
CacheEvictVolume: 0.33,
217+
MinFreeSpacePercentage: 5,
217218
BadgerNoTruncate: false,
218219
DisablePprofEndpoint: false,
219220
EnableExperimentalAdmin: true,

pkg/cli/server.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import (
3737
"github.com/pyroscope-io/pyroscope/pkg/service"
3838
"github.com/pyroscope-io/pyroscope/pkg/sqlstore"
3939
"github.com/pyroscope-io/pyroscope/pkg/storage"
40-
"github.com/pyroscope-io/pyroscope/pkg/util/bytesize"
4140
"github.com/pyroscope-io/pyroscope/pkg/util/debug"
4241
)
4342

@@ -93,7 +92,7 @@ func newServerService(c *config.Server) (*serverService, error) {
9392
}
9493

9594
diskPressure := health.DiskPressure{
96-
Threshold: 512 * bytesize.MB,
95+
Threshold: c.MinFreeSpacePercentage,
9796
Path: c.StoragePath,
9897
}
9998

pkg/config/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ type Server struct {
104104
LogLevel string `def:"info" desc:"log level: debug|info|warn|error" mapstructure:"log-level"`
105105
BadgerLogLevel string `def:"error" desc:"log level: debug|info|warn|error" mapstructure:"badger-log-level"`
106106

107-
StoragePath string `def:"<installPrefix>/var/lib/pyroscope" desc:"directory where pyroscope stores profiling data" mapstructure:"storage-path"`
107+
StoragePath string `def:"<installPrefix>/var/lib/pyroscope" desc:"directory where pyroscope stores profiling data" mapstructure:"storage-path"`
108+
MinFreeSpacePercentage float64 `def:"5" desc:"percentage of available disk space at which ingestion requests are discarded. Defaults to 5% but not less than 1GB. Set 0 to disable" mapstructure:"min-free-space-percentage"`
109+
108110
APIBindAddr string `def:":4040" desc:"port for the HTTP(S) server used for data ingestion and web UI" mapstructure:"api-bind-addr"`
109111
BaseURL string `def:"" desc:"base URL for when the server is behind a reverse proxy with a different path" mapstructure:"base-url"`
110112
BaseURLBindAddr string `def:"" deprecated:"true" desc:"server for debugging base url" mapstructure:"base-url-bind-addr"`

pkg/health/disk_pressure.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,57 @@
11
package health
22

33
import (
4+
"errors"
45
"fmt"
56

67
"github.com/pyroscope-io/pyroscope/pkg/util/bytesize"
78
"github.com/pyroscope-io/pyroscope/pkg/util/disk"
89
)
910

11+
var (
12+
errZeroTotalSize = errors.New("total disk size is zero")
13+
errTotalLessThanAvailable = errors.New("total disk size is less than available space")
14+
)
15+
16+
const minAvailSpace = bytesize.GB
17+
1018
type DiskPressure struct {
11-
Threshold bytesize.ByteSize
19+
Threshold float64
1220
Path string
1321
}
1422

1523
func (d DiskPressure) Probe() (StatusMessage, error) {
16-
var m StatusMessage
17-
available, err := disk.FreeSpace(d.Path)
24+
if d.Threshold == 0 {
25+
return StatusMessage{Status: Healthy}, nil
26+
}
27+
u, err := disk.Usage(d.Path)
1828
if err != nil {
19-
return m, err
29+
return StatusMessage{}, err
2030
}
21-
if available < d.Threshold {
31+
return d.makeProbe(u)
32+
}
33+
34+
func (d DiskPressure) makeProbe(u disk.UsageStats) (StatusMessage, error) {
35+
var m StatusMessage
36+
if u.Total == 0 {
37+
return m, errZeroTotalSize
38+
}
39+
if u.Available > u.Total {
40+
return m, errTotalLessThanAvailable
41+
}
42+
m.Status = Healthy
43+
if u.Available < d.minRequired(u) {
44+
availPercent := 100 * float64(u.Available) / float64(u.Total)
45+
m.Message = fmt.Sprintf("Disk space is running low: %v available (%.1f%%)", u.Available, availPercent)
2246
m.Status = Critical
23-
} else {
24-
m.Status = Healthy
2547
}
26-
m.Message = fmt.Sprintf("Disk space is running low: %v available", available)
2748
return m, nil
2849
}
50+
51+
func (d DiskPressure) minRequired(u disk.UsageStats) bytesize.ByteSize {
52+
t := bytesize.ByteSize(float64(u.Total) / 100 * d.Threshold)
53+
if t > minAvailSpace {
54+
return t
55+
}
56+
return minAvailSpace
57+
}

pkg/health/disk_pressure_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package health
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
. "github.com/onsi/gomega"
6+
"github.com/pyroscope-io/pyroscope/pkg/util/bytesize"
7+
"github.com/pyroscope-io/pyroscope/pkg/util/disk"
8+
)
9+
10+
var _ = Describe("DiskPressure", func() {
11+
It("does not fire if threshold is zero", func() {
12+
var d DiskPressure
13+
m, err := d.Probe()
14+
Expect(err).ToNot(HaveOccurred())
15+
Expect(m.Status).To(Equal(Healthy))
16+
Expect(m.Message).To(BeEmpty())
17+
})
18+
19+
It("does not fire if available space is greater than the configured threshold", func() {
20+
d := DiskPressure{
21+
Threshold: 5,
22+
}
23+
m, err := d.makeProbe(disk.UsageStats{
24+
Total: 10 * bytesize.GB,
25+
Available: 1 * bytesize.GB,
26+
})
27+
Expect(err).ToNot(HaveOccurred())
28+
Expect(m.Status).To(Equal(Healthy))
29+
Expect(m.Message).To(BeEmpty())
30+
})
31+
32+
It("fires if less than the configured threshold is available", func() {
33+
d := DiskPressure{
34+
Threshold: 5,
35+
}
36+
m, err := d.makeProbe(disk.UsageStats{
37+
Total: 100 * bytesize.GB,
38+
Available: 4 * bytesize.GB,
39+
})
40+
Expect(err).ToNot(HaveOccurred())
41+
Expect(m.Status).To(Equal(Critical))
42+
Expect(m.Message).To(Equal("Disk space is running low: 4.00 GB available (4.0%)"))
43+
})
44+
45+
It("fires if less than 1GB is available", func() {
46+
d := DiskPressure{
47+
Threshold: 5,
48+
}
49+
m, err := d.makeProbe(disk.UsageStats{
50+
Total: 5 * bytesize.GB,
51+
Available: bytesize.GB - 1,
52+
})
53+
Expect(err).ToNot(HaveOccurred())
54+
Expect(m.Status).To(Equal(Critical))
55+
Expect(m.Message).To(Equal("Disk space is running low: 1024.00 MB available (20.0%)"))
56+
})
57+
58+
It("fires if available is less than the configured threshold", func() {
59+
d := DiskPressure{
60+
Threshold: 5,
61+
}
62+
m, err := d.makeProbe(disk.UsageStats{
63+
Total: 1 * bytesize.GB,
64+
Available: 1 * bytesize.MB,
65+
})
66+
Expect(err).ToNot(HaveOccurred())
67+
Expect(m.Status).To(Equal(Critical))
68+
Expect(m.Message).To(Equal("Disk space is running low: 1.00 MB available (0.1%)"))
69+
})
70+
71+
It("fires if no space available", func() {
72+
d := DiskPressure{
73+
Threshold: 5,
74+
}
75+
m, err := d.makeProbe(disk.UsageStats{
76+
Total: 100 * bytesize.MB,
77+
Available: 0,
78+
})
79+
Expect(err).ToNot(HaveOccurred())
80+
Expect(m.Status).To(Equal(Critical))
81+
Expect(m.Message).To(Equal("Disk space is running low: 0 bytes available (0.0%)"))
82+
})
83+
84+
It("fails if Available > Total", func() {
85+
var d DiskPressure
86+
_, err := d.makeProbe(disk.UsageStats{
87+
Total: 1 * bytesize.GB,
88+
Available: 2 * bytesize.GB,
89+
})
90+
Expect(err).To(MatchError(errTotalLessThanAvailable))
91+
})
92+
93+
It("fails if Total is zero", func() {
94+
var d DiskPressure
95+
_, err := d.makeProbe(disk.UsageStats{
96+
Total: 0,
97+
Available: 2 * bytesize.GB,
98+
})
99+
Expect(err).To(MatchError(errZeroTotalSize))
100+
})
101+
})

pkg/util/disk/disk.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package disk
2+
3+
import (
4+
"github.com/pyroscope-io/pyroscope/pkg/util/bytesize"
5+
)
6+
7+
type UsageStats struct {
8+
Total bytesize.ByteSize
9+
Available bytesize.ByteSize
10+
}

pkg/util/disk/usage_test.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,29 @@ import (
99
)
1010

1111
var _ = Describe("disk package", func() {
12+
var (
13+
u UsageStats
14+
err error
15+
)
1216
testing.WithConfig(func(cfg **config.Config) {
13-
Describe("FreeSpace", func() {
17+
BeforeEach(func() {
18+
u, err = Usage((*cfg).Server.StoragePath)
19+
})
20+
Describe("Usage", func() {
1421
It("doesn't return an error", func() {
15-
_, err := FreeSpace((*cfg).Server.StoragePath)
1622
Expect(err).To(Not(HaveOccurred()))
1723
})
1824

19-
It("returns non-zero value for storage space", func() {
20-
Expect(FreeSpace((*cfg).Server.StoragePath)).To(BeNumerically(">", 0))
25+
It("returns non-zero Total", func() {
26+
Expect(u.Total).To(BeNumerically(">", 0))
27+
})
28+
29+
It("returns non-zero Available", func() {
30+
Expect(u.Available).To(BeNumerically(">", 0))
31+
})
32+
33+
It("returns Available < Total", func() {
34+
Expect(u.Available).To(BeNumerically("<", u.Total))
2135
})
2236
})
2337
})

pkg/util/disk/usage_unix.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import (
99
"github.com/pyroscope-io/pyroscope/pkg/util/bytesize"
1010
)
1111

12-
func FreeSpace(storagePath string) (bytesize.ByteSize, error) {
13-
fs := syscall.Statfs_t{}
14-
err := syscall.Statfs(storagePath, &fs)
15-
if err != nil {
16-
return 0, err
12+
func Usage(path string) (UsageStats, error) {
13+
var fs syscall.Statfs_t
14+
if err := syscall.Statfs(path, &fs); err != nil {
15+
return UsageStats{}, err
1716
}
18-
19-
return bytesize.ByteSize(fs.Bavail) * bytesize.ByteSize(fs.Bsize), nil
17+
u := UsageStats{
18+
Total: bytesize.ByteSize(fs.Blocks) * bytesize.ByteSize(fs.Bsize),
19+
Available: bytesize.ByteSize(fs.Bavail) * bytesize.ByteSize(fs.Bsize),
20+
}
21+
return u, nil
2022
}

pkg/util/disk/usage_windows.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ var (
1616
getDiskFreeSpaceEx = kernel32.NewProc("GetDiskFreeSpaceExW")
1717
)
1818

19-
func FreeSpace(path string) (bytesize.ByteSize, error) {
19+
func Usage(path string) (UsageStats, error) {
2020
dirPath, err := syscall.UTF16PtrFromString(path)
2121
if err != nil {
22-
return 0, err
22+
return UsageStats{}, err
2323
}
2424

2525
var (
@@ -34,8 +34,12 @@ func FreeSpace(path string) (bytesize.ByteSize, error) {
3434
uintptr(unsafe.Pointer(&totalNumberOfBytes)),
3535
uintptr(unsafe.Pointer(&totalNumberOfFreeBytes)))
3636
if ret == 0 {
37-
return 0, os.NewSyscallError("GetDiskFreeSpaceEx", err)
37+
return UsageStats{}, os.NewSyscallError("GetDiskFreeSpaceEx", err)
3838
}
3939

40-
return bytesize.ByteSize(freeBytesAvailableToCaller), nil
40+
u := UsageStats{
41+
Total: bytesize.ByteSize(totalNumberOfBytes),
42+
Available: bytesize.ByteSize(freeBytesAvailableToCaller),
43+
}
44+
return u, nil
4145
}

0 commit comments

Comments
 (0)