Merge branch 'master' into gpu-clk

pull/3093/head
Xuhui Zhu 2024-09-30 19:20:03 -07:00 committed by GitHub
commit 878d1d5358
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 657 additions and 251 deletions

76
collector/cpu_aix.go Normal file
View File

@ -0,0 +1,76 @@
// Copyright 2024 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.
//go:build !nocpu
// +build !nocpu
package collector
/*
#include <unistd.h> // Include the standard Unix header
#include <errno.h> // For errno
*/
import "C"
import (
"fmt"
"log/slog"
"strconv"
"github.com/power-devops/perfstat"
"github.com/prometheus/client_golang/prometheus"
)
type cpuCollector struct {
cpu typedDesc
logger *slog.Logger
tickPerSecond int64
}
func init() {
registerCollector("cpu", defaultEnabled, NewCpuCollector)
}
func tickPerSecond() (int64, error) {
ticks, err := C.sysconf(C._SC_CLK_TCK)
if ticks == -1 || err != nil {
return 0, fmt.Errorf("failed to get clock ticks per second: %v", err)
}
return int64(ticks), nil
}
func NewCpuCollector(logger *slog.Logger) (Collector, error) {
ticks, err := tickPerSecond()
if err != nil {
return nil, err
}
return &cpuCollector{
cpu: typedDesc{nodeCPUSecondsDesc, prometheus.CounterValue},
logger: logger,
tickPerSecond: ticks,
}, nil
}
func (c *cpuCollector) Update(ch chan<- prometheus.Metric) error {
stats, err := perfstat.CpuStat()
if err != nil {
return err
}
for n, stat := range stats {
ch <- c.cpu.mustNewConstMetric(float64(stat.User/c.tickPerSecond), strconv.Itoa(n), "user")
ch <- c.cpu.mustNewConstMetric(float64(stat.Sys/c.tickPerSecond), strconv.Itoa(n), "system")
ch <- c.cpu.mustNewConstMetric(float64(stat.Idle/c.tickPerSecond), strconv.Itoa(n), "idle")
ch <- c.cpu.mustNewConstMetric(float64(stat.Wait/c.tickPerSecond), strconv.Itoa(n), "wait")
}
return nil
}

View File

@ -0,0 +1,82 @@
// Copyright 2024 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.
//go:build !nodiskstats
// +build !nodiskstats
package collector
import (
"fmt"
"log/slog"
"github.com/power-devops/perfstat"
"github.com/prometheus/client_golang/prometheus"
)
const diskstatsDefaultIgnoredDevices = ""
type diskstatsCollector struct {
rbytes typedDesc
wbytes typedDesc
time typedDesc
deviceFilter deviceFilter
logger *slog.Logger
tickPerSecond int64
}
func init() {
registerCollector("diskstats", defaultEnabled, NewDiskstatsCollector)
}
// NewDiskstatsCollector returns a new Collector exposing disk device stats.
func NewDiskstatsCollector(logger *slog.Logger) (Collector, error) {
ticks, err := tickPerSecond()
if err != nil {
return nil, err
}
deviceFilter, err := newDiskstatsDeviceFilter(logger)
if err != nil {
return nil, fmt.Errorf("failed to parse device filter flags: %w", err)
}
return &diskstatsCollector{
rbytes: typedDesc{readBytesDesc, prometheus.CounterValue},
wbytes: typedDesc{writtenBytesDesc, prometheus.CounterValue},
time: typedDesc{ioTimeSecondsDesc, prometheus.CounterValue},
deviceFilter: deviceFilter,
logger: logger,
tickPerSecond: ticks,
}, nil
}
func (c *diskstatsCollector) Update(ch chan<- prometheus.Metric) error {
stats, err := perfstat.DiskStat()
if err != nil {
return err
}
for _, stat := range stats {
if c.deviceFilter.ignored(stat.Name) {
continue
}
ch <- c.rbytes.mustNewConstMetric(float64(stat.Rblks*512), stat.Name)
ch <- c.wbytes.mustNewConstMetric(float64(stat.Wblks*512), stat.Name)
ch <- c.time.mustNewConstMetric(float64(stat.Time/c.tickPerSecond), stat.Name)
}
return nil
}

View File

@ -11,9 +11,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !nodiskstats && (openbsd || linux || darwin) //go:build !nodiskstats && (openbsd || linux || darwin || aix)
// +build !nodiskstats // +build !nodiskstats
// +build openbsd linux darwin // +build openbsd linux darwin aix
package collector package collector

View File

