-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathstat.go
More file actions
265 lines (235 loc) · 6.04 KB
/
stat.go
File metadata and controls
265 lines (235 loc) · 6.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
package clistat
import (
"errors"
"math"
"strconv"
"strings"
"time"
"cdr.dev/slog/v3"
"github.com/elastic/go-sysinfo"
"github.com/spf13/afero"
"golang.org/x/xerrors"
"tailscale.com/types/ptr"
sysinfotypes "github.com/elastic/go-sysinfo/types"
)
// Prefix is a scale multiplier for a result.
// Used when creating a human-readable representation.
type Prefix float64
const (
PrefixDefault = 1.0
PrefixKibi = 1024.0
PrefixMebi = PrefixKibi * 1024.0
PrefixGibi = PrefixMebi * 1024.0
PrefixTebi = PrefixGibi * 1024.0
)
var (
PrefixHumanKibi = "Ki"
PrefixHumanMebi = "Mi"
PrefixHumanGibi = "Gi"
PrefixHumanTebi = "Ti"
)
func (s *Prefix) String() string {
switch *s {
case PrefixKibi:
return "Ki"
case PrefixMebi:
return "Mi"
case PrefixGibi:
return "Gi"
case PrefixTebi:
return "Ti"
default:
return ""
}
}
func ParsePrefix(s string) Prefix {
switch s {
case PrefixHumanKibi:
return PrefixKibi
case PrefixHumanMebi:
return PrefixMebi
case PrefixHumanGibi:
return PrefixGibi
case PrefixHumanTebi:
return PrefixTebi
default:
return PrefixDefault
}
}
// Result is a generic result type for a statistic.
// Total is the total amount of the resource available.
// It is nil if the resource is not a finite quantity.
// Unit is the unit of the resource.
// Used is the amount of the resource used.
type Result struct {
Total *float64 `json:"total"`
Unit string `json:"unit"`
Used float64 `json:"used"`
Prefix Prefix `json:"-"`
}
// String returns a human-readable representation of the result.
func (r *Result) String() string {
if r == nil {
return "-"
}
scale := 1.0
if r.Prefix != 0.0 {
scale = float64(r.Prefix)
}
var sb strings.Builder
var usedScaled, totalScaled float64
usedScaled = r.Used / scale
_, _ = sb.WriteString(humanizeFloat(usedScaled))
if r.Total != (*float64)(nil) {
_, _ = sb.WriteString("/")
totalScaled = *r.Total / scale
_, _ = sb.WriteString(humanizeFloat(totalScaled))
}
_, _ = sb.WriteString(" ")
_, _ = sb.WriteString(r.Prefix.String())
_, _ = sb.WriteString(r.Unit)
if r.Total != (*float64)(nil) && *r.Total > 0 {
_, _ = sb.WriteString(" (")
pct := r.Used / *r.Total * 100.0
_, _ = sb.WriteString(strconv.FormatFloat(pct, 'f', 0, 64))
_, _ = sb.WriteString("%)")
}
return strings.TrimSpace(sb.String())
}
func humanizeFloat(f float64) string {
// humanize.FtoaWithDigits does not round correctly.
prec := precision(f)
rat := math.Pow(10, float64(prec))
rounded := math.Round(f*rat) / rat
return strconv.FormatFloat(rounded, 'f', -1, 64)
}
// limit precision to 3 digits at most to preserve space
func precision(f float64) int {
fabs := math.Abs(f)
if fabs == 0.0 {
return 0
}
if fabs < 1.0 {
return 3
}
if fabs < 10.0 {
return 2
}
if fabs < 100.0 {
return 1
}
return 0
}
// Statter is a system statistics collector.
// It is a thin wrapper around the elastic/go-sysinfo library.
type Statter struct {
hi sysinfotypes.Host
fs afero.Fs
sampleInterval time.Duration
nproc int
wait func(time.Duration)
logger slog.Logger
cgroupStatter cgroupStatter
cgroupV2Detector func(afero.Fs) bool
}
type Option func(*Statter)
// WithSampleInterval sets the sample interval for the statter.
func WithSampleInterval(d time.Duration) Option {
return func(s *Statter) {
s.sampleInterval = d
}
}
// WithFS sets the fs for the statter.
func WithFS(fs afero.Fs) Option {
return func(s *Statter) {
s.fs = fs
}
}
// WithLogger sets the logger for the statter.
func WithLogger(logger slog.Logger) Option {
return func(s *Statter) {
s.logger = logger
}
}
func WithCGroupV2Detector(detector func(fs afero.Fs) bool) Option {
return func(s *Statter) {
s.cgroupV2Detector = detector
}
}
func New(opts ...Option) (*Statter, error) {
hi, err := sysinfo.Host()
if err != nil {
return nil, xerrors.Errorf("get host info: %w", err)
}
s := &Statter{
hi: hi,
fs: afero.NewReadOnlyFs(afero.NewOsFs()),
sampleInterval: 100 * time.Millisecond,
wait: func(d time.Duration) {
<-time.After(d)
},
cgroupV2Detector: func(_ afero.Fs) bool {
return isCgroupV2(cgroupRootPath)
},
}
for _, opt := range opts {
opt(s)
}
s.nproc = s.numCPU()
statter, err := s.getCgroupStatter()
if err != nil && !errors.Is(err, errNotContainerized) {
return nil, xerrors.Errorf("get cgroup statter: %v", err)
}
s.cgroupStatter = statter
return s, nil
}
// HostCPU returns the CPU usage of the host. This is calculated by
// taking two samples of CPU usage and calculating the difference.
// Total will always be equal to the number of cores.
// Used will be an estimate of the number of cores used during the sample interval.
// This is calculated by taking the difference between the total and idle HostCPU time
// and scaling it by the number of cores.
// Units are in "cores".
func (s *Statter) HostCPU() (*Result, error) {
r := &Result{
Unit: "cores",
Total: ptr.To(float64(s.nproc)),
Prefix: PrefixDefault,
}
c1, err := s.hi.CPUTime()
if err != nil {
return nil, xerrors.Errorf("get first cpu sample: %w", err)
}
s.wait(s.sampleInterval)
c2, err := s.hi.CPUTime()
if err != nil {
return nil, xerrors.Errorf("get second cpu sample: %w", err)
}
total := c2.Total() - c1.Total()
if total == 0 {
return r, nil // no change
}
idle := c2.Idle - c1.Idle
used := total - idle
scaleFactor := float64(s.nproc) / total.Seconds()
r.Used = used.Seconds() * scaleFactor
return r, nil
}
// HostMemory returns the memory usage of the host, in gigabytes.
func (s *Statter) HostMemory(p Prefix) (*Result, error) {
r := &Result{
Unit: "B",
Prefix: p,
}
hm, err := s.hi.Memory()
if err != nil {
return nil, xerrors.Errorf("get memory info: %w", err)
}
r.Total = ptr.To(float64(hm.Total))
// On Linux, hm.Used equates to MemTotal - MemFree in /proc/stat.
// This includes buffers and cache.
// So use MemAvailable instead, which only equates to physical memory.
// On Windows, this is also calculated as Total - Available.
r.Used = float64(hm.Total - hm.Available)
return r, nil
}