Merge branch 'master' into gpu-clk
commit
878d1d5358
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
158
collector/zfs.go
158
collector/zfs.go
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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{
|
||||||
{
|
{
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
1
go.mod
|
@ -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
3
go.sum
|
@ -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=
|
||||||
|
|
Loading…
Reference in New Issue