diff --git a/README.md b/README.md index 7374032f..943c8569 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ lastlogin | Exposes the last time there was a login. megacli | Exposes RAID statistics from MegaCLI. ntp | Exposes time drift from an NTP server. runit | Exposes service status from [runit](http://smarden.org/runit/). +tcpstat | Exposes TCP connection status information from /proc/net/tcp and /proc/net/tcp6. (Warning: the current version has potential performance issues in high load situations.) ## Textfile Collector diff --git a/collector/fixtures/tcpstat b/collector/fixtures/tcpstat new file mode 100644 index 00000000..8b3777a9 --- /dev/null +++ b/collector/fixtures/tcpstat @@ -0,0 +1,3 @@ + sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 2740 1 ffff88003d3af3c0 100 0 0 10 0 + 1: 0F02000A:0016 0202000A:8B6B 01 00000000:00000000 02:000AC99B 00000000 0 0 3652 4 ffff88003d3ae040 21 4 31 47 46 diff --git a/collector/tcpstat.go b/collector/tcpstat.go new file mode 100644 index 00000000..d8bee653 --- /dev/null +++ b/collector/tcpstat.go @@ -0,0 +1,150 @@ +// +build !notcpstat + +package collector + +import ( + "bufio" + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/prometheus/client_golang/prometheus" +) + +const ( + procTCPStat = "/proc/net/tcp" + procTCP6Stat = "/proc/net/tcp6" +) + +type TCPConnectionState int + +const ( + TCP_ESTABLISHED TCPConnectionState = iota + 1 + TCP_SYN_SENT + TCP_SYN_RECV + TCP_FIN_WAIT1 + TCP_FIN_WAIT2 + TCP_TIME_WAIT + TCP_CLOSE + TCP_CLOSE_WAIT + TCP_LAST_ACK + TCP_LISTEN + TCP_CLOSING +) + +type tcpStatCollector struct { + config Config + metric *prometheus.GaugeVec +} + +func init() { + Factories["tcpstat"] = NewTCPStatCollector +} + +// NewTCPStatCollector takes a config struct and returns +// a new Collector exposing network stats. +func NewTCPStatCollector(config Config) (Collector, error) { + return &tcpStatCollector{ + config: config, + metric: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: Namespace, + Name: "tcp_connection_states", + Help: "Number of connection states.", + }, + []string{"state"}, + ), + }, nil +} + +func (c *tcpStatCollector) Update(ch chan<- prometheus.Metric) (err error) { + tcpStats, err := getTCPStats(procTCPStat) + if err != nil { + return fmt.Errorf("couldn't get tcpstats: %s", err) + } + + // if enabled ipv6 system + if _, hasIPv6 := os.Stat(procTCP6Stat); hasIPv6 == nil { + tcp6Stats, err := getTCPStats(procTCP6Stat) + if err != nil { + return fmt.Errorf("couldn't get tcp6stats: %s", err) + } + + for st, value := range tcp6Stats { + tcpStats[st] += value + } + } + + for st, value := range tcpStats { + c.metric.WithLabelValues(st.String()).Set(value) + } + + c.metric.Collect(ch) + return err +} + +func getTCPStats(statsFile string) (map[TCPConnectionState]float64, error) { + file, err := os.Open(statsFile) + if err != nil { + return nil, err + } + defer file.Close() + + return parseTCPStats(file) +} + +func parseTCPStats(r io.Reader) (map[TCPConnectionState]float64, error) { + var ( + tcpStats = map[TCPConnectionState]float64{} + scanner = bufio.NewScanner(r) + ) + + for scanner.Scan() { + parts := strings.Fields(scanner.Text()) + if len(parts) == 0 { + continue + } + if strings.HasPrefix(parts[0], "sl") { + continue + } + st, err := strconv.ParseInt(parts[3], 16, 8) + if err != nil { + return nil, err + } + + tcpStats[TCPConnectionState(st)]++ + } + + return tcpStats, nil +} + +func (st TCPConnectionState) String() string { + switch st { + case TCP_ESTABLISHED: + return "established" + case TCP_SYN_SENT: + return "syn_sent" + case TCP_SYN_RECV: + return "syn_recv" + case TCP_FIN_WAIT1: + return "fin_wait1" + case TCP_FIN_WAIT2: + return "fin_wait2" + case TCP_TIME_WAIT: + return "time_wait" + case TCP_CLOSE: + return "close" + case TCP_CLOSE_WAIT: + return "close_wait" + case TCP_LAST_ACK: + return "last_ack" + case TCP_LISTEN: + return "listen" + case TCP_CLOSING: + return "closing" + default: + return "unknown" + } +} diff --git a/collector/tcpstat_test.go b/collector/tcpstat_test.go new file mode 100644 index 00000000..c86222fa --- /dev/null +++ b/collector/tcpstat_test.go @@ -0,0 +1,27 @@ +package collector + +import ( + "os" + "testing" +) + +func TestTCPStat(t *testing.T) { + file, err := os.Open("fixtures/tcpstat") + if err != nil { + t.Fatal(err) + } + defer file.Close() + + tcpStats, err := parseTCPStats(file) + if err != nil { + t.Fatal(err) + } + + if want, got := 1, int(tcpStats[TCP_ESTABLISHED]); want != got { + t.Errorf("want tcpstat number of established state %d, got %d", want, got) + } + + if want, got := 1, int(tcpStats[TCP_LISTEN]); want != got { + t.Errorf("want tcpstat number of listen state %d, got %d", want, got) + } +}