1
0
Fork 0
telegraf/plugins/inputs/cgroup/cgroup_linux.go
Daniel Baumann 4978089aab
Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-24 07:26:29 +02:00

305 lines
7 KiB
Go

//go:build linux
package cgroup
import (
"fmt"
"math"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/influxdata/telegraf"
)
const metricName = "cgroup"
func (cg *CGroup) Gather(acc telegraf.Accumulator) error {
list := make(chan pathInfo)
go cg.generateDirs(list)
for dir := range list {
if dir.err != nil {
acc.AddError(dir.err)
continue
}
if err := cg.gatherDir(acc, dir.path); err != nil {
acc.AddError(err)
}
}
return nil
}
func (cg *CGroup) gatherDir(acc telegraf.Accumulator, dir string) error {
fields := make(map[string]interface{})
list := make(chan pathInfo)
go cg.generateFiles(dir, list)
for file := range list {
if file.err != nil {
return file.err
}
raw, err := os.ReadFile(file.path)
if err != nil {
return err
}
if len(raw) == 0 {
continue
}
fd := fileData{data: raw, path: file.path}
if err := fd.parse(fields); err != nil {
if !cg.logged[file.path] {
acc.AddError(err)
}
cg.logged[file.path] = true
continue
}
}
tags := map[string]string{"path": dir}
acc.AddFields(metricName, fields, tags)
return nil
}
// ======================================================================
type pathInfo struct {
path string
err error
}
func isDir(pathToCheck string) (bool, error) {
result, err := os.Stat(pathToCheck)
if err != nil {
return false, err
}
return result.IsDir(), nil
}
func (cg *CGroup) generateDirs(list chan<- pathInfo) {
defer close(list)
for _, dir := range cg.Paths {
// getting all dirs that match the pattern 'dir'
items, err := filepath.Glob(dir)
if err != nil {
list <- pathInfo{err: err}
return
}
for _, item := range items {
ok, err := isDir(item)
if err != nil {
list <- pathInfo{err: err}
return
}
// supply only dirs
if ok {
list <- pathInfo{path: item}
}
}
}
}
func (cg *CGroup) generateFiles(dir string, list chan<- pathInfo) {
dir = strings.Replace(dir, "\\", "\\\\", -1)
defer close(list)
for _, file := range cg.Files {
// getting all file paths that match the pattern 'dir + file'
// path.Base make sure that file variable does not contains part of path
items, err := filepath.Glob(path.Join(dir, path.Base(file)))
if err != nil {
list <- pathInfo{err: err}
return
}
for _, item := range items {
ok, err := isDir(item)
if err != nil {
list <- pathInfo{err: err}
return
}
// supply only files not dirs
if !ok {
list <- pathInfo{path: item}
}
}
}
}
// ======================================================================
type fileData struct {
data []byte
path string
}
func (fd *fileData) format() (*fileFormat, error) {
for _, ff := range fileFormats {
ok, err := ff.match(fd.data)
if err != nil {
return nil, err
}
if ok {
return &ff, nil
}
}
return nil, fmt.Errorf("%v: unknown file format", fd.path)
}
func (fd *fileData) parse(fields map[string]interface{}) error {
format, err := fd.format()
if err != nil {
return err
}
format.parser(filepath.Base(fd.path), fields, fd.data)
return nil
}
// ======================================================================
type fileFormat struct {
name string
pattern string
parser func(measurement string, fields map[string]interface{}, b []byte)
}
const keyPattern = "[[:alnum:]:_.]+"
const valuePattern = "(?:max|[\\d-\\.]+)"
var fileFormats = [...]fileFormat{
// VAL\n
{
name: "Single value",
pattern: "^" + valuePattern + "\n$",
parser: func(measurement string, fields map[string]interface{}, b []byte) {
re := regexp.MustCompile("^(" + valuePattern + ")\n$")
matches := re.FindAllStringSubmatch(string(b), -1)
fields[measurement] = numberOrString(matches[0][1])
},
},
// VAL0\n
// VAL1\n
// ...
{
name: "New line separated values",
pattern: "^(" + valuePattern + "\n){2,}$",
parser: func(measurement string, fields map[string]interface{}, b []byte) {
re := regexp.MustCompile("(" + valuePattern + ")\n")
matches := re.FindAllStringSubmatch(string(b), -1)
for i, v := range matches {
fields[measurement+"."+strconv.Itoa(i)] = numberOrString(v[1])
}
},
},
// VAL0 VAL1 ...\n
{
name: "Space separated values",
pattern: "^(" + valuePattern + " ?)+\n$",
parser: func(measurement string, fields map[string]interface{}, b []byte) {
re := regexp.MustCompile("(" + valuePattern + ")")
matches := re.FindAllStringSubmatch(string(b), -1)
for i, v := range matches {
fields[measurement+"."+strconv.Itoa(i)] = numberOrString(v[1])
}
},
},
// KEY0 ... VAL0\n
// KEY1 ... VAL1\n
// ...
{
name: "Space separated keys and value, separated by new line",
pattern: "^((" + keyPattern + " )+" + valuePattern + "\n)+$",
parser: func(measurement string, fields map[string]interface{}, b []byte) {
re := regexp.MustCompile("((?:" + keyPattern + " ?)+) (" + valuePattern + ")\n")
matches := re.FindAllStringSubmatch(string(b), -1)
for _, v := range matches {
k := strings.ReplaceAll(v[1], " ", ".")
fields[measurement+"."+k] = numberOrString(v[2])
}
},
},
// NAME0 KEY0=VAL0 ...\n
// NAME1 KEY1=VAL1 ...\n
// ...
{
name: "Equal sign separated key-value pairs, multiple lines with name",
pattern: fmt.Sprintf("^(%s( %s=%s)+\n)+$", keyPattern, keyPattern, valuePattern),
parser: func(measurement string, fields map[string]interface{}, b []byte) {
lines := strings.Split(string(b), "\n")
for _, line := range lines {
f := strings.Fields(line)
if len(f) == 0 {
continue
}
name := f[0]
for _, field := range f[1:] {
k, v, found := strings.Cut(field, "=")
if found {
fields[strings.Join([]string{measurement, name, k}, ".")] = numberOrString(v)
}
}
}
},
},
// KEY0=VAL0 KEY1=VAL1 ...\n
{
name: "Equal sign separated key-value pairs on a single line",
pattern: fmt.Sprintf("^(%s=%s ?)+\n$", keyPattern, valuePattern),
parser: func(measurement string, fields map[string]interface{}, b []byte) {
f := strings.Fields(string(b))
if len(f) == 0 {
return
}
for _, field := range f {
k, v, found := strings.Cut(field, "=")
if found {
fields[strings.Join([]string{measurement, k}, ".")] = numberOrString(v)
}
}
},
},
}
func numberOrString(s string) interface{} {
i, err := strconv.ParseInt(s, 10, 64)
if err == nil {
return i
}
if s == "max" {
return int64(math.MaxInt64)
}
// Care should be taken to always interpret each field as the same type on every cycle.
// *.pressure files follow the PSI format and contain numbers with fractional parts
// that always have a decimal separator, even when the fractional part is 0 (e.g., "0.00"),
// thus they will always be interpreted as floats.
// https://www.kernel.org/doc/Documentation/accounting/psi.txt
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return f
}
return s
}
func (f fileFormat) match(b []byte) (bool, error) {
ok, err := regexp.Match(f.pattern, b)
if err != nil {
return false, err
}
if ok {
return true, nil
}
return false, nil
}