metric/system/cgroup/cgv2/io.go (117 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 cgv2
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/elastic/elastic-agent-libs/logp"
"github.com/elastic/elastic-agent-system-metrics/metric/system/cgroup/cgcommon"
)
// IOSubsystem is the replacement for the bulkio controller in cgroupsV1
type IOSubsystem struct {
ID string `json:"id,omitempty"` // ID of the cgroup.
Path string `json:"path,omitempty"` // Path to the cgroup relative to the cgroup subsystem's mountpoint.
Stats map[string]IOStat `json:"stats" struct:"stats"`
Pressure map[string]cgcommon.Pressure `json:"pressure" struct:"pressure"`
}
// IOStat carries io.Stat data for the controllers
// This data is broken down per-device, based on the maj-minor device ID
type IOStat struct {
Read IOMetric `json:"read" struct:"read"`
Write IOMetric `json:"write" struct:"write"`
Discarded IOMetric `json:"discarded" struct:"discarded"`
}
// IOMetric groups together the common IO sub-metrics by bytes and IOOps count
type IOMetric struct {
Bytes uint64 `json:"bytes" struct:"bytes"`
IOs uint64 `json:"ios" struct:"ios"`
}
// Get fetches metrics for the IO subsystem
// resolveDevIDs determines if Get will try to resolve the major-minor ID pairs reported by io.stat
// are resolved to a device name
func (io *IOSubsystem) Get(path string, resolveDevIDs bool) error {
var err error
io.Stats, err = getIOStats(path, resolveDevIDs)
if err != nil {
return fmt.Errorf("error getting io.stats for path %s: %w", path, err)
}
//Pressure doesn't exist on certain V2 implementations.
_, err = os.Stat(filepath.Join(path, "io.pressure"))
if errors.Is(err, os.ErrNotExist) {
logp.L().Debugf("io.pressure does not exist. Skipping.")
return nil
}
io.Pressure, err = cgcommon.GetPressure(filepath.Join(path, "io.pressure"))
if err != nil {
return fmt.Errorf("error fetching io.pressure for path %s: %w", path, err)
}
return nil
}
// getIOStats fetches and formats the io.stats object
func getIOStats(path string, resolveDevIDs bool) (map[string]IOStat, error) {
stats := make(map[string]IOStat)
file := filepath.Join(path, "io.stat")
f, err := os.Open(file)
if err != nil {
return stats, fmt.Errorf("error reading cpu.stat: %w", err)
}
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
devices, metrics, foundMetrics, err := parseStatLine(sc.Text(), resolveDevIDs)
if err != nil {
return nil, fmt.Errorf("error parsing line in file: %w", err)
}
if !foundMetrics {
continue
}
for _, dev := range devices {
stats[dev] = metrics
}
}
return stats, nil
}
// parses a single line in io.stat; a bit complicated, since these files are more complex then they look.
// returns a list of device names associated with the metrics, the metric set, and a bool indicating if metrics were found
func parseStatLine(line string, resolveDevIDs bool) ([]string, IOStat, bool, error) {
devIds := []string{}
stats := IOStat{}
foundMetrics := false
// cautiously iterate over a line to find the components
// under certain conditions, the stat.io will combine different loopback devices onto a single line,
// 7:7 7:6 7:5 7:4 rbytes=556032 wbytes=0 rios=78 wios=0 dbytes=0 dios=0
// we can also get lines without metrics, like
// 7:7 7:6 7:5 7:4
for _, component := range strings.Split(line, " ") {
if strings.Contains(component, ":") {
var major, minor uint64
_, err := fmt.Sscanf(component, "%d:%d", &major, &minor)
if err != nil {
return nil, IOStat{}, false, fmt.Errorf("could not read device ID: %s: %w", component, err)
}
var found bool
var devName string
// try to find the device name associated with the major/minor pair
// This isn't guaranteed to work, for a number of reasons, so we'll need to fall back
if resolveDevIDs {
found, devName, _ = fetchDeviceName(major, minor)
}
if found {
devIds = append(devIds, devName)
} else {
devIds = append(devIds, component)
}
} else if strings.Contains(component, "=") {
foundMetrics = true
counterSplit := strings.Split(component, "=")
if len(counterSplit) < 2 {
continue
}
name := counterSplit[0]
counter, err := strconv.ParseUint(counterSplit[1], 10, 64)
if err != nil {
return nil, IOStat{}, false, fmt.Errorf("error parsing counter '%s' in stat: %w", counterSplit[1], err)
}
switch name {
case "rbytes":
stats.Read.Bytes = counter
case "wbytes":
stats.Write.Bytes = counter
case "rios":
stats.Read.IOs = counter
case "wios":
stats.Write.IOs = counter
case "dbytes":
stats.Discarded.Bytes = counter
case "dios":
stats.Discarded.IOs = counter
}
}
}
return devIds, stats, foundMetrics, nil
}