Repository: metallb/metallb
Pull request #971 (2021-09-30 09:18:51)

FRR: Prometheus metrics exporter
* In order to keep feature-parity with the current
BGP implementation we need to collect and export
the same Prometheus metrics.

* With FRR, we can't use the same approach to
collect the metrics as we currently do by producing the metrics ourselves
(https://prometheus.io/docs/introduction/glossary/#direct-instrumentation).
Rather we need to "collect" them from FRR, which is best done
using a Prometheus exporter and writing a collector for the BGP daemon
(https://prometheus.io/docs/instrumenting/writing_exporters/).
Each time Prometheus scrapes the /metrics endpoint the metrics are
gathered by the collector from FRR using vtysh.
The collector needs to satisfy the Prometheus.Collector interface.

* Adding new collectors to the exporter per FRR daemon
is possible in the future.

* The exporter will be built and shipped with the
speaker, and would be deployed as a seperate sidecar
container that has access to the FRR sockets like the reloader.

fedepaol: > A general question - did you do an evaluation of any existing FRR promethus exporters before starting a new one for metallb?
> 
> Could you explain the tradeoffs and why writing our own is best for our needs?
> 
> For example, I see https://github.com/tynany/frr_exporter, and maybe there's more ...

We had a discussion about this with @oribon , with existing exportes we won't be able to mimic the same set of metrics that are currently available in metallb, which is the bare minimum we need to provide to the users in order to guarantee no regressions.

russellb: will need a rebase now
oribon: > will need a rebase now

Fixed, also changed the exporter to reflect the switch to bgp views and added a commit that integrates it in the manifests.
The last missing piece is enabling the metrics tests for FRR in the CI but they are flakey without https://github.com/metallb/metallb/pull/1012 (specifically we need the Gauge change) so we should probably wait for that (locally with some sleeps in the right place they pass).
oribon: Reverted the views change since we're probably dropping it
oribon: enabled metrics for FRR lane, fails on ipv6 are expected and should be fixed by https://github.com/metallb/metallb/pull/1040
fedepaol: LGTM

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index f126aad2..53343da1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -38,7 +38,6 @@ jobs:
       - run: inv dev-env -i  << parameters.ip-family >> -b  << parameters.bgp-type >>
       - run: |
           SKIP="none"
-          if [ "<< parameters.bgp-type >>" = "frr" ]; then SKIP="$SKIP\|metrics"; fi # TODO: enable metrics tests when implementing in FRR
           if [ "<< parameters.bgp-type >>" = "native" ]; then SKIP="$SKIP\|FRR"; fi
           if [ "<< parameters.ip-family >>" = "ipv4" ]; then SKIP="$SKIP\|IPV6"; fi
           if [ "<< parameters.ip-family >>" = "dual" ]; then SKIP="$SKIP\|IPV6"; fi # TODO: until dual stack is supported, we can't run the ipv6 tests against the cluster
diff --git a/frr-metrics/collector/bgp.go b/frr-metrics/collector/bgp.go
new file mode 100644
index 00000000..dd089def
--- /dev/null
+++ b/frr-metrics/collector/bgp.go
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package collector
+
+import (
+	"fmt"
+	"os/exec"
+
+	"github.com/go-kit/kit/log"
+	"github.com/go-kit/kit/log/level"
+	"github.com/prometheus/client_golang/prometheus"
+
+	bgpfrr "go.universe.tf/metallb/internal/bgp/frr"
+	bgpstats "go.universe.tf/metallb/internal/bgp/native"
+)
+
+var (
+	sessionUpDesc = prometheus.NewDesc(
+		prometheus.BuildFQName(bgpstats.Namespace, bgpstats.Subsystem, bgpstats.SessionUp.Name),
+		bgpstats.SessionUp.Help,
+		bgpstats.Labels,
+		nil,
+	)
+
+	updatesSentDesc = prometheus.NewDesc(
+		prometheus.BuildFQName(bgpstats.Namespace, bgpstats.Subsystem, bgpstats.UpdatesSent.Name),
+		bgpstats.UpdatesSent.Help,
+		bgpstats.Labels,
+		nil,
+	)
+
+	prefixesDesc = prometheus.NewDesc(
+		prometheus.BuildFQName(bgpstats.Namespace, bgpstats.Subsystem, bgpstats.Prefixes.Name),
+		bgpstats.Prefixes.Help,
+		bgpstats.Labels,
+		nil,
+	)
+)
+
+type bgp struct {
+	Log    log.Logger
+	frrCli func(args ...string) (string, error)
+}
+
+func NewBGP(l log.Logger) *bgp {
+	log := log.With(l, "collector", bgpstats.Subsystem)
+	return &bgp{Log: log, frrCli: runVtysh}
+}
+
+func (c *bgp) Describe(ch chan<- *prometheus.Desc) {
+	ch <- sessionUpDesc
+	ch <- updatesSentDesc
+	ch <- prefixesDesc
+}
+
+func (c *bgp) Collect(ch chan<- prometheus.Metric) {
+	neighbors, err := getBGPNeighbors(c.frrCli)
+	if err != nil {
+		level.Error(c.Log).Log("error", err, "msg", "failed to fetch BGP neighbors from FRR")
+		return
+	}
+
+	updateNeighborsMetrics(ch, neighbors)
+}
+
+func updateNeighborsMetrics(ch chan<- prometheus.Metric, neighbors []*bgpfrr.Neighbor) {
+	for _, n := range neighbors {
+		sessionUp := 1
+		if !n.Connected {
+			sessionUp = 0
+		}
+		peerLabel := fmt.Sprintf("%s:%d", n.Ip.String(), n.Port)
+
+		ch <- prometheus.MustNewConstMetric(sessionUpDesc, prometheus.GaugeValue, float64(sessionUp), peerLabel)
+		ch <- prometheus.MustNewConstMetric(updatesSentDesc, prometheus.CounterValue, float64(n.UpdatesSent), peerLabel)
+		ch <- prometheus.MustNewConstMetric(prefixesDesc, prometheus.GaugeValue, float64(n.PrefixSent), peerLabel)
+	}
+}
+
+func getBGPNeighbors(frrCli func(args ...string) (string, error)) ([]*bgpfrr.Neighbor, error) {
+	res, err := frrCli("show bgp neighbors json")
+	if err != nil {
+		return nil, err
+	}
+
+	return bgpfrr.ParseNeighbours(res)
+}
+
+func runVtysh(args ...string) (string, error) {
+	newArgs := append([]string{"-c"}, args...)
+	out, err := exec.Command("/usr/bin/vtysh", newArgs...).CombinedOutput()
+	return string(out), err
+}
diff --git a/frr-metrics/exporter.go b/frr-metrics/exporter.go
new file mode 100644
index 00000000..60a234fe
--- /dev/null
+++ b/frr-metrics/exporter.go
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package main
+
+import (
+	"flag"
+	"fmt"
+	stdlog "log"
+	"net/http"
+	"os"
+
+	"github.com/go-kit/kit/log"
+	"github.com/go-kit/log/level"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+	"github.com/prometheus/exporter-toolkit/web"
+
+	"go.universe.tf/metallb/frr-metrics/collector"
+	"go.universe.tf/metallb/internal/logging"
+	"go.universe.tf/metallb/internal/version"
+)
+
+var (
+	metricsPort   = flag.Uint("metrics-port", 7473, "Port to listen on for web interface.")
+	metricsPath   = flag.String("metrics-path", "/metrics", "Path under which to expose metrics.")
+	tlsConfigPath = flag.String("tls-config-path", "", "[EXPERIMENTAL] Path to config yaml file that can enable TLS or authentication.")
+)
+
+func metricsHandler(logger log.Logger) http.Handler {
+	BGPCollector := collector.NewBGP(logger)
+
+	registry := prometheus.NewRegistry()
+	registry.MustRegister(BGPCollector)
+
+	gatherers := prometheus.Gatherers{
+		prometheus.DefaultGatherer,
+		registry,
+	}
+
+	handlerOpts := promhttp.HandlerOpts{
+		ErrorLog:      stdlog.New(log.NewStdlibAdapter(level.Error(logger)), "", 0),
+		ErrorHandling: promhttp.ContinueOnError,
+		Registry:      registry,
+	}
+
+	return promhttp.HandlerFor(gatherers, handlerOpts)
+}
+
+func main() {
+	flag.Parse()
+
+	logger, err := logging.Init("error")
+	if err != nil {
+		fmt.Printf("failed to initialize logging: %s\n", err)
+		os.Exit(1)
+	}
+
+	_ = level.Info(logger).Log("version", version.Version(), "commit", version.CommitHash(), "branch", version.Branch(), "goversion", version.GoString(), "msg", "FRR metrics exporter starting "+version.String())
+
+	http.Handle(*metricsPath, metricsHandler(logger))
+	srv := &http.Server{Addr: fmt.Sprintf(":%d", *metricsPort)}
+	_ = level.Info(logger).Log("msg", "Starting exporter", "metricsPath", metricsPath, "port", metricsPort)
+
+	if err := web.ListenAndServe(srv, *tlsConfigPath, logger); err != nil {
+		_ = level.Error(logger).Log("error", err)
+		os.Exit(1)
+	}
+}
diff --git a/go.mod b/go.mod
index 4480e4fd..4eec3c4b 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/eapache/channels v1.1.0 // indirect
 	github.com/eapache/queue v1.1.0 // indirect
 	github.com/go-kit/kit v0.9.0
+	github.com/go-kit/log v0.2.0
 	github.com/golang/protobuf v1.5.2
 	github.com/google/go-cmp v0.5.5
 	github.com/hashicorp/memberlist v0.1.7
@@ -18,15 +19,16 @@ require (
 	github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721
 	github.com/onsi/ginkgo v1.11.0
 	github.com/onsi/gomega v1.7.0
-	github.com/ory/dockertest/v3 v3.8.0 // indirect
+	github.com/ory/dockertest/v3 v3.8.0
 	github.com/osrg/gobgp v2.0.0+incompatible
 	github.com/pkg/errors v0.9.1
-	github.com/prometheus/client_golang v1.7.1
+	github.com/prometheus/client_golang v1.11.0
 	github.com/prometheus/client_model v0.2.0
-	github.com/prometheus/common v0.10.0
+	github.com/prometheus/common v0.29.0
+	github.com/prometheus/exporter-toolkit v0.6.1
 	github.com/spf13/viper v1.8.1 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
-	golang.org/x/sys v0.0.0-20210510120138-977fb7262007
+	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
 	gopkg.in/yaml.v2 v2.4.0
 	k8s.io/api v0.20.2
 	k8s.io/apimachinery v0.20.2
diff --git a/go.sum b/go.sum
index 379e1c8a..9e58f575 100644
--- a/go.sum
+++ b/go.sum
@@ -76,6 +76,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@@ -163,6 +164,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
 github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -233,9 +235,14 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-kit/log v0.2.0 h1:7i2K3eKTos3Vc0enKCfnVcgHh2olr/MyfboYq7cAcFw=
+github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
+github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
 github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
 github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
@@ -421,7 +428,6 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
@@ -431,6 +437,8 @@ github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mo
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -441,6 +449,7 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
 github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@@ -450,20 +459,19 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
+github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2 h1:hRGSmZu7j271trc9sneMrpOW7GN5ngLm8YUZIPzf394=
 github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
@@ -542,6 +550,8 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
 github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
@@ -594,8 +604,9 @@ github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prY
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
 github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -604,16 +615,21 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/common v0.29.0 h1:3jqPBvKT4OHAbje2Ql7KeaaSicDBCxMYwEJU1zRJceE=
+github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
+github.com/prometheus/exporter-toolkit v0.6.1 h1:Aqk75wQD92N9CqmTlZwjKwq6272nOGrWIbc8Z7+xQO0=
+github.com/prometheus/exporter-toolkit v0.6.1/go.mod h1:ZUBIj498ePooX9t/2xtDjeQYwvRpiPP2lh5u4iblj2g=
 github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
 github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/quobyte/api v0.1.8/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H6VI=
 github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
@@ -636,7 +652,6 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
@@ -755,8 +770,9 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
+golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -849,8 +865,9 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -862,8 +879,9 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc=
 golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -931,6 +949,7 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200602100848-8d3cce7afc34/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -950,9 +969,13 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -960,8 +983,9 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1187,8 +1211,10 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
+gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
 gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/internal/bgp/native/stats.go b/internal/bgp/native/stats.go
index bea27c0c..10089cba 100644
--- a/internal/bgp/native/stats.go
+++ b/internal/bgp/native/stats.go
@@ -4,42 +4,60 @@ package native
 
 import "github.com/prometheus/client_golang/prometheus"
 
+type stat struct {
+	Name string
+	Help string
+}
+
+var (
+	Namespace = "metallb"
+	Subsystem = "bgp"
+	Labels    = []string{"peer"}
+
+	SessionUp = stat{
+		Name: "session_up",
+		Help: "BGP session state (1 is up, 0 is down)",
+	}
+
+	UpdatesSent = stat{
+		Name: "updates_total",
+		Help: "Number of BGP UPDATE messages sent",
+	}
+
+	Prefixes = stat{
+		Name: "announced_prefixes_total",
+		Help: "Number of prefixes currently being advertised on the BGP session",
+	}
+)
+
 var stats = metrics{
 	sessionUp: prometheus.NewGaugeVec(prometheus.GaugeOpts{
-		Namespace: "metallb",
-		Subsystem: "bgp",
-		Name:      "session_up",
-		Help:      "BGP session state (1 is up, 0 is down)",
-	}, []string{
-		"peer",
-	}),
+		Namespace: Namespace,
+		Subsystem: Subsystem,
+		Name:      SessionUp.Name,
+		Help:      SessionUp.Help,
+	}, Labels),
 
 	updatesSent: prometheus.NewCounterVec(prometheus.CounterOpts{
-		Namespace: "metallb",
-		Subsystem: "bgp",
-		Name:      "updates_total",
-		Help:      "Number of BGP UPDATE messages sent",
-	}, []string{
-		"peer",
-	}),
+		Namespace: Namespace,
+		Subsystem: Subsystem,
+		Name:      UpdatesSent.Name,
+		Help:      UpdatesSent.Help,
+	}, Labels),
 
 	prefixes: prometheus.NewGaugeVec(prometheus.GaugeOpts{
-		Namespace: "metallb",
-		Subsystem: "bgp",
-		Name:      "announced_prefixes_total",
-		Help:      "Number of prefixes currently being advertised on the BGP session",
-	}, []string{
-		"peer",
-	}),
+		Namespace: Namespace,
+		Subsystem: Subsystem,
+		Name:      Prefixes.Name,
+		Help:      Prefixes.Help,
+	}, Labels),
 
 	pendingPrefixes: prometheus.NewGaugeVec(prometheus.GaugeOpts{
-		Namespace: "metallb",
-		Subsystem: "bgp",
+		Namespace: Namespace,
+		Subsystem: Subsystem,
 		Name:      "pending_prefixes_total",
 		Help:      "Number of prefixes that should be advertised on the BGP session",
-	}, []string{
-		"peer",
-	}),
+	}, Labels),
 }
 
 type metrics struct {
diff --git a/manifests/metallb-frr.yaml b/manifests/metallb-frr.yaml
index 091bda27..965744a9 100644
--- a/manifests/metallb-frr.yaml
+++ b/manifests/metallb-frr.yaml
@@ -426,7 +426,6 @@ spec:
   template:
     metadata:
       annotations:
-        prometheus.io/port: '7472'
         prometheus.io/scrape: 'true'
       labels:
         app: metallb
@@ -442,6 +441,8 @@ spec:
           emptyDir: {}
         - name: reloader
           emptyDir: {}
+        - name: metrics
+          emptyDir: {}
       initContainers:
         # Copies the initial config files with the right permissions to the shared volume.
         - name: cp-frr-files
@@ -462,6 +463,13 @@ spec:
           volumeMounts:
             - name: reloader
               mountPath: /etc/frr_reloader
+        # Copies the metrics exporter
+        - name: cp-metrics
+          image: quay.io/metallb/speaker:main
+          command: ["/bin/sh", "-c", "cp -f /frr-metrics /etc/frr_metrics/"]
+          volumeMounts:
+            - name: metrics
+              mountPath: /etc/frr_metrics
       containers:
         - name: frr
           securityContext:
@@ -486,6 +494,21 @@ spec:
               mountPath: /etc/frr
             - name: reloader
               mountPath: /etc/frr_reloader
+        - name: frr-metrics
+          image: frrouting/frr:v7.5.1
+          command: ["/etc/frr_metrics/frr-metrics"]
+          args:
+            - --metrics-port=7473
+          ports:
+            - containerPort: 7473
+              name: monitoring
+          volumeMounts:
+            - name: frr-sockets
+              mountPath: /var/run/frr
+            - name: frr-conf
+              mountPath: /etc/frr
+            - name: metrics
+              mountPath: /etc/frr_metrics
         - args:
             - --port=7472
             - --config=config
diff --git a/speaker/Dockerfile b/speaker/Dockerfile
index d62d0fa5..9952ffc1 100644
--- a/speaker/Dockerfile
+++ b/speaker/Dockerfile
@@ -1,6 +1,7 @@
 FROM alpine:latest
 
 ADD frr-reloader.sh /frr-reloader.sh
+ADD frr-metrics /frr-metrics
 ADD speaker /speaker
 
 # When running as non root and building in an environment that `umask` masks out
diff --git a/tasks.py b/tasks.py
index b8d3bf6a..5ef8fb85 100644
--- a/tasks.py
+++ b/tasks.py
@@ -147,6 +147,16 @@ def build(ctx, binaries, architectures, registry="quay.io", repo="metallb", tag=
         }
         if "speaker" in binaries:
             shutil.copy("frr-reloader/frr-reloader.sh","build/{arch}/speaker/".format(arch=arch))
+            run("go build -v -o build/{arch}/speaker/frr-metrics -ldflags "
+                "'-X go.universe.tf/metallb/internal/version.gitCommit={commit} "
+                "-X go.universe.tf/metallb/internal/version.gitBranch={branch}' "
+                "frr-metrics/exporter.go".format(
+                    arch=arch,
+                    commit=commit,
+                    branch=branch),
+                    env=env,
+                    echo=True,
+                )
 
         for bin in binaries:
             run("go build -v -o build/{arch}/{bin}/{bin} -ldflags "


Test patch:
diff --git a/e2etest/pkg/frr/bgp.go b/e2etest/pkg/frr/bgp.go
index d5b11b14..b213dec2 100644
--- a/e2etest/pkg/frr/bgp.go
+++ b/e2etest/pkg/frr/bgp.go
@@ -7,6 +7,8 @@ import (
 
 	"github.com/pkg/errors"
 	"go.universe.tf/metallb/e2etest/pkg/executor"
+
+	bgpfrr "go.universe.tf/metallb/internal/bgp/frr"
 )
 
 // TODO: Leaving this package "test unaware" on purpose, since we may find it
@@ -15,13 +17,13 @@ import (
 
 // NeighborForContainer returns informations for the given neighbor in the given
 // executor.
-func NeighborInfo(neighborName, exec executor.Executor) (*Neighbor, error) {
+func NeighborInfo(neighborName, exec executor.Executor) (*bgpfrr.Neighbor, error) {
 	res, err := exec.Exec("vtysh", "-c", fmt.Sprintf("show bgp neighbor %s json", neighborName))
 
 	if err != nil {
 		return nil, errors.Wrapf(err, "Failed to query neighbour %s", neighborName)
 	}
-	neighbor, err := parseNeighbour(res)
+	neighbor, err := bgpfrr.ParseNeighbour(res)
 	if err != nil {
 		return nil, errors.Wrapf(err, "Failed to parse neighbour %s", neighborName)
 	}
@@ -30,12 +32,12 @@ func NeighborInfo(neighborName, exec executor.Executor) (*Neighbor, error) {
 
 // NeighborsForContainer returns informations for the all the neighbors in the given
 // executor.
-func NeighborsInfo(exec executor.Executor) ([]*Neighbor, error) {
+func NeighborsInfo(exec executor.Executor) ([]*bgpfrr.Neighbor, error) {
 	res, err := exec.Exec("vtysh", "-c", "show bgp neighbor json")
 	if err != nil {
 		return nil, errors.Wrapf(err, "Failed to query neighbours")
 	}
-	neighbors, err := parseNeighbours(res)
+	neighbors, err := bgpfrr.ParseNeighbours(res)
 	if err != nil {
 		return nil, errors.Wrapf(err, "Failed to parse neighbours %s", res)
 	}
@@ -44,12 +46,12 @@ func NeighborsInfo(exec executor.Executor) ([]*Neighbor, error) {
 
 // Routes returns informations about routes in the given executor
 // first for ipv4 routes and then for ipv6 routes.
-func Routes(exec executor.Executor) (map[string]Route, map[string]Route, error) {
+func Routes(exec executor.Executor) (map[string]bgpfrr.Route, map[string]bgpfrr.Route, error) {
 	res, err := exec.Exec("vtysh", "-c", "show bgp ipv4 json")
 	if err != nil {
 		return nil, nil, errors.Wrapf(err, "Failed to query routes")
 	}
-	v4Routes, err := parseRoutes(res)
+	v4Routes, err := bgpfrr.ParseRoutes(res)
 	if err != nil {
 		return nil, nil, errors.Wrapf(err, "Failed to parse routes %s", res)
 	}
@@ -57,9 +59,19 @@ func Routes(exec executor.Executor) (map[string]Route, map[string]Route, error)
 	if err != nil {
 		return nil, nil, errors.Wrapf(err, "Failed to query routes")
 	}
-	v6Routes, err := parseRoutes(res)
+	v6Routes, err := bgpfrr.ParseRoutes(res)
 	if err != nil {
 		return nil, nil, errors.Wrapf(err, "Failed to parse routes %s", res)
 	}
 	return v4Routes, v6Routes, nil
 }
+
+// NeighborConnected tells if the neighbor in the given
+// json format is connected.
+func NeighborConnected(neighborJson string) (bool, error) {
+	n, err := bgpfrr.ParseNeighbour(neighborJson)
+	if err != nil {
+		return false, err
+	}
+	return n.Connected, nil
+}
diff --git a/e2etest/pkg/frr/validation.go b/e2etest/pkg/frr/validation.go
index 3adf546c..74d63a15 100644
--- a/e2etest/pkg/frr/validation.go
+++ b/e2etest/pkg/frr/validation.go
@@ -6,13 +6,15 @@ import (
 	"fmt"
 
 	v1 "k8s.io/api/core/v1"
+
+	bgpfrr "go.universe.tf/metallb/internal/bgp/frr"
 )
 
 // NeighborsMatchNodes tells if ALL the given nodes are peered with the
 // frr instance. We only care about established connections, as the
 // frr instance may be configured with more nodes than are currently
 // paired.
-func NeighborsMatchNodes(nodes []v1.Node, neighbors []*Neighbor) error {
+func NeighborsMatchNodes(nodes []v1.Node, neighbors []*bgpfrr.Neighbor) error {
 	nodesIPs := map[string]struct{}{}
 
 	for _, n := range nodes {
@@ -23,13 +25,13 @@ func NeighborsMatchNodes(nodes []v1.Node, neighbors []*Neighbor) error {
 		}
 	}
 	for _, n := range neighbors {
-		if _, ok := nodesIPs[n.ip.String()]; !ok { // skipping neighbors that are not nodes
+		if _, ok := nodesIPs[n.Ip.String()]; !ok { // skipping neighbors that are not nodes
 			continue
 		}
-		if !n.connected {
-			return fmt.Errorf("node %s BGP session not established", n.ip.String())
+		if !n.Connected {
+			return fmt.Errorf("node %s BGP session not established", n.Ip.String())
 		}
-		delete(nodesIPs, n.ip.String())
+		delete(nodesIPs, n.Ip.String())
 	}
 	if len(nodesIPs) != 0 { // some leftover, meaning more nodes than neighbors
 		return fmt.Errorf("IP %v found in nodes but not in neighbors", nodesIPs)
@@ -39,7 +41,7 @@ func NeighborsMatchNodes(nodes []v1.Node, neighbors []*Neighbor) error {
 
 // RoutesMatchNodes tells if ALL the given nodes are exposed as
 // destinations for the given address.
-func RoutesMatchNodes(nodes []v1.Node, route Route) error {
+func RoutesMatchNodes(nodes []v1.Node, route bgpfrr.Route) error {
 	nodesIPs := map[string]struct{}{}
 
 	for _, n := range nodes {
diff --git a/frr-metrics/collector/bgp_test.go b/frr-metrics/collector/bgp_test.go
new file mode 100644
index 00000000..ad5701c9
--- /dev/null
+++ b/frr-metrics/collector/bgp_test.go
@@ -0,0 +1,351 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package collector
+
+import (
+	"bytes"
+	"testing"
+	"text/template"
+
+	"github.com/go-kit/kit/log"
+	"github.com/prometheus/client_golang/prometheus/testutil"
+)
+
+var (
+	metricsTmpl = `
+	# HELP metallb_bgp_announced_prefixes_total Number of prefixes currently being advertised on the BGP session
+	# TYPE metallb_bgp_announced_prefixes_total gauge
+	metallb_bgp_announced_prefixes_total{peer="{{ .NeighborIP }}"} {{ .AnnouncedPrefixes }}
+	# HELP metallb_bgp_session_up BGP session state (1 is up, 0 is down)
+	# TYPE metallb_bgp_session_up gauge
+	metallb_bgp_session_up{peer="{{ .NeighborIP }}"} {{ .SessionUp }}
+	# HELP metallb_bgp_updates_total Number of BGP UPDATE messages sent
+	# TYPE metallb_bgp_updates_total counter
+	metallb_bgp_updates_total{peer="{{ .NeighborIP }}"} {{ .UpdatesTotal }}
+	`
+
+	tests = []struct {
+		desc              string
+		vtyshOutput       string
+		neighborIP        string
+		announcedPrefixes int
+		sessionUp         int
+		updatesTotal      int
+	}{
+		{
+			desc:              "Output contains only IPv4 advertisements",
+			vtyshOutput:       neighborsIPv4Only,
+			neighborIP:        "172.18.0.4:179",
+			announcedPrefixes: 3,
+			sessionUp:         1,
+			updatesTotal:      2,
+		},
+		{
+			desc:              "Output contains mixed IPv4 and IPv6 advertisements",
+			vtyshOutput:       neighborsDual,
+			neighborIP:        "172.18.0.4:180",
+			announcedPrefixes: 6,
+			sessionUp:         1,
+			updatesTotal:      5,
+		},
+	}
+	neighborsIPv4Only = `
+	{
+		"172.18.0.4":{
+		  "remoteAs":64512,
+		  "localAs":64513,
+		  "nbrExternalLink":true,
+		  "hostname":"bgpd",
+		  "bgpVersion":4,
+		  "remoteRouterId":"172.18.0.4",
+		  "localRouterId":"172.18.0.3",
+		  "bgpState":"Established",
+		  "bgpTimerUpMsec":1082000,
+		  "bgpTimerUpString":"00:18:02",
+		  "bgpTimerUpEstablishedEpoch":1632032518,
+		  "bgpTimerLastRead":2000,
+		  "bgpTimerLastWrite":2000,
+		  "bgpInUpdateElapsedTimeMsecs":1081000,
+		  "bgpTimerHoldTimeMsecs":180000,
+		  "bgpTimerKeepAliveIntervalMsecs":60000,
+		  "neighborCapabilities":{
+			"4byteAs":"advertisedAndReceived",
+			"addPath":{
+			  "ipv4Unicast":{
+				"rxAdvertisedAndReceived":true
+			  }
+			},
+			"routeRefresh":"advertisedAndReceivedOldNew",
+			"multiprotocolExtensions":{
+			  "ipv4Unicast":{
+				"advertisedAndReceived":true
+			  }
+			},
+			"hostName":{
+			  "advHostName":"kind-control-plane",
+			  "advDomainName":"n\/a",
+			  "rcvHostName":"bgpd",
+			  "rcvDomainName":"n\/a"
+			},
+			"gracefulRestart":"advertisedAndReceived",
+			"gracefulRestartRemoteTimerMsecs":120000,
+			"addressFamiliesByPeer":"none"
+		  },
+		  "gracefulRestartInfo":{
+			"endOfRibSend":{
+			  "ipv4Unicast":true
+			},
+			"endOfRibRecv":{
+			  "ipv4Unicast":true
+			},
+			"localGrMode":"Helper*",
+			"remoteGrMode":"Helper",
+			"rBit":true,
+			"timers":{
+			  "configuredRestartTimer":120,
+			  "receivedRestartTimer":120
+			},
+			"ipv4Unicast":{
+			  "fBit":false,
+			  "endOfRibStatus":{
+				"endOfRibSend":true,
+				"endOfRibSentAfterUpdate":true,
+				"endOfRibRecv":true
+			  },
+			  "timers":{
+				"stalePathTimer":360
+			  }
+			}
+		  },
+		  "messageStats":{
+			"depthInq":0,
+			"depthOutq":0,
+			"opensSent":1,
+			"opensRecv":1,
+			"notificationsSent":0,
+			"notificationsRecv":0,
+			"updatesSent":2,
+			"updatesRecv":2,
+			"keepalivesSent":19,
+			"keepalivesRecv":19,
+			"routeRefreshSent":0,
+			"routeRefreshRecv":0,
+			"capabilitySent":0,
+			"capabilityRecv":0,
+			"totalSent":22,
+			"totalRecv":22
+		  },
+		  "minBtwnAdvertisementRunsTimerMsecs":0,
+		  "addressFamilyInfo":{
+			"ipv4Unicast":{
+			  "updateGroupId":1,
+			  "subGroupId":1,
+			  "packetQueueLength":0,
+			  "commAttriSentToNbr":"extendedAndStandard",
+			  "acceptedPrefixCounter":0,
+			  "sentPrefixCounter":3
+			}
+		  },
+		  "connectionsEstablished":1,
+		  "connectionsDropped":0,
+		  "lastResetTimerMsecs":1083000,
+		  "lastResetDueTo":"Waiting for peer OPEN",
+		  "lastResetCode":32,
+		  "hostLocal":"172.18.0.3",
+		  "portLocal":42692,
+		  "hostForeign":"172.18.0.4",
+		  "portForeign":179,
+		  "nexthop":"172.18.0.3",
+		  "nexthopGlobal":"fc00:f853:ccd:e793::3",
+		  "nexthopLocal":"fe80::42:acff:fe12:3",
+		  "bgpConnection":"sharedNetwork",
+		  "connectRetryTimer":120,
+		  "estimatedRttInMsecs":2,
+		  "readThread":"on",
+		  "writeThread":"on"
+		}
+	  }	  
+	`
+	neighborsDual = `
+	{
+		"172.18.0.4":{
+		  "remoteAs":64512,
+		  "localAs":64513,
+		  "nbrExternalLink":true,
+		  "hostname":"bgpd",
+		  "bgpVersion":4,
+		  "remoteRouterId":"172.18.0.4",
+		  "localRouterId":"172.18.0.3",
+		  "bgpState":"Established",
+		  "bgpTimerUpMsec":1082000,
+		  "bgpTimerUpString":"00:18:02",
+		  "bgpTimerUpEstablishedEpoch":1632032518,
+		  "bgpTimerLastRead":2000,
+		  "bgpTimerLastWrite":2000,
+		  "bgpInUpdateElapsedTimeMsecs":1081000,
+		  "bgpTimerHoldTimeMsecs":180000,
+		  "bgpTimerKeepAliveIntervalMsecs":60000,
+		  "neighborCapabilities":{
+			"4byteAs":"advertisedAndReceived",
+			"addPath":{
+			  "ipv4Unicast":{
+				"rxAdvertisedAndReceived":true
+			  },
+			  "ipv6Unicast":{
+				"rxAdvertisedAndReceived":true
+			  }
+			},
+			"routeRefresh":"advertisedAndReceivedOldNew",
+			"multiprotocolExtensions":{
+			  "ipv4Unicast":{
+				"advertisedAndReceived":true
+			  },
+			  "ipv6Unicast":{
+				"advertisedAndReceived":true
+			  }
+			},
+			"hostName":{
+			  "advHostName":"kind-control-plane",
+			  "advDomainName":"n\/a",
+			  "rcvHostName":"bgpd",
+			  "rcvDomainName":"n\/a"
+			},
+			"gracefulRestart":"advertisedAndReceived",
+			"gracefulRestartRemoteTimerMsecs":120000,
+			"addressFamiliesByPeer":"none"
+		  },
+		  "gracefulRestartInfo":{
+			"endOfRibSend":{
+			  "ipv4Unicast":true,
+			  "ipv6Unicast":true
+			},
+			"endOfRibRecv":{
+			  "ipv4Unicast":true,
+			  "ipv6Unicast":true
+			},
+			"localGrMode":"Helper*",
+			"remoteGrMode":"Helper",
+			"rBit":true,
+			"timers":{
+			  "configuredRestartTimer":120,
+			  "receivedRestartTimer":120
+			},
+			"ipv4Unicast":{
+			  "fBit":false,
+			  "endOfRibStatus":{
+				"endOfRibSend":true,
+				"endOfRibSentAfterUpdate":true,
+				"endOfRibRecv":true
+			  },
+			  "timers":{
+				"stalePathTimer":360
+			  }
+			},
+			"ipv6Unicast":{
+			  "fBit":false,
+			  "endOfRibStatus":{
+				"endOfRibSend":true,
+				"endOfRibSentAfterUpdate":true,
+				"endOfRibRecv":true
+			  },
+			  "timers":{
+				"stalePathTimer":360
+			  }
+			}
+		  },
+		  "messageStats":{
+			"depthInq":0,
+			"depthOutq":0,
+			"opensSent":1,
+			"opensRecv":1,
+			"notificationsSent":0,
+			"notificationsRecv":0,
+			"updatesSent":5,
+			"updatesRecv":5,
+			"keepalivesSent":19,
+			"keepalivesRecv":19,
+			"routeRefreshSent":0,
+			"routeRefreshRecv":0,
+			"capabilitySent":0,
+			"capabilityRecv":0,
+			"totalSent":25,
+			"totalRecv":25
+		  },
+		  "minBtwnAdvertisementRunsTimerMsecs":0,
+		  "addressFamilyInfo":{
+			"ipv4Unicast":{
+			  "updateGroupId":1,
+			  "subGroupId":1,
+			  "packetQueueLength":0,
+			  "commAttriSentToNbr":"extendedAndStandard",
+			  "acceptedPrefixCounter":0,
+			  "sentPrefixCounter":3
+			},
+			"ipv6Unicast":{
+			  "peerGroupMember":"uplink",
+			  "updateGroupId":2,
+			  "subGroupId":2,
+			  "packetQueueLength":0,
+			  "commAttriSentToNbr":"extendedAndStandard",
+			  "outboundPathPolicyConfig":true,
+			  "outgoingUpdatePrefixFilterList":"only-host-prefixes",
+			  "acceptedPrefixCounter":13,
+			  "sentPrefixCounter":3
+			}
+		  },
+		  "connectionsEstablished":1,
+		  "connectionsDropped":0,
+		  "lastResetTimerMsecs":1083000,
+		  "lastResetDueTo":"Waiting for peer OPEN",
+		  "lastResetCode":32,
+		  "hostLocal":"172.18.0.3",
+		  "portLocal":42692,
+		  "hostForeign":"172.18.0.4",
+		  "portForeign":180,
+		  "nexthop":"172.18.0.3",
+		  "nexthopGlobal":"fc00:f853:ccd:e793::3",
+		  "nexthopLocal":"fe80::42:acff:fe12:3",
+		  "bgpConnection":"sharedNetwork",
+		  "connectRetryTimer":120,
+		  "estimatedRttInMsecs":2,
+		  "readThread":"on",
+		  "writeThread":"on"
+		}
+	  }	  
+	`
+)
+
+func TestCollect(t *testing.T) {
+	for _, tc := range tests {
+		t.Run(tc.desc, func(t *testing.T) {
+			tmpl, err := template.New(tc.desc).Parse(metricsTmpl)
+			if err != nil {
+				t.Errorf("expected no error but got %s", err)
+			}
+
+			var w bytes.Buffer
+			err = tmpl.Execute(&w, map[string]interface{}{
+				"NeighborIP":        tc.neighborIP,
+				"AnnouncedPrefixes": tc.announcedPrefixes,
+				"SessionUp":         tc.sessionUp,
+				"UpdatesTotal":      tc.updatesTotal,
+			})
+
+			if err != nil {
+				t.Errorf("expected no error but got %s", err)
+			}
+
+			l := log.NewNopLogger()
+			collector := NewBGP(l)
+			collector.frrCli = func(args ...string) (string, error) {
+				return tc.vtyshOutput, nil
+			}
+			buf := bytes.NewReader(w.Bytes())
+			err = testutil.CollectAndCompare(collector, buf)
+			if err != nil {
+				t.Errorf("expected no error but got %s", err)
+			}
+		})
+
+	}
+}
diff --git a/e2etest/pkg/frr/parse.go b/internal/bgp/frr/parse.go
similarity index 66%
rename from e2etest/pkg/frr/parse.go
rename to internal/bgp/frr/parse.go
index 1a596ee5..d1c1505b 100644
--- a/e2etest/pkg/frr/parse.go
+++ b/internal/bgp/frr/parse.go
@@ -12,10 +12,13 @@ import (
 )
 
 type Neighbor struct {
-	ip        net.IP
-	connected bool
-	localAS   string
-	remoteAS  string
+	Ip          net.IP
+	Connected   bool
+	LocalAS     string
+	RemoteAS    string
+	UpdatesSent int
+	PrefixSent  int
+	Port        int
 }
 
 type Route struct {
@@ -26,10 +29,17 @@ type Route struct {
 const bgpConnected = "Established"
 
 type FRRNeighbor struct {
-	RemoteAs   int    `json:"remoteAs"`
-	LocalAs    int    `json:"localAs"`
-	BgpVersion int    `json:"bgpVersion"`
-	BgpState   string `json:"bgpState"`
+	RemoteAs     int    `json:"remoteAs"`
+	LocalAs      int    `json:"localAs"`
+	BgpVersion   int    `json:"bgpVersion"`
+	BgpState     string `json:"bgpState"`
+	PortForeign  int    `json:"portForeign"`
+	MessageStats struct {
+		UpdatesSent int `json:"updatesSent"`
+	} `json:"messageStats"`
+	AddressFamilyInfo map[string]struct {
+		SentPrefixCounter int `json:"sentPrefixCounter"`
+	} `json:"addressFamilyInfo"`
 }
 
 type IPInfo struct {
@@ -47,7 +57,7 @@ type FRRRoute struct {
 
 // parseNeighbour takes the result of a show bgp neighbor x.y.w.z
 // and parses the informations related to the neighbour.
-func parseNeighbour(vtyshRes string) (*Neighbor, error) {
+func ParseNeighbour(vtyshRes string) (*Neighbor, error) {
 	res := map[string]FRRNeighbor{}
 	err := json.Unmarshal([]byte(vtyshRes), &res)
 	if err != nil {
@@ -68,11 +78,18 @@ func parseNeighbour(vtyshRes string) (*Neighbor, error) {
 		if n.BgpState != bgpConnected {
 			connected = false
 		}
+		prefixSent := 0
+		for _, s := range n.AddressFamilyInfo {
+			prefixSent += s.SentPrefixCounter
+		}
 		return &Neighbor{
-			ip:        ip,
-			connected: connected,
-			localAS:   strconv.Itoa(n.LocalAs),
-			remoteAS:  strconv.Itoa(n.RemoteAs),
+			Ip:          ip,
+			Connected:   connected,
+			LocalAS:     strconv.Itoa(n.LocalAs),
+			RemoteAS:    strconv.Itoa(n.RemoteAs),
+			UpdatesSent: n.MessageStats.UpdatesSent,
+			PrefixSent:  prefixSent,
+			Port:        n.PortForeign,
 		}, nil
 	}
 	return nil, errors.New("no peers were returned")
@@ -80,7 +97,7 @@ func parseNeighbour(vtyshRes string) (*Neighbor, error) {
 
 // parseNeighbour takes the result of a show bgp neighbor
 // and parses the informations related to all the neighbours.
-func parseNeighbours(vtyshRes string) ([]*Neighbor, error) {
+func ParseNeighbours(vtyshRes string) ([]*Neighbor, error) {
 	toParse := map[string]FRRNeighbor{}
 	err := json.Unmarshal([]byte(vtyshRes), &toParse)
 	if err != nil {
@@ -97,11 +114,18 @@ func parseNeighbours(vtyshRes string) ([]*Neighbor, error) {
 		if n.BgpState != bgpConnected {
 			connected = false
 		}
+		prefixSent := 0
+		for _, s := range n.AddressFamilyInfo {
+			prefixSent += s.SentPrefixCounter
+		}
 		res = append(res, &Neighbor{
-			ip:        ip,
-			connected: connected,
-			localAS:   strconv.Itoa(n.LocalAs),
-			remoteAS:  strconv.Itoa(n.RemoteAs),
+			Ip:          ip,
+			Connected:   connected,
+			LocalAS:     strconv.Itoa(n.LocalAs),
+			RemoteAS:    strconv.Itoa(n.RemoteAs),
+			UpdatesSent: n.MessageStats.UpdatesSent,
+			PrefixSent:  prefixSent,
+			Port:        n.PortForeign,
 		})
 	}
 	return res, nil
@@ -109,7 +133,7 @@ func parseNeighbours(vtyshRes string) ([]*Neighbor, error) {
 
 // parseRoute takes the result of a show bgp neighbor
 // and parses the informations related to all the neighbours.
-func parseRoutes(vtyshRes string) (map[string]Route, error) {
+func ParseRoutes(vtyshRes string) (map[string]Route, error) {
 	toParse := IPInfo{}
 	err := json.Unmarshal([]byte(vtyshRes), &toParse)
 	if err != nil {
@@ -143,13 +167,3 @@ func parseRoutes(vtyshRes string) (map[string]Route, error) {
 	}
 	return res, nil
 }
-
-// NeighborConnected tells if the neighbor in the given
-// json format is connected.
-func NeighborConnected(neighborJson string) (bool, error) {
-	n, err := parseNeighbour(neighborJson)
-	if err != nil {
-		return false, err
-	}
-	return n.connected, nil
-}
diff --git a/e2etest/pkg/frr/parse_test.go b/internal/bgp/frr/parse_test.go
similarity index 86%
rename from e2etest/pkg/frr/parse_test.go
rename to internal/bgp/frr/parse_test.go
index 4eacddda..bd2cb85f 100644
--- a/e2etest/pkg/frr/parse_test.go
+++ b/internal/bgp/frr/parse_test.go
@@ -45,7 +45,7 @@ func TestNeighbour(t *testing.T) {
         "opensRecv":0,
         "notificationsSent":0,
         "notificationsRecv":0,
-        "updatesSent":0,
+        "updatesSent":%d,
         "updatesRecv":0,
         "keepalivesSent":0,
         "keepalivesRecv":0,
@@ -61,7 +61,14 @@ func TestNeighbour(t *testing.T) {
         "ipv4Unicast":{
           "routerAlwaysNextHop":true,
           "commAttriSentToNbr":"extendedAndStandard",
-          "acceptedPrefixCounter":0
+          "acceptedPrefixCounter":0,
+          "sentPrefixCounter":%d
+        },
+        "ipv6Unicast":{
+          "routerAlwaysNextHop":true,
+          "commAttriSentToNbr":"extendedAndStandard",
+          "acceptedPrefixCounter":0,
+          "sentPrefixCounter":%d
         }
       },
       "connectionsEstablished":0,
@@ -69,6 +76,7 @@ func TestNeighbour(t *testing.T) {
       "lastResetTimerMsecs":253000,
       "lastResetDueTo":"Waiting for peer OPEN",
       "lastResetCode":32,
+      "portForeign":%d,
       "connectRetryTimer":120,
       "nextConnectTimerDueInMsecs":107000,
       "readThread":"off",
@@ -77,12 +85,16 @@ func TestNeighbour(t *testing.T) {
   }`
 
 	tests := []struct {
-		name          string
-		neighborIP    string
-		remoteAS      string
-		localAS       string
-		status        string
-		expectedError string
+		name           string
+		neighborIP     string
+		remoteAS       string
+		localAS        string
+		status         string
+		updatesSent    int
+		ipv4PrefixSent int
+		ipv6PrefixSent int
+		port           int
+		expectedError  string
 	}{
 		{
 			"ipv4, connected",
@@ -90,6 +102,10 @@ func TestNeighbour(t *testing.T) {
 			"64512",
 			"64512",
 			"Established",
+			1,
+			1,
+			0,
+			179,
 			"",
 		},
 		{
@@ -98,6 +114,10 @@ func TestNeighbour(t *testing.T) {
 			"64512",
 			"64512",
 			"Active",
+			0,
+			0,
+			0,
+			180,
 			"",
 		},
 		{
@@ -106,30 +126,43 @@ func TestNeighbour(t *testing.T) {
 			"64512",
 			"64512",
 			"Established",
+			2,
+			1,
+			1,
+			181,
 			"",
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			n, err := parseNeighbour(fmt.Sprintf(sample, tt.neighborIP, tt.remoteAS, tt.localAS, tt.status))
+			n, err := ParseNeighbour(fmt.Sprintf(sample, tt.neighborIP, tt.remoteAS, tt.localAS, tt.status, tt.updatesSent, tt.ipv4PrefixSent, tt.ipv6PrefixSent, tt.port))
 			if err != nil {
 				t.Fatal("Failed to parse ", err)
 			}
-			if !n.ip.Equal(net.ParseIP(tt.neighborIP)) {
-				t.Fatal("Expected neighbour ip", tt.neighborIP, "got", n.ip.String())
+			if !n.Ip.Equal(net.ParseIP(tt.neighborIP)) {
+				t.Fatal("Expected neighbour ip", tt.neighborIP, "got", n.Ip.String())
+			}
+			if n.RemoteAS != tt.remoteAS {
+				t.Fatal("Expected remote as", tt.remoteAS, "got", n.RemoteAS)
+			}
+			if n.LocalAS != tt.localAS {
+				t.Fatal("Expected local as", tt.localAS, "got", n.LocalAS)
+			}
+			if tt.status == "Established" && n.Connected != true {
+				t.Fatal("Expected connected", true, "got", n.Connected)
 			}
-			if n.remoteAS != tt.remoteAS {
-				t.Fatal("Expected remote as", tt.remoteAS, "got", n.remoteAS)
+			if tt.status != "Established" && n.Connected == true {
+				t.Fatal("Expected connected", false, "got", n.Connected)
 			}
-			if n.localAS != tt.localAS {
-				t.Fatal("Expected local as", tt.localAS, "got", n.localAS)
+			if tt.updatesSent != n.UpdatesSent {
+				t.Fatal("Expected updates sent", tt.updatesSent, "got", n.UpdatesSent)
 			}
-			if tt.status == "Established" && n.connected != true {
-				t.Fatal("Expected connected", true, "got", n.connected)
+			if tt.ipv4PrefixSent+tt.ipv6PrefixSent != n.PrefixSent {
+				t.Fatal("Expected prefix sent", tt.ipv4PrefixSent+tt.ipv6PrefixSent, "got", n.PrefixSent)
 			}
-			if tt.status != "Established" && n.connected == true {
-				t.Fatal("Expected connected", false, "got", n.connected)
+			if tt.port != n.Port {
+				t.Fatal("Expected port", tt.port, "got", n.Port)
 			}
 		})
 	}
@@ -444,7 +477,7 @@ const threeNeighbours = `
 }`
 
 func TestNeighbours(t *testing.T) {
-	nn, err := parseNeighbours(threeNeighbours)
+	nn, err := ParseNeighbours(threeNeighbours)
 	if err != nil {
 		t.Fatalf("Failed to parse %s", err)
 	}
@@ -452,16 +485,16 @@ func TestNeighbours(t *testing.T) {
 		t.Fatalf("Expected 4 neighbours, got %d", len(nn))
 	}
 	sort.Slice(nn, func(i, j int) bool {
-		return (bytes.Compare(nn[i].ip, nn[j].ip) < 0)
+		return (bytes.Compare(nn[i].Ip, nn[j].Ip) < 0)
 	})
 
-	if !nn[0].ip.Equal(net.ParseIP("172.18.0.2")) {
+	if !nn[0].Ip.Equal(net.ParseIP("172.18.0.2")) {
 		t.Fatal("neighbour ip not matching")
 	}
-	if !nn[1].ip.Equal(net.ParseIP("172.18.0.3")) {
+	if !nn[1].Ip.Equal(net.ParseIP("172.18.0.3")) {
 		t.Fatal("neighbour ip not matching")
 	}
-	if !nn[2].ip.Equal(net.ParseIP("172.18.0.4")) {
+	if !nn[2].Ip.Equal(net.ParseIP("172.18.0.4")) {
 		t.Fatal("neighbour ip not matching")
 	}
 }
@@ -537,7 +570,7 @@ const routes = `{
  ] }  }`
 
 func TestRoutes(t *testing.T) {
-	rr, err := parseRoutes(routes)
+	rr, err := ParseRoutes(routes)
 	if err != nil {
 		t.Fatalf("Failed to parse %s", err)
 	}