@ -0,0 +1,65 @@
// Copyright 2024 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.
//go:build !nofilesystem
// +build !nofilesystem
package collector
import (
"github.com/power-devops/perfstat"
)
const (
defMountPointsExcluded = "^/(dev|aha)($|/)"
defFSTypesExcluded = "^procfs$"
)
// Expose filesystem fullness.
func (c *filesystemCollector) GetStats() (stats []filesystemStats, err error) {
fsStat, err := perfstat.FileSystemStat()
if err != nil {
return nil, err
}
for _, stat := range fsStat {
if c.excludedMountPointsPattern.MatchString(stat.MountPoint) {
c.logger.Debug("Ignoring mount point", "mountpoint", stat.MountPoint)
continue
}
fstype := stat.TypeString()
if c.excludedFSTypesPattern.MatchString(fstype) {
c.logger.Debug("Ignoring fs type", "type", fstype)
continue
}
ro := 0.0
if stat.Flags&perfstat.VFS_READONLY != 0 {
ro = 1.0
}
stats = append(stats, filesystemStats{
labels: filesystemLabels{
device: stat.Device,
mountPoint: stat.MountPoint,
fsType: fstype,
},
size: float64(stat.TotalBlocks / 512.0),
free: float64(stat.FreeBlocks / 512.0),
avail: float64(stat.FreeBlocks / 512.0), // AIX doesn't distinguish between free and available blocks.
files: float64(stat.TotalInodes),
filesFree: float64(stat.FreeInodes),
ro: ro,
})
}
return stats, nil
}

View File

@ -11,9 +11,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !nofilesystem && (linux || freebsd || netbsd || openbsd || darwin || dragonfly) //go:build !nofilesystem && (linux || freebsd || netbsd || openbsd || darwin || dragonfly || aix)
// +build !nofilesystem // +build !nofilesystem
// +build linux freebsd netbsd openbsd darwin dragonfly // +build linux freebsd netbsd openbsd darwin dragonfly aix
package collector package collector

View File

@ -18,6 +18,7 @@ package collector
import ( import (
"errors" "errors"
"fmt"
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
@ -107,6 +108,9 @@ func sysReadFile(file string) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if n < 0 {
return nil, fmt.Errorf("failed to read file: %q, read returned negative bytes value: %d", file, n)
}
return b[:n], nil return b[:n], nil
} }

View File

@ -11,8 +11,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build (darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris) && !noloadavg //go:build (darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix) && !noloadavg
// +build darwin dragonfly freebsd linux netbsd openbsd solaris // +build darwin dragonfly freebsd linux netbsd openbsd solaris aix
// +build !noloadavg // +build !noloadavg
package collector package collector

30
collector/loadavg_aix.go Normal file
View File

@ -0,0 +1,30 @@
// Copyright 2024 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.
//go:build !noloadavg
// +build !noloadavg
package collector
import (
"github.com/power-devops/perfstat"
)
func getLoad() ([]float64, error) {
stat, err := perfstat.CpuTotalStat()
if err != nil {
return nil, err
}
return []float64{float64(stat.LoadAvg1), float64(stat.LoadAvg5), float64(stat.LoadAvg15)}, nil
}

View File

@ -11,8 +11,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build (darwin || linux || openbsd || netbsd) && !nomeminfo //go:build (darwin || linux || openbsd || netbsd || aix) && !nomeminfo
// +build darwin linux openbsd netbsd // +build darwin linux openbsd netbsd aix
// +build !nomeminfo // +build !nomeminfo
package collector package collector

47
collector/meminfo_aix.go Normal file
View File

@ -0,0 +1,47 @@
// Copyright 2024 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.
//go:build !nomeminfo
// +build !nomeminfo
package collector
import (
"log/slog"
"github.com/power-devops/perfstat"
)
type meminfoCollector struct {
logger *slog.Logger
}
// NewMeminfoCollector returns a new Collector exposing memory stats.
func NewMeminfoCollector(logger *slog.Logger) (Collector, error) {
return &meminfoCollector{
logger: logger,
}, nil
}
func (c *meminfoCollector) getMemInfo() (map[string]float64, error) {
stats, err := perfstat.MemoryTotalStat()
if err != nil {
return nil, err
}
return map[string]float64{
"total_bytes": float64(stats.RealTotal * 4096),
"free_bytes": float64(stats.RealFree * 4096),
"available_bytes": float64(stats.RealAvailable * 4096),
}, nil
}

54
collector/netdev_aix.go Normal file
View File

@ -0,0 +1,54 @@
// Copyright 2024 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.
//go:build !nonetdev
// +build !nonetdev
package collector
import (
"log/slog"
"github.com/power-devops/perfstat"
)
func getNetDevStats(filter *deviceFilter, logger *slog.Logger) (netDevStats, error) {
netDev := netDevStats{}
stats, err := perfstat.NetAdapterStat()
if err != nil {
return nil, err
}
for _, stat := range stats {
netDev[stat.Name] = map[string]uint64{
"receive_packets": uint64(stat.RxPackets),
"transmit_packets": uint64(stat.TxPackets),
"receive_bytes": uint64(stat.RxBytes),
"transmit_bytes": uint64(stat.TxBytes),
"receive_errors": uint64(stat.RxErrors),
"transmit_errors": uint64(stat.TxErrors),
"receive_dropped": uint64(stat.RxPacketsDropped),
"transmit_dropped": uint64(stat.TxPacketsDropped),
"receive_multicast": uint64(stat.RxMulticastPackets),
"transmit_multicast": uint64(stat.TxMulticastPackets),
}
}
return netDev, nil
}
func getNetDevLabels() (map[string]map[string]string, error) {
// to be implemented if needed
return nil, nil
}

View File

