306 lines
7 KiB
Go
306 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
|
||
|
}
|