diff --git a/examples/bgp/main.go b/examples/bgp/main.go
index e2e7d9d883c7ea1b34d88ddc80bb2a31020b8776..54047b24a09818631e50c7d5f51657bb8e9e5670 100644
--- a/examples/bgp/main.go
+++ b/examples/bgp/main.go
@@ -2,7 +2,13 @@ package main
 
 import (
 	"log"
+	"net/http"
 
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+
+	prom_bgp "github.com/bio-routing/bio-rd/metrics/bgp/adapter/prom"
+	prom_vrf "github.com/bio-routing/bio-rd/metrics/vrf/adapter/prom"
 	bnet "github.com/bio-routing/bio-rd/net"
 	"github.com/bio-routing/bio-rd/protocols/bgp/server"
 	"github.com/bio-routing/bio-rd/routingtable/vrf"
@@ -23,7 +29,19 @@ func main() {
 		log.Fatal(err)
 	}
 
+	go startMetricsEndpoint(b)
+
 	startServer(b, v)
 
 	select {}
 }
+
+func startMetricsEndpoint(server server.BGPServer) {
+	prometheus.MustRegister(prom_bgp.NewCollector(server))
+	prometheus.MustRegister(prom_vrf.NewCollector())
+
+	http.Handle("/metrics", promhttp.Handler())
+
+	logrus.Info("Metrics are available :8080/metrics")
+	logrus.Error(http.ListenAndServe(":8080", nil))
+}
diff --git a/metrics/bgp/adapter/prom/bgp_prom_adapter.go b/metrics/bgp/adapter/prom/bgp_prom_adapter.go
new file mode 100644
index 0000000000000000000000000000000000000000..2ba446e7557005bf1bb4d9f0c6f13666d9be8205
--- /dev/null
+++ b/metrics/bgp/adapter/prom/bgp_prom_adapter.go
@@ -0,0 +1,111 @@
+package prom
+
+import (
+	"strconv"
+	"time"
+
+	"github.com/bio-routing/bio-rd/protocols/bgp/metrics"
+	"github.com/bio-routing/bio-rd/protocols/bgp/server"
+	"github.com/pkg/errors"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/common/log"
+)
+
+const (
+	prefix = "bio_bgp_"
+)
+
+var (
+	upDesc              *prometheus.Desc
+	stateDesc           *prometheus.Desc
+	uptimeDesc          *prometheus.Desc
+	updatesReceivedDesc *prometheus.Desc
+	updatesSentDesc     *prometheus.Desc
+	routesReceivedDesc  *prometheus.Desc
+	routesSentDesc      *prometheus.Desc
+	routesRejectedDesc  *prometheus.Desc
+	routesAcceptedDesc  *prometheus.Desc
+)
+
+func init() {
+	labels := []string{"peer_ip", "local_asn", "peer_asn", "vrf"}
+	upDesc = prometheus.NewDesc(prefix+"up", "Returns if the session is up", labels, nil)
+	stateDesc = prometheus.NewDesc(prefix+"state", "State of the BGP session (Down = 0, Idle = 1, Connect = 2, Active = 3, OpenSent = 4, OpenConfirm = 5, Established = 6)", labels, nil)
+	uptimeDesc = prometheus.NewDesc(prefix+"uptime_second", "Time since the session was established in seconds", labels, nil)
+	updatesReceivedDesc = prometheus.NewDesc(prefix+"update_received_count", "Number of updates received", labels, nil)
+	updatesSentDesc = prometheus.NewDesc(prefix+"update_sent_count", "Number of updates sent", labels, nil)
+
+	labels = append(labels, "afi", "safi")
+	routesReceivedDesc = prometheus.NewDesc(prefix+"route_received_count", "Number of routes received", labels, nil)
+	routesSentDesc = prometheus.NewDesc(prefix+"route_sent_count", "Number of routes sent", labels, nil)
+	routesRejectedDesc = prometheus.NewDesc(prefix+"route_rejected_count", "Number of routes rejected", labels, nil)
+	routesAcceptedDesc = prometheus.NewDesc(prefix+"route_accepted_count", "Number of routes accepted", labels, nil)
+}
+
+// NewCollector creates a new collector instance for the given BGP server
+func NewCollector(server server.BGPServer) prometheus.Collector {
+	return &bgpCollector{server}
+}
+
+// BGPCollector provides a collector for BGP metrics of BIO to use with Prometheus
+type bgpCollector struct {
+	server server.BGPServer
+}
+
+// Describe conforms to the prometheus collector interface
+func (c *bgpCollector) Describe(ch chan<- *prometheus.Desc) {
+	ch <- upDesc
+	ch <- stateDesc
+	ch <- uptimeDesc
+	ch <- updatesReceivedDesc
+	ch <- updatesSentDesc
+	ch <- routesReceivedDesc
+	ch <- routesSentDesc
+	ch <- routesRejectedDesc
+	ch <- routesAcceptedDesc
+}
+
+// Collect conforms to the prometheus collector interface
+func (c *bgpCollector) Collect(ch chan<- prometheus.Metric) {
+	m, err := c.server.Metrics()
+	if err != nil {
+		log.Error(errors.Wrap(err, "Could not retrieve metrics from BGP server"))
+		return
+	}
+
+	for _, peer := range m.Peers {
+		c.collectForPeer(ch, peer)
+	}
+}
+
+func (c *bgpCollector) collectForPeer(ch chan<- prometheus.Metric, peer *metrics.BGPPeerMetrics) {
+	l := []string{
+		peer.IP.String(),
+		strconv.Itoa(int(peer.LocalASN)),
+		strconv.Itoa(int(peer.ASN)),
+		peer.VRF}
+
+	var up float64
+	var uptime float64
+	if peer.Up {
+		up = 1
+		uptime = float64(time.Since(peer.Since) * time.Second)
+	}
+	ch <- prometheus.MustNewConstMetric(upDesc, prometheus.GaugeValue, up, l...)
+	ch <- prometheus.MustNewConstMetric(uptimeDesc, prometheus.GaugeValue, uptime, l...)
+	ch <- prometheus.MustNewConstMetric(stateDesc, prometheus.GaugeValue, float64(peer.State), l...)
+
+	ch <- prometheus.MustNewConstMetric(updatesReceivedDesc, prometheus.CounterValue, float64(peer.UpdatesReceived), l...)
+	ch <- prometheus.MustNewConstMetric(updatesSentDesc, prometheus.CounterValue, float64(peer.UpdatesSent), l...)
+
+	for _, family := range peer.AddressFamilies {
+		c.collectForFamily(ch, family, l)
+	}
+}
+
+func (c *bgpCollector) collectForFamily(ch chan<- prometheus.Metric, family *metrics.BGPAddressFamilyMetrics, l []string) {
+	l = append(l, strconv.Itoa(int(family.AFI)), strconv.Itoa(int(family.SAFI)))
+
+	ch <- prometheus.MustNewConstMetric(routesReceivedDesc, prometheus.CounterValue, float64(family.RoutesReceived), l...)
+	ch <- prometheus.MustNewConstMetric(routesSentDesc, prometheus.CounterValue, float64(family.RoutesSent), l...)
+}
diff --git a/metrics/vrf/adapter/prom/vrf_prom_adapter.go b/metrics/vrf/adapter/prom/vrf_prom_adapter.go
new file mode 100644
index 0000000000000000000000000000000000000000..f2a4fa0c3392a9b2848454710310a3b995606666
--- /dev/null
+++ b/metrics/vrf/adapter/prom/vrf_prom_adapter.go
@@ -0,0 +1,52 @@
+package prom
+
+import (
+	"strconv"
+
+	"github.com/bio-routing/bio-rd/protocols/bgp/server"
+	"github.com/bio-routing/bio-rd/routingtable/vrf"
+	"github.com/bio-routing/bio-rd/routingtable/vrf/metrics"
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+const (
+	prefix = "bio_vrf_"
+)
+
+var (
+	routeCountDesc *prometheus.Desc
+)
+
+func init() {
+	labels := []string{"vrf", "rib", "afi", "safi"}
+	routeCountDesc = prometheus.NewDesc(prefix+"route_count", "Number of routes in the RIB", labels, nil)
+}
+
+// NewCollector creates a new collector instance for the given BGP server
+func NewCollector() prometheus.Collector {
+	return &vrfCollector{}
+}
+
+// BGPCollector provides a collector for BGP metrics of BIO to use with Prometheus
+type vrfCollector struct {
+	server server.BGPServer
+}
+
+// Describe conforms to the prometheus collector interface
+func (c *vrfCollector) Describe(ch chan<- *prometheus.Desc) {
+	ch <- routeCountDesc
+}
+
+// Collect conforms to the prometheus collector interface
+func (c *vrfCollector) Collect(ch chan<- prometheus.Metric) {
+	for _, v := range vrf.Metrics() {
+		c.collectForVRF(ch, v)
+	}
+}
+
+func (c *vrfCollector) collectForVRF(ch chan<- prometheus.Metric, v *metrics.VRFMetrics) {
+	for _, rib := range v.RIBs {
+		ch <- prometheus.MustNewConstMetric(routeCountDesc, prometheus.GaugeValue, float64(rib.RouteCount),
+			v.Name, rib.Name, strconv.Itoa(int(rib.AFI)), strconv.Itoa(int(rib.SAFI)))
+	}
+}
diff --git a/protocols/bgp/metrics/bgp_address_family_metrics.go b/protocols/bgp/metrics/bgp_address_family_metrics.go
new file mode 100644
index 0000000000000000000000000000000000000000..5458358bf6bdcd66ffd2d7ca74f6c4657f2f8d8e
--- /dev/null
+++ b/protocols/bgp/metrics/bgp_address_family_metrics.go
@@ -0,0 +1,16 @@
+package metrics
+
+// BGPAddressFamilyMetrics provides metrics on AFI/SAFI level for one session
+type BGPAddressFamilyMetrics struct {
+	// AFI is the identifier for the address family
+	AFI uint16
+
+	// SAFI is the identifier for the sub address family
+	SAFI uint8
+
+	// RoutesReceived is the number of routes we recevied
+	RoutesReceived uint64
+
+	// RoutesAccepted is the number of routes we sent
+	RoutesSent uint64
+}
diff --git a/protocols/bgp/metrics/bgp_metrics.go b/protocols/bgp/metrics/bgp_metrics.go
new file mode 100644
index 0000000000000000000000000000000000000000..f6ed84970dc9aa02505f2af945f91222b8697427
--- /dev/null
+++ b/protocols/bgp/metrics/bgp_metrics.go
@@ -0,0 +1,7 @@
+package metrics
+
+// BGPMetrics provides metrics for a single BGP server instance
+type BGPMetrics struct {
+	// Peers is the collection of per peer metrics
+	Peers []*BGPPeerMetrics
+}
diff --git a/protocols/bgp/metrics/bgp_peer_metrics.go b/protocols/bgp/metrics/bgp_peer_metrics.go
new file mode 100644
index 0000000000000000000000000000000000000000..4c70b843d01ffddc2c0d80a7552bfcf8bf6ffe1b
--- /dev/null
+++ b/protocols/bgp/metrics/bgp_peer_metrics.go
@@ -0,0 +1,40 @@
+package metrics
+
+import (
+	"time"
+
+	bnet "github.com/bio-routing/bio-rd/net"
+)
+
+// BGPPeerMetrics provides metrics for one BGP session
+type BGPPeerMetrics struct {
+	// IP is the remote IP of the peer
+	IP bnet.IP
+
+	// ASN is the ASN of the peer
+	ASN uint32
+
+	// LocalASN is our local ASN
+	LocalASN uint32
+
+	// VRF is the name of the VRF the peer is configured in
+	VRF string
+
+	// Since is the time the session was established
+	Since time.Time
+
+	// State of the BGP session (Down = 0, Idle = 1, Connect = 2, Active = 3, OpenSent = 4, OpenConfirm = 5, Established = 6)
+	State uint8
+
+	// Up returns if the session is established
+	Up bool
+
+	// UpdatesReceived is the number of update messages received on this session
+	UpdatesReceived uint64
+
+	// UpdatesReceived is the number of update messages we sent on this session
+	UpdatesSent uint64
+
+	// AddressFamilies provides metrics on AFI/SAFI level
+	AddressFamilies []*BGPAddressFamilyMetrics
+}
diff --git a/protocols/bgp/server/fsm.go b/protocols/bgp/server/fsm.go
index 115a1eafada9bd3ed486f68fc899315dc7cdc50e..b52bb10076ed98ae1e7fc765f30fe7dee9b037ba 100644
--- a/protocols/bgp/server/fsm.go
+++ b/protocols/bgp/server/fsm.go
@@ -22,6 +22,13 @@ const (
 	AutomaticStartWithPassiveTcpEstablishment = 5
 	AutomaticStop                             = 8
 	Cease                                     = 100
+	stateNameIdle                             = "idle"
+	stateNameConnect                          = "connect"
+	stateNameActive                           = "active"
+	stateNameOpenSent                         = "openSent"
+	stateNameOpenConfirm                      = "openConfirm"
+	stateNameEstablished                      = "established"
+	stateNameCease                            = "cease"
 )
 
 type state interface {
@@ -70,6 +77,9 @@ type FSM struct {
 	reason     string
 	active     bool
 
+	establishedTime time.Time
+	counters        fsmCounters
+
 	connectionCancelFunc context.CancelFunc
 }
 
@@ -100,6 +110,7 @@ func newFSM(peer *peer) *FSM {
 		msgRecvCh:        make(chan []byte),
 		msgRecvFailCh:    make(chan error),
 		stopMsgRecvCh:    make(chan struct{}),
+		counters:         fsmCounters{},
 	}
 
 	if peer.ipv4 != nil {
@@ -162,10 +173,14 @@ func (fsm *FSM) run() {
 			}).Info("FSM: Neighbor state change")
 		}
 
-		if newState == "cease" {
+		if newState == stateNameCease {
 			return
 		}
 
+		if oldState != newState && newState == stateNameEstablished {
+			fsm.establishedTime = time.Now()
+		}
+
 		fsm.stateMu.Lock()
 		fsm.state = next
 		fsm.stateMu.Unlock()
@@ -183,19 +198,19 @@ func (fsm *FSM) cancelRunningGoRoutines() {
 func stateName(s state) string {
 	switch s.(type) {
 	case *idleState:
-		return "idle"
+		return stateNameIdle
 	case *connectState:
-		return "connect"
+		return stateNameConnect
 	case *activeState:
-		return "active"
+		return stateNameActive
 	case *openSentState:
-		return "openSent"
+		return stateNameOpenSent
 	case *openConfirmState:
-		return "openConfirm"
+		return stateNameOpenConfirm
 	case *establishedState:
-		return "established"
+		return stateNameEstablished
 	case *ceaseState:
-		return "cease"
+		return stateNameCease
 	default:
 		panic(fmt.Sprintf("Unknown state: %v", s))
 	}
diff --git a/protocols/bgp/server/fsm_counters.go b/protocols/bgp/server/fsm_counters.go
new file mode 100644
index 0000000000000000000000000000000000000000..944719d5e0804ce606c4e117f4dc68b7caeae720
--- /dev/null
+++ b/protocols/bgp/server/fsm_counters.go
@@ -0,0 +1,11 @@
+package server
+
+type fsmCounters struct {
+	updatesReceived uint64
+	updatesSent     uint64
+}
+
+func (c *fsmCounters) reset() {
+	c.updatesReceived = 0
+	c.updatesSent = 0
+}
diff --git a/protocols/bgp/server/fsm_established.go b/protocols/bgp/server/fsm_established.go
index 3152605151a56680fdb5e401cd5c198c26a0a0e3..3722bd3364a79b051e786f2f1ecb6dd84798bf01 100644
--- a/protocols/bgp/server/fsm_established.go
+++ b/protocols/bgp/server/fsm_established.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"net"
+	"sync/atomic"
 	"time"
 
 	bnet "github.com/bio-routing/bio-rd/net"
@@ -105,6 +106,8 @@ func (s *establishedState) uninit() {
 	if s.fsm.ipv6Unicast != nil {
 		s.fsm.ipv6Unicast.dispose()
 	}
+
+	s.fsm.counters.reset()
 }
 
 func (s *establishedState) manualStop() (state, string) {
@@ -192,6 +195,8 @@ func (s *establishedState) notification() (state, string) {
 }
 
 func (s *establishedState) update(u *packet.BGPUpdate) (state, string) {
+	atomic.AddUint64(&s.fsm.counters.updatesReceived, 1)
+
 	if s.fsm.holdTime != 0 {
 		s.fsm.updateLastUpdateOrKeepalive()
 	}
diff --git a/protocols/bgp/server/metrics_service.go b/protocols/bgp/server/metrics_service.go
new file mode 100644
index 0000000000000000000000000000000000000000..1f1fa90b02fa85478a155360167cb5ff92dc31ab
--- /dev/null
+++ b/protocols/bgp/server/metrics_service.go
@@ -0,0 +1,100 @@
+package server
+
+import (
+	"github.com/bio-routing/bio-rd/protocols/bgp/metrics"
+)
+
+const (
+	stateDown        = 0
+	stateIdle        = 1
+	stateConnect     = 2
+	stateActive      = 3
+	stateOpenSent    = 4
+	stateOpenConfirm = 5
+	stateEstablished = 6
+)
+
+type metricsService struct {
+	server *bgpServer
+}
+
+func (b *metricsService) metrics() *metrics.BGPMetrics {
+	return &metrics.BGPMetrics{
+		Peers: b.peerMetrics(),
+	}
+}
+
+func (b *metricsService) peerMetrics() []*metrics.BGPPeerMetrics {
+	peers := make([]*metrics.BGPPeerMetrics, 0)
+
+	for _, peer := range b.server.peers.list() {
+		m := b.metricsForPeer(peer)
+		peers = append(peers, m)
+	}
+
+	return peers
+}
+
+func (b *metricsService) metricsForPeer(peer *peer) *metrics.BGPPeerMetrics {
+	m := &metrics.BGPPeerMetrics{
+		ASN:             peer.peerASN,
+		LocalASN:        peer.localASN,
+		IP:              peer.addr,
+		AddressFamilies: make([]*metrics.BGPAddressFamilyMetrics, 0),
+		VRF:             peer.vrf.Name(),
+	}
+
+	var fsms = peer.fsms
+	if len(fsms) == 0 {
+		return m
+	}
+
+	fsm := fsms[0]
+	m.State = b.statusFromFSM(fsm)
+	m.Up = m.State == stateEstablished
+
+	if m.Up {
+		m.Since = fsm.establishedTime
+	}
+
+	m.UpdatesReceived = fsm.counters.updatesReceived
+	m.UpdatesSent = fsm.counters.updatesSent
+
+	if peer.ipv4 != nil {
+		m.AddressFamilies = append(m.AddressFamilies, b.metricsForFamily(fsm.ipv4Unicast))
+	}
+
+	if peer.ipv6 != nil {
+		m.AddressFamilies = append(m.AddressFamilies, b.metricsForFamily(fsm.ipv6Unicast))
+	}
+
+	return m
+}
+
+func (b *metricsService) metricsForFamily(family *fsmAddressFamily) *metrics.BGPAddressFamilyMetrics {
+	return &metrics.BGPAddressFamilyMetrics{
+		AFI:            family.afi,
+		SAFI:           family.safi,
+		RoutesReceived: uint64(family.adjRIBIn.RouteCount()),
+		RoutesSent:     uint64(family.adjRIBOut.RouteCount()),
+	}
+}
+
+func (b *metricsService) statusFromFSM(fsm *FSM) uint8 {
+	switch fsm.state.(type) {
+	case *idleState:
+		return stateIdle
+	case *connectState:
+		return stateConnect
+	case *activeState:
+		return stateActive
+	case *openSentState:
+		return stateOpenSent
+	case *openConfirmState:
+		return stateOpenConfirm
+	case *establishedState:
+		return stateEstablished
+	}
+
+	return stateDown
+}
diff --git a/protocols/bgp/server/metrics_service_test.go b/protocols/bgp/server/metrics_service_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f98ec61ef378185bb59f71892c102eacd28ebc7b
--- /dev/null
+++ b/protocols/bgp/server/metrics_service_test.go
@@ -0,0 +1,299 @@
+package server
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bio-routing/bio-rd/protocols/bgp/packet"
+	"github.com/bio-routing/bio-rd/routingtable"
+	"github.com/bio-routing/bio-rd/routingtable/vrf"
+	"github.com/stretchr/testify/assert"
+
+	bnet "github.com/bio-routing/bio-rd/net"
+	"github.com/bio-routing/bio-rd/protocols/bgp/metrics"
+)
+
+func TestMetrics(t *testing.T) {
+	vrf, _ := vrf.New("inet.0")
+	establishedTime := time.Now()
+
+	tests := []struct {
+		name               string
+		peer               *peer
+		withoutFSM         bool
+		state              state
+		updatesReceived    uint64
+		updatesSent        uint64
+		ipv4RoutesReceived int64
+		ipv4RoutesSent     int64
+		ipv6RoutesReceived int64
+		ipv6RoutesSent     int64
+		expected           *metrics.BGPMetrics
+	}{
+		{
+			name: "Established",
+			peer: &peer{
+				peerASN:  202739,
+				localASN: 201701,
+				addr:     bnet.IPv4(100),
+				ipv4:     &peerAddressFamily{},
+				ipv6:     &peerAddressFamily{},
+				vrf:      vrf,
+			},
+			state:              &establishedState{},
+			updatesReceived:    3,
+			updatesSent:        4,
+			ipv4RoutesReceived: 5,
+			ipv4RoutesSent:     6,
+			ipv6RoutesReceived: 7,
+			ipv6RoutesSent:     8,
+			expected: &metrics.BGPMetrics{
+				Peers: []*metrics.BGPPeerMetrics{
+					{
+						IP:              bnet.IPv4(100),
+						ASN:             202739,
+						LocalASN:        201701,
+						UpdatesReceived: 3,
+						UpdatesSent:     4,
+						VRF:             "inet.0",
+						Up:              true,
+						State:           stateEstablished,
+						Since:           establishedTime,
+						AddressFamilies: []*metrics.BGPAddressFamilyMetrics{
+							{
+								AFI:            packet.IPv4AFI,
+								SAFI:           packet.UnicastSAFI,
+								RoutesReceived: 5,
+								RoutesSent:     6,
+							},
+							{
+								AFI:            packet.IPv6AFI,
+								SAFI:           packet.UnicastSAFI,
+								RoutesReceived: 7,
+								RoutesSent:     8,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Idle",
+			peer: &peer{
+				peerASN:  202739,
+				localASN: 201701,
+				addr:     bnet.IPv4(100),
+				ipv4:     &peerAddressFamily{},
+				ipv6:     &peerAddressFamily{},
+				vrf:      vrf,
+			},
+			state: &idleState{},
+			expected: &metrics.BGPMetrics{
+				Peers: []*metrics.BGPPeerMetrics{
+					{
+						IP:       bnet.IPv4(100),
+						ASN:      202739,
+						LocalASN: 201701,
+						VRF:      "inet.0",
+						State:    stateIdle,
+						AddressFamilies: []*metrics.BGPAddressFamilyMetrics{
+							{
+								AFI:  packet.IPv4AFI,
+								SAFI: packet.UnicastSAFI,
+							},
+							{
+								AFI:  packet.IPv6AFI,
+								SAFI: packet.UnicastSAFI,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Active",
+			peer: &peer{
+				peerASN:  202739,
+				localASN: 201701,
+				addr:     bnet.IPv4(100),
+				ipv4:     &peerAddressFamily{},
+				ipv6:     &peerAddressFamily{},
+				vrf:      vrf,
+			},
+			state: &activeState{},
+			expected: &metrics.BGPMetrics{
+				Peers: []*metrics.BGPPeerMetrics{
+					{
+						IP:       bnet.IPv4(100),
+						ASN:      202739,
+						LocalASN: 201701,
+						VRF:      "inet.0",
+						State:    stateActive,
+						AddressFamilies: []*metrics.BGPAddressFamilyMetrics{
+							{
+								AFI:  packet.IPv4AFI,
+								SAFI: packet.UnicastSAFI,
+							},
+							{
+								AFI:  packet.IPv6AFI,
+								SAFI: packet.UnicastSAFI,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "OpenSent",
+			peer: &peer{
+				peerASN:  202739,
+				localASN: 201701,
+				addr:     bnet.IPv4(100),
+				ipv4:     &peerAddressFamily{},
+				ipv6:     &peerAddressFamily{},
+				vrf:      vrf,
+			},
+			state: &openSentState{},
+			expected: &metrics.BGPMetrics{
+				Peers: []*metrics.BGPPeerMetrics{
+					{
+						IP:       bnet.IPv4(100),
+						ASN:      202739,
+						LocalASN: 201701,
+						VRF:      "inet.0",
+						State:    stateOpenSent,
+						AddressFamilies: []*metrics.BGPAddressFamilyMetrics{
+							{
+								AFI:  packet.IPv4AFI,
+								SAFI: packet.UnicastSAFI,
+							},
+							{
+								AFI:  packet.IPv6AFI,
+								SAFI: packet.UnicastSAFI,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "OpenConfirm",
+			peer: &peer{
+				peerASN:  202739,
+				localASN: 201701,
+				addr:     bnet.IPv4(100),
+				ipv4:     &peerAddressFamily{},
+				ipv6:     &peerAddressFamily{},
+				vrf:      vrf,
+			},
+			state: &openConfirmState{},
+			expected: &metrics.BGPMetrics{
+				Peers: []*metrics.BGPPeerMetrics{
+					{
+						IP:       bnet.IPv4(100),
+						ASN:      202739,
+						LocalASN: 201701,
+						VRF:      "inet.0",
+						State:    stateOpenConfirm,
+						AddressFamilies: []*metrics.BGPAddressFamilyMetrics{
+							{
+								AFI:  packet.IPv4AFI,
+								SAFI: packet.UnicastSAFI,
+							},
+							{
+								AFI:  packet.IPv6AFI,
+								SAFI: packet.UnicastSAFI,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Connect",
+			peer: &peer{
+				peerASN:  202739,
+				localASN: 201701,
+				addr:     bnet.IPv4(100),
+				ipv4:     &peerAddressFamily{},
+				ipv6:     &peerAddressFamily{},
+				vrf:      vrf,
+			},
+			state: &connectState{},
+			expected: &metrics.BGPMetrics{
+				Peers: []*metrics.BGPPeerMetrics{
+					{
+						IP:       bnet.IPv4(100),
+						ASN:      202739,
+						LocalASN: 201701,
+						VRF:      "inet.0",
+						State:    stateConnect,
+						AddressFamilies: []*metrics.BGPAddressFamilyMetrics{
+							{
+								AFI:  packet.IPv4AFI,
+								SAFI: packet.UnicastSAFI,
+							},
+							{
+								AFI:  packet.IPv6AFI,
+								SAFI: packet.UnicastSAFI,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:       "without fsm",
+			withoutFSM: true,
+			peer: &peer{
+				peerASN:  202739,
+				localASN: 201701,
+				addr:     bnet.IPv4(100),
+				ipv4:     &peerAddressFamily{},
+				ipv6:     &peerAddressFamily{},
+				vrf:      vrf,
+			},
+			expected: &metrics.BGPMetrics{
+				Peers: []*metrics.BGPPeerMetrics{
+					{
+						IP:              bnet.IPv4(100),
+						ASN:             202739,
+						LocalASN:        201701,
+						VRF:             "inet.0",
+						AddressFamilies: []*metrics.BGPAddressFamilyMetrics{},
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if !test.withoutFSM {
+				fsm := newFSM(test.peer)
+				test.peer.fsms = append(test.peer.fsms, fsm)
+
+				fsm.state = test.state
+				fsm.counters.updatesReceived = test.updatesReceived
+				fsm.counters.updatesSent = test.updatesSent
+
+				fsm.ipv4Unicast.adjRIBIn = &routingtable.RTMockClient{FakeRouteCount: test.ipv4RoutesReceived}
+				fsm.ipv4Unicast.adjRIBOut = &routingtable.RTMockClient{FakeRouteCount: test.ipv4RoutesSent}
+				fsm.ipv6Unicast.adjRIBIn = &routingtable.RTMockClient{FakeRouteCount: test.ipv6RoutesReceived}
+				fsm.ipv6Unicast.adjRIBOut = &routingtable.RTMockClient{FakeRouteCount: test.ipv6RoutesSent}
+
+				fsm.establishedTime = establishedTime
+			}
+
+			s := newBgpServer()
+			s.peers.add(test.peer)
+
+			actual, err := s.Metrics()
+			if err != nil {
+				t.Fatalf("unecpected error: %v", err)
+			}
+
+			assert.Equal(t, test.expected, actual)
+		})
+	}
+}
diff --git a/protocols/bgp/server/peer.go b/protocols/bgp/server/peer.go
index f537b58e1c337eda998d0202358c7a9ec960587b..2659d1e4131a0b1fbee66fbbc3d49206ed49f625 100644
--- a/protocols/bgp/server/peer.go
+++ b/protocols/bgp/server/peer.go
@@ -2,6 +2,7 @@ package server
 
 import (
 	"fmt"
+	"github.com/bio-routing/bio-rd/routingtable/vrf"
 	"sync"
 	"time"
 
@@ -36,6 +37,7 @@ type peer struct {
 	ipv4MultiProtocolAdvertised bool
 	clusterID                   uint32
 
+	vrf  *vrf.VRF
 	ipv4 *peerAddressFamily
 	ipv6 *peerAddressFamily
 }
@@ -164,6 +166,7 @@ func newPeer(c config.Peer, server *bgpServer) (*peer, error) {
 		routeServerClient:    c.RouteServerClient,
 		routeReflectorClient: c.RouteReflectorClient,
 		clusterID:            c.RouteReflectorClusterID,
+		vrf:                  c.VRF,
 	}
 
 	if c.IPv4 != nil {
diff --git a/protocols/bgp/server/server.go b/protocols/bgp/server/server.go
index d3d0a67447f82598c9a9be4a14e63e9fef4e0a7a..980e922a3695686e9e7f61ddfb36271969094796 100644
--- a/protocols/bgp/server/server.go
+++ b/protocols/bgp/server/server.go
@@ -1,6 +1,7 @@
 package server
 
 import (
+	"fmt"
 	"net"
 
 	"github.com/bio-routing/bio-rd/routingtable/adjRIBOut"
@@ -9,6 +10,7 @@ import (
 
 	"github.com/bio-routing/bio-rd/config"
 	bnet "github.com/bio-routing/bio-rd/net"
+	"github.com/bio-routing/bio-rd/protocols/bgp/metrics"
 	bnetutils "github.com/bio-routing/bio-rd/util/net"
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
@@ -24,21 +26,31 @@ type bgpServer struct {
 	peers     *peerManager
 	routerID  uint32
 	localASN  uint32
+	metrics   *metricsService
 }
 
 type BGPServer interface {
 	RouterID() uint32
 	Start(*config.Global) error
 	AddPeer(config.Peer) error
+	Metrics() (*metrics.BGPMetrics, error)
 	GetRIBIn(peerIP bnet.IP, afi uint16, safi uint8) *adjRIBIn.AdjRIBIn
 	GetRIBOut(peerIP bnet.IP, afi uint16, safi uint8) *adjRIBOut.AdjRIBOut
 	ConnectMockPeer(peer config.Peer, con net.Conn)
 }
 
+// NewBgpServer creates a new instance of bgpServer
 func NewBgpServer() BGPServer {
-	return &bgpServer{
+	return newBgpServer()
+}
+
+func newBgpServer() *bgpServer {
+	server := &bgpServer{
 		peers: newPeerManager(),
 	}
+
+	server.metrics = &metricsService{server}
+	return server
 }
 
 func (b *bgpServer) RouterID() uint32 {
@@ -163,3 +175,11 @@ func (b *bgpServer) AddPeer(c config.Peer) error {
 
 	return nil
 }
+
+func (b *bgpServer) Metrics() (*metrics.BGPMetrics, error) {
+	if b.metrics == nil {
+		return nil, fmt.Errorf("Server not started yet")
+	}
+
+	return b.metrics.metrics(), nil
+}
diff --git a/protocols/bgp/server/update_sender.go b/protocols/bgp/server/update_sender.go
index c43d1778ea225b7586c9e2d442832b1f4785d6e0..1b7bca8c8e957b0e00d71982ca0dc3de3bfd9d3b 100644
--- a/protocols/bgp/server/update_sender.go
+++ b/protocols/bgp/server/update_sender.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"io"
 	"sync"
+	"sync/atomic"
 	"time"
 
 	bnet "github.com/bio-routing/bio-rd/net"
@@ -180,6 +181,7 @@ func (u *UpdateSender) sendUpdates(pathAttrs *packet.PathAttribute, updatePrefix
 		if err != nil {
 			log.Errorf("Failed to serialize and send: %v", err)
 		}
+		atomic.AddUint64(&u.fsm.counters.updatesSent, 1)
 	}
 }
 
diff --git a/routingtable/locRIB/loc_rib.go b/routingtable/locRIB/loc_rib.go
index b7a630ed516f6c63b8ce05c1544b778d3641be9a..3503aacfce8e9204ce06b58303e2553221d618ee 100644
--- a/routingtable/locRIB/loc_rib.go
+++ b/routingtable/locRIB/loc_rib.go
@@ -48,12 +48,9 @@ func (a *LocRIB) GetContributingASNs() *routingtable.ContributingASNs {
 	return a.contributingASNs
 }
 
-//Count routes from the LocRIP
+// Count routes from the LocRIB
 func (a *LocRIB) Count() uint64 {
-	a.mu.RLock()
-	defer a.mu.RUnlock()
-
-	return uint64(len(a.rt.Dump()))
+	return uint64(a.rt.GetRouteCount())
 }
 
 // Dump dumps the RIB
diff --git a/routingtable/mock_client.go b/routingtable/mock_client.go
index 25a26c400549c1458ec697720856a6898c457821..6538a2cea3a01174b54cca6bd4ed6bce904f6d7b 100644
--- a/routingtable/mock_client.go
+++ b/routingtable/mock_client.go
@@ -13,7 +13,8 @@ type RemovePathParams struct {
 }
 
 type RTMockClient struct {
-	removed []*RemovePathParams
+	removed        []*RemovePathParams
+	FakeRouteCount int64
 }
 
 func NewRTMockClient() *RTMockClient {
@@ -67,5 +68,5 @@ func (m *RTMockClient) RemovePath(pfx net.Prefix, p *route.Path) bool {
 }
 
 func (m *RTMockClient) RouteCount() int64 {
-	return 0
+	return m.FakeRouteCount
 }
diff --git a/routingtable/vrf/metrics.go b/routingtable/vrf/metrics.go
new file mode 100644
index 0000000000000000000000000000000000000000..b8d43ee42f7b39217d90a1333e301117e28a9a48
--- /dev/null
+++ b/routingtable/vrf/metrics.go
@@ -0,0 +1,37 @@
+package vrf
+
+import (
+	"github.com/bio-routing/bio-rd/routingtable/vrf/metrics"
+)
+
+// Metrics returns metrics for all VRFs
+func Metrics() []*metrics.VRFMetrics {
+	vrfs := globalRegistry.list()
+
+	m := make([]*metrics.VRFMetrics, len(vrfs))
+	i := 0
+	for _, v := range vrfs {
+		m[i] = metricsForVRF(v)
+		i++
+	}
+
+	return m
+}
+
+func metricsForVRF(v *VRF) *metrics.VRFMetrics {
+	m := &metrics.VRFMetrics{
+		Name: v.Name(),
+		RIBs: make([]*metrics.RIBMetrics, 0),
+	}
+
+	for family, rib := range v.ribs {
+		m.RIBs = append(m.RIBs, &metrics.RIBMetrics{
+			Name:       v.nameForRIB(rib),
+			AFI:        family.afi,
+			SAFI:       family.safi,
+			RouteCount: rib.Count(),
+		})
+	}
+
+	return m
+}
diff --git a/routingtable/vrf/metrics/rib_metrics.go b/routingtable/vrf/metrics/rib_metrics.go
new file mode 100644
index 0000000000000000000000000000000000000000..6758d771753e7cd3e59705754df296d71a8ed5e6
--- /dev/null
+++ b/routingtable/vrf/metrics/rib_metrics.go
@@ -0,0 +1,16 @@
+package metrics
+
+// RIBMetrics represents metrics of a RIB in a VRF
+type RIBMetrics struct {
+	// Name of the RIB
+	Name string
+
+	// AFI is the identifier for the address family
+	AFI uint16
+
+	// SAFI is the identifier for the sub address family
+	SAFI uint8
+
+	// Number of routes in the RIB
+	RouteCount uint64
+}
diff --git a/routingtable/vrf/metrics/vrf_metrics.go b/routingtable/vrf/metrics/vrf_metrics.go
new file mode 100644
index 0000000000000000000000000000000000000000..3c5ba2c0e37c293bb2467c86594f1e6222df05e2
--- /dev/null
+++ b/routingtable/vrf/metrics/vrf_metrics.go
@@ -0,0 +1,10 @@
+package metrics
+
+// VRFMetrics represents a collection of metrics of one VRF
+type VRFMetrics struct {
+	// Name of the VRF
+	Name string
+
+	// RIBs returns the RIB specific metrics
+	RIBs []*RIBMetrics
+}
diff --git a/routingtable/vrf/metrics_test.go b/routingtable/vrf/metrics_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b946699e1cea38d53116062f9eceb486bbcf888e
--- /dev/null
+++ b/routingtable/vrf/metrics_test.go
@@ -0,0 +1,83 @@
+package vrf
+
+import (
+	"sort"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	bnet "github.com/bio-routing/bio-rd/net"
+	"github.com/bio-routing/bio-rd/route"
+	"github.com/bio-routing/bio-rd/routingtable/vrf/metrics"
+)
+
+func TestMetrics(t *testing.T) {
+	green, err := New("green")
+	if err != nil {
+		t.Fatal(err)
+	}
+	green.IPv4UnicastRIB().AddPath(bnet.NewPfx(bnet.IPv4FromOctets(8, 0, 0, 0), 8), &route.Path{})
+	green.IPv4UnicastRIB().AddPath(bnet.NewPfx(bnet.IPv4FromOctets(8, 0, 0, 0), 16), &route.Path{})
+	green.IPv6UnicastRIB().AddPath(bnet.NewPfx(bnet.IPv6FromBlocks(0x2001, 0x678, 0x1e0, 0, 0, 0, 0, 0), 48), &route.Path{})
+
+	red, err := New("red")
+	if err != nil {
+		t.Fatal(err)
+	}
+	red.IPv6UnicastRIB().AddPath(bnet.NewPfx(bnet.IPv6FromBlocks(0x2001, 0x678, 0x1e0, 0x100, 0, 0, 0, 0), 64), &route.Path{})
+	red.IPv6UnicastRIB().AddPath(bnet.NewPfx(bnet.IPv6FromBlocks(0x2001, 0x678, 0x1e0, 0x200, 0, 0, 0, 0), 64), &route.Path{})
+
+	expected := []*metrics.VRFMetrics{
+		{
+			Name: "green",
+			RIBs: []*metrics.RIBMetrics{
+				{
+					Name:       "inet.0",
+					AFI:        afiIPv4,
+					SAFI:       safiUnicast,
+					RouteCount: 2,
+				},
+				{
+					Name:       "inet6.0",
+					AFI:        afiIPv6,
+					SAFI:       safiUnicast,
+					RouteCount: 1,
+				},
+			},
+		},
+		{
+			Name: "red",
+			RIBs: []*metrics.RIBMetrics{
+				{
+					Name:       "inet.0",
+					AFI:        afiIPv4,
+					SAFI:       safiUnicast,
+					RouteCount: 0,
+				},
+				{
+					Name:       "inet6.0",
+					AFI:        afiIPv6,
+					SAFI:       safiUnicast,
+					RouteCount: 2,
+				},
+			},
+		},
+	}
+
+	actual := Metrics()
+	sortResult(actual)
+
+	assert.Equal(t, expected, actual)
+}
+
+func sortResult(m []*metrics.VRFMetrics) {
+	sort.Slice(m, func(i, j int) bool {
+		return m[i].Name < m[j].Name
+	})
+
+	for _, v := range m {
+		sort.Slice(v.RIBs, func(i, j int) bool {
+			return m[i].Name < m[j].Name
+		})
+	}
+}
diff --git a/routingtable/vrf/vrf.go b/routingtable/vrf/vrf.go
index eb9d4a3c7cceb3e480249d85cc55dc69468b2b09..bdc290588ab88830fa99f4a9a3c74e911781828f 100644
--- a/routingtable/vrf/vrf.go
+++ b/routingtable/vrf/vrf.go
@@ -85,6 +85,7 @@ func (v *VRF) IPv6UnicastRIB() *locRIB.LocRIB {
 	return v.ribForAddressFamily(addressFamily{afi: afiIPv6, safi: safiUnicast})
 }
 
+// Name is the name of the VRF
 func (v *VRF) Name() string {
 	return v.name
 }
@@ -108,3 +109,13 @@ func (v *VRF) RIBByName(name string) (rib *locRIB.LocRIB, found bool) {
 	rib, found = v.ribNames[name]
 	return rib, found
 }
+
+func (v *VRF) nameForRIB(rib *locRIB.LocRIB) string {
+	for name, r := range v.ribNames {
+		if r == rib {
+			return name
+		}
+	}
+
+	return ""
+}
diff --git a/routingtable/vrf/vrf_registry.go b/routingtable/vrf/vrf_registry.go
index 15cfc143c18abefd7148e34f5026d10e02faf86c..0fae0720ba623be30f3d06e2349ef7340d0c715a 100644
--- a/routingtable/vrf/vrf_registry.go
+++ b/routingtable/vrf/vrf_registry.go
@@ -41,3 +41,17 @@ func (r *vrfRegistry) unregisterVRF(v *VRF) {
 
 	delete(r.vrfs, v.name)
 }
+
+func (r *vrfRegistry) list() []*VRF {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	l := make([]*VRF, len(r.vrfs))
+	i := 0
+	for _, v := range r.vrfs {
+		l[i] = v
+		i++
+	}
+
+	return l
+}