@ -70,3 +70,7 @@ func getNetDevStats(filter *deviceFilter, logger *slog.Logger) (netDevStats, err
return netDev, nil return netDev, nil
} }
func getNetDevLabels() (map[string]map[string]string, error) {
return nil, nil
}

View File

@ -11,9 +11,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !nonetdev && (linux || freebsd || openbsd || dragonfly || darwin) //go:build !nonetdev && (linux || freebsd || openbsd || dragonfly || darwin || aix)
// +build !nonetdev // +build !nonetdev
// +build linux freebsd openbsd dragonfly darwin // +build linux freebsd openbsd dragonfly darwin aix
package collector package collector

View File

@ -11,6 +11,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !noosrelease && !aix
// +build !noosrelease,!aix
package collector package collector
import ( import (

View File

@ -32,8 +32,8 @@ import (
) )
var ( var (
textFileDirectory = kingpin.Flag("collector.textfile.directory", "Directory to read text files with metrics from.").Default("").String() textFileDirectories = kingpin.Flag("collector.textfile.directory", "Directory to read text files with metrics from, supports glob matching. (repeatable)").Default("").Strings()
mtimeDesc = prometheus.NewDesc( mtimeDesc = prometheus.NewDesc(
"node_textfile_mtime_seconds", "node_textfile_mtime_seconds",
"Unixtime mtime of textfiles successfully read.", "Unixtime mtime of textfiles successfully read.",
[]string{"file"}, []string{"file"},
@ -42,7 +42,7 @@ var (
) )
type textFileCollector struct { type textFileCollector struct {
path string paths []string
// Only set for testing to get predictable output. // Only set for testing to get predictable output.
mtime *float64 mtime *float64
logger *slog.Logger logger *slog.Logger
@ -56,7 +56,7 @@ func init() {
// in the given textfile directory. // in the given textfile directory.
func NewTextFileCollector(logger *slog.Logger) (Collector, error) { func NewTextFileCollector(logger *slog.Logger) (Collector, error) {
c := &textFileCollector{ c := &textFileCollector{
path: *textFileDirectory, paths: *textFileDirectories,
logger: logger, logger: logger,
} }
return c, nil return c, nil
@ -194,11 +194,15 @@ func (c *textFileCollector) Update(ch chan<- prometheus.Metric) error {
metricsNamesToFiles := map[string][]string{} metricsNamesToFiles := map[string][]string{}
metricsNamesToHelpTexts := map[string][2]string{} metricsNamesToHelpTexts := map[string][2]string{}
paths, err := filepath.Glob(c.path) paths := []string{}
if err != nil || len(paths) == 0 { for _, glob := range c.paths {
// not glob or not accessible path either way assume single ps, err := filepath.Glob(glob)
// directory and let os.ReadDir handle it if err != nil || len(ps) == 0 {
paths = []string{c.path} // not glob or not accessible path either way assume single
// directory and let os.ReadDir handle it
ps = []string{glob}
}
paths = append(paths, ps...)
} }
mtimes := make(map[string]time.Time) mtimes := make(map[string]time.Time)

View File

@ -52,75 +52,82 @@ func (a collectorAdapter) Collect(ch chan<- prometheus.Metric) {
func TestTextfileCollector(t *testing.T) { func TestTextfileCollector(t *testing.T) {
tests := []struct { tests := []struct {
path string paths []string
out string out string
}{ }{
{ {
path: "fixtures/textfile/no_metric_files", paths: []string{"fixtures/textfile/no_metric_files"},
out: "fixtures/textfile/no_metric_files.out", out: "fixtures/textfile/no_metric_files.out",
}, },
{ {
path: "fixtures/textfile/two_metric_files", paths: []string{"fixtures/textfile/two_metric_files"},
out: "fixtures/textfile/two_metric_files.out", out: "fixtures/textfile/two_metric_files.out",
}, },
{ {
path: "fixtures/textfile/nonexistent_path", paths: []string{"fixtures/textfile/nonexistent_path"},
out: "fixtures/textfile/nonexistent_path.out", out: "fixtures/textfile/nonexistent_path.out",
}, },
{ {
path: "fixtures/textfile/client_side_timestamp", paths: []string{"fixtures/textfile/client_side_timestamp"},
out: "fixtures/textfile/client_side_timestamp.out", out: "fixtures/textfile/client_side_timestamp.out",
}, },
{ {
path: "fixtures/textfile/different_metric_types", paths: []string{"fixtures/textfile/different_metric_types"},
out: "fixtures/textfile/different_metric_types.out", out: "fixtures/textfile/different_metric_types.out",
}, },
{ {
path: "fixtures/textfile/inconsistent_metrics", paths: []string{"fixtures/textfile/inconsistent_metrics"},
out: "fixtures/textfile/inconsistent_metrics.out", out: "fixtures/textfile/inconsistent_metrics.out",
}, },
{ {
path: "fixtures/textfile/histogram", paths: []string{"fixtures/textfile/histogram"},
out: "fixtures/textfile/histogram.out", out: "fixtures/textfile/histogram.out",
}, },
{ {
path: "fixtures/textfile/histogram_extra_dimension", paths: []string{"fixtures/textfile/histogram_extra_dimension"},
out: "fixtures/textfile/histogram_extra_dimension.out", out: "fixtures/textfile/histogram_extra_dimension.out",
}, },
{ {
path: "fixtures/textfile/summary", paths: []string{"fixtures/textfile/summary"},
out: "fixtures/textfile/summary.out", out: "fixtures/textfile/summary.out",
}, },
{ {
path: "fixtures/textfile/summary_extra_dimension", paths: []string{"fixtures/textfile/summary_extra_dimension"},
out: "fixtures/textfile/summary_extra_dimension.out", out: "fixtures/textfile/summary_extra_dimension.out",
}, },
{ {
path: "fixtures/textfile/*_extra_dimension", paths: []string{
out: "fixtures/textfile/glob_extra_dimension.out", "fixtures/textfile/histogram_extra_dimension",
"fixtures/textfile/summary_extra_dimension",
},
out: "fixtures/textfile/glob_extra_dimension.out",
}, },
{ {
path: "fixtures/textfile/metrics_merge_empty_help", paths: []string{"fixtures/textfile/*_extra_dimension"},
out: "fixtures/textfile/metrics_merge_empty_help.out", out: "fixtures/textfile/glob_extra_dimension.out",
}, },
{ {
path: "fixtures/textfile/metrics_merge_no_help", paths: []string{"fixtures/textfile/metrics_merge_empty_help"},
out: "fixtures/textfile/metrics_merge_no_help.out", out: "fixtures/textfile/metrics_merge_empty_help.out",
}, },
{ {
path: "fixtures/textfile/metrics_merge_same_help", paths: []string{"fixtures/textfile/metrics_merge_no_help"},
out: "fixtures/textfile/metrics_merge_same_help.out", out: "fixtures/textfile/metrics_merge_no_help.out",
}, },
{ {
path: "fixtures/textfile/metrics_merge_different_help", paths: []string{"fixtures/textfile/metrics_merge_same_help"},
out: "fixtures/textfile/metrics_merge_different_help.out", out: "fixtures/textfile/metrics_merge_same_help.out",
},
{
paths: []string{"fixtures/textfile/metrics_merge_different_help"},
out: "fixtures/textfile/metrics_merge_different_help.out",
}, },
} }
for i, test := range tests { for i, test := range tests {
mtime := 1.0 mtime := 1.0
c := &textFileCollector{ c := &textFileCollector{
path: test.path, paths: test.paths,
mtime: &mtime, mtime: &mtime,
logger: slog.New(slog.NewTextHandler(io.Discard, nil)), logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
} }
@ -146,7 +153,7 @@ func TestTextfileCollector(t *testing.T) {
} }
if string(want) != got { if string(want) != got {
t.Fatalf("%d.%q want:\n\n%s\n\ngot:\n\n%s", i, test.path, string(want), got) t.Fatalf("%d.%q want:\n\n%s\n\ngot:\n\n%s", i, test.paths, string(want), got)
} }
} }
} }

View File

@ -11,8 +11,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build (darwin || freebsd || openbsd || netbsd || linux) && !nouname //go:build (darwin || freebsd || openbsd || netbsd || linux || aix) && !nouname
// +build darwin freebsd openbsd netbsd linux // +build darwin freebsd openbsd netbsd linux aix
// +build !nouname // +build !nouname
package collector package collector

View File

@ -11,8 +11,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build (darwin || freebsd || openbsd || netbsd) && !nouname //go:build (darwin || freebsd || openbsd || netbsd || aix) && !nouname
// +build darwin freebsd openbsd netbsd // +build darwin freebsd openbsd netbsd aix
// +build !nouname // +build !nouname
package collector package collector

View File

@ -1,158 +0,0 @@
// Copyright 2016 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.
//go:build linux && !nozfs
// +build linux,!nozfs
package collector
import (
"errors"
"log/slog"
"strings"
"github.com/prometheus/client_golang/prometheus"
)
var errZFSNotAvailable = errors.New("ZFS / ZFS statistics are not available")
type zfsSysctl string
func init() {
registerCollector("zfs", defaultEnabled, NewZFSCollector)
}
type zfsCollector struct {
linuxProcpathBase string
linuxZpoolIoPath string
linuxZpoolObjsetPath string
linuxZpoolStatePath string
linuxPathMap map[string]string
logger *slog.Logger
}
// NewZFSCollector returns a new Collector exposing ZFS statistics.
func NewZFSCollector(logger *slog.Logger) (Collector, error) {
return &zfsCollector{
linuxProcpathBase: "spl/kstat/zfs",
linuxZpoolIoPath: "/*/io",
linuxZpoolObjsetPath: "/*/objset-*",
linuxZpoolStatePath: "/*/state",
linuxPathMap: map[string]string{
"zfs_abd": "abdstats",
"zfs_arc": "arcstats",
"zfs_dbuf": "dbufstats",
"zfs_dmu_tx": "dmu_tx",
"zfs_dnode": "dnodestats",
"zfs_fm": "fm",
"zfs_vdev_cache": "vdev_cache_stats", // vdev_cache is deprecated
"zfs_vdev_mirror": "vdev_mirror_stats",
"zfs_xuio": "xuio_stats", // no known consumers of the XUIO interface on Linux exist
"zfs_zfetch": "zfetchstats",
"zfs_zil": "zil",
},
logger: logger,
}, nil
}
func (c *zfsCollector) Update(ch chan<- prometheus.Metric) error {
if _, err := c.openProcFile(c.linuxProcpathBase); err != nil {
if err == errZFSNotAvailable {
c.logger.Debug(err.Error())
return ErrNoData
}
}
for subsystem := range c.linuxPathMap {
if err := c.updateZfsStats(subsystem, ch); err != nil {
if err == errZFSNotAvailable {
c.logger.Debug(err.Error())
// ZFS /proc files are added as new features to ZFS arrive, it is ok to continue
continue
}
return err
}
}
// Pool stats
return c.updatePoolStats(ch)
}
func (s zfsSysctl) metricName() string {
parts := strings.Split(string(s), ".")
return strings.Replace(parts[len(parts)-1], "-", "_", -1)
}
func (c *zfsCollector) constSysctlMetric(subsystem string, sysctl zfsSysctl, value float64) prometheus.Metric {
metricName := sysctl.metricName()
return prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, metricName),
string(sysctl),
nil,
nil,
),
prometheus.UntypedValue,
value,
)
}
func (c *zfsCollector) constPoolMetric(poolName string, sysctl zfsSysctl, value uint64) prometheus.Metric {
metricName := sysctl.metricName()
return prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, "zfs_zpool", metricName),
string(sysctl),
[]string{"zpool"},
nil,
),
prometheus.UntypedValue,
float64(value),
poolName,
)
}
func (c *zfsCollector) constPoolObjsetMetric(poolName string, datasetName string, sysctl zfsSysctl, value uint64) prometheus.Metric {
metricName := sysctl.metricName()
return prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, "zfs_zpool_dataset", metricName),
string(sysctl),
[]string{"zpool", "dataset"},
nil,
),
prometheus.UntypedValue,
float64(value),
poolName,
datasetName,
)
}
func (c *zfsCollector) constPoolStateMetric(poolName string, stateName string, isActive uint64) prometheus.Metric {
return prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, "zfs_zpool", "state"),
"kstat.zfs.misc.state",
[]string{"zpool", "state"},
nil,
),
prometheus.GaugeValue,
float64(isActive),
poolName,
stateName,
)
}

