Add qdisc collector for Linux (#580)

* Add qdisc collector for Linux

This collector gathers basic queueing discipline metrics via netlink,
similarly to what `tc -s qdisc show` does.

* qdisc collector: nl-specific code moved, names fixed

- netlink-specific parts moved to github.com/ema/qdisc
- avoid using shortened names
- counters renamed into XXX_total

* Get rid of parseMessage error checking leftover

* Add github.com/ema/qdisc to vendored packages

* Update help texts and comments

* Add qdisc collector to README file

* qdisc collector end-to-end testing

* Update qdisc dependency to latest version

Update github.com/ema/qdisc dependency to revision 2c7e72d, which
includes unit testing.

* qdisc collector: rename "iface" label into "device"
This commit is contained in:
Emanuele Rocca 2017-05-23 11:55:50 +02:00 committed by Ben Kochie
commit 047003b6bb
10 changed files with 440 additions and 0 deletions

10
vendor/github.com/ema/qdisc/LICENSE.md generated vendored Normal file
View file

@ -0,0 +1,10 @@
MIT License
===========
Copyright (C) 2017 Emanuele Rocca
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

11
vendor/github.com/ema/qdisc/Makefile generated vendored Normal file
View file

@ -0,0 +1,11 @@
build:
go fmt
go build
go vet
staticcheck
#golint -set_exit_status
go test -v -race -tags=integration
cover:
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

26
vendor/github.com/ema/qdisc/README.md generated vendored Normal file
View file

@ -0,0 +1,26 @@
qdisc [![Build Status](https://travis-ci.org/ema/qdisc.svg?branch=master)](https://travis-ci.org/ema/qdisc)
=====
Package `qdisc` allows to get queuing discipline information via netlink,
similarly to what `tc -s qdisc show` does.
Example usage
-------------
package main
import (
"fmt"
"github.com/ema/qdisc"
)
func main() {
info, err := qdisc.Get()
if err == nil {
for _, msg := range info {
fmt.Printf("%+v\n", msg)
}
}
}

230
vendor/github.com/ema/qdisc/get.go generated vendored Normal file
View file

@ -0,0 +1,230 @@
package qdisc
import (
"fmt"
"math"
"net"
"github.com/mdlayher/netlink"
"github.com/mdlayher/netlink/nlenc"
)
const (
TCA_UNSPEC = iota
TCA_KIND
TCA_OPTIONS
TCA_STATS
TCA_XSTATS
TCA_RATE
TCA_FCNT
TCA_STATS2
TCA_STAB
__TCA_MAX
)
const (
TCA_STATS_UNSPEC = iota
TCA_STATS_BASIC
TCA_STATS_RATE_EST
TCA_STATS_QUEUE
TCA_STATS_APP
TCA_STATS_RATE_EST64
__TCA_STATS_MAX
)
// See struct tc_stats in /usr/include/linux/pkt_sched.h
type TC_Stats struct {
Bytes uint64
Packets uint32
Drops uint32
Overlimits uint32
Bps uint32
Pps uint32
Qlen uint32
Backlog uint32
}
// See /usr/include/linux/gen_stats.h
type TC_Stats2 struct {
// struct gnet_stats_basic
Bytes uint64
Packets uint32
// struct gnet_stats_queue
Qlen uint32
Backlog uint32
Drops uint32
Requeues uint32
Overlimits uint32
}
type QdiscInfo struct {
IfaceName string
Parent uint32
Handle uint32
Kind string
Bytes uint64
Packets uint32
Drops uint32
Requeues uint32
Overlimits uint32
}
func parseTCAStats(attr netlink.Attribute) TC_Stats {
var stats TC_Stats
stats.Bytes = nlenc.Uint64(attr.Data[0:8])
stats.Packets = nlenc.Uint32(attr.Data[8:12])
stats.Drops = nlenc.Uint32(attr.Data[12:16])
stats.Overlimits = nlenc.Uint32(attr.Data[16:20])
stats.Bps = nlenc.Uint32(attr.Data[20:24])
stats.Pps = nlenc.Uint32(attr.Data[24:28])
stats.Qlen = nlenc.Uint32(attr.Data[28:32])
stats.Backlog = nlenc.Uint32(attr.Data[32:36])
return stats
}
func parseTCAStats2(attr netlink.Attribute) TC_Stats2 {
var stats TC_Stats2
nested, _ := netlink.UnmarshalAttributes(attr.Data)
for _, a := range nested {
switch a.Type {
case TCA_STATS_BASIC:
stats.Bytes = nlenc.Uint64(a.Data[0:8])
stats.Packets = nlenc.Uint32(a.Data[8:12])
case TCA_STATS_QUEUE:
stats.Qlen = nlenc.Uint32(a.Data[0:4])
stats.Backlog = nlenc.Uint32(a.Data[4:8])
stats.Drops = nlenc.Uint32(a.Data[8:12])
stats.Requeues = nlenc.Uint32(a.Data[12:16])
stats.Overlimits = nlenc.Uint32(a.Data[16:20])
default:
// TODO: TCA_STATS_APP
}
}
return stats
}
func getQdiscMsgs(c *netlink.Conn) ([]netlink.Message, error) {
req := netlink.Message{
Header: netlink.Header{
Flags: netlink.HeaderFlagsRequest | netlink.HeaderFlagsDump,
Type: 38, // RTM_GETQDISC
},
Data: []byte{0},
}
// Perform a request, receive replies, and validate the replies
msgs, err := c.Execute(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %v", err)
}
return msgs, nil
}
// See https://tools.ietf.org/html/rfc3549#section-3.1.3
func parseMessage(msg netlink.Message) (QdiscInfo, error) {
var m QdiscInfo
var s TC_Stats
var s2 TC_Stats2
/*
struct tcmsg {
unsigned char tcm_family;
unsigned char tcm__pad1;
unsigned short tcm__pad2;
int tcm_ifindex;
__u32 tcm_handle;
__u32 tcm_parent;
__u32 tcm_info;
};
*/
if len(msg.Data) < 20 {
return m, fmt.Errorf("Short message, len=%d", len(msg.Data))
}
ifaceIdx := nlenc.Uint32(msg.Data[4:8])
m.Handle = nlenc.Uint32(msg.Data[8:12])
m.Parent = nlenc.Uint32(msg.Data[12:16])
if m.Parent == math.MaxUint32 {
m.Parent = 0
}
// The first 20 bytes are taken by tcmsg
attrs, err := netlink.UnmarshalAttributes(msg.Data[20:])
if err != nil {
return m, fmt.Errorf("failed to unmarshal attributes: %v", err)
}
for _, attr := range attrs {
switch attr.Type {
case TCA_KIND:
m.Kind = nlenc.String(attr.Data)
case TCA_STATS2:
s2 = parseTCAStats2(attr)
m.Bytes = s2.Bytes
m.Packets = s2.Packets
m.Drops = s2.Drops
// requeues only available in TCA_STATS2, not in TCA_STATS
m.Requeues = s2.Requeues
m.Overlimits = s2.Overlimits
case TCA_STATS:
// Legacy
s = parseTCAStats(attr)
m.Bytes = s.Bytes
m.Packets = s.Packets
m.Drops = s.Drops
m.Overlimits = s.Overlimits
default:
// TODO: TCA_OPTIONS and TCA_XSTATS
}
}
iface, err := net.InterfaceByIndex(int(ifaceIdx))
if err == nil {
m.IfaceName = iface.Name
}
return m, err
}
func getAndParse(c *netlink.Conn) ([]QdiscInfo, error) {
var res []QdiscInfo
msgs, err := getQdiscMsgs(c)
if err != nil {
return nil, err
}
for _, msg := range msgs {
m, err := parseMessage(msg)
if err != nil {
return nil, err
}
res = append(res, m)
}
return res, nil
}
func Get() ([]QdiscInfo, error) {
const familyRoute = 0
c, err := netlink.Dial(familyRoute, nil)
if err != nil {
return nil, fmt.Errorf("failed to dial netlink: %v", err)
}
defer c.Close()
return getAndParse(c)
}