Commit 15b7a081 authored by Daniel Swarbrick's avatar Daniel Swarbrick

Merge branch 'upstream' into repackaged

parents eb842137 d984aa91
language: go
go:
- 1.8
script: make init test vet lint errcheck
script: make init test vet lint
from golang:1.10 as builder
arg CMD
run wget -o/dev/null -O/usr/local/bin/dep https://github.com/golang/dep/releases/download/v0.3.2/dep-linux-amd64 && \
chmod +x /usr/local/bin/dep
workdir ${GOPATH}/src/github.com/czerwonk/bird_exporter
copy . .
run make deps build && cp bird_exporter /bird_exporter
from golang:1.10
copy --from=builder /bird_exporter /bird_exporter
entrypoint ["/bird_exporter"]
......@@ -19,6 +19,7 @@ MAKE_COLOR=\033[33;01m%-20s\033[0m
MAIN = github.com/czerwonk/bird_exporter
SRCS = $(shell git ls-files '*.go' | grep -v '^vendor/')
PKGS = $(shell go list ./... | grep -v '/vendor/')
.DEFAULT_GOAL := help
......@@ -32,7 +33,6 @@ init: ## Install requirements
@echo -e "$(OK_COLOR)[$(APP)] Install requirements$(NO_COLOR)"
@go get -u github.com/golang/dep/cmd/dep
@go get -u github.com/golang/lint/golint
@go get -u github.com/kisielk/errcheck
.PHONY: deps
deps: ## Update dependencies
......@@ -47,7 +47,7 @@ build: ## Make binary
.PHONY: test
test: ## Launch unit tests
@echo -e "$(OK_COLOR)[$(APP)] Launch unit tests $(NO_COLOR)"
@go test .
@$(foreach pkg,$(PKGS),go test $(pkg) || exit;)
.PHONY: lint
lint: ## Launch golint
......@@ -57,11 +57,6 @@ lint: ## Launch golint
vet: ## Launch go vet
@$(foreach file,$(SRCS),$(GO) vet $(file) || exit;)
.PHONY: errcheck
errcheck: ## Launch go errcheck
@echo -e "$(OK_COLOR)[$(APP)] Go Errcheck $(NO_COLOR)"
@$(foreach pkg,$(PKGS),errcheck $(pkg) || exit;)
.PHONY: coverage
coverage: ## Launch code coverage
@$(foreach pkg,$(PKGS),$(GO) test -cover $(pkg) || exit;)
......
......@@ -59,8 +59,16 @@ go get -u github.com/czerwonk/bird_exporter
bird_exporter -format.new=true
```
## BIRD RS Dashboard
this sample dashboard was created by [openbsod](https://github.com/openbsod). Thanks for contributing!
https://grafana.com/dashboards/5259
![alt text](https://github.com/czerwonk/bird_exporter/blob/master/grafana/img/bird_exporter.png)
## Features
* BGP session state
* OSPF neighbor/interface count
* imported / exported / filtered prefix counts / route state changes (BGP, OSPF, Kernel, Static, Device, Direct)
* protocol uptimes (BGP, OSPF)
......
---
date: 2018-06-20
footer: bird_exporter
header: "bird_exporter's Manual"
layout: page
license: "Licensed under the MIT license"
section: 1
title: BIRD_EXPORTER
---
# NAME
bird_exporter - A protocol state exporter for the BIRD routing daemon to use
with Prometheus
# SYNOPSIS
**bird_exporter** [**OPTIONS**]
# DESCRIPTION
**bird_exporter** is a metric exporter for the BIRD routing daemon to use with
Prometheus. Since **bird_exporter** uses the BIRD Unix socket(s), BIRD needs to
be installed on the same machine as bird_exporter. The user executing
bird_exporter must have read/write permission to access the BIRD Unix sockets.
# OPTIONS
**-bird.ipv4**
Get protocols from bird (not compatible with **-bird.v2**)
**-bird.ipv6**
Get protocols from bird6 (not compatible with **-bird.v2**)
**-bird.socket** */path/to/socket*
Socket to communicate with bird routing daemon
**-bird.socket6** */path/to/socket*
Socket to communicate with bird6 routing daemon (not compatible with
**-bird.v2**)
**-bird.v2**
BIRD major version >= 2.0 (multi channel protocols)
**-format.new**
New metric format (more convenient / generic)
**-proto.bgp**
Enables metrics for protocol BGP
**-proto.direct**
Enables metrics for protocol Direct
**-proto.kernel**
Enables metrics for protocol Kernel
**-proto.ospf**
Enables metrics for protocol OSPF
**-proto.static**
Enables metrics for protocol Static
**-version**
Print version information
**-web.listen-address** *[address]:port*
Address on which to expose metrics and web interface
**-web.telemetry-path** *path*
Path under which to expose metrics (default "/metrics")
Version 2.0 of BIRD supports both IPv4 and IPv6 in a single daemon. Since
version 1.1 of **bird_exporter**, it can be used with BIRD 2.0+ using the
**-bird.v2** option. When using this option, **bird_exporter** queries the same
socket for both IPv4 and IPv6. In this mode the IP protocol is determined by
the channel information, and options **-bird.ipv4**, **-bird.ipv6** and
**-bird.socket6** are ignored.
# BIRD CONFIGURATION
To get meaningful uptime information, BIRD needs to be configured to use
ISO-format timestamps:
```
timeformat protocol iso long;
```
# METRIC FORMATS
In version 1.0, a new metric format was introduced. To avoid backwards
incompatibility, the new format is optional and can be enabled by using the
**-format.new** option. The new format handles protocols more generically and
allows for a better query structure. It also adheres more to the Prometheus
metric naming best practices. In both formats protocol specific metrics are
prefixed with the protocol name (e.g. OSPF running metric).
## OLD METRIC FORMAT EXAMPLE
```
bgp4_session_prefix_count_import{name="bgp1"} 600000
bgp6_session_prefix_count_import{name="bgp1"} 50000
ospfv3_running{name="ospf1"} 1
```
## NEW METRIC FORMAT EXAMPLE
```
bird_protocol_prefix_import_count{name="bgp1",proto="BGP",ip_version="4"} 600000
bird_protocol_prefix_import_count{name="bgp1",proto="BGP",ip_version="6"} 50000
bird_ospfv3_running{name="ospf1"} 1
```
# AUTHOR
Daniel Czerwonk <daniel@dan-nrw.de>
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: bird-exporter
namespace: kube-system
labels:
app: bird-exporter
spec:
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app: bird-exporter
spec:
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: bird-exporter
image: bird_exporter:latest
args: ["-format.new=true", "-bird.socket=/var/run/bird/bird.ctl"]
resources:
limits:
cpu: 100m
memory: 32Mi
requests:
cpu: 100m
memory: 32Mi
volumeMounts:
- mountPath: /var/run/bird/
name: bird-socket
readOnly: true
ports:
- containerPort: 9324
name: metrics
volumes:
- name: bird-socket
hostPath:
path: /var/run/bird/
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "Prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "4.6.3"
},
{
"type": "panel",
"id": "graph",
"name": "Graph",
"version": ""
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 1,
"hideControls": true,
"id": null,
"links": [],
"rows": [
{
"collapse": false,
"height": 509,
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 0,
"id": 2,
"legend": {
"alignAsTable": true,
"avg": false,
"current": true,
"hideEmpty": true,
"hideZero": true,
"max": false,
"min": false,
"rightSide": true,
"show": true,
"sort": "current",
"sortDesc": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 1,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 12,
"stack": false,
"steppedLine": true,
"targets": [
{
"dateTimeType": "DATETIME",
"expr": "bird_protocol_prefix_export_count{ip_version=\"4\",proto=\"BGP\"}",
"format": "time_series",
"formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t",
"intervalFactor": 1,
"legendFormat": "{{name}}",
"query": "SELECT\n $timeSeries as t,\n count()\nFROM $table\nWHERE $timeFilter\nGROUP BY t\nORDER BY t",
"refId": "A",
"round": "0s"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "PITER-IX prefixes exported",
"tooltip": {
"shared": true,
"sort": 2,
"value_type": "individual"
},
"transparent": true,
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": "",
"logBase": 1,
"max": null,
"min": null,
"show": false
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
]
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 0,
"id": 3,
"legend": {
"alignAsTable": true,
"avg": false,
"current": true,
"hideEmpty": true,
"hideZero": true,
"max": false,
"min": false,
"rightSide": true,
"show": true,
"sort": "current",
"sortDesc": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 12,
"stack": false,
"steppedLine": true,
"targets": [
{
"dateTimeType": "DATETIME",
"expr": "bird_protocol_prefix_import_count{ip_version=\"4\",proto=\"BGP\"}",
"format": "time_series",
"formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t",
"intervalFactor": 1,
"legendFormat": "{{name}}",
"query": "SELECT\n $timeSeries as t,\n count()\nFROM $table\nWHERE $timeFilter\nGROUP BY t\nORDER BY t",
"refId": "A",
"round": "0s"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "PITER-IX prefixes imported",
"tooltip": {
"shared": true,
"sort": 2,
"value_type": "individual"
},
"transparent": true,
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "none",
"label": "prefixes",
"logBase": 1,
"max": null,
"min": null,
"show": false
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
]
}
],
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"showTitle": false,
"title": "Dashboard Row",
"titleSize": "h6"
}
],
"schemaVersion": 14,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-30m",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "BIRD RS",
"version": 15
}
\ No newline at end of file
......@@ -12,7 +12,7 @@ import (
"github.com/prometheus/common/log"
)
const version string = "1.2.1"
const version string = "1.2.2"
var (
showVersion = flag.Bool("version", false, "Print version information.")
......@@ -20,7 +20,7 @@ var (
metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.")
birdSocket = flag.String("bird.socket", "/var/run/bird.ctl", "Socket to communicate with bird routing daemon")
birdV2 = flag.Bool("bird.v2", false, "Bird major version >= 2.0 (multi channel protocols)")
newFormat = flag.Bool("format.new", false, "New metric format (more convinient / generic)")
newFormat = flag.Bool("format.new", false, "New metric format (more convenient / generic)")
enableBgp = flag.Bool("proto.bgp", true, "Enables metrics for protocol BGP")
enableOspf = flag.Bool("proto.ospf", true, "Enables metrics for protocol OSPF")
enableKernel = flag.Bool("proto.kernel", true, "Enables metrics for protocol Kernel")
......@@ -62,7 +62,7 @@ func startServer() {
log.Infof("Starting bird exporter (Version: %s)\n", version)
if !*newFormat {
log.Info("INFO: You are using the old metric format. Please consider using the new (more convinient one) by setting -format.new=true.")
log.Info("INFO: You are using the old metric format. Please consider using the new (more convenient one) by setting -format.new=true.")
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
......
......@@ -7,13 +7,14 @@ import (
type DefaultLabelStrategy struct {
}
func (*DefaultLabelStrategy) labelNames() []string {
func (*DefaultLabelStrategy) LabelNames() []string {
return []string{"name", "proto", "ip_version"}
}
func (*DefaultLabelStrategy) labelValues(p *protocol.Protocol) []string {
func (*DefaultLabelStrategy) LabelValues(p *protocol.Protocol) []string {
return []string{p.Name, protoString(p), p.IpVersion}
}
func protoString(p *protocol.Protocol) string {
switch p.Proto {
case protocol.BGP:
......
......@@ -43,7 +43,7 @@ func NewGenericProtocolMetricExporter(prefix string, newNaming bool, labelStrate
}
func (m *GenericProtocolMetricExporter) initDesc(prefix string, newNaming bool) {
labels := m.labelStrategy.labelNames()
labels := m.labelStrategy.LabelNames()
m.upDesc = prometheus.NewDesc(prefix+"_up", "Protocol is up", labels, nil)
if newNaming {
......@@ -111,7 +111,7 @@ func (m *GenericProtocolMetricExporter) Describe(ch chan<- *prometheus.Desc) {
}
func (m *GenericProtocolMetricExporter) Export(p *protocol.Protocol, ch chan<- prometheus.Metric) {
l := m.labelStrategy.labelValues(p)
l := m.labelStrategy.LabelValues(p)
ch <- prometheus.MustNewConstMetric(m.upDesc, prometheus.GaugeValue, float64(p.Up), l...)
ch <- prometheus.MustNewConstMetric(m.importCountDesc, prometheus.GaugeValue, float64(p.Imported), l...)
ch <- prometheus.MustNewConstMetric(m.exportCountDesc, prometheus.GaugeValue, float64(p.Exported), l...)
......
......@@ -2,7 +2,11 @@ package metrics
import "github.com/czerwonk/bird_exporter/protocol"
// LabelStrategy abstracts the label generation for protocol metrics
type LabelStrategy interface {
labelNames() []string
labelValues(p *protocol.Protocol) []string
// LabelNames is the list of label names
LabelNames() []string
// Label values is the list of values for the labels specified in `LabelNames()`
LabelValues(p *protocol.Protocol) []string
}
......@@ -5,10 +5,10 @@ import "github.com/czerwonk/bird_exporter/protocol"
type LegacyLabelStrategy struct {
}
func (*LegacyLabelStrategy) labelNames() []string {
func (*LegacyLabelStrategy) LabelNames() []string {
return []string{"name"}
}
func (*LegacyLabelStrategy) labelValues(p *protocol.Protocol) []string {
func (*LegacyLabelStrategy) LabelValues(p *protocol.Protocol) []string {
return []string{p.Name}
}
......@@ -30,11 +30,11 @@ type context struct {
}
func init() {
protocolRegex = regexp.MustCompile("^(?:1002\\-)?([^\\s]+)\\s+(BGP|OSPF|Direct|Device|Kernel)\\s+([^\\s]+)\\s+([^\\s]+)\\s+(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}|[^\\s]+)(?:\\s+(.*?))?$")
routeRegex = regexp.MustCompile("^\\s+Routes:\\s+(\\d+) imported, (?:(\\d+) filtered, )?(\\d+) exported(?:, (\\d+) preferred)?")
uptimeRegex = regexp.MustCompile("^(?:((\\d+):(\\d{2}):(\\d{2}))|(\\d+)|(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}))$")
routeChangeRegex = regexp.MustCompile("(Import|Export) (updates|withdraws):\\s+(\\d+|---)\\s+(\\d+|---)\\s+(\\d+|---)\\s+(\\d+|---)\\s+(\\d+|---)\\s*")
channelRegex = regexp.MustCompile("Channel ipv(4|6)")
protocolRegex = regexp.MustCompile(`^(?:1002\-)?([^\s]+)\s+(BGP|OSPF|Direct|Device|Kernel|Static)\s+([^\s]+)\s+([^\s]+)\s+(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}|[^\s]+)(?:\s+(.*?))?$`)
routeRegex = regexp.MustCompile(`^\s+Routes:\s+(\d+) imported, (?:(\d+) filtered, )?(\d+) exported(?:, (\d+) preferred)?`)
uptimeRegex = regexp.MustCompile(`^(?:((\d+):(\d{2}):(\d{2}))|(\d+)|(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}))$`)
routeChangeRegex = regexp.MustCompile(`(Import|Export) (updates|withdraws):\s+(\d+|---)\s+(\d+|---)\s+(\d+|---)\s+(\d+|---)\s+(\d+|---)\s*`)
channelRegex = regexp.MustCompile(`Channel ipv(4|6)`)
}
// Parser parses bird output and returns protocol.Protocol structs
......
package main
package parser
import (
"testing"
"time"
"github.com/czerwonk/bird_exporter/parser"
"github.com/czerwonk/bird_exporter/protocol"
"github.com/czerwonk/testutils/assert"
)
......@@ -13,7 +12,7 @@ func TestEstablishedBgpOldTimeFormat(t *testing.T) {
data := "foo BGP master up 1514768400 Established\ntest\nbar\n Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\nxxx"
s := time.Date(2018, time.January, 1, 1, 0, 0, 0, time.UTC)
min := int(time.Since(s).Seconds())
p := parser.ParseProtocols([]byte(data), "4")
p := ParseProtocols([]byte(data), "4")
max := int(time.Since(s).Seconds())
x := p[0]
......@@ -30,7 +29,7 @@ func TestEstablishedBgpOldTimeFormat(t *testing.T) {
func TestEstablishedBgpCurrentTimeFormat(t *testing.T) {
data := "foo BGP master up 00:01:00 Established\ntest\nbar\n Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\nxxx"
p := parser.ParseProtocols([]byte(data), "4")
p := ParseProtocols([]byte(data), "4")
assert.IntEqual("protocols", 1, len(p), t)
x := p[0]
......@@ -49,7 +48,7 @@ func TestEstablishedBgpIsoLongTimeFormat(t *testing.T) {
data := "foo BGP master up 2018-01-01 01:00:00 Established\ntest\nbar\n Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\nxxx"
s := time.Date(2018, time.January, 1, 1, 0, 0, 0, time.UTC)
min := int(time.Since(s).Seconds())
p := parser.ParseProtocols([]byte(data), "4")
p := ParseProtocols([]byte(data), "4")
max := int(time.Since(s).Seconds())
assert.IntEqual("protocols", 1, len(p), t)
......@@ -68,7 +67,7 @@ func TestEstablishedBgpIsoLongTimeFormat(t *testing.T) {
func TestIpv6Bgp(t *testing.T) {
data := "foo BGP master up 00:01:00 Established\ntest\nbar\n Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\nxxx"
p := parser.ParseProtocols([]byte(data), "6")
p := ParseProtocols([]byte(data), "6")
assert.IntEqual("protocols", 1, len(p), t)
x := p[0]
......@@ -77,7 +76,7 @@ func TestIpv6Bgp(t *testing.T) {
func TestActiveBgp(t *testing.T) {
data := "bar BGP master start 2016-01-01 Active\ntest\nbar"
p := parser.ParseProtocols([]byte(data), "4")
p := ParseProtocols([]byte(data), "4")
assert.IntEqual("protocols", 1, len(p), t)
x := p[0]
......@@ -92,7 +91,7 @@ func TestActiveBgp(t *testing.T) {
func Test2BgpSessions(t *testing.T) {
data := "foo BGP master up 00:01:00 Established\ntest\n Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\nbar BGP master start 2016-01-01 Active\nxxx"
p := parser.ParseProtocols([]byte(data), "4")
p := ParseProtocols([]byte(data), "4")
assert.IntEqual("protocols", 2, len(p), t)
}
......@@ -104,7 +103,7 @@ func TestUpdateAndWithdrawCounts(t *testing.T) {
" Import withdraws: 6 7 8 9 10\n" +
" Export updates: 11 12 13 14 15\n" +
" Export withdraws: 16 17 18 19 ---"
p := parser.ParseProtocols([]byte(data), "4")
p := ParseProtocols([]byte(data), "4")
x := p[0]
assert.Int64Equal("import updates received", 1, x.ImportUpdates.Received, t)
......@@ -166,7 +165,7 @@ func TestWithBird2(t *testing.T) {
" Routes: 4 imported, 3 filtered, 2 exported, 1 preferred\n" +
"\n"
p := parser.ParseProtocols([]byte(data), "")
p := ParseProtocols([]byte(data), "")
assert.IntEqual("protocols", 4, len(p), t)
x := p[0]
......@@ -248,7 +247,7 @@ func TestWithBird2(t *testing.T) {
func TestOspfOldTimeFormat(t *testing.T) {
data := "ospf1 OSPF master up 1481973060 Running\ntest\nbar\n Routes: 12 imported, 34 exported, 100 preferred\nxxx"
p := parser.ParseProtocols([]byte(data), "4")
p := ParseProtocols([]byte(data), "4")
assert.IntEqual("protocols", 1, len(p), t)
x := p[0]
......@@ -263,7 +262,7 @@ func TestOspfOldTimeFormat(t *testing.T) {
func TestOspfCurrentTimeFormat(t *testing.T) {
data := "ospf1 OSPF master up 00:01:00 Running\ntest\nbar\n Routes: 12 imported, 34 exported, 100 preferred\nxxx"
p := parser.ParseProtocols([]byte(data), "4")
p := ParseProtocols([]byte(data), "4")
assert.IntEqual("protocols", 1, len(p), t)
x := p[0]
......@@ -279,7 +278,7 @@ func TestOspfCurrentTimeFormat(t *testing.T) {
func TestOspfProtocolDown(t *testing.T) {
data := "o_hrz OSPF t_hrz down 1494926415 \n Preference: 150\n Input filter: ACCEPT\n Output filter: REJECT\nxxx"
p := parser.ParseProtocols([]byte(data), "6")
p := ParseProtocols([]byte(data), "6")
assert.IntEqual("protocols", 1, len(p), t)
x := p[0]
......@@ -293,7 +292,7 @@ func TestOspfProtocolDown(t *testing.T) {
func TestOspfRunning(t *testing.T) {
data := "ospf1 OSPF master up 00:01:00 Running\ntest\nbar\n Routes: 12 imported, 34 exported, 100 preferred\nxxx"
p := parser.ParseProtocols([]byte(data), "4")
p := ParseProtocols([]byte(data), "4")
assert.IntEqual("protocols", 1, len(p), t)