22
collector/zfs_common.go Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2016 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.
//go:build !nozfs && (freebsd || linux || solaris)
// +build !nozfs
// +build freebsd linux solaris
package collector
func init() {
registerCollector("zfs", defaultEnabled, NewZFSCollector)
}

View File

@ -30,11 +30,7 @@ const (
zfsCollectorSubsystem = "zfs" zfsCollectorSubsystem = "zfs"
) )
func init() { func NewZFSCollector(logger *slog.Logger) (Collector, error) {
registerCollector("zfs", defaultEnabled, NewZfsCollector)
}
func NewZfsCollector(logger *slog.Logger) (Collector, error) {
return &zfsCollector{ return &zfsCollector{
sysctls: []bsdSysctl{ sysctls: []bsdSysctl{
{ {

View File

@ -18,8 +18,10 @@ package collector
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -41,7 +43,67 @@ const (
// kstatDataString = "7" // kstatDataString = "7"
) )
var zfsPoolStatesName = []string{"online", "degraded", "faulted", "offline", "removed", "unavail", "suspended"} var (
errZFSNotAvailable = errors.New("ZFS / ZFS statistics are not available")
zfsPoolStatesName = [...]string{"online", "degraded", "faulted", "offline", "removed", "unavail", "suspended"}
)
type zfsCollector struct {
linuxProcpathBase string
linuxZpoolIoPath string
linuxZpoolObjsetPath string
linuxZpoolStatePath string
linuxPathMap map[string]string
logger *slog.Logger
}
// NewZFSCollector returns a new Collector exposing ZFS statistics.
func NewZFSCollector(logger *slog.Logger) (Collector, error) {
return &zfsCollector{
linuxProcpathBase: "spl/kstat/zfs",
linuxZpoolIoPath: "/*/io",
linuxZpoolObjsetPath: "/*/objset-*",
linuxZpoolStatePath: "/*/state",
linuxPathMap: map[string]string{
"zfs_abd": "abdstats",
"zfs_arc": "arcstats",
"zfs_dbuf": "dbufstats",
"zfs_dmu_tx": "dmu_tx",
"zfs_dnode": "dnodestats",
"zfs_fm": "fm",
"zfs_vdev_cache": "vdev_cache_stats", // vdev_cache is deprecated
"zfs_vdev_mirror": "vdev_mirror_stats",
"zfs_xuio": "xuio_stats", // no known consumers of the XUIO interface on Linux exist
"zfs_zfetch": "zfetchstats",
"zfs_zil": "zil",
},
logger: logger,
}, nil
}
func (c *zfsCollector) Update(ch chan<- prometheus.Metric) error {
if _, err := c.openProcFile(c.linuxProcpathBase); err != nil {
if err == errZFSNotAvailable {
c.logger.Debug(err.Error())
return ErrNoData
}
}
for subsystem := range c.linuxPathMap {
if err := c.updateZfsStats(subsystem, ch); err != nil {
if err == errZFSNotAvailable {
c.logger.Debug(err.Error())
// ZFS /proc files are added as new features to ZFS arrive, it is ok to continue
continue
}
return err
}
}
// Pool stats
return c.updatePoolStats(ch)
}
func (c *zfsCollector) openProcFile(path string) (*os.File, error) { func (c *zfsCollector) openProcFile(path string) (*os.File, error) {
file, err := os.Open(procFilePath(path)) file, err := os.Open(procFilePath(path))
@ -304,3 +366,73 @@ func (c *zfsCollector) parsePoolStateFile(reader io.Reader, zpoolPath string, ha
return nil return nil
} }
func (c *zfsCollector) constSysctlMetric(subsystem string, sysctl zfsSysctl, value float64) prometheus.Metric {
metricName := sysctl.metricName()
return prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, metricName),
string(sysctl),
nil,
nil,
),
prometheus.UntypedValue,
value,
)
}
func (c *zfsCollector) constPoolMetric(poolName string, sysctl zfsSysctl, value uint64) prometheus.Metric {
metricName := sysctl.metricName()
return prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, "zfs_zpool", metricName),
string(sysctl),
[]string{"zpool"},
nil,
),
prometheus.UntypedValue,
float64(value),
poolName,
)
}
func (c *zfsCollector) constPoolObjsetMetric(poolName string, datasetName string, sysctl zfsSysctl, value uint64) prometheus.Metric {
metricName := sysctl.metricName()
return prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, "zfs_zpool_dataset", metricName),
string(sysctl),
[]string{"zpool", "dataset"},
nil,
),
prometheus.UntypedValue,
float64(value),
poolName,
datasetName,
)
}
func (c *zfsCollector) constPoolStateMetric(poolName string, stateName string, isActive uint64) prometheus.Metric {
return prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, "zfs_zpool", "state"),
"kstat.zfs.misc.state",
[]string{"zpool", "state"},
nil,
),
prometheus.GaugeValue,
float64(isActive),
poolName,
stateName,
)
}
type zfsSysctl string
func (s zfsSysctl) metricName() string {
parts := strings.Split(string(s), ".")
return strings.Replace(parts[len(parts)-1], "-", "_", -1)
}

