Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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

Expand Down
3 changes: 3 additions & 0 deletions collector/fixtures/tcpstat
Original file line number Diff line number Diff line change
@@ -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
150 changes: 150 additions & 0 deletions collector/tcpstat.go
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it'd also make sense to differentiate IPv4 / IPv6 via a label, but not sure about the practical use. @brian-brazil any opinion on this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it does make sense

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can imagine uses for it if say you were debugging an issue with a buggy v6 client/server. I'm not too bothered either way, I think I'd tend to keep it simple for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not need IPv4 / IPv6 labels, but I think good to label as an option.

if _, hasIPv6 := os.Stat(procTCP6Stat); hasIPv6 == nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I have added modification. (93d21b5)
Fix always error on not exists /proc/net/tcp6.

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"
}
}
27 changes: 27 additions & 0 deletions collector/tcpstat_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}