Repository navigation
Expand file tree
/
Copy pathdisk.go
More file actions
214 lines (190 loc) · 5.9 KB
/
Copy pathdisk.go
File metadata and controls
214 lines (190 loc) · 5.9 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
package systats
import (
"errors"
"math"
"strconv"
"strings"
"time"
"github.com/dhamith93/systats/internal/fileops"
"golang.org/x/sys/unix"
)
// Disk holds information on single disk
type Disk struct {
FileSystem string `json:"fileSystem"`
Type string `json:"type"`
MountedOn string `json:"mountedOn"`
Usage DiskUsage `json:"usage"`
Inodes InodeUsage `json:"inodes"`
Time int64 `json:"time"`
}
// DiskUsage holds information on single disk usage information.
//
// The sizes are float64 for the same reason Memory's are: as integers the
// larger units discard too much to be usable. A 512 MiB partition converted
// to Gigabyte truncated to 0, and converting away from Byte and back could
// not return the original value.
type DiskUsage struct {
Size float64 `json:"size"`
Used float64 `json:"used"`
Available float64 `json:"available"`
Usage string `json:"usage"`
Unit Unit `json:"unit"`
}
// InodeUsage holds information on single disk inode usage
type InodeUsage struct {
Inodes uint64 `json:"inodes"`
Available uint64 `json:"available"`
Used uint64 `json:"used"`
Usage string `json:"usage"`
}
type mountEntry struct {
device string
mountPoint string
fsType string
}
// excludedFsTypes mirrors the --exclude-type filters the old `df`-based
// implementation used for pseudo filesystems that (unlike sysfs/proc/
// cgroup, which self-exclude below via a zero block count) report a
// real, non-zero size.
var excludedFsTypes = map[string]bool{
"tmpfs": true,
"devtmpfs": true,
"udev": true,
}
func getDisks(systats *SyStats) ([]Disk, error) {
mounts, err := readMounts(systats.MountsPath)
if err != nil {
return nil, err
}
output := []Disk{}
for _, m := range mounts {
if excludedFsTypes[m.fsType] {
continue
}
var stat unix.Statfs_t
if err := unix.Statfs(m.mountPoint, &stat); err != nil {
continue // mount point disappeared, or isn't statable
}
// df itself never shows filesystems reporting zero blocks - this
// is what filters out sysfs/proc/cgroup/etc without needing them
// in excludedFsTypes above.
if stat.Blocks == 0 {
continue
}
blockSize := uint64(stat.Bsize)
size := uint64(stat.Blocks) * blockSize
free := uint64(stat.Bfree) * blockSize
available := uint64(stat.Bavail) * blockSize
used := size - free
inodesTotal := uint64(stat.Files)
inodesFree := uint64(stat.Ffree)
inodesUsed := inodesTotal - inodesFree
output = append(output, Disk{
FileSystem: m.device,
Type: m.fsType,
MountedOn: m.mountPoint,
// The byte figures stay integral up to here so usagePercent
// matches df exactly; float64 starts at the API boundary.
Usage: DiskUsage{
Size: float64(size),
Used: float64(used),
Available: float64(available),
Usage: usagePercent(used, available),
Unit: Byte,
},
Inodes: InodeUsage{
Inodes: inodesTotal,
Used: inodesUsed,
Available: inodesFree,
Usage: usagePercent(inodesUsed, inodesFree),
},
Time: time.Now().Unix(),
})
}
return output, nil
}
// readMounts parses a /proc/mounts-formatted file into mountEntry values.
func readMounts(path string) ([]mountEntry, error) {
content, err := fileops.ReadFileWithError(path)
if err != nil {
return nil, err
}
return parseMounts(content), nil
}
// parseMounts is split out from readMounts so the parsing/escaping logic
// can be unit-tested against fixture text without needing a real
// /proc/mounts file.
func parseMounts(content string) []mountEntry {
entries := []mountEntry{}
for _, line := range strings.Split(content, "\n") {
fields := strings.Fields(line)
if len(fields) < 3 {
continue
}
entries = append(entries, mountEntry{
device: unescapeMountField(fields[0]),
mountPoint: unescapeMountField(fields[1]),
fsType: fields[2],
})
}
return entries
}
var mountFieldEscapes = strings.NewReplacer(
`\040`, " ",
`\011`, "\t",
`\012`, "\n",
`\134`, `\`,
)
func unescapeMountField(field string) string {
return mountFieldEscapes.Replace(field)
}
// usagePercent replicates df's Use%/IUse% formula: ceil(100 * used /
// (used + avail)), not used/size*100 - avail excludes blocks/inodes
// reserved for root, so it isn't the same as (size - used).
func usagePercent(used, avail uint64) string {
denom := used + avail
if denom == 0 {
return "0%"
}
pct := int(math.Ceil(100 * float64(used) / float64(denom)))
return strconv.Itoa(pct) + "%"
}
// bytesPerUnit is how many bytes one of each unit holds. All binary, so
// every factor is a power of two and conversions through it are exact in
// float64.
var bytesPerUnit = map[Unit]float64{
Byte: 1,
Kilobyte: 1024,
Megabyte: 1024 * 1024,
Gigabyte: 1024 * 1024 * 1024,
}
// Convert rewrites d's usage figures into unit, in place. The unit
// constants are binary (Megabyte is MiB, Gigabyte is GiB), matching
// GetMemory/GetSwap and what df(1) reports.
//
// An unrecognized unit - on either side of the conversion - returns an
// error and leaves d untouched. Convert used to relabel the figures
// without converting them, so the struct reported values in a unit they
// weren't in.
//
// Note the pointer receiver: ranging over a []Disk gives copies, so
// `for _, d := range disks { d.Convert(...) }` won't change the slice.
// Index instead: `for i := range disks { disks[i].Convert(...) }`.
func (d *Disk) Convert(unit Unit) error {
from, ok := bytesPerUnit[d.Usage.Unit]
if !ok {
return errors.New(string(d.Usage.Unit) + " is not a supported unit to convert from")
}
to, ok := bytesPerUnit[unit]
if !ok {
return errors.New(string(unit) + " is not a supported unit to convert to")
}
// Normalize through bytes rather than special-casing each pair. Both
// factors are powers of two, so this round-trips exactly.
factor := from / to
d.Usage.Size *= factor
d.Usage.Used *= factor
d.Usage.Available *= factor
d.Usage.Unit = unit
return nil
}