View File

@ -38,11 +38,11 @@ func TestArcstatsParsing(t *testing.T) {
err = c.parseProcfsFile(arcstatsFile, "arcstats", func(s zfsSysctl, v interface{}) { err = c.parseProcfsFile(arcstatsFile, "arcstats", func(s zfsSysctl, v interface{}) {
if s == zfsSysctl("kstat.zfs.misc.arcstats.hits") { if s == zfsSysctl("kstat.zfs.misc.arcstats.hits") {
if v.(uint64) != uint64(8772612) { if v.(uint64) != 8772612 {
t.Fatalf("Incorrect value parsed from procfs data") t.Fatalf("Incorrect value parsed from procfs data")
} }
} else if s == zfsSysctl("kstat.zfs.misc.arcstats.memory_available_bytes") { } else if s == zfsSysctl("kstat.zfs.misc.arcstats.memory_available_bytes") {
if v.(int64) != int64(-922337203685477580) { if v.(int64) != -922337203685477580 {
t.Fatalf("Incorrect value parsed from procfs data") t.Fatalf("Incorrect value parsed from procfs data")
} }
} else { } else {
@ -80,7 +80,7 @@ func TestZfetchstatsParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v.(uint64) != uint64(7067992) { if v.(uint64) != 7067992 {
t.Fatalf("Incorrect value parsed from procfs data") t.Fatalf("Incorrect value parsed from procfs data")
} }
@ -116,7 +116,7 @@ func TestZilParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v.(uint64) != uint64(10) { if v.(uint64) != 10 {
t.Fatalf("Incorrect value parsed from procfs data") t.Fatalf("Incorrect value parsed from procfs data")
} }
@ -152,7 +152,7 @@ func TestVdevCacheStatsParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v.(uint64) != uint64(40) { if v.(uint64) != 40 {
t.Fatalf("Incorrect value parsed from procfs data") t.Fatalf("Incorrect value parsed from procfs data")
} }
@ -188,7 +188,7 @@ func TestXuioStatsParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v.(uint64) != uint64(32) { if v.(uint64) != 32 {
t.Fatalf("Incorrect value parsed from procfs data") t.Fatalf("Incorrect value parsed from procfs data")
} }
@ -224,7 +224,7 @@ func TestFmParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v.(uint64) != uint64(18) { if v.(uint64) != 18 {
t.Fatalf("Incorrect value parsed from procfs data") t.Fatalf("Incorrect value parsed from procfs data")
} }
@ -260,7 +260,7 @@ func TestDmuTxParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v.(uint64) != uint64(3532844) { if v.(uint64) != 3532844 {
t.Fatalf("Incorrect value parsed from procfs data") t.Fatalf("Incorrect value parsed from procfs data")
} }
@ -300,7 +300,7 @@ func TestZpoolParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v != uint64(1884160) && v != uint64(2826240) { if v != 1884160 && v != 2826240 {
t.Fatalf("Incorrect value parsed from procfs data %v", v) t.Fatalf("Incorrect value parsed from procfs data %v", v)
} }
@ -340,7 +340,7 @@ func TestZpoolObjsetParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v != uint64(0) && v != uint64(4) && v != uint64(10) { if v != 0 && v != 4 && v != 10 {
t.Fatalf("Incorrect value parsed from procfs data %v", v) t.Fatalf("Incorrect value parsed from procfs data %v", v)
} }
@ -376,7 +376,7 @@ func TestAbdstatsParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v.(uint64) != uint64(223232) { if v.(uint64) != 223232 {
t.Fatalf("Incorrect value parsed from procfs abdstats data") t.Fatalf("Incorrect value parsed from procfs abdstats data")
} }
@ -412,7 +412,7 @@ func TestDbufstatsParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v.(uint64) != uint64(108807) { if v.(uint64) != 108807 {
t.Fatalf("Incorrect value parsed from procfs dbufstats data") t.Fatalf("Incorrect value parsed from procfs dbufstats data")
} }
@ -448,7 +448,7 @@ func TestDnodestatsParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v.(uint64) != uint64(37617) { if v.(uint64) != 37617 {
t.Fatalf("Incorrect value parsed from procfs dnodestats data") t.Fatalf("Incorrect value parsed from procfs dnodestats data")
} }
@ -484,7 +484,7 @@ func TestVdevMirrorstatsParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if v.(uint64) != uint64(94) { if v.(uint64) != 94 {
t.Fatalf("Incorrect value parsed from procfs vdev_mirror_stats data") t.Fatalf("Incorrect value parsed from procfs vdev_mirror_stats data")
} }
@ -521,26 +521,26 @@ func TestPoolStateParsing(t *testing.T) {
handlerCalled = true handlerCalled = true
if poolName == "pool1" { if poolName == "pool1" {
if isActive != uint64(1) && stateName == "online" { if isActive != 1 && stateName == "online" {
t.Fatalf("Incorrect parsed value for online state") t.Fatalf("Incorrect parsed value for online state")
} }
if isActive != uint64(0) && stateName != "online" { if isActive != 0 && stateName != "online" {
t.Fatalf("Incorrect parsed value for online state") t.Fatalf("Incorrect parsed value for online state")
} }
} }
if poolName == "poolz1" { if poolName == "poolz1" {
if isActive != uint64(1) && stateName == "degraded" { if isActive != 1 && stateName == "degraded" {
t.Fatalf("Incorrect parsed value for degraded state") t.Fatalf("Incorrect parsed value for degraded state")
} }
if isActive != uint64(0) && stateName != "degraded" { if isActive != 0 && stateName != "degraded" {
t.Fatalf("Incorrect parsed value for degraded state") t.Fatalf("Incorrect parsed value for degraded state")
} }
} }
if poolName == "pool2" { if poolName == "pool2" {
if isActive != uint64(1) && stateName == "suspended" { if isActive != 1 && stateName == "suspended" {
t.Fatalf("Incorrect parsed value for suspended state") t.Fatalf("Incorrect parsed value for suspended state")
} }
if isActive != uint64(0) && stateName != "suspended" { if isActive != 0 && stateName != "suspended" {
t.Fatalf("Incorrect parsed value for suspended state") t.Fatalf("Incorrect parsed value for suspended state")
} }
} }

View File

@ -61,11 +61,7 @@ const (
zfsCollectorSubsystem = "zfs" zfsCollectorSubsystem = "zfs"
) )
func init() { func NewZFSCollector(logger *slog.Logger) (Collector, error) {
registerCollector("zfs", defaultEnabled, NewZfsCollector)
}
func NewZfsCollector(logger *slog.Logger) (Collector, error) {
return &zfsCollector{ return &zfsCollector{
abdstatsLinearCount: prometheus.NewDesc( abdstatsLinearCount: prometheus.NewDesc(
prometheus.BuildFQName(namespace, zfsCollectorSubsystem, "abdstats_linear_count_total"), prometheus.BuildFQName(namespace, zfsCollectorSubsystem, "abdstats_linear_count_total"),

View File

@ -3,5 +3,6 @@
grafanaDashboards+:: { grafanaDashboards+:: {
'nodes.json': nodemixin.new(config=$._config, platform='Linux', uid=std.md5('nodes.json')).dashboard, 'nodes.json': nodemixin.new(config=$._config, platform='Linux', uid=std.md5('nodes.json')).dashboard,
'nodes-darwin.json': nodemixin.new(config=$._config, platform='Darwin', uid=std.md5('nodes-darwin.json')).dashboard, 'nodes-darwin.json': nodemixin.new(config=$._config, platform='Darwin', uid=std.md5('nodes-darwin.json')).dashboard,
'nodes-aix.json': nodemixin.new(config=$._config, platform='AIX', uid=std.md5('nodes-aix.json')).dashboard,
}, },
} }

View File

@ -147,7 +147,19 @@ local table = grafana70.panel.table;
||| % config, legendFormat='App Memory' ||| % config, legendFormat='App Memory'
)) ))
.addTarget(prometheus.target('node_memory_wired_bytes{%(nodeExporterSelector)s, instance="$instance", %(clusterLabel)s="$cluster"}' % config, legendFormat='Wired Memory')) .addTarget(prometheus.target('node_memory_wired_bytes{%(nodeExporterSelector)s, instance="$instance", %(clusterLabel)s="$cluster"}' % config, legendFormat='Wired Memory'))
.addTarget(prometheus.target('node_memory_compressed_bytes{%(nodeExporterSelector)s, instance="$instance", %(clusterLabel)s="$cluster"}' % config, legendFormat='Compressed')), .addTarget(prometheus.target('node_memory_compressed_bytes{%(nodeExporterSelector)s, instance="$instance", %(clusterLabel)s="$cluster"}' % config, legendFormat='Compressed'))
else if platform == 'AIX' then
memoryGraphPanelPrototype { stack: false }
.addTarget(prometheus.target('node_memory_total_bytes{%(nodeExporterSelector)s, instance="$instance", %(clusterLabel)s="$cluster"}' % config, legendFormat='Physical Memory'))
.addTarget(prometheus.target(
|||
(
node_memory_total_bytes{%(nodeExporterSelector)s, instance="$instance", %(clusterLabel)s="$cluster"} -
node_memory_available_bytes{%(nodeExporterSelector)s, instance="$instance", %(clusterLabel)s="$cluster"}
)
||| % config, legendFormat='Memory Used'
)),
// NOTE: avg() is used to circumvent a label change caused by a node_exporter rollout. // NOTE: avg() is used to circumvent a label change caused by a node_exporter rollout.
local memoryGaugePanelPrototype = local memoryGaugePanelPrototype =
@ -194,8 +206,21 @@ local table = grafana70.panel.table;
* *
100 100
||| % config ||| % config
))
else if platform == 'AIX' then
memoryGaugePanelPrototype
.addTarget(prometheus.target(
|||
100 -
(
avg(node_memory_available_bytes{%(nodeExporterSelector)s, instance="$instance", %(clusterLabel)s="$cluster"}) /
avg(node_memory_total_bytes{%(nodeExporterSelector)s, instance="$instance", %(clusterLabel)s="$cluster"})
* 100
)
||| % config
)), )),
local diskIO = local diskIO =
graphPanel.new( graphPanel.new(
'Disk I/O', 'Disk I/O',
@ -501,8 +526,8 @@ local table = grafana70.panel.table;
tags=(config.dashboardTags), tags=(config.dashboardTags),
timezone='utc', timezone='utc',
refresh='30s', refresh='30s',
graphTooltip='shared_crosshair', uid=std.md5(uid),
uid=std.md5(uid) graphTooltip='shared_crosshair'
) )
.addTemplates(templates) .addTemplates(templates)
.addRows(rows) .addRows(rows)
@ -513,8 +538,20 @@ local table = grafana70.panel.table;
tags=(config.dashboardTags), tags=(config.dashboardTags),
timezone='utc', timezone='utc',
refresh='30s', refresh='30s',
graphTooltip='shared_crosshair', uid=std.md5(uid),
uid=std.md5(uid) graphTooltip='shared_crosshair'
)
.addTemplates(templates)
.addRows(rows)
else if platform == 'AIX' then
dashboard.new(
'%sAIX' % config.dashboardNamePrefix,
time_from='now-1h',
tags=(config.dashboardTags),
timezone='utc',
refresh='30s',
uid=std.md5(uid),
graphTooltip='shared_crosshair'
) )
.addTemplates(templates) .addTemplates(templates)
.addRows(rows), .addRows(rows),

1
go.mod
View File

@ -20,6 +20,7 @@ require (
github.com/mdlayher/netlink v1.7.2 github.com/mdlayher/netlink v1.7.2
github.com/mdlayher/wifi v0.2.0 github.com/mdlayher/wifi v0.2.0
github.com/opencontainers/selinux v1.11.0 github.com/opencontainers/selinux v1.11.0
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55
github.com/prometheus-community/go-runit v0.1.0 github.com/prometheus-community/go-runit v0.1.0
github.com/prometheus/client_golang v1.20.3 github.com/prometheus/client_golang v1.20.3
github.com/prometheus/client_model v0.6.1 github.com/prometheus/client_model v0.6.1

3
go.sum
View File

@ -71,6 +71,8 @@ github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaL
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus-community/go-runit v0.1.0 h1:uTWEj/Fn2RoLdfg/etSqwzgYNOYPrARx1BHUN052tGA= github.com/prometheus-community/go-runit v0.1.0 h1:uTWEj/Fn2RoLdfg/etSqwzgYNOYPrARx1BHUN052tGA=
github.com/prometheus-community/go-runit v0.1.0/go.mod h1:AvJ9Jo3gAFu2lbM4+qfjdpq30FfiLDJZKbQ015u08IQ= github.com/prometheus-community/go-runit v0.1.0/go.mod h1:AvJ9Jo3gAFu2lbM4+qfjdpq30FfiLDJZKbQ015u08IQ=
github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4=
@ -110,6 +112,7 @@ golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=