diff --git a/collector/fixtures/sockstat b/collector/fixtures/sockstat new file mode 100644 index 00000000..97ea5d1a --- /dev/null +++ b/collector/fixtures/sockstat @@ -0,0 +1,6 @@ +sockets: used 229 +TCP: inuse 4 orphan 0 tw 4 alloc 17 mem 1 +UDP: inuse 0 mem 0 +UDPLITE: inuse 0 +RAW: inuse 0 +FRAG: inuse 0 memory 0 diff --git a/collector/sockstat_linux.go b/collector/sockstat_linux.go new file mode 100644 index 00000000..e415b12d --- /dev/null +++ b/collector/sockstat_linux.go @@ -0,0 +1,114 @@ +// +build !nonetstat + +package collector + +import ( + "bufio" + "fmt" + "github.com/prometheus/client_golang/prometheus" + "io" + "os" + "strconv" + "strings" +) + +const ( + procSockStat = "/proc/net/sockstat" + sockStatSubsystem = "sockstat" +) + +// Used for calculating the total memory bytes on TCP and UDP +var pageSize = os.Getpagesize() + +type sockStatCollector struct { + metrics map[string]prometheus.Gauge +} + +func init() { + Factories["sockstat"] = NewSockStatCollector +} + +// NewSockStatCollector returns a new Collector exposing socket stats +func NewSockStatCollector() (Collector, error) { + return &sockStatCollector{ + metrics: map[string]prometheus.Gauge{}, + }, nil +} + +func (c *sockStatCollector) Update(ch chan<- prometheus.Metric) (err error) { + sockStats, err := getSockStats(procSockStat) + if err != nil { + return fmt.Errorf("couldn't get sockstats: %s", err) + } + for protocol, protocolStats := range sockStats { + for name, value := range protocolStats { + key := protocol + "_" + name + if _, ok := c.metrics[key]; !ok { + c.metrics[key] = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: sockStatSubsystem, + Name: key, + Help: fmt.Sprintf("%s %s from /proc/net/sockstat.", protocol, name), + }, + ) + } + v, err := strconv.ParseFloat(value, 64) + if err != nil { + return fmt.Errorf("invalid value %s in sockstats: %s", value, err) + } + c.metrics[key].Set(v) + } + } + for _, m := range c.metrics { + m.Collect(ch) + } + return err +} + +func getSockStats(fileName string) (map[string]map[string]string, error) { + file, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer file.Close() + + return parseSockStats(file, fileName) +} + +func parseSockStats(r io.Reader, fileName string) (map[string]map[string]string, error) { + var ( + sockStat = map[string]map[string]string{} + scanner = bufio.NewScanner(r) + ) + + for scanner.Scan() { + line := strings.Split(string(scanner.Text()), " ") + // Remove trailing : + protocol := line[0][:len(line[0])-1] + sockStat[protocol] = map[string]string{} + + for i := 1; i < len(line) && i+1 < len(line); i++ { + sockStat[protocol][line[i]] = line[i+1] + i++ + } + } + + // The mem options are reported in pages + // Multiply them by the pagesize to get bytes + // Update TCP Mem + pageCount, err := strconv.Atoi(sockStat["TCP"]["mem"]) + if err != nil { + return nil, fmt.Errorf("invalid value %s in sockstats: %s", sockStat["TCP"]["mem"], err) + } + sockStat["TCP"]["mem"] = strconv.Itoa(pageCount * pageSize) + + // Update UDP Mem + pageCount, err = strconv.Atoi(sockStat["UDP"]["mem"]) + if err != nil { + return nil, fmt.Errorf("invalid value %s in sockstats: %s", sockStat["UDP"]["mem"], err) + } + sockStat["UDP"]["mem"] = strconv.Itoa(pageCount * pageSize) + + return sockStat, nil +} diff --git a/collector/sockstat_linux_test.go b/collector/sockstat_linux_test.go new file mode 100644 index 00000000..36b1aec9 --- /dev/null +++ b/collector/sockstat_linux_test.go @@ -0,0 +1,41 @@ +package collector + +import ( + "os" + "strconv" + "testing" +) + +func TestSockStats(t *testing.T) { + file, err := os.Open("fixtures/sockstat") + if err != nil { + t.Fatal(err) + } + + defer file.Close() + + sockStats, err := parseSockStats(file, fileName) + if err != nil { + t.Fatal(err) + } + + if want, got := "229", sockStats["sockets"]["used"]; want != got { + t.Errorf("want sockstat sockets used %s, got %s", want, got) + } + + if want, got := "4", sockStats["TCP"]["tw"]; want != got { + t.Errorf("want sockstat sockets used %s, got %s", want, got) + } + + if want, got := "17", sockStats["TCP"]["alloc"]; want != got { + t.Errorf("want sockstat sockets used %s, got %s", want, got) + } + + // The test file has 1 for TCP mem, which is one page. So we should get the + // page size in bytes back from sockstat_linux. We get the page size from + // os here because this value can change from system to system. The value is + // 4096 by default from linux 2.4 onward. + if want, got := strconv.Itoa(os.Getpagesize()), sockStats["TCP"]["mem"]; want != got { + t.Errorf("want sockstat sockets used %s, got %s", want, got) + } +} diff --git a/node_exporter.go b/node_exporter.go index c15b4643..56f2d2d2 100644 --- a/node_exporter.go +++ b/node_exporter.go @@ -28,7 +28,7 @@ var ( memProfile = flag.String("debug.memprofile-file", "", "Write memory profile to this file upon receipt of SIGUSR1.") listenAddress = flag.String("web.listen-address", ":9100", "Address on which to expose metrics and web interface.") metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.") - enabledCollectors = flag.String("collectors.enabled", "diskstats,filesystem,loadavg,meminfo,stat,textfile,time,netdev,netstat", "Comma-separated list of collectors to use.") + enabledCollectors = flag.String("collectors.enabled", "diskstats,filesystem,loadavg,meminfo,stat,textfile,time,netdev,netstat,sockstat", "Comma-separated list of collectors to use.") printCollectors = flag.Bool("collectors.print", false, "If true, print available collectors and exit.") authUser = flag.String("auth.user", "", "Username for basic auth.") authPass = flag.String("auth.pass", "", "Password for basic auth.")