Merge branch 'prometheus:master' into master

This commit is contained in:
Shashwat Hiregoudar 2025-06-17 18:16:48 +05:30 committed by GitHub
commit f7f2c6f56d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 543 additions and 137 deletions

View file

@ -69,7 +69,7 @@ func (c *btrfsCollector) Update(ch chan<- prometheus.Metric) error {
for _, s := range stats {
// match up procfs and ioctl info by filesystem UUID (without dashes)
var fsUUID = strings.Replace(s.UUID, "-", "", -1)
var fsUUID = strings.ReplaceAll(s.UUID, "-", "")
ioctlStats := ioctlStatsMap[fsUUID]
c.updateBtrfsStats(ch, s, ioctlStats)
}

View file

@ -30,22 +30,50 @@ import (
"github.com/prometheus/client_golang/prometheus"
)
var (
nodeCPUPhysicalSecondsDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, cpuCollectorSubsystem, "physical_seconds_total"),
"Seconds the physical CPUs spent in each mode.",
[]string{"cpu", "mode"}, nil,
)
nodeCPUSRunQueueDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, cpuCollectorSubsystem, "runqueue"),
"Length of the run queue.", []string{"cpu"}, nil,
)
nodeCPUFlagsDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, cpuCollectorSubsystem, "flags"),
"CPU flags.",
[]string{"cpu", "flag"}, nil,
)
nodeCPUContextSwitchDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, cpuCollectorSubsystem, "context_switches_total"),
"Number of context switches.",
[]string{"cpu"}, nil,
)
)
type cpuCollector struct {
cpu typedDesc
logger *slog.Logger
tickPerSecond int64
cpu typedDesc
cpuPhysical typedDesc
cpuRunQueue typedDesc
cpuFlags typedDesc
cpuContextSwitch typedDesc
logger *slog.Logger
tickPerSecond float64
purrTicksPerSecond float64
}
func init() {
registerCollector("cpu", defaultEnabled, NewCpuCollector)
}
func tickPerSecond() (int64, error) {
func tickPerSecond() (float64, 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
return float64(ticks), nil
}
func NewCpuCollector(logger *slog.Logger) (Collector, error) {
@ -53,10 +81,22 @@ func NewCpuCollector(logger *slog.Logger) (Collector, error) {
if err != nil {
return nil, err
}
pconfig, err := perfstat.PartitionStat()
if err != nil {
return nil, err
}
return &cpuCollector{
cpu: typedDesc{nodeCPUSecondsDesc, prometheus.CounterValue},
logger: logger,
tickPerSecond: ticks,
cpu: typedDesc{nodeCPUSecondsDesc, prometheus.CounterValue},
cpuPhysical: typedDesc{nodeCPUPhysicalSecondsDesc, prometheus.CounterValue},
cpuRunQueue: typedDesc{nodeCPUSRunQueueDesc, prometheus.GaugeValue},
cpuFlags: typedDesc{nodeCPUFlagsDesc, prometheus.GaugeValue},
cpuContextSwitch: typedDesc{nodeCPUContextSwitchDesc, prometheus.CounterValue},
logger: logger,
tickPerSecond: ticks,
purrTicksPerSecond: float64(pconfig.ProcessorMhz * 1e6),
}, nil
}
@ -67,10 +107,26 @@ func (c *cpuCollector) Update(ch chan<- prometheus.Metric) error {
}
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")
// LPAR metrics
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")
// Physical CPU metrics
ch <- c.cpuPhysical.mustNewConstMetric(float64(stat.PIdle)/c.purrTicksPerSecond, strconv.Itoa(n), "pidle")
ch <- c.cpuPhysical.mustNewConstMetric(float64(stat.PUser)/c.purrTicksPerSecond, strconv.Itoa(n), "puser")
ch <- c.cpuPhysical.mustNewConstMetric(float64(stat.PSys)/c.purrTicksPerSecond, strconv.Itoa(n), "psys")
ch <- c.cpuPhysical.mustNewConstMetric(float64(stat.PWait)/c.purrTicksPerSecond, strconv.Itoa(n), "pwait")
// Run queue length
ch <- c.cpuRunQueue.mustNewConstMetric(float64(stat.RunQueue), strconv.Itoa(n))
// Flags
ch <- c.cpuFlags.mustNewConstMetric(float64(stat.SpurrFlag), strconv.Itoa(n), "spurr")
// Context switches
ch <- c.cpuContextSwitch.mustNewConstMetric(float64(stat.CSwitches), strconv.Itoa(n))
}
return nil
}

