blob: 101ec297d5d1e91d6c6e74dee5c4a00138da1ac9 [file] [log] [blame]
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bcachefs
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/prometheus/procfs/internal/fs"
"github.com/prometheus/procfs/internal/util"
)
// FS represents the pseudo-filesystem sys, which provides an interface to
// kernel data structures.
type FS struct {
sys *fs.FS
}
// NewDefaultFS returns a new Bcachefs using the default sys fs mount point. It will error
// if the mount point can't be read.
func NewDefaultFS() (FS, error) {
return NewFS(fs.DefaultSysMountPoint)
}
// NewFS returns a new Bcachefs filesystem using the given sys fs mount point. It will error
// if the mount point can't be read.
func NewFS(mountPoint string) (FS, error) {
if strings.TrimSpace(mountPoint) == "" {
mountPoint = fs.DefaultSysMountPoint
}
sys, err := fs.NewFS(mountPoint)
if err != nil {
return FS{}, err
}
return FS{&sys}, nil
}
// Stats retrieves Bcachefs filesystem runtime statistics for each mounted Bcachefs filesystem.
func (fs FS) Stats() ([]*Stats, error) {
base := fs.sys.Path("fs/bcachefs")
entries, err := os.ReadDir(base)
if err != nil {
return nil, err
}
stats := make([]*Stats, 0, len(entries))
for _, entry := range entries {
if !entry.IsDir() {
continue
}
uuidPath := filepath.Join(base, entry.Name())
s, err := GetStats(uuidPath)
if err != nil {
return nil, err
}
s.UUID = entry.Name()
stats = append(stats, s)
}
return stats, nil
}
// GetStats collects all Bcachefs statistics from sysfs.
func GetStats(uuidPath string) (*Stats, error) {
r := &reader{path: uuidPath}
s := r.readFilesystemStats()
if r.err != nil {
return nil, r.err
}
return s, nil
}
type reader struct {
path string
err error
}
// readFile reads a file relative to the path of the reader.
// Non-existing files are ignored.
func (r *reader) readFile(n string) string {
if r.err != nil {
return ""
}
b, err := util.ReadFileNoStat(filepath.Join(r.path, n))
if err != nil {
if !os.IsNotExist(err) {
r.err = err
}
return ""
}
return strings.TrimSpace(string(b))
}
func (r *reader) readHumanBytes(n string) uint64 {
s := r.readFile(n)
if r.err != nil || s == "" {
return 0
}
v, err := parseHumanReadableBytes(s)
if err != nil {
r.err = err
return 0
}
return v
}
func (r *reader) readFilesystemStats() *Stats {
stats := &Stats{
Compression: make(map[string]CompressionStats),
Errors: make(map[string]ErrorStats),
Counters: make(map[string]CounterStats),
BtreeWrites: make(map[string]BtreeWriteStats),
Devices: make(map[string]*DeviceStats),
}
stats.BtreeCacheSizeBytes = r.readHumanBytes("btree_cache_size")
if r.err != nil {
return stats
}
comp, err := parseCompressionStats(filepath.Join(r.path, "compression_stats"))
if err != nil {
r.err = err
return stats
}
stats.Compression = comp
errs, err := parseErrors(filepath.Join(r.path, "errors"))
if err != nil {
r.err = err
return stats
}
stats.Errors = errs
counters, err := parseCounters(filepath.Join(r.path, "counters"))
if err != nil {
r.err = err
return stats
}
stats.Counters = counters
writes, err := parseBtreeWriteStats(filepath.Join(r.path, "btree_write_stats"))
if err != nil {
r.err = err
return stats
}
stats.BtreeWrites = writes
devices, err := parseDevices(r.path)
if err != nil {
r.err = err
return stats
}
stats.Devices = devices
return stats
}
func parseHumanReadableBytes(s string) (uint64, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, fmt.Errorf("empty string")
}
multiplier := float64(1)
lastChar := s[len(s)-1]
switch lastChar {
case 'k', 'K':
multiplier = 1024
s = s[:len(s)-1]
case 'm', 'M':
multiplier = 1024 * 1024
s = s[:len(s)-1]
case 'g', 'G':
multiplier = 1024 * 1024 * 1024
s = s[:len(s)-1]
case 't', 'T':
multiplier = 1024 * 1024 * 1024 * 1024
s = s[:len(s)-1]
case 'p', 'P':
multiplier = 1024 * 1024 * 1024 * 1024 * 1024
s = s[:len(s)-1]
case 'e', 'E':
multiplier = 1024 * 1024 * 1024 * 1024 * 1024 * 1024
s = s[:len(s)-1]
}
value, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
if err != nil {
return 0, err
}
return uint64(value * multiplier), nil
}
func parseCompressionStats(path string) (map[string]CompressionStats, error) {
file, err := openIfExists(path)
if err != nil {
return nil, err
}
if file == nil {
return map[string]CompressionStats{}, nil
}
defer file.Close()
stats := make(map[string]CompressionStats)
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
if lineNum == 1 {
continue
}
fields := strings.Fields(line)
if len(fields) < 3 {
continue
}
algorithm := strings.TrimSuffix(fields[0], ":")
compressed, err := parseHumanReadableBytes(fields[1])
if err != nil {
return nil, err
}
uncompressed, err := parseHumanReadableBytes(fields[2])
if err != nil {
return nil, err
}
var avgExtent uint64
if len(fields) >= 4 {
avgExtent, err = parseHumanReadableBytes(fields[3])
if err != nil {
return nil, err
}
}
stats[algorithm] = CompressionStats{
CompressedBytes: compressed,
UncompressedBytes: uncompressed,
AverageExtentSizeBytes: avgExtent,
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return stats, nil
}
func parseErrors(path string) (map[string]ErrorStats, error) {
file, err := openIfExists(path)
if err != nil {
return nil, err
}
if file == nil {
return map[string]ErrorStats{}, nil
}
defer file.Close()
stats := make(map[string]ErrorStats)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
count, err := strconv.ParseUint(fields[1], 10, 64)
if err != nil {
return nil, err
}
var ts uint64
if len(fields) >= 3 {
ts, err = strconv.ParseUint(fields[2], 10, 64)
if err != nil {
return nil, err
}
}
stats[fields[0]] = ErrorStats{Count: count, Timestamp: ts}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return stats, nil
}
func parseCounters(countersPath string) (map[string]CounterStats, error) {
entries, err := os.ReadDir(countersPath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
stats := make(map[string]CounterStats, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
counterPath := filepath.Join(countersPath, entry.Name())
counter, err := parseCounterFile(counterPath)
if err != nil {
return nil, err
}
stats[entry.Name()] = counter
}
return stats, nil
}
func parseCounterFile(path string) (CounterStats, error) {
file, err := openIfExists(path)
if err != nil || file == nil {
return CounterStats{}, err
}
defer file.Close()
var stats CounterStats
var seenCreation bool
var seenMount bool
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if valueStr, ok := strings.CutPrefix(line, "since mount:"); ok {
value, err := parseHumanReadableBytes(valueStr)
if err != nil {
return CounterStats{}, err
}
stats.SinceMount = value
seenMount = true
continue
}
if valueStr, ok := strings.CutPrefix(line, "since filesystem creation:"); ok {
value, err := parseHumanReadableBytes(valueStr)
if err != nil {
return CounterStats{}, err
}
stats.SinceFilesystemCreation = value
seenCreation = true
continue
}
}
if err := scanner.Err(); err != nil {
return CounterStats{}, err
}
if !seenCreation && !seenMount {
return CounterStats{}, fmt.Errorf("counter file format not recognized")
}
return stats, nil
}
func parseBtreeWriteStats(path string) (map[string]BtreeWriteStats, error) {
file, err := openIfExists(path)
if err != nil {
return nil, err
}
if file == nil {
return map[string]BtreeWriteStats{}, nil
}
defer file.Close()
stats := make(map[string]BtreeWriteStats)
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
if lineNum == 1 {
continue
}
fields := strings.Fields(line)
if len(fields) < 3 {
continue
}
writeType := strings.TrimSuffix(fields[0], ":")
count, err := strconv.ParseUint(fields[1], 10, 64)
if err != nil {
return nil, err
}
size, err := parseHumanReadableBytes(fields[2])
if err != nil {
return nil, err
}
stats[writeType] = BtreeWriteStats{Count: count, SizeBytes: size}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return stats, nil
}
func parseDevices(fsPath string) (map[string]*DeviceStats, error) {
entries, err := os.ReadDir(fsPath)
if err != nil {
return nil, err
}
devices := make(map[string]*DeviceStats)
for _, entry := range entries {
if !entry.IsDir() || !strings.HasPrefix(entry.Name(), "dev-") {
continue
}
device := strings.TrimPrefix(entry.Name(), "dev-")
devPath := filepath.Join(fsPath, entry.Name())
stats := &DeviceStats{
Label: readSysfsFile(filepath.Join(devPath, "label")),
State: readSysfsFile(filepath.Join(devPath, "state")),
IODone: make(map[string]map[string]uint64),
IOErrors: make(map[string]uint64),
}
if bucketSizeRaw := readSysfsFile(filepath.Join(devPath, "bucket_size")); bucketSizeRaw != "" {
bucketSize, err := parseHumanReadableBytes(bucketSizeRaw)
if err != nil {
return nil, err
}
stats.BucketSizeBytes = bucketSize
}
nbuckets, err := readUintFile(filepath.Join(devPath, "nbuckets"))
if err != nil {
return nil, err
}
stats.Buckets = nbuckets
durability, err := readUintFile(filepath.Join(devPath, "durability"))
if err != nil {
return nil, err
}
stats.Durability = durability
ioDone, err := parseDeviceIODone(filepath.Join(devPath, "io_done"))
if err != nil {
return nil, err
}
stats.IODone = ioDone
ioErrors, err := parseDeviceIOErrors(filepath.Join(devPath, "io_errors"))
if err != nil {
return nil, err
}
stats.IOErrors = ioErrors
devices[device] = stats
}
return devices, nil
}
func readSysfsFile(path string) string {
data, err := util.ReadFileNoStat(path)
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
}
func readUintFile(path string) (uint64, error) {
data, err := util.ReadFileNoStat(path)
if err != nil {
if os.IsNotExist(err) {
return 0, nil
}
return 0, err
}
return strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
}
func parseDeviceIODone(path string) (map[string]map[string]uint64, error) {
file, err := openIfExists(path)
if err != nil {
return nil, err
}
if file == nil {
return map[string]map[string]uint64{}, nil
}
defer file.Close()
stats := make(map[string]map[string]uint64)
scanner := bufio.NewScanner(file)
var currentOp string
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if line == "read:" || line == "write:" {
currentOp = strings.TrimSuffix(line, ":")
if _, ok := stats[currentOp]; !ok {
stats[currentOp] = make(map[string]uint64)
}
continue
}
if currentOp == "" {
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
continue
}
dataType := strings.TrimSpace(parts[0])
valueStr := strings.TrimSpace(parts[1])
value, err := strconv.ParseUint(valueStr, 10, 64)
if err != nil {
return nil, err
}
stats[currentOp][dataType] = value
}
if err := scanner.Err(); err != nil {
return nil, err
}
return stats, nil
}
func parseDeviceIOErrors(path string) (map[string]uint64, error) {
file, err := openIfExists(path)
if err != nil {
return nil, err
}
if file == nil {
return map[string]uint64{}, nil
}
defer file.Close()
stats := make(map[string]uint64)
scanner := bufio.NewScanner(file)
inCreationSection := false
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "IO errors since filesystem creation") {
inCreationSection = true
continue
}
if strings.HasPrefix(line, "IO errors since ") {
break
}
if !inCreationSection {
continue
}
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
parts := strings.SplitN(trimmed, ":", 2)
if len(parts) != 2 {
continue
}
errorType := strings.TrimSpace(parts[0])
valueStr := strings.TrimSpace(parts[1])
value, err := strconv.ParseUint(valueStr, 10, 64)
if err != nil {
return nil, err
}
stats[errorType] = value
}
if err := scanner.Err(); err != nil {
return nil, err
}
return stats, nil
}
func openIfExists(path string) (*os.File, error) {
file, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
return file, nil
}