View file

@ -87,7 +87,7 @@ func NewCPUCollector(logger *slog.Logger) (Collector, error) {
isolcpus, err := sfs.IsolatedCPUs()
if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("Unable to get isolated cpus: %w", err)
return nil, fmt.Errorf("unable to get isolated cpus: %w", err)
}
logger.Debug("Could not open isolated file", "error", err)
}

View file

@ -155,7 +155,7 @@ func getCPUTemperatures() (map[int]float64, error) {
}
keys := sortFilterSysmonProperties(props, "coretemp")
for idx, _ := range keys {
for idx := range keys {
convertTemperatures(props[keys[idx]], res)
}

View file

@ -22,12 +22,12 @@ import (
)
const (
cpuVulerabilitiesCollector = "cpu_vulnerabilities"
cpuVulnerabilitiesCollectorSubsystem = "cpu_vulnerabilities"
)
var (
vulnerabilityDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, cpuVulerabilitiesCollector, "info"),
prometheus.BuildFQName(namespace, cpuVulnerabilitiesCollectorSubsystem, "info"),
"Details of each CPU vulnerability reported by sysfs. The value of the series is an int encoded state of the vulnerability. The same state is stored as a string in the label",
[]string{"codename", "state", "mitigation"},
nil,
@ -37,7 +37,7 @@ var (
type cpuVulnerabilitiesCollector struct{}
func init() {
registerCollector(cpuVulerabilitiesCollector, defaultDisabled, NewVulnerabilitySysfsCollector)
registerCollector(cpuVulnerabilitiesCollectorSubsystem, defaultDisabled, NewVulnerabilitySysfsCollector)
}
func NewVulnerabilitySysfsCollector(logger *slog.Logger) (Collector, error) {

View file

@ -30,11 +30,19 @@ type diskstatsCollector struct {
rbytes typedDesc
wbytes typedDesc
time typedDesc
bsize typedDesc
qdepth typedDesc
rserv typedDesc
wserv typedDesc
xfers typedDesc
xrate typedDesc
deviceFilter deviceFilter
logger *slog.Logger
tickPerSecond int64
tickPerSecond float64
}
func init() {
@ -57,6 +65,54 @@ func NewDiskstatsCollector(logger *slog.Logger) (Collector, error) {
wbytes: typedDesc{writtenBytesDesc, prometheus.CounterValue},
time: typedDesc{ioTimeSecondsDesc, prometheus.CounterValue},
bsize: typedDesc{
prometheus.NewDesc(
prometheus.BuildFQName(namespace, diskSubsystem, "block_size_bytes"),
"Size of the block device in bytes.",
diskLabelNames, nil,
),
prometheus.GaugeValue,
},
qdepth: typedDesc{
prometheus.NewDesc(
prometheus.BuildFQName(namespace, diskSubsystem, "queue_depth"),
"Number of requests in the queue.",
diskLabelNames, nil,
),
prometheus.GaugeValue,
},
rserv: typedDesc{
prometheus.NewDesc(
prometheus.BuildFQName(namespace, diskSubsystem, "read_time_seconds_total"),
"The total time spent servicing read requests.",
diskLabelNames, nil,
),
prometheus.CounterValue,
},
wserv: typedDesc{
prometheus.NewDesc(
prometheus.BuildFQName(namespace, diskSubsystem, "write_time_seconds_total"),
"The total time spent servicing write requests.",
diskLabelNames, nil,
),
prometheus.CounterValue,
},
xfers: typedDesc{
prometheus.NewDesc(
prometheus.BuildFQName(namespace, diskSubsystem, "transfers_total"),
"The total number of transfers to/from disk.",
diskLabelNames, nil,
),
prometheus.CounterValue,
},
xrate: typedDesc{
prometheus.NewDesc(
prometheus.BuildFQName(namespace, diskSubsystem, "transfers_to_disk_total"),
"The total number of transfers from disk.",
diskLabelNames, nil,
),
prometheus.CounterValue,
},
deviceFilter: deviceFilter,
logger: logger,
@ -76,7 +132,14 @@ func (c *diskstatsCollector) Update(ch chan<- prometheus.Metric) error {
}
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)
ch <- c.time.mustNewConstMetric(float64(stat.Time)/float64(c.tickPerSecond), stat.Name)
ch <- c.bsize.mustNewConstMetric(float64(stat.BSize), stat.Name)
ch <- c.qdepth.mustNewConstMetric(float64(stat.QDepth), stat.Name)
ch <- c.rserv.mustNewConstMetric(float64(stat.Rserv)/1e9, stat.Name)
ch <- c.wserv.mustNewConstMetric(float64(stat.Wserv)/1e9, stat.Name)
ch <- c.xfers.mustNewConstMetric(float64(stat.Xfers), stat.Name)
ch <- c.xrate.mustNewConstMetric(float64(stat.XRate), stat.Name)
}
return nil
}

View file

@ -453,6 +453,7 @@ func (c *ethtoolCollector) Update(ch chan<- prometheus.Metric) error {
// Sanitizing the metric names can lead to duplicate metric names. Therefore check for clashes beforehand.
metricFQNames := make(map[string]string)
renamedStats := make(map[string]uint64, len(stats))
for metric := range stats {
metricName := SanitizeMetricName(metric)
if !c.metricsPattern.MatchString(metricName) {
@ -467,6 +468,8 @@ func (c *ethtoolCollector) Update(ch chan<- prometheus.Metric) error {
metricFQNames[metricFQName] = ""
} else {
metricFQNames[metricFQName] = metricName
// Later we'll go look for the stat with the "sanitized" metric name, so we can copy it there already
renamedStats[metricName] = stats[metric]
}
}
@ -484,7 +487,7 @@ func (c *ethtoolCollector) Update(ch chan<- prometheus.Metric) error {
continue
}
val := stats[metric]
val := renamedStats[metric]
// Check to see if this metric exists; if not then create it and store it in c.entries.
entry := c.entryWithCreate(metric, metricFQName)

View file

@ -212,16 +212,18 @@ func (e *EthtoolFixture) LinkInfo(intf string) (ethtool.EthtoolCmd, error) {
items := strings.Split(line, ": ")
if items[0] == "Supported pause frame use" {
if items[1] == "Symmetric" {
switch items[1] {
case "Symmetric":
res.Supported |= (1 << unix.ETHTOOL_LINK_MODE_Pause_BIT)
} else if items[1] == "Receive-only" {
case "Receive-only":
res.Supported |= (1 << unix.ETHTOOL_LINK_MODE_Asym_Pause_BIT)
}
}
if items[0] == "Advertised pause frame use" {
if items[1] == "Symmetric" {
switch items[1] {
case "Symmetric":
res.Advertising |= (1 << unix.ETHTOOL_LINK_MODE_Pause_BIT)
} else if items[1] == "Receive-only" {
case "Receive-only":
res.Advertising |= (1 << unix.ETHTOOL_LINK_MODE_Asym_Pause_BIT)
}
}
@ -269,6 +271,7 @@ func NewEthtoolTestCollector(logger *slog.Logger) (Collector, error) {
func TestBuildEthtoolFQName(t *testing.T) {
testcases := map[string]string{
"port.rx_errors": "node_ethtool_port_received_errors",
"rx_errors": "node_ethtool_received_errors",
"Queue[0] AllocFails": "node_ethtool_queue_0_allocfails",
"Tx LPI entry count": "node_ethtool_transmitted_lpi_entry_count",
@ -292,6 +295,9 @@ node_ethtool_align_errors{device="eth0"} 0
# HELP node_ethtool_info A metric with a constant '1' value labeled by bus_info, device, driver, expansion_rom_version, firmware_version, version.
# TYPE node_ethtool_info gauge
node_ethtool_info{bus_info="0000:00:1f.6",device="eth0",driver="e1000e",expansion_rom_version="",firmware_version="0.5-4",version="5.11.0-22-generic"} 1
# HELP node_ethtool_port_received_dropped Network interface port_rx_dropped
# TYPE node_ethtool_port_received_dropped untyped
node_ethtool_port_received_dropped{device="eth0"} 12028
# HELP node_ethtool_received_broadcast Network interface rx_broadcast
# TYPE node_ethtool_received_broadcast untyped
node_ethtool_received_broadcast{device="eth0"} 5792

View file

@ -53,9 +53,9 @@ func (c *filesystemCollector) GetStats() (stats []filesystemStats, err error) {
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.
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,

View file

@ -215,8 +215,8 @@ func parseFilesystemLabels(r io.Reader) ([]filesystemLabels, error) {
// Ensure we handle the translation of \040 and \011
// as per fstab(5).
parts[4] = strings.Replace(parts[4], "\\040", " ", -1)
parts[4] = strings.Replace(parts[4], "\\011", "\t", -1)
parts[4] = strings.ReplaceAll(parts[4], "\\040", " ")
parts[4] = strings.ReplaceAll(parts[4], "\\011", "\t")
filesystems = append(filesystems, filesystemLabels{
device: parts[m+3],

View file

@ -21,21 +21,25 @@ package collector
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
Float64 purgeable(char *path) {
CFNumberRef tmp;
NSError *error = nil;
NSString *str = [NSString stringWithUTF8String:path];
NSURL *fileURL = [[NSURL alloc] initFileURLWithPath:str];
NSDictionary *results = [fileURL resourceValuesForKeys:@[NSURLVolumeAvailableCapacityForImportantUsageKey] error:&error];
if (results) {
if ((tmp = CFDictionaryGetValue((CFDictionaryRef)results, NSURLVolumeAvailableCapacityForImportantUsageKey)) == NULL) {
return -1.0f;
}
Float64 value;
if (CFNumberGetValue(tmp, kCFNumberFloat64Type, &value)) {
return value;
Float64 value = -1.0f;
@autoreleasepool {
NSError *error = nil;
NSString *str = [NSString stringWithUTF8String:path];
NSURL *fileURL = [[NSURL alloc] initFileURLWithPath:str];
NSDictionary *results = [fileURL resourceValuesForKeys:@[NSURLVolumeAvailableCapacityForImportantUsageKey] error:&error];
if (results) {
CFNumberRef tmp = CFDictionaryGetValue((CFDictionaryRef)results, NSURLVolumeAvailableCapacityForImportantUsageKey);
if (tmp != NULL) {
CFNumberGetValue(tmp, kCFNumberFloat64Type, &value);
}
}
[fileURL release];
}
return -1.0f;
return value;
}
*/
import "C"
@ -88,6 +92,9 @@ func (c *filesystemCollector) GetStats() (stats []filesystemStats, err error) {
ro = 1
}
mountpointCString := C.CString(mountpoint)
defer C.free(unsafe.Pointer(mountpointCString))
stats = append(stats, filesystemStats{
labels: filesystemLabels{
device: device,
@ -99,7 +106,7 @@ func (c *filesystemCollector) GetStats() (stats []filesystemStats, err error) {
avail: float64(mnt[i].f_bavail) * float64(mnt[i].f_bsize),
files: float64(mnt[i].f_files),
filesFree: float64(mnt[i].f_ffree),
purgeable: float64(C.purgeable(C.CString(mountpoint))),
purgeable: float64(C.purgeable(mountpointCString)),
ro: ro,
})
}

View file

@ -4,6 +4,7 @@ NIC statistics:
rx_packets: 1260062
tx_errors: 0
rx_errors: 0
port.rx_dropped: 12028
rx_missed: 401
align_errors: 0
tx_single_collisions: 0

View file

@ -40,8 +40,12 @@ func (c *meminfoCollector) getMemInfo() (map[string]float64, error) {
}
return map[string]float64{
"total_bytes": float64(stats.RealTotal * 4096),
"free_bytes": float64(stats.RealFree * 4096),
"available_bytes": float64(stats.RealAvailable * 4096),
"total_bytes": float64(stats.RealTotal * 4096),
"free_bytes": float64(stats.RealFree * 4096),
"available_bytes": float64(stats.RealAvailable * 4096),
"process_bytes": float64(stats.RealProcess * 4096),
"paging_space_total_bytes": float64(stats.PgSpTotal * 4096),
"paging_space_free_bytes": float64(stats.PgSpFree * 4096),
"page_scans_total": float64(stats.Scans),
}, nil
}

View file

@ -44,7 +44,7 @@ func NewMeminfoCollector(logger *slog.Logger) (Collector, error) {
func (c *meminfoCollector) getMemInfo() (map[string]float64, error) {
meminfo, err := c.fs.Meminfo()
if err != nil {
return nil, fmt.Errorf("Failed to get memory info: %s", err)
return nil, fmt.Errorf("failed to get memory info: %w", err)
}
metrics := make(map[string]float64)

View file

@ -32,16 +32,20 @@ func getNetDevStats(filter *deviceFilter, logger *slog.Logger) (netDevStats, 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),
"receive_bytes": uint64(stat.RxBytes),
"receive_dropped": uint64(stat.RxPacketsDropped),
"receive_errors": uint64(stat.RxErrors),
"receive_multicast": uint64(stat.RxMulticastPackets),
"receive_packets": uint64(stat.RxPackets),
"receive_collision_errors": uint64(stat.RxCollisionErrors),
"transmit_bytes": uint64(stat.TxBytes),
"transmit_dropped": uint64(stat.TxPacketsDropped),
"transmit_errors": uint64(stat.TxErrors),
"transmit_multicast": uint64(stat.TxMulticastPackets),
"transmit_packets": uint64(stat.TxPackets),
"transmit_queue_overflow": uint64(stat.TxQueueOverflow),
"transmit_collision_single_errors": uint64(stat.TxSingleCollisionCount),
"transmit_collision_multiple_errors": uint64(stat.TxMultipleCollisionCount),
}
}

View file

@ -22,6 +22,7 @@ import (
"fmt"
"log/slog"
"net"
"unsafe"
"golang.org/x/sys/unix"
)
@ -71,51 +72,107 @@ func getIfaceData(index int) (*ifMsghdr2, error) {
return nil, err
}
err = binary.Read(bytes.NewReader(rawData), binary.LittleEndian, &data)
if err != nil {
return &data, err
}
/*
As of macOS Ventura 13.2.1, theres a kernel bug which truncates traffic values at the 4GiB mark.
This is a workaround to fetch the interface traffic metrics using a sysctl call.
Apple wants to prevent fingerprinting by 3rdparty apps and might fix this bug in future which would break this implementation.
*/
mib := []int32{
unix.CTL_NET,
unix.AF_LINK,
0, // NETLINK_GENERIC: functions not specific to a type of iface
2, //IFMIB_IFDATA: per-interface data table
int32(index),
1, // IFDATA_GENERAL: generic stats for all kinds of ifaces
}
var mibData ifMibData
size := unsafe.Sizeof(mibData)
if _, _, errno := unix.Syscall6(
unix.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
uintptr(len(mib)),
uintptr(unsafe.Pointer(&mibData)),
uintptr(unsafe.Pointer(&size)),
uintptr(unsafe.Pointer(nil)),
0,
); errno != 0 {
return &data, err
}
var ifdata ifData64
err = binary.Read(bytes.NewReader(mibData.Data[:]), binary.LittleEndian, &ifdata)
if err != nil {
return &data, err
}
data.Data.Ibytes = ifdata.Ibytes
data.Data.Obytes = ifdata.Obytes
return &data, err
}
// https://github.com/apple-oss-distributions/xnu/blob/main/bsd/net/if.h#L220-L232
type ifMsghdr2 struct {
Msglen uint16
Version uint8
Type uint8
Addrs int32
Flags int32
Index uint16
_ [2]byte
SndLen int32
SndMaxlen int32
SndDrops int32
Timer int32
Data ifData64
Msglen uint16 // to skip over non-understood messages
Version uint8 // future binary compatabilit
Type uint8 // message type
Addrs int32 // like rtm_addrs
Flags int32 // value of if_flags
Index uint16 // index for associated ifp
_ [2]byte // padding for alignment
SndLen int32 // instantaneous length of send queue
SndMaxlen int32 // maximum length of send queue
SndDrops int32 // number of drops in send queue
Timer int32 // time until if_watchdog called
Data ifData64 // statistics and other data
}
// https://github.com/apple/darwin-xnu/blob/main/bsd/net/if_var.h#L199-L231
// https://github.com/apple-oss-distributions/xnu/blob/main/bsd/net/if_var.h#L207-L235
type ifData64 struct {
Type uint8
Typelen uint8
Physical uint8
Addrlen uint8
Hdrlen uint8
Recvquota uint8
Xmitquota uint8
Unused1 uint8
Mtu uint32
Metric uint32
Baudrate uint64
Ipackets uint64
Ierrors uint64
Opackets uint64
Oerrors uint64
Collisions uint64
Ibytes uint64
Obytes uint64
Imcasts uint64
Omcasts uint64
Iqdrops uint64
Noproto uint64
Recvtiming uint32
Xmittiming uint32
Lastchange unix.Timeval32
Type uint8 // ethernet, tokenring, etc
Typelen uint8 // Length of frame type id
Physical uint8 // e.g., AUI, Thinnet, 10base-T, etc
Addrlen uint8 // media address length
Hdrlen uint8 // media header length
Recvquota uint8 // polling quota for receive intrs
Xmitquota uint8 // polling quota for xmit intrs
Unused1 uint8 // for future use
Mtu uint32 // maximum transmission unit
Metric uint32 // routing metric (external only)
Baudrate uint64 // linespeed
// volatile statistics
Ipackets uint64 // packets received on interface
Ierrors uint64 // input errors on interface
Opackets uint64 // packets sent on interface
Oerrors uint64 // output errors on interface
Collisions uint64 // collisions on csma interfaces
Ibytes uint64 // total number of octets received
Obytes uint64 // total number of octets sent
Imcasts uint64 // packets received via multicast
Omcasts uint64 // packets sent via multicast
Iqdrops uint64 // dropped on input, this interface
Noproto uint64 // destined for unsupported protocol
Recvtiming uint32 // usec spent receiving when timing
Xmittiming uint32 // usec spent xmitting when timing
Lastchange unix.Timeval32 // time of last administrative change
}
// https://github.com/apple-oss-distributions/xnu/blob/main/bsd/net/if_mib.h#L65-L74
type ifMibData struct {
Name [16]byte // name of interface
PCount uint32 // number of promiscuous listeners
Flags uint32 // interface flags
SendLength uint32 // instantaneous length of send queue
MaxSendLength uint32 // maximum length of send queue
SendDrops uint32 // number of drops in send queue
_ [4]uint32 // for future expansion
Data [128]byte // generic information and statistics
}
func getNetDevLabels() (map[string]map[string]string, error) {

View file

@ -0,0 +1,86 @@
// Copyright 2025 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 !nonetinterface
// +build !nonetinterface
package collector
import (
"log/slog"
"github.com/power-devops/perfstat"
"github.com/prometheus/client_golang/prometheus"
)
type netinterfaceCollector struct {
logger *slog.Logger
collisions *prometheus.Desc
ibytes *prometheus.Desc
ipackets *prometheus.Desc
obytes *prometheus.Desc
opackets *prometheus.Desc
}
const (
netinterfaceSubsystem = "netinterface"
)
func init() {
registerCollector("netinterface", defaultEnabled, NewNetinterfaceCollector)
}
func NewNetinterfaceCollector(logger *slog.Logger) (Collector, error) {
labels := []string{"interface"}
return &netinterfaceCollector{
logger: logger,
collisions: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netinterfaceSubsystem, "collisions_total"),
"Total number of CSMA collisions on the interface.", labels, nil,
),
ibytes: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netinterfaceSubsystem, "receive_bytes_total"),
"Total number of bytes received on the interface.", labels, nil,
),
ipackets: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netinterfaceSubsystem, "receive_packets_total"),
"Total number of packets received on the interface.", labels, nil,
),
obytes: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netinterfaceSubsystem, "transmit_bytes_total"),
"Total number of bytes transmitted on the interface.", labels, nil,
),
opackets: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netinterfaceSubsystem, "transmit_packets_total"),
"Total number of packets transmitted on the interface.", labels, nil,
),
}, nil
}
func (c *netinterfaceCollector) Update(ch chan<- prometheus.Metric) error {
stats, err := perfstat.NetIfaceStat()
if err != nil {
return err
}
for _, stat := range stats {
iface := stat.Name
ch <- prometheus.MustNewConstMetric(c.collisions, prometheus.CounterValue, float64(stat.Collisions), iface)
ch <- prometheus.MustNewConstMetric(c.ibytes, prometheus.CounterValue, float64(stat.IBytes), iface)
ch <- prometheus.MustNewConstMetric(c.ipackets, prometheus.CounterValue, float64(stat.IPackets), iface)
ch <- prometheus.MustNewConstMetric(c.obytes, prometheus.CounterValue, float64(stat.OBytes), iface)
ch <- prometheus.MustNewConstMetric(c.opackets, prometheus.CounterValue, float64(stat.OPackets), iface)
}
return nil
}

118
collector/partition_aix.go Normal file
View file

@ -0,0 +1,118 @@
// Copyright 2025 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 !nopartition
// +build !nopartition
package collector
import (
"log/slog"
"github.com/power-devops/perfstat"
"github.com/prometheus/client_golang/prometheus"
)
type partitionCollector struct {
logger *slog.Logger
entitledCapacity *prometheus.Desc
memoryMax *prometheus.Desc
memoryOnline *prometheus.Desc
cpuOnline *prometheus.Desc
cpuSys *prometheus.Desc
cpuPool *prometheus.Desc
powerSaveMode *prometheus.Desc
smtThreads *prometheus.Desc
}
const (
partitionCollectorSubsystem = "partition"
)
func init() {
registerCollector("partition", defaultEnabled, NewPartitionCollector)
}
func NewPartitionCollector(logger *slog.Logger) (Collector, error) {
return &partitionCollector{
logger: logger,
entitledCapacity: prometheus.NewDesc(
prometheus.BuildFQName(namespace, partitionCollectorSubsystem, "entitled_capacity"),
"Entitled processor capacity of the partition in CPU units (e.g. 1.0 = one core).",
nil, nil,
),
memoryMax: prometheus.NewDesc(
prometheus.BuildFQName(namespace, partitionCollectorSubsystem, "memory_max"),
"Maximum memory of the partition in bytes.",
nil, nil,
),
memoryOnline: prometheus.NewDesc(
prometheus.BuildFQName(namespace, partitionCollectorSubsystem, "memory_online"),
"Online memory of the partition in bytes.",
nil, nil,
),
cpuOnline: prometheus.NewDesc(
prometheus.BuildFQName(namespace, partitionCollectorSubsystem, "cpus_online"),
"Number of online CPUs in the partition.",
nil, nil,
),
cpuSys: prometheus.NewDesc(
prometheus.BuildFQName(namespace, partitionCollectorSubsystem, "cpus_sys"),
"Number of physical CPUs in the system.",
nil, nil,
),
cpuPool: prometheus.NewDesc(
prometheus.BuildFQName(namespace, partitionCollectorSubsystem, "cpus_pool"),
"Number of physical CPUs in the pool.",
nil, nil,
),
powerSaveMode: prometheus.NewDesc(
prometheus.BuildFQName(namespace, partitionCollectorSubsystem, "power_save_mode"),
"Power save mode of the partition (1 for enabled, 0 for disabled).",
nil, nil,
),
smtThreads: prometheus.NewDesc(
prometheus.BuildFQName(namespace, partitionCollectorSubsystem, "smt_threads"),
"Number of SMT threads per core.",
nil, nil,
),
}, nil
}
func (c *partitionCollector) Update(ch chan<- prometheus.Metric) error {
stats, err := perfstat.PartitionStat()
if err != nil {
return err
}
powerSaveMode := 0.0
if stats.Conf.PowerSave {
powerSaveMode = 1.0
}
ch <- prometheus.MustNewConstMetric(c.entitledCapacity, prometheus.GaugeValue, float64(stats.EntCapacity)/100.0)
ch <- prometheus.MustNewConstMetric(c.memoryMax, prometheus.GaugeValue, float64(stats.Mem.Max)*1024*1024)
ch <- prometheus.MustNewConstMetric(c.memoryOnline, prometheus.GaugeValue, float64(stats.Mem.Online)*1024*1024)
ch <- prometheus.MustNewConstMetric(c.cpuOnline, prometheus.GaugeValue, float64(stats.VCpus.Online))
ch <- prometheus.MustNewConstMetric(c.cpuSys, prometheus.GaugeValue, float64(stats.NumProcessors.Online))
ch <- prometheus.MustNewConstMetric(c.cpuPool, prometheus.GaugeValue, float64(stats.ActiveCpusInPool))
ch <- prometheus.MustNewConstMetric(c.powerSaveMode, prometheus.GaugeValue, powerSaveMode)
ch <- prometheus.MustNewConstMetric(c.smtThreads, prometheus.GaugeValue, float64(stats.SmtThreads))
return nil
}

View file

@ -33,7 +33,7 @@ func canTestPerf(t *testing.T) {
if err != nil {
t.Skip("Procfs not mounted, skipping perf tests")
}
paranoidStr := strings.Replace(string(paranoidBytes), "\n", "", -1)
paranoidStr := strings.ReplaceAll(string(paranoidBytes), "\n", "")
paranoid, err := strconv.Atoi(paranoidStr)
if err != nil {
t.Fatalf("Expected perf_event_paranoid to be an int, got: %s", paranoidStr)

View file

@ -106,7 +106,7 @@ func (c *processCollector) Update(ch chan<- prometheus.Metric) error {
pidM, err := readUintFromFile(procFilePath("sys/kernel/pid_max"))
if err != nil {
return fmt.Errorf("unable to retrieve limit number of maximum pids alloved: %w", err)
return fmt.Errorf("unable to retrieve limit number of maximum pids allowed: %w", err)
}
ch <- prometheus.MustNewConstMetric(c.pidUsed, prometheus.GaugeValue, float64(pids))
ch <- prometheus.MustNewConstMetric(c.pidMax, prometheus.GaugeValue, float64(pidM))

View file

@ -48,7 +48,7 @@ func TestReadProcessStatus(t *testing.T) {
}
maxPid, err := readUintFromFile(procFilePath("sys/kernel/pid_max"))
if err != nil {
t.Fatalf("Unable to retrieve limit number of maximum pids alloved %v\n", err)
t.Fatalf("Unable to retrieve limit number of maximum pids allowed %v\n", err)
}
if uint64(pids) > maxPid || pids == 0 {
t.Fatalf("Total running pids cannot be greater than %d or equals to 0", maxPid)

View file

@ -122,11 +122,12 @@ func TestSystemdSummary(t *testing.T) {
summary := summarizeUnits(fixtures[0])
for _, state := range unitStatesName {
if state == "inactive" {
switch state {
case "inactive":
testSummaryHelper(t, state, summary[state], 3.0)
} else if state == "active" {
case "active":
testSummaryHelper(t, state, summary[state], 1.0)
} else {
default:
testSummaryHelper(t, state, summary[state], 0.0)
}
}

View file

@ -435,5 +435,5 @@ type zfsSysctl string
func (s zfsSysctl) metricName() string {
parts := strings.Split(string(s), ".")
return strings.Replace(parts[len(parts)-1], "-", "_", -1)
return strings.ReplaceAll(parts[len(parts)-1], "-", "_")
}

22
go.mod
View file

@ -18,18 +18,18 @@ require (
github.com/mattn/go-xmlrpc v0.0.3
github.com/mdlayher/ethtool v0.2.0
github.com/mdlayher/netlink v1.7.2
github.com/mdlayher/wifi v0.3.1
github.com/mdlayher/wifi v0.5.0
github.com/opencontainers/selinux v1.11.1
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55
github.com/prometheus-community/go-runit v0.1.0
github.com/prometheus/client_golang v1.21.1
github.com/prometheus/client_model v0.6.1
github.com/prometheus/common v0.62.0
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.64.0
github.com/prometheus/exporter-toolkit v0.14.0
github.com/prometheus/procfs v0.16.0
github.com/prometheus/procfs v0.16.1
github.com/safchain/ethtool v0.5.10
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
golang.org/x/sys v0.30.0
golang.org/x/sys v0.33.0
howett.net/plist v1.0.1
)
@ -51,11 +51,11 @@ require (
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/net v0.36.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

44
go.sum
View file

@ -61,8 +61,8 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=
github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=
github.com/mdlayher/wifi v0.3.1 h1:bZDuMI1f7z5BtUUO3NgHRdR/R88YtywIe6dsEFI0Txs=
github.com/mdlayher/wifi v0.3.1/go.mod h1:ODQaObvsglghTuNhezD9grkTB4shVNc28aJfTXmvSi8=
github.com/mdlayher/wifi v0.5.0 h1:TGZIcrhL6h3710amshpEJnMzLs74MrZOF+8qbm8Gx/I=
github.com/mdlayher/wifi v0.5.0/go.mod h1:yfQs+5zr1eOIfdsWDcZonWdznnt/Iiz0/4772cfZuHk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
@ -77,14 +77,14 @@ github.com/prometheus-community/go-runit v0.1.0 h1:uTWEj/Fn2RoLdfg/etSqwzgYNOYPr
github.com/prometheus-community/go-runit v0.1.0/go.mod h1:AvJ9Jo3gAFu2lbM4+qfjdpq30FfiLDJZKbQ015u08IQ=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg=
github.com/prometheus/exporter-toolkit v0.14.0/go.mod h1:Gu5LnVvt7Nr/oqTBUC23WILZepW0nffNo10XdhQcwWA=
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/safchain/ethtool v0.5.10 h1:Im294gZtuf4pSGJRAOGKaASNi3wMeFaGaWuSaomedpc=
@ -102,25 +102,25 @@ go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View file

@ -112,7 +112,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
h.logger.Warn("Couldn't create filtered metrics handler:", "err", err)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(fmt.Sprintf("Couldn't create filtered metrics handler: %s", err)))
fmt.Fprintf(w, "Couldn't create filtered metrics handler: %s", err)
return
}
filteredHandler.ServeHTTP(w, r)