Merge branch 'jerlbeck/wip/stats'

* This adds a new counter type (to measure time or delay)
* A statsd reporting backend. This can be fed into graphite
or similar tools.
* A periodic log backend for performance values
diff --git a/.gitignore b/.gitignore
index 24c3af7..598f88a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,6 +56,7 @@
 tests/testsuite.log
 
 tests/utils/utils_test
+tests/stats/stats_test
 tests/kasumi/kasumi_test
 tests/sms/sms_test
 tests/timer/timer_test
diff --git a/TODO-RELEASE b/TODO-RELEASE
index 43b1e8e..4d22f95 100644
--- a/TODO-RELEASE
+++ b/TODO-RELEASE
@@ -1 +1,3 @@
 #library	what		description / commit summary line
+libosmovty	abi-change	stats/vty: Add stats configuration (enum node_type has changed)
+libosmovty	abi-change	vty: Add reserved nodes to enum node_type
diff --git a/include/Makefile.am b/include/Makefile.am
index 52c6a38..2073580 100644
--- a/include/Makefile.am
+++ b/include/Makefile.am
@@ -22,12 +22,14 @@
                        osmocom/core/linuxrbtree.h \
                        osmocom/core/logging.h \
                        osmocom/core/loggingrb.h \
+                       osmocom/core/stats.h \
                        osmocom/core/macaddr.h \
                        osmocom/core/msgb.h \
                        osmocom/core/panic.h \
                        osmocom/core/prim.h \
                        osmocom/core/process.h \
                        osmocom/core/rate_ctr.h \
+                       osmocom/core/stat_item.h \
                        osmocom/core/select.h \
                        osmocom/core/signal.h \
                        osmocom/core/socket.h \
@@ -112,6 +114,7 @@
                           osmocom/vty/buffer.h \
                           osmocom/vty/command.h \
                           osmocom/vty/logging.h \
+                          osmocom/vty/stats.h \
                           osmocom/vty/misc.h \
                           osmocom/vty/telnet_interface.h \
                           osmocom/vty/vector.h \
diff --git a/include/osmocom/core/logging.h b/include/osmocom/core/logging.h
index ba41762..1c159d0 100644
--- a/include/osmocom/core/logging.h
+++ b/include/osmocom/core/logging.h
@@ -69,7 +69,8 @@
 #define DLSMS		-7
 #define DLCTRL		-8
 #define DLGTP		-9
-#define OSMO_NUM_DLIB	9
+#define DLSTATS		-10
+#define OSMO_NUM_DLIB	10
 
 struct log_category {
 	uint8_t loglevel;
diff --git a/include/osmocom/core/rate_ctr.h b/include/osmocom/core/rate_ctr.h
index 821c7cf..03b1bfb 100644
--- a/include/osmocom/core/rate_ctr.h
+++ b/include/osmocom/core/rate_ctr.h
@@ -30,6 +30,7 @@
 /*! \brief data we keep for each actual value */
 struct rate_ctr {
 	uint64_t current;	/*!< \brief current value */
+	uint64_t previous;	/*!< \brief previous value, used for delta */
 	/*! \brief per-interval data */
 	struct rate_ctr_per_intv intv[RATE_CTR_INTV_NUM];
 };
@@ -46,6 +47,8 @@
 	const char *group_name_prefix;
 	/*! \brief The human-readable description of the group */
 	const char *group_description;
+	/*! \brief The class to which this group belongs */
+	int class_id;
 	/*! \brief The number of counters in this group */
 	const unsigned int num_ctr;
 	/*! \brief Pointer to array of counter names */
@@ -78,9 +81,27 @@
 	rate_ctr_add(ctr, 1);
 }
 
+/*! \brief Return the counter difference since the last call to this function */
+int64_t rate_ctr_difference(struct rate_ctr *ctr);
+
 int rate_ctr_init(void *tall_ctx);
 
 struct rate_ctr_group *rate_ctr_get_group_by_name_idx(const char *name, const unsigned int idx);
 const struct rate_ctr *rate_ctr_get_by_name(const struct rate_ctr_group *ctrg, const char *name);
 
+typedef int (*rate_ctr_handler_t)(
+	struct rate_ctr_group *, struct rate_ctr *,
+	const struct rate_ctr_desc *, void *);
+typedef int (*rate_ctr_group_handler_t)(struct rate_ctr_group *, void *);
+
+
+/*! \brief Iterate over all counters
+ *  \param[in] handle_item Call-back function, aborts if rc < 0
+ *  \param[in] data Private data handed through to \a handle_counter
+ */
+int rate_ctr_for_each_counter(struct rate_ctr_group *ctrg,
+	rate_ctr_handler_t handle_counter, void *data);
+
+int rate_ctr_for_each_group(rate_ctr_group_handler_t handle_group, void *data);
+
 /*! @} */
diff --git a/include/osmocom/core/stat_item.h b/include/osmocom/core/stat_item.h
new file mode 100644
index 0000000..c2ad8cf
--- /dev/null
+++ b/include/osmocom/core/stat_item.h
@@ -0,0 +1,130 @@
+#pragma once
+
+/*! \defgroup osmo_stat_item Statistics value item
+ *  @{
+ */
+
+/*! \file stat_item.h */
+
+#include <stdint.h>
+
+#include <osmocom/core/linuxlist.h>
+
+struct osmo_stat_item_desc;
+
+#define STAT_ITEM_NOVALUE_ID 0
+
+struct osmo_stat_item_value {
+	int32_t id;
+	int32_t value;
+};
+
+/*! \brief data we keep for each actual value */
+struct osmo_stat_item {
+	const struct osmo_stat_item_desc *desc;
+	/*! \brief the index of the freshest value */
+	int32_t last_value_index;
+	/*! \brief offset to the freshest value in the value fifo */
+	int16_t last_offs;
+	/*! \brief value fifo */
+	struct osmo_stat_item_value values[0];
+};
+
+/*! \brief statistics value description */
+struct osmo_stat_item_desc {
+	const char *name;	/*!< \brief name of the item */
+	const char *description;/*!< \brief description of the item */
+	const char *unit;	/*!< \brief unit of a value */
+	unsigned int num_values;/*!< \brief number of values to store */
+	int32_t default_value;
+};
+
+/*! \brief description of a statistics value group */
+struct osmo_stat_item_group_desc {
+	/*! \brief The prefix to the name of all values in this group */
+	const char *group_name_prefix;
+	/*! \brief The human-readable description of the group */
+	const char *group_description;
+	/*! \brief The class to which this group belongs */
+	int class_id;
+	/*! \brief The number of values in this group */
+	const unsigned int num_items;
+	/*! \brief Pointer to array of value names */
+	const struct osmo_stat_item_desc *item_desc;
+};
+
+/*! \brief One instance of a counter group class */
+struct osmo_stat_item_group {
+	/*! \brief Linked list of all value groups in the system */
+	struct llist_head list;
+	/*! \brief Pointer to the counter group class */
+	const struct osmo_stat_item_group_desc *desc;
+	/*! \brief The index of this value group within its class */
+	unsigned int idx;
+	/*! \brief Actual counter structures below */
+	struct osmo_stat_item *items[0];
+};
+
+struct osmo_stat_item_group *osmo_stat_item_group_alloc(
+	void *ctx,
+	const struct osmo_stat_item_group_desc *desc,
+	unsigned int idx);
+
+void osmo_stat_item_group_free(struct osmo_stat_item_group *statg);
+
+void osmo_stat_item_set(struct osmo_stat_item *item, int32_t value);
+
+int osmo_stat_item_init(void *tall_ctx);
+
+struct osmo_stat_item_group *osmo_stat_item_get_group_by_name_idx(
+	const char *name, const unsigned int idx);
+
+const struct osmo_stat_item *osmo_stat_item_get_by_name(
+	const struct osmo_stat_item_group *statg, const char *name);
+
+/*! \brief Retrieve the next value from the osmo_stat_item object.
+ * If a new value has been set, it is returned. The idx is used to decide
+ * which value to return.
+ * On success, *idx is updated to refer to the next unread value. If
+ * values have been missed due to FIFO overflow, *idx is incremented by
+ * (1 + num_lost).
+ * This way, the osmo_stat_item object can be kept stateless from the reader's
+ * perspective and therefore be used by several backends simultaneously.
+ *
+ * \param val	the osmo_stat_item object
+ * \param idx	identifies the next value to be read
+ * \param value	a pointer to store the value
+ * \returns  the increment of the index (0: no value has been read,
+ *           1: one value has been taken,
+ *           (1+n): n values have been skipped, one has been taken)
+ */
+int osmo_stat_item_get_next(const struct osmo_stat_item *item, int32_t *idx, int32_t *value);
+
+/*! \brief Get the last (freshest) value */
+static int32_t osmo_stat_item_get_last(const struct osmo_stat_item *item);
+
+/*! \brief Skip all values of the item and update idx accordingly */
+int osmo_stat_item_discard(const struct osmo_stat_item *item, int32_t *idx);
+
+/*! \brief Skip all values of all items and update idx accordingly */
+int osmo_stat_item_discard_all(int32_t *idx);
+
+typedef int (*osmo_stat_item_handler_t)(
+	struct osmo_stat_item_group *, struct osmo_stat_item *, void *);
+
+typedef int (*osmo_stat_item_group_handler_t)(struct osmo_stat_item_group *, void *);
+
+/*! \brief Iteate over all items
+ *  \param[in] handle_item Call-back function, aborts if rc < 0
+ *  \param[in] data Private data handed through to \a handle_item
+ */
+int osmo_stat_item_for_each_item(struct osmo_stat_item_group *statg,
+	osmo_stat_item_handler_t handle_item, void *data);
+
+int osmo_stat_item_for_each_group(osmo_stat_item_group_handler_t handle_group, void *data);
+
+static inline int32_t osmo_stat_item_get_last(const struct osmo_stat_item *item)
+{
+	return item->values[item->last_offs].value;
+}
+/*! @} */
diff --git a/include/osmocom/core/statistics.h b/include/osmocom/core/statistics.h
index de250be..1e472ff 100644
--- a/include/osmocom/core/statistics.h
+++ b/include/osmocom/core/statistics.h
@@ -9,6 +9,7 @@
 	const char *name;		/*!< \brief human-readable name */
 	const char *description;	/*!< \brief humn-readable description */
 	unsigned long value;		/*!< \brief current value */
+	unsigned long previous;		/*!< \brief previous value */
 };
 
 /*! \brief Increment counter */
@@ -37,8 +38,8 @@
  */
 void osmo_counter_free(struct osmo_counter *ctr);
 
-/*! \brief Iteate over all counters
- *  \param[in] handle_counter Call-back function
+/*! \brief Iterate over all counters
+ *  \param[in] handle_counter Call-back function, aborts if rc < 0
  *  \param[in] data Private dtata handed through to \a handle_counter
  */
 int osmo_counters_for_each(int (*handle_counter)(struct osmo_counter *, void *), void *data);
@@ -48,3 +49,6 @@
  *  \returns pointer to counter (\ref osmo_counter) or NULL otherwise
  */
 struct osmo_counter *osmo_counter_get_by_name(const char *name);
+
+/*! \brief Return the counter difference since the last call to this function */
+int osmo_counter_difference(struct osmo_counter *ctr);
diff --git a/include/osmocom/core/stats.h b/include/osmocom/core/stats.h
new file mode 100644
index 0000000..731fdb9
--- /dev/null
+++ b/include/osmocom/core/stats.h
@@ -0,0 +1,110 @@
+/* (C) 2015 by Sysmocom s.f.m.c. GmbH
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+#pragma once
+
+#include <sys/socket.h>
+#include <osmocom/core/linuxlist.h>
+
+struct msgb;
+struct osmo_stat_item_group;
+struct osmo_stat_item_desc;
+struct rate_ctr_group;
+struct rate_ctr_desc;
+
+enum osmo_stats_class {
+	OSMO_STATS_CLASS_UNKNOWN,
+	OSMO_STATS_CLASS_GLOBAL,
+	OSMO_STATS_CLASS_PEER,
+	OSMO_STATS_CLASS_SUBSCRIBER,
+};
+
+enum osmo_stats_reporter_type {
+	OSMO_STATS_REPORTER_STATSD,
+	OSMO_STATS_REPORTER_LOG,
+};
+
+struct osmo_stats_reporter {
+	enum osmo_stats_reporter_type type;
+	char *name;
+
+	unsigned int have_net_config : 1;
+
+	/* config */
+	int enabled;
+	char *name_prefix;
+	char *dest_addr_str;
+	char *bind_addr_str;
+	int dest_port;
+	int mtu;
+	enum osmo_stats_class max_class;
+
+	/* state */
+	int running;
+	struct sockaddr dest_addr;
+	int dest_addr_len;
+	struct sockaddr bind_addr;
+	int bind_addr_len;
+	int fd;
+	struct msgb *buffer;
+	int agg_enabled;
+
+	struct llist_head list;
+	int (*open)(struct osmo_stats_reporter *srep);
+	int (*close)(struct osmo_stats_reporter *srep);
+	int (*send_counter)(struct osmo_stats_reporter *srep,
+		const struct rate_ctr_group *ctrg,
+		const struct rate_ctr_desc *desc,
+		int64_t value, int64_t delta);
+	int (*send_item)(struct osmo_stats_reporter *srep,
+		const struct osmo_stat_item_group *statg,
+		const struct osmo_stat_item_desc *desc,
+		int32_t value);
+};
+
+struct osmo_stats_config {
+	int interval;
+};
+
+extern struct osmo_stats_config *osmo_stats_config;
+
+void osmo_stats_init(void *ctx);
+int osmo_stats_report();
+
+int osmo_stats_set_interval(int interval);
+
+struct osmo_stats_reporter *osmo_stats_reporter_alloc(enum osmo_stats_reporter_type type,
+	const char *name);
+void osmo_stats_reporter_free(struct osmo_stats_reporter *srep);
+
+struct osmo_stats_reporter *osmo_stats_reporter_create_statsd(const char *name);
+struct osmo_stats_reporter *osmo_stats_reporter_create_log(const char *name);
+
+struct osmo_stats_reporter *osmo_stats_reporter_find(enum osmo_stats_reporter_type type,
+	const char *name);
+
+int osmo_stats_reporter_set_remote_addr(struct osmo_stats_reporter *srep, const char *addr);
+int osmo_stats_reporter_set_remote_port(struct osmo_stats_reporter *srep, int port);
+int osmo_stats_reporter_set_local_addr(struct osmo_stats_reporter *srep, const char *addr);
+int osmo_stats_reporter_set_mtu(struct osmo_stats_reporter *srep, int mtu);
+int osmo_stats_reporter_set_max_class(struct osmo_stats_reporter *srep,
+	enum osmo_stats_class class_id);
+int osmo_stats_reporter_set_name_prefix(struct osmo_stats_reporter *srep, const char *prefix);
+int osmo_stats_reporter_enable(struct osmo_stats_reporter *srep);
+int osmo_stats_reporter_disable(struct osmo_stats_reporter *srep);
diff --git a/include/osmocom/gprs/gprs_ns.h b/include/osmocom/gprs/gprs_ns.h
index d5a605d..7c3b23c 100644
--- a/include/osmocom/gprs/gprs_ns.h
+++ b/include/osmocom/gprs/gprs_ns.h
@@ -118,6 +118,7 @@
 
 	struct osmo_timer_list timer;
 	enum nsvc_timer_mode timer_mode;
+	struct timeval timer_started;
 	int alive_retries;
 
 	unsigned int remote_end_is_sgsn:1;
@@ -125,6 +126,7 @@
 	unsigned int nsvci_is_valid:1;
 
 	struct rate_ctr_group *ctrg;
+	struct osmo_stat_item_group *statg;
 
 	/*! \brief which link-layer are we based on? */
 	enum gprs_ns_ll ll;
diff --git a/include/osmocom/vty/command.h b/include/osmocom/vty/command.h
index 4eb519f..2ef4109 100644
--- a/include/osmocom/vty/command.h
+++ b/include/osmocom/vty/command.h
@@ -75,6 +75,7 @@
 	SERVICE_NODE,		/*!< \brief Service node. */
 	DEBUG_NODE,		/*!< \brief Debug node. */
 	CFG_LOG_NODE,		/*!< \brief Configure the logging */
+	CFG_STATS_NODE,		/*!< \brief Configure the statistics */
 
 	VTY_NODE,		/*!< \brief Vty node. */
 
@@ -83,6 +84,15 @@
 	L_NS_NODE,		/*!< \brief NS node in libosmo-gb. */
 	L_BSSGP_NODE,		/*!< \brief BSSGP node in libosmo-gb. */
 
+	/*
+	 * When adding new nodes to the libosmocore project, these nodes can be
+	 * used to avoid ABI changes for unrelated projects.
+	 */
+	RESERVED1_NODE,		/*!< \brief Reserved for later extensions */
+	RESERVED2_NODE,		/*!< \brief Reserved for later extensions */
+	RESERVED3_NODE,		/*!< \brief Reserved for later extensions */
+	RESERVED4_NODE,		/*!< \brief Reserved for later extensions */
+
 	_LAST_OSMOVTY_NODE
 };
 
diff --git a/include/osmocom/vty/misc.h b/include/osmocom/vty/misc.h
index db552e7..f3b46db 100644
--- a/include/osmocom/vty/misc.h
+++ b/include/osmocom/vty/misc.h
@@ -2,6 +2,7 @@
 
 #include <osmocom/vty/vty.h>
 #include <osmocom/core/rate_ctr.h>
+#include <osmocom/core/stat_item.h>
 #include <osmocom/core/utils.h>
 
 #define VTY_DO_LOWER		1
@@ -10,7 +11,12 @@
 				 const char *end, int do_lower);
 
 void vty_out_rate_ctr_group(struct vty *vty, const char *prefix,
-                            struct rate_ctr_group *ctrg);
+			    struct rate_ctr_group *ctrg);
+
+void vty_out_stat_item_group(struct vty *vty, const char *prefix,
+			     struct osmo_stat_item_group *statg);
+
+void vty_out_statistics_full(struct vty *vty, const char *prefix);
 
 int osmo_vty_write_config_file(const char *filename);
 int osmo_vty_save_config_file(void);
diff --git a/include/osmocom/vty/stats.h b/include/osmocom/vty/stats.h
new file mode 100644
index 0000000..3851b4d
--- /dev/null
+++ b/include/osmocom/vty/stats.h
@@ -0,0 +1,3 @@
+#pragma once
+
+void osmo_stats_vty_add_cmds();
diff --git a/src/Makefile.am b/src/Makefile.am
index 4bf3408..7aa6a78 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -15,7 +15,7 @@
 			 gsmtap_util.c crc16.c panic.c backtrace.c \
 			 conv.c application.c rbtree.c strrb.c \
 			 loggingrb.c crc8gen.c crc16gen.c crc32gen.c crc64gen.c \
-			 macaddr.c
+			 macaddr.c stat_item.c stats.c
 
 BUILT_SOURCES = crc8gen.c crc16gen.c crc32gen.c crc64gen.c
 
diff --git a/src/gb/gprs_bssgp.c b/src/gb/gprs_bssgp.c
index fe4fcca..e3e69c9 100644
--- a/src/gb/gprs_bssgp.c
+++ b/src/gb/gprs_bssgp.c
@@ -31,6 +31,7 @@
 #include <osmocom/gsm/tlv.h>
 #include <osmocom/core/talloc.h>
 #include <osmocom/core/rate_ctr.h>
+#include <osmocom/core/stats.h>
 
 #include <osmocom/gprs/gprs_bssgp.h>
 #include <osmocom/gprs/gprs_ns.h>
@@ -54,6 +55,7 @@
 	.group_description = "BSSGP Peer Statistics",
 	.num_ctr = ARRAY_SIZE(bssgp_ctr_description),
 	.ctr_desc = bssgp_ctr_description,
+	.class_id = OSMO_STATS_CLASS_PEER,
 };
 
 LLIST_HEAD(bssgp_bvc_ctxts);
diff --git a/src/gb/gprs_ns.c b/src/gb/gprs_ns.c
index 827d09d..2b189cd 100644
--- a/src/gb/gprs_ns.c
+++ b/src/gb/gprs_ns.c
@@ -75,6 +75,8 @@
 #include <osmocom/core/talloc.h>
 #include <osmocom/core/select.h>
 #include <osmocom/core/rate_ctr.h>
+#include <osmocom/core/stat_item.h>
+#include <osmocom/core/stats.h>
 #include <osmocom/core/socket.h>
 #include <osmocom/core/signal.h>
 #include <osmocom/gprs/gprs_ns.h>
@@ -104,6 +106,8 @@
 	NS_CTR_NSEI_CHG,
 	NS_CTR_INV_VCI,
 	NS_CTR_INV_NSEI,
+	NS_CTR_LOST_ALIVE,
+	NS_CTR_LOST_RESET,
 };
 
 static const struct rate_ctr_desc nsvc_ctr_description[] = {
@@ -117,6 +121,8 @@
 	{ "nsei-chg",	"NS-VC changed NSEI count  " },
 	{ "inv-nsvci",	"NS-VCI was invalid count  " },
 	{ "inv-nsei",	"NSEI was invalid count    " },
+	{ "lost.alive",	"ALIVE ACK missing count   " },
+	{ "lost.reset",	"RESET ACK missing count   " },
 };
 
 static const struct rate_ctr_group_desc nsvc_ctrg_desc = {
@@ -126,6 +132,22 @@
 	.ctr_desc = nsvc_ctr_description,
 };
 
+enum ns_stat {
+	NS_STAT_ALIVE_DELAY,
+};
+
+static const struct osmo_stat_item_desc nsvc_stat_description[] = {
+	{ "alive.delay", "ALIVE reponse time        ", "ms", 16, 0 },
+};
+
+static const struct osmo_stat_item_group_desc nsvc_statg_desc = {
+	.group_name_prefix = "ns.nsvc",
+	.group_description = "NSVC Peer Statistics",
+	.num_items = ARRAY_SIZE(nsvc_stat_description),
+	.item_desc = nsvc_stat_description,
+	.class_id = OSMO_STATS_CLASS_PEER,
+};
+
 #define CHECK_TX_RC(rc, nsvc) \
 		if (rc < 0)							\
 			LOGP(DNS, LOGL_ERROR, "TX failed (%d) to peer %s\n",	\
@@ -218,6 +240,7 @@
 	nsvc->timer.cb = gprs_ns_timer_cb;
 	nsvc->timer.data = nsvc;
 	nsvc->ctrg = rate_ctr_group_alloc(nsvc, &nsvc_ctrg_desc, nsvci);
+	nsvc->statg = osmo_stat_item_group_alloc(nsvc, &nsvc_statg_desc, nsvci);
 
 	llist_add(&nsvc->list, &nsi->gprs_nsvcs);
 
@@ -531,10 +554,20 @@
 	if (osmo_timer_pending(&nsvc->timer))
 		osmo_timer_del(&nsvc->timer);
 
+	gettimeofday(&nsvc->timer_started, NULL);
 	nsvc->timer_mode = mode;
 	osmo_timer_schedule(&nsvc->timer, seconds, 0);
 }
 
+static int nsvc_timer_elapsed_ms(struct gprs_nsvc *nsvc)
+{
+	struct timeval now, elapsed;
+	gettimeofday(&now, NULL);
+	timersub(&now, &nsvc->timer_started, &elapsed);
+
+	return 1000 * elapsed.tv_sec + elapsed.tv_usec / 1000;
+}
+
 static void gprs_ns_timer_cb(void *data)
 {
 	struct gprs_nsvc *nsvc = data;
@@ -549,6 +582,7 @@
 	switch (nsvc->timer_mode) {
 	case NSVC_TIMER_TNS_ALIVE:
 		/* Tns-alive case: we expired without response ! */
+		rate_ctr_inc(&nsvc->ctrg->ctr[NS_CTR_LOST_ALIVE]);
 		nsvc->alive_retries++;
 		if (nsvc->alive_retries >
 			nsvc->nsi->timeout[NS_TOUT_TNS_ALIVE_RETRIES]) {
@@ -578,6 +612,7 @@
 		nsvc_start_timer(nsvc, NSVC_TIMER_TNS_ALIVE);
 		break;
 	case NSVC_TIMER_TNS_RESET:
+		rate_ctr_inc(&nsvc->ctrg->ctr[NS_CTR_LOST_RESET]);
 		/* Chapter 7.3: Re-send the RESET */
 		gprs_ns_tx_reset(nsvc, NS_CAUSE_OM_INTERVENTION);
 		/* Re-start Tns-reset timer */
@@ -1272,6 +1307,9 @@
 			rc = gprs_ns_tx_alive_ack(*nsvc);
 		break;
 	case NS_PDUT_ALIVE_ACK:
+		if ((*nsvc)->timer_mode == NSVC_TIMER_TNS_ALIVE)
+			osmo_stat_item_set((*nsvc)->statg->items[NS_STAT_ALIVE_DELAY],
+				nsvc_timer_elapsed_ms(*nsvc));
 		/* stop Tns-alive and start Tns-test */
 		nsvc_start_timer(*nsvc, NSVC_TIMER_TNS_TEST);
 		if ((*nsvc)->remote_end_is_sgsn) {
diff --git a/src/gb/gprs_ns_vty.c b/src/gb/gprs_ns_vty.c
index 155e1e9..5a951dc 100644
--- a/src/gb/gprs_ns_vty.c
+++ b/src/gb/gprs_ns_vty.c
@@ -167,8 +167,10 @@
 			inet_ntoa(nsvc->ip.bts_addr.sin_addr),
 			ntohs(nsvc->ip.bts_addr.sin_port));
 	vty_out(vty, "%s", VTY_NEWLINE);
-	if (stats)
+	if (stats) {
 		vty_out_rate_ctr_group(vty, " ", nsvc->ctrg);
+		vty_out_stat_item_group(vty, " ", nsvc->statg);
+	}
 }
 
 static void dump_ns(struct vty *vty, struct gprs_ns_inst *nsi, int stats)
diff --git a/src/logging.c b/src/logging.c
index 20b0596..876964a 100644
--- a/src/logging.c
+++ b/src/logging.c
@@ -117,6 +117,11 @@
 		.description = "GPRS GTP library",
 		.enabled = 1, .loglevel = LOGL_NOTICE,
 	},
+	[INT2IDX(DLSTATS)] = {
+		.name = "DLSTATS",
+		.description = "Statistics messages and logging",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
 };
 
 /*! \brief descriptive string for each log level */
diff --git a/src/rate_ctr.c b/src/rate_ctr.c
index 8a232e8..50b3fe7 100644
--- a/src/rate_ctr.c
+++ b/src/rate_ctr.c
@@ -83,6 +83,15 @@
 	ctr->current += inc;
 }
 
+/*! \brief Return the counter difference since the last call to this function */
+int64_t rate_ctr_difference(struct rate_ctr *ctr)
+{
+	int64_t result = ctr->current - ctr->previous;
+	ctr->previous = ctr->current;
+
+	return result;
+}
+
 static void interval_expired(struct rate_ctr *ctr, enum rate_ctr_intv intv)
 {
 	/* calculate rate over last interval */
@@ -177,4 +186,36 @@
 	return NULL;
 }
 
+int rate_ctr_for_each_counter(struct rate_ctr_group *ctrg,
+	rate_ctr_handler_t handle_counter, void *data)
+{
+	int rc = 0;
+	int i;
+
+	for (i = 0; i < ctrg->desc->num_ctr; i++) {
+		struct rate_ctr *ctr = &ctrg->ctr[i];
+		rc = handle_counter(ctrg,
+			ctr, &ctrg->desc->ctr_desc[i], data);
+		if (rc < 0)
+			return rc;
+	}
+
+	return rc;
+}
+
+int rate_ctr_for_each_group(rate_ctr_group_handler_t handle_group, void *data)
+{
+	struct rate_ctr_group *statg;
+	int rc = 0;
+
+	llist_for_each_entry(statg, &rate_ctr_groups, list) {
+		rc = handle_group(statg, data);
+		if (rc < 0)
+			return rc;
+	}
+
+	return rc;
+}
+
+
 /*! @} */
diff --git a/src/stat_item.c b/src/stat_item.c
new file mode 100644
index 0000000..0545ea0
--- /dev/null
+++ b/src/stat_item.c
@@ -0,0 +1,268 @@
+/* utility routines for keeping conters about events and the event rates */
+
+/* (C) 2015 by Sysmocom s.f.m.c. GmbH
+ * (C) 2009-2010 by Harald Welte <laforge@gnumonks.org>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+/*! \addtogroup osmo_stat_item
+ *  @{
+ */
+
+/*! \file stat_item.c */
+
+
+#include <stdint.h>
+#include <string.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/timer.h>
+#include <osmocom/core/stat_item.h>
+
+static LLIST_HEAD(osmo_stat_item_groups);
+static int32_t global_value_id = 0;
+
+static void *tall_stat_item_ctx;
+
+/*! \brief Allocate a new group of counters according to description
+ *  \param[in] ctx \ref talloc context
+ *  \param[in] desc Statistics item group description
+ *  \param[in] idx Index of new stat item group
+ */
+struct osmo_stat_item_group *osmo_stat_item_group_alloc(void *ctx,
+					    const struct osmo_stat_item_group_desc *desc,
+					    unsigned int idx)
+{
+	unsigned int group_size;
+	unsigned int items_size = 0;
+	unsigned int item_idx;
+	void *items;
+
+	struct osmo_stat_item_group *group;
+
+	group_size = sizeof(struct osmo_stat_item_group) +
+			desc->num_items * sizeof(struct osmo_stat_item *);
+
+	if (!ctx)
+		ctx = tall_stat_item_ctx;
+
+	group = talloc_zero_size(ctx, group_size);
+	if (!group)
+		return NULL;
+
+	group->desc = desc;
+	group->idx = idx;
+
+	/* Get combined size of all items */
+	for (item_idx = 0; item_idx < desc->num_items; item_idx++) {
+		unsigned int size;
+		size = sizeof(struct osmo_stat_item) +
+			sizeof(struct osmo_stat_item_value) *
+			desc->item_desc[item_idx].num_values;
+		/* Align to pointer size */
+		size = (size + sizeof(void *) - 1) & ~(sizeof(void *) - 1);
+
+		/* Store offsets into the item array */
+		group->items[item_idx] = (void *)items_size;
+
+		items_size += size;
+	}
+
+	items = talloc_zero_size(group, items_size);
+	if (!items) {
+		talloc_free(group);
+		return NULL;
+	}
+
+	/* Update item pointers */
+	for (item_idx = 0; item_idx < desc->num_items; item_idx++) {
+		struct osmo_stat_item *item = (struct osmo_stat_item *)
+			((uint8_t *)items + (int)group->items[item_idx]);
+		unsigned int i;
+
+		group->items[item_idx] = item;
+		item->last_offs = desc->item_desc[item_idx].num_values - 1;
+		item->last_value_index = -1;
+		item->desc = &desc->item_desc[item_idx];
+
+		for (i = 0; i <= item->last_offs; i++) {
+			item->values[i].value = desc->item_desc[item_idx].default_value;
+			item->values[i].id = STAT_ITEM_NOVALUE_ID;
+		}
+	}
+
+	llist_add(&group->list, &osmo_stat_item_groups);
+
+	return group;
+}
+
+/*! \brief Free the memory for the specified group of counters */
+void osmo_stat_item_group_free(struct osmo_stat_item_group *grp)
+{
+	llist_del(&grp->list);
+	talloc_free(grp);
+}
+
+void osmo_stat_item_set(struct osmo_stat_item *item, int32_t value)
+{
+	item->last_offs += 1;
+	if (item->last_offs >= item->desc->num_values)
+		item->last_offs = 0;
+
+	global_value_id += 1;
+	if (global_value_id == STAT_ITEM_NOVALUE_ID)
+		global_value_id += 1;
+
+	item->values[item->last_offs].value = value;
+	item->values[item->last_offs].id    = global_value_id;
+}
+
+int osmo_stat_item_get_next(const struct osmo_stat_item *item, int32_t *next_idx,
+	int32_t *value)
+{
+	const struct osmo_stat_item_value *next_value;
+	const struct osmo_stat_item_value *item_value = NULL;
+	int idx_delta;
+	int next_offs;
+
+	next_offs = item->last_offs;
+	next_value = &item->values[next_offs];
+
+	while (next_value->id - *next_idx >= 0 &&
+		next_value->id != STAT_ITEM_NOVALUE_ID)
+	{
+		item_value = next_value;
+
+		next_offs -= 1;
+		if (next_offs < 0)
+			next_offs = item->desc->num_values - 1;
+		if (next_offs == item->last_offs)
+			break;
+		next_value = &item->values[next_offs];
+	}
+
+	if (!item_value)
+		/* All items have been read */
+		return 0;
+
+	*value = item_value->value;
+
+	idx_delta = item_value->id + 1 - *next_idx;
+
+	*next_idx = item_value->id + 1;
+
+	return idx_delta;
+}
+
+/*! \brief Skip all values of this item and update idx accordingly */
+int osmo_stat_item_discard(const struct osmo_stat_item *item, int32_t *idx)
+{
+	int discarded = item->values[item->last_offs].id + 1 - *idx;
+	*idx = item->values[item->last_offs].id + 1;
+
+	return discarded;
+}
+
+/*! \brief Skip all values of all items and update idx accordingly */
+int osmo_stat_item_discard_all(int32_t *idx)
+{
+	int discarded = global_value_id + 1 - *idx;
+	*idx = global_value_id + 1;
+
+	return discarded;
+}
+
+/*! \brief Initialize the stat item module */
+int osmo_stat_item_init(void *tall_ctx)
+{
+	tall_stat_item_ctx = tall_ctx;
+
+	return 0;
+}
+
+/*! \brief Search for item group based on group name and index */
+struct osmo_stat_item_group *osmo_stat_item_get_group_by_name_idx(
+	const char *name, const unsigned int idx)
+{
+	struct osmo_stat_item_group *statg;
+
+	llist_for_each_entry(statg, &osmo_stat_item_groups, list) {
+		if (!statg->desc)
+			continue;
+
+		if (!strcmp(statg->desc->group_name_prefix, name) &&
+				statg->idx == idx)
+			return statg;
+	}
+	return NULL;
+}
+
+/*! \brief Search for item group based on group name */
+const struct osmo_stat_item *osmo_stat_item_get_by_name(
+	const struct osmo_stat_item_group *statg, const char *name)
+{
+	int i;
+	const struct osmo_stat_item_desc *item_desc;
+
+	if (!statg->desc)
+		return NULL;
+
+	for (i = 0; i < statg->desc->num_items; i++) {
+		item_desc = &statg->desc->item_desc[i];
+
+		if (!strcmp(item_desc->name, name)) {
+			return statg->items[i];
+		}
+	}
+	return NULL;
+}
+
+int osmo_stat_item_for_each_item(struct osmo_stat_item_group *statg,
+	osmo_stat_item_handler_t handle_item, void *data)
+{
+	int rc = 0;
+	int i;
+
+	for (i = 0; i < statg->desc->num_items; i++) {
+		struct osmo_stat_item *item = statg->items[i];
+		rc = handle_item(statg, item, data);
+		if (rc < 0)
+			return rc;
+	}
+
+	return rc;
+}
+
+int osmo_stat_item_for_each_group(osmo_stat_item_group_handler_t handle_group, void *data)
+{
+	struct osmo_stat_item_group *statg;
+	int rc = 0;
+
+	llist_for_each_entry(statg, &osmo_stat_item_groups, list) {
+		rc = handle_group(statg, data);
+		if (rc < 0)
+			return rc;
+	}
+
+	return rc;
+}
+
+/*! @} */
diff --git a/src/statistics.c b/src/statistics.c
index e28541b..ad069ce 100644
--- a/src/statistics.c
+++ b/src/statistics.c
@@ -74,3 +74,11 @@
 	}
 	return NULL;
 }
+
+int osmo_counter_difference(struct osmo_counter *ctr)
+{
+	int delta = ctr->value - ctr->previous;
+	ctr->previous = ctr->value;
+
+	return delta;
+}
diff --git a/src/stats.c b/src/stats.c
new file mode 100644
index 0000000..f979bdc
--- /dev/null
+++ b/src/stats.c
@@ -0,0 +1,696 @@
+/*
+ * (C) 2015 by Sysmocom s.f.m.c. GmbH
+ *
+ * Author: Jacob Erlbeck <jerlbeck@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include <osmocom/core/stats.h>
+
+#include <unistd.h>
+#include <string.h>
+#include <stdint.h>
+#include <errno.h>
+#include <stdio.h>
+#include <sys/socket.h>
+#include <netinet/ip.h>
+#include <arpa/inet.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/rate_ctr.h>
+#include <osmocom/core/stat_item.h>
+#include <osmocom/core/timer.h>
+#include <osmocom/core/statistics.h>
+#include <osmocom/core/msgb.h>
+
+#define STATS_DEFAULT_INTERVAL 5 /* secs */
+#define STATS_DEFAULT_STATSD_BUFLEN 256
+
+static LLIST_HEAD(osmo_stats_reporter_list);
+static void *osmo_stats_ctx = NULL;
+static int is_initialised = 0;
+static int32_t current_stat_item_index = 0;
+
+static struct osmo_stats_config s_stats_config = {
+	.interval = STATS_DEFAULT_INTERVAL,
+};
+struct osmo_stats_config *osmo_stats_config = &s_stats_config;
+
+static struct osmo_timer_list osmo_stats_timer;
+
+static int osmo_stats_reporter_statsd_open(struct osmo_stats_reporter *srep);
+static int osmo_stats_reporter_statsd_close(struct osmo_stats_reporter *srep);
+static int osmo_stats_reporter_statsd_send_counter(struct osmo_stats_reporter *srep,
+	const struct rate_ctr_group *ctrg,
+	const struct rate_ctr_desc *desc,
+	int64_t value, int64_t delta);
+static int osmo_stats_reporter_statsd_send_item(struct osmo_stats_reporter *srep,
+	const struct osmo_stat_item_group *statg,
+	const struct osmo_stat_item_desc *desc, int value);
+
+static int osmo_stats_reporter_log_send_counter(struct osmo_stats_reporter *srep,
+	const struct rate_ctr_group *ctrg,
+	const struct rate_ctr_desc *desc,
+	int64_t value, int64_t delta);
+static int osmo_stats_reporter_log_send_item(struct osmo_stats_reporter *srep,
+	const struct osmo_stat_item_group *statg,
+	const struct osmo_stat_item_desc *desc, int value);
+
+static int osmo_stats_reporter_send(struct osmo_stats_reporter *srep, const char *data,
+	int data_len);
+static int osmo_stats_reporter_send_buffer(struct osmo_stats_reporter *srep);
+
+static int update_srep_config(struct osmo_stats_reporter *srep)
+{
+	int rc = 0;
+
+	if (srep->running) {
+		if (srep->close)
+			rc = srep->close(srep);
+		srep->running = 0;
+	}
+
+	if (!srep->enabled)
+		return rc;
+
+	if (srep->open)
+		rc = srep->open(srep);
+	else
+		rc = 0;
+
+	if (rc < 0)
+		srep->enabled = 0;
+	else
+		srep->running = 1;
+
+	return rc;
+}
+
+static void osmo_stats_timer_cb(void *data)
+{
+	int interval = osmo_stats_config->interval;
+
+	if (!llist_empty(&osmo_stats_reporter_list))
+		osmo_stats_report();
+
+	osmo_timer_schedule(&osmo_stats_timer, interval, 0);
+}
+
+static int start_timer()
+{
+	if (!is_initialised)
+		return -ESRCH;
+
+	osmo_stats_timer.cb = osmo_stats_timer_cb;
+	osmo_timer_schedule(&osmo_stats_timer, 0, 1);
+
+	return 0;
+}
+
+struct osmo_stats_reporter *osmo_stats_reporter_alloc(enum osmo_stats_reporter_type type,
+	const char *name)
+{
+	struct osmo_stats_reporter *srep;
+	srep = talloc_zero(osmo_stats_ctx, struct osmo_stats_reporter);
+	OSMO_ASSERT(srep);
+	srep->type = type;
+	if (name)
+		srep->name = talloc_strdup(srep, name);
+	srep->fd = -1;
+
+	llist_add(&srep->list, &osmo_stats_reporter_list);
+
+	return srep;
+}
+
+void osmo_stats_reporter_free(struct osmo_stats_reporter *srep)
+{
+	osmo_stats_reporter_disable(srep);
+	llist_del(&srep->list);
+	talloc_free(srep);
+}
+
+void osmo_stats_init(void *ctx)
+{
+	osmo_stats_ctx = ctx;
+	osmo_stat_item_discard_all(&current_stat_item_index);
+
+	is_initialised = 1;
+	start_timer();
+}
+
+struct osmo_stats_reporter *osmo_stats_reporter_find(enum osmo_stats_reporter_type type,
+	const char *name)
+{
+	struct osmo_stats_reporter *srep;
+	llist_for_each_entry(srep, &osmo_stats_reporter_list, list) {
+		if (srep->type != type)
+			continue;
+		if (srep->name != name) {
+			if (name == NULL || srep->name == NULL ||
+				strcmp(name, srep->name) != 0)
+				continue;
+		}
+		return srep;
+	}
+	return NULL;
+}
+
+int osmo_stats_reporter_set_remote_addr(struct osmo_stats_reporter *srep, const char *addr)
+{
+	int rc;
+	struct sockaddr_in *sock_addr = (struct sockaddr_in *)&srep->dest_addr;
+	struct in_addr inaddr;
+
+	if (!srep->have_net_config)
+		return -ENOTSUP;
+
+	OSMO_ASSERT(addr != NULL);
+
+	rc = inet_pton(AF_INET, addr, &inaddr);
+	if (rc <= 0)
+		return -EINVAL;
+
+	sock_addr->sin_addr = inaddr;
+	sock_addr->sin_family = AF_INET;
+	srep->dest_addr_len = sizeof(*sock_addr);
+
+	talloc_free(srep->dest_addr_str);
+	srep->dest_addr_str = talloc_strdup(srep, addr);
+
+	return update_srep_config(srep);
+}
+
+int osmo_stats_reporter_set_remote_port(struct osmo_stats_reporter *srep, int port)
+{
+	struct sockaddr_in *sock_addr = (struct sockaddr_in *)&srep->dest_addr;
+
+	if (!srep->have_net_config)
+		return -ENOTSUP;
+
+	srep->dest_port = port;
+	sock_addr->sin_port = htons(port);
+
+	return update_srep_config(srep);
+}
+
+int osmo_stats_reporter_set_local_addr(struct osmo_stats_reporter *srep, const char *addr)
+{
+	int rc;
+	struct sockaddr_in *sock_addr = (struct sockaddr_in *)&srep->bind_addr;
+	struct in_addr inaddr;
+
+	if (!srep->have_net_config)
+		return -ENOTSUP;
+
+	if (addr) {
+		rc = inet_pton(AF_INET, addr, &inaddr);
+		if (rc <= 0)
+			return -EINVAL;
+	} else {
+		inaddr.s_addr = INADDR_ANY;
+	}
+
+	sock_addr->sin_addr = inaddr;
+	sock_addr->sin_family = AF_INET;
+	srep->bind_addr_len = addr ? sizeof(*sock_addr) : 0;
+
+	talloc_free(srep->bind_addr_str);
+	srep->bind_addr_str = addr ? talloc_strdup(srep, addr) : NULL;
+
+	return update_srep_config(srep);
+}
+
+int osmo_stats_reporter_set_mtu(struct osmo_stats_reporter *srep, int mtu)
+{
+	if (!srep->have_net_config)
+		return -ENOTSUP;
+
+	if (mtu < 0)
+		return -EINVAL;
+
+	srep->mtu = mtu;
+
+	return update_srep_config(srep);
+}
+
+int osmo_stats_reporter_set_max_class(struct osmo_stats_reporter *srep,
+	enum osmo_stats_class class_id)
+{
+	if (class_id == OSMO_STATS_CLASS_UNKNOWN)
+		return -EINVAL;
+
+	srep->max_class = class_id;
+
+	return 0;
+}
+
+int osmo_stats_set_interval(int interval)
+{
+	if (interval <= 0)
+		return -EINVAL;
+
+	osmo_stats_config->interval = interval;
+	if (is_initialised)
+		start_timer();
+
+	return 0;
+}
+
+int osmo_stats_reporter_set_name_prefix(struct osmo_stats_reporter *srep, const char *prefix)
+{
+	talloc_free(srep->name_prefix);
+	srep->name_prefix = prefix ? talloc_strdup(srep, prefix) : NULL;
+
+	return update_srep_config(srep);
+}
+
+int osmo_stats_reporter_enable(struct osmo_stats_reporter *srep)
+{
+	srep->enabled = 1;
+
+	return update_srep_config(srep);
+}
+
+int osmo_stats_reporter_disable(struct osmo_stats_reporter *srep)
+{
+	srep->enabled = 0;
+
+	return update_srep_config(srep);
+}
+
+static int osmo_stats_reporter_send(struct osmo_stats_reporter *srep, const char *data,
+	int data_len)
+{
+	int rc;
+
+	rc = sendto(srep->fd, data, data_len, MSG_NOSIGNAL | MSG_DONTWAIT,
+		&srep->dest_addr, srep->dest_addr_len);
+
+	if (rc == -1)
+		rc = -errno;
+
+	return rc;
+}
+
+static int osmo_stats_reporter_send_buffer(struct osmo_stats_reporter *srep)
+{
+	int rc;
+
+	if (!srep->buffer || msgb_length(srep->buffer) == 0)
+		return 0;
+
+	rc = osmo_stats_reporter_send(srep,
+		(const char *)msgb_data(srep->buffer), msgb_length(srep->buffer));
+
+	msgb_trim(srep->buffer, 0);
+
+	return rc;
+}
+
+static int osmo_stats_reporter_check_config(struct osmo_stats_reporter *srep,
+	unsigned int index, int class_id)
+{
+	if (class_id == OSMO_STATS_CLASS_UNKNOWN)
+		class_id = index != 0 ?
+			OSMO_STATS_CLASS_SUBSCRIBER : OSMO_STATS_CLASS_GLOBAL;
+
+	return class_id <= srep->max_class;
+}
+
+/*** log reporter ***/
+
+struct osmo_stats_reporter *osmo_stats_reporter_create_log(const char *name)
+{
+	struct osmo_stats_reporter *srep;
+	srep = osmo_stats_reporter_alloc(OSMO_STATS_REPORTER_LOG, name);
+
+	srep->have_net_config = 0;
+
+	srep->send_counter = osmo_stats_reporter_log_send_counter;
+	srep->send_item = osmo_stats_reporter_log_send_item;
+
+	return srep;
+}
+
+static int osmo_stats_reporter_log_send(struct osmo_stats_reporter *srep,
+	const char *type,
+	const char *name1, unsigned int index1, const char *name2, int value,
+	const char *unit)
+{
+	LOGP(DLSTATS, LOGL_INFO,
+		"stats t=%s p=%s g=%s i=%u n=%s v=%d u=%s\n",
+		type, srep->name_prefix ? srep->name_prefix : "",
+		name1 ? name1 : "", index1,
+		name2, value, unit ? unit : "");
+
+	return 0;
+}
+
+
+static int osmo_stats_reporter_log_send_counter(struct osmo_stats_reporter *srep,
+	const struct rate_ctr_group *ctrg,
+	const struct rate_ctr_desc *desc,
+	int64_t value, int64_t delta)
+{
+	if (ctrg)
+		return osmo_stats_reporter_log_send(srep, "c",
+			ctrg->desc->group_name_prefix,
+			ctrg->idx,
+			desc->name, value, NULL);
+	else
+		return osmo_stats_reporter_log_send(srep, "c",
+			NULL, 0,
+			desc->name, value, NULL);
+}
+
+static int osmo_stats_reporter_log_send_item(struct osmo_stats_reporter *srep,
+	const struct osmo_stat_item_group *statg,
+	const struct osmo_stat_item_desc *desc, int value)
+{
+	return osmo_stats_reporter_log_send(srep, "i",
+		statg->desc->group_name_prefix, statg->idx,
+		desc->name, value, desc->unit);
+}
+
+/*** statsd reporter ***/
+
+struct osmo_stats_reporter *osmo_stats_reporter_create_statsd(const char *name)
+{
+	struct osmo_stats_reporter *srep;
+	srep = osmo_stats_reporter_alloc(OSMO_STATS_REPORTER_STATSD, name);
+
+	srep->have_net_config = 1;
+
+	srep->open = osmo_stats_reporter_statsd_open;
+	srep->close = osmo_stats_reporter_statsd_close;
+	srep->send_counter = osmo_stats_reporter_statsd_send_counter;
+	srep->send_item = osmo_stats_reporter_statsd_send_item;
+
+	return srep;
+}
+
+static int osmo_stats_reporter_statsd_open(struct osmo_stats_reporter *srep)
+{
+	int sock;
+	int rc;
+	int buffer_size = STATS_DEFAULT_STATSD_BUFLEN;
+
+	if (srep->fd != -1)
+		osmo_stats_reporter_statsd_close(srep);
+
+	sock = socket(AF_INET, SOCK_DGRAM, 0);
+	if (sock == -1)
+		return -errno;
+
+	if (srep->bind_addr_len > 0) {
+		rc = bind(sock, &srep->bind_addr, srep->bind_addr_len);
+		if (rc == -1)
+			goto failed;
+	}
+
+	srep->fd = sock;
+
+	if (srep->mtu > 0) {
+		buffer_size = srep->mtu - 20 /* IP */ - 8 /* UDP */;
+		srep->agg_enabled = 1;
+	}
+
+	srep->buffer = msgb_alloc(buffer_size, "stats buffer");
+
+	return 0;
+
+failed:
+	rc = -errno;
+	close(sock);
+
+	return rc;
+}
+
+static int osmo_stats_reporter_statsd_close(struct osmo_stats_reporter *srep)
+{
+	int rc;
+	if (srep->fd == -1)
+		return -EBADF;
+
+	osmo_stats_reporter_send_buffer(srep);
+
+	rc = close(srep->fd);
+	srep->fd = -1;
+	msgb_free(srep->buffer);
+	srep->buffer = NULL;
+	return rc == -1 ? -errno : 0;
+}
+
+static int osmo_stats_reporter_statsd_send(struct osmo_stats_reporter *srep,
+	const char *name1, unsigned int index1, const char *name2, int value,
+	const char *unit)
+{
+	char *buf;
+	int buf_size;
+	int nchars, rc = 0;
+	char *fmt = NULL;
+	int old_len = msgb_length(srep->buffer);
+
+	if (name1) {
+		if (index1 != 0)
+			fmt = "%1$s.%2$s.%6$u.%3$s:%4$d|%5$s";
+		else
+			fmt = "%1$s.%2$s.%3$s:%4$d|%5$s";
+	} else {
+		fmt = "%1$s.%2$0.0s%3$s:%4$d|%5$s";
+	}
+	if (!srep->name_prefix)
+		fmt += 5; /* skip prefix part */
+
+	if (srep->agg_enabled) {
+		if (msgb_length(srep->buffer) > 0 &&
+			msgb_tailroom(srep->buffer) > 0)
+		{
+			msgb_put_u8(srep->buffer, '\n');
+		}
+	}
+
+	buf = (char *)msgb_put(srep->buffer, 0);
+	buf_size = msgb_tailroom(srep->buffer);
+
+	nchars = snprintf(buf, buf_size, fmt,
+		srep->name_prefix, name1, name2,
+		value, unit, index1);
+
+	if (nchars >= buf_size) {
+		/* Truncated */
+		/* Restore original buffer (without trailing LF) */
+		msgb_trim(srep->buffer, old_len);
+		/* Send it */
+		rc = osmo_stats_reporter_send_buffer(srep);
+
+		/* Try again */
+		buf = (char *)msgb_put(srep->buffer, 0);
+		buf_size = msgb_tailroom(srep->buffer);
+
+		nchars = snprintf(buf, buf_size, fmt,
+			srep->name_prefix, name1, name2,
+			value, unit, index1);
+
+		if (nchars >= buf_size)
+			return -EMSGSIZE;
+	}
+
+	if (nchars > 0)
+		msgb_trim(srep->buffer, msgb_length(srep->buffer) + nchars);
+
+	if (!srep->agg_enabled)
+		rc = osmo_stats_reporter_send_buffer(srep);
+
+	return rc;
+}
+
+static int osmo_stats_reporter_statsd_send_counter(struct osmo_stats_reporter *srep,
+	const struct rate_ctr_group *ctrg,
+	const struct rate_ctr_desc *desc,
+	int64_t value, int64_t delta)
+{
+	if (ctrg)
+		return osmo_stats_reporter_statsd_send(srep,
+			ctrg->desc->group_name_prefix,
+			ctrg->idx,
+			desc->name, delta, "c");
+	else
+		return osmo_stats_reporter_statsd_send(srep,
+			NULL, 0,
+			desc->name, delta, "c");
+}
+
+static int osmo_stats_reporter_statsd_send_item(struct osmo_stats_reporter *srep,
+	const struct osmo_stat_item_group *statg,
+	const struct osmo_stat_item_desc *desc, int value)
+{
+	return osmo_stats_reporter_statsd_send(srep,
+		statg->desc->group_name_prefix,
+		statg->idx,
+		desc->name, value, desc->unit);
+}
+
+/*** generic rate counter support ***/
+
+static int osmo_stats_reporter_send_counter(struct osmo_stats_reporter *srep,
+	const struct rate_ctr_group *ctrg,
+	const struct rate_ctr_desc *desc,
+	int64_t value, int64_t delta)
+{
+	if (!srep->send_counter)
+		return 0;
+
+	return srep->send_counter(srep, ctrg, desc, value, delta);
+}
+
+static int rate_ctr_handler(
+	struct rate_ctr_group *ctrg, struct rate_ctr *ctr,
+	const struct rate_ctr_desc *desc, void *sctx_)
+{
+	struct osmo_stats_reporter *srep;
+	int64_t delta = rate_ctr_difference(ctr);
+
+	if (delta == 0)
+		return 0;
+
+	llist_for_each_entry(srep, &osmo_stats_reporter_list, list) {
+		if (!srep->running)
+			continue;
+
+		if (!osmo_stats_reporter_check_config(srep,
+			       ctrg->idx, ctrg->desc->class_id))
+			return 0;
+
+		osmo_stats_reporter_send_counter(srep, ctrg, desc,
+			ctr->current, delta);
+
+		/* TODO: handle result (log?, inc counter(!)?) or remove it */
+	}
+
+	return 0;
+}
+
+static int rate_ctr_group_handler(struct rate_ctr_group *ctrg, void *sctx_)
+{
+	rate_ctr_for_each_counter(ctrg, rate_ctr_handler, sctx_);
+
+	return 0;
+}
+
+/*** stat item support ***/
+
+static int osmo_stats_reporter_send_item(struct osmo_stats_reporter *srep,
+	const struct osmo_stat_item_group *statg,
+	const struct osmo_stat_item_desc *desc,
+	int32_t value)
+{
+	if (!srep->send_item)
+		return 0;
+
+	return srep->send_item(srep, statg, desc, value);
+}
+
+static int osmo_stat_item_handler(
+	struct osmo_stat_item_group *statg, struct osmo_stat_item *item, void *sctx_)
+{
+	struct osmo_stats_reporter *srep;
+	int32_t idx = current_stat_item_index;
+	int32_t value;
+
+	while (osmo_stat_item_get_next(item, &idx, &value) > 0) {
+		llist_for_each_entry(srep, &osmo_stats_reporter_list, list) {
+			if (!srep->running)
+				continue;
+
+			if (!osmo_stats_reporter_check_config(srep,
+					statg->idx, statg->desc->class_id))
+				return 0;
+
+			osmo_stats_reporter_send_item(srep, statg,
+				item->desc, value);
+		}
+	}
+
+	return 0;
+}
+
+static int osmo_stat_item_group_handler(struct osmo_stat_item_group *statg, void *sctx_)
+{
+	osmo_stat_item_for_each_item(statg, osmo_stat_item_handler, sctx_);
+	osmo_stat_item_discard_all(&current_stat_item_index);
+
+	return 0;
+}
+
+/*** osmo counter support ***/
+
+static int handle_counter(struct osmo_counter *counter, void *sctx_)
+{
+	struct osmo_stats_reporter *srep;
+	struct rate_ctr_desc desc = {0};
+	/* Fake a rate counter description */
+	desc.name = counter->name;
+	desc.description = counter->description;
+
+	int delta = osmo_counter_difference(counter);
+
+	if (delta == 0)
+		return 0;
+
+	llist_for_each_entry(srep, &osmo_stats_reporter_list, list) {
+		if (!srep->running)
+			continue;
+
+		osmo_stats_reporter_send_counter(srep, NULL, &desc,
+			counter->value, delta);
+
+		/* TODO: handle result (log?, inc counter(!)?) */
+	}
+
+	return 0;
+}
+
+
+/*** main reporting function ***/
+
+static void flush_all_reporters()
+{
+	struct osmo_stats_reporter *srep;
+
+	llist_for_each_entry(srep, &osmo_stats_reporter_list, list) {
+		if (!srep->running)
+			continue;
+
+		osmo_stats_reporter_send_buffer(srep);
+	}
+}
+
+int osmo_stats_report()
+{
+	osmo_counters_for_each(handle_counter, NULL);
+	rate_ctr_for_each_group(rate_ctr_group_handler, NULL);
+	osmo_stat_item_for_each_group(osmo_stat_item_group_handler, NULL);
+
+	flush_all_reporters();
+
+	return 0;
+}
diff --git a/src/vty/Makefile.am b/src/vty/Makefile.am
index 4225d27..7c549d9 100644
--- a/src/vty/Makefile.am
+++ b/src/vty/Makefile.am
@@ -9,7 +9,7 @@
 lib_LTLIBRARIES = libosmovty.la
 
 libosmovty_la_SOURCES = buffer.c command.c vty.c vector.c utils.c \
-			telnet_interface.c logging_vty.c
+			telnet_interface.c logging_vty.c stats_vty.c
 libosmovty_la_LDFLAGS = -version-info $(LIBVERSION) -no-undefined
 libosmovty_la_LIBADD = $(top_builddir)/src/libosmocore.la
 endif
diff --git a/src/vty/stats_vty.c b/src/vty/stats_vty.c
new file mode 100644
index 0000000..98253ff
--- /dev/null
+++ b/src/vty/stats_vty.c
@@ -0,0 +1,430 @@
+/* OpenBSC stats helper for the VTY */
+/* (C) 2009-2010 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2009-2014 by Holger Hans Peter Freyther
+ * (C) 2015      by Sysmocom s.f.m.c. GmbH
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "../../config.h"
+
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/buffer.h>
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/telnet_interface.h>
+#include <osmocom/vty/telnet_interface.h>
+#include <osmocom/vty/misc.h>
+
+#include <osmocom/core/stats.h>
+
+#define CFG_STATS_STR "Configure stats sub-system\n"
+#define CFG_REPORTER_STR "Configure a stats reporter\n"
+
+#define SHOW_STATS_STR "Show statistical values\n"
+
+struct cmd_node cfg_stats_node = {
+	CFG_STATS_NODE,
+	"%s(config-stats)# ",
+	1
+};
+
+static const struct value_string stats_class_strs[] = {
+	{ OSMO_STATS_CLASS_GLOBAL,     "global" },
+	{ OSMO_STATS_CLASS_PEER,       "peer" },
+	{ OSMO_STATS_CLASS_SUBSCRIBER, "subscriber" },
+	{ 0, NULL }
+};
+
+static struct osmo_stats_reporter *osmo_stats_vty2srep(struct vty *vty)
+{
+	if (vty->node == CFG_STATS_NODE)
+		return vty->index;
+
+	return NULL;
+}
+
+static int set_srep_parameter_str(struct vty *vty,
+	int (*fun)(struct osmo_stats_reporter *, const char *),
+	const char *val, const char *param_name)
+{
+	int rc;
+	struct osmo_stats_reporter *srep = osmo_stats_vty2srep(vty);
+	OSMO_ASSERT(srep);
+
+	rc = fun(srep, val);
+	if (rc < 0) {
+		vty_out(vty, "%% Unable to set %s: %s%s",
+			param_name, strerror(-rc), VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	return CMD_SUCCESS;
+}
+
+static int set_srep_parameter_int(struct vty *vty,
+	int (*fun)(struct osmo_stats_reporter *, int),
+	const char *val, const char *param_name)
+{
+	int rc;
+	int int_val;
+	struct osmo_stats_reporter *srep = osmo_stats_vty2srep(vty);
+	OSMO_ASSERT(srep);
+
+	int_val = atoi(val);
+
+	rc = fun(srep, int_val);
+	if (rc < 0) {
+		vty_out(vty, "%% Unable to set %s: %s%s",
+			param_name, strerror(-rc), VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_stats_reporter_local_ip, cfg_stats_reporter_local_ip_cmd,
+	"local-ip ADDR",
+	"Set the IP address to which we bind locally\n"
+	"IP Address\n")
+{
+	return set_srep_parameter_str(vty, osmo_stats_reporter_set_local_addr,
+		argv[0], "local address");
+}
+
+DEFUN(cfg_no_stats_reporter_local_ip, cfg_no_stats_reporter_local_ip_cmd,
+	"no local-ip",
+	NO_STR
+	"Set the IP address to which we bind locally\n")
+{
+	return set_srep_parameter_str(vty, osmo_stats_reporter_set_local_addr,
+		NULL, "local address");
+}
+
+DEFUN(cfg_stats_reporter_remote_ip, cfg_stats_reporter_remote_ip_cmd,
+	"remote-ip ADDR",
+	"Set the remote IP address to which we connect\n"
+	"IP Address\n")
+{
+	return set_srep_parameter_str(vty, osmo_stats_reporter_set_remote_addr,
+		argv[0], "remote address");
+}
+
+DEFUN(cfg_stats_reporter_remote_port, cfg_stats_reporter_remote_port_cmd,
+	"remote-port <1-65535>",
+	"Set the remote port to which we connect\n"
+	"Remote port number\n")
+{
+	return set_srep_parameter_int(vty, osmo_stats_reporter_set_remote_port,
+		argv[0], "remote port");
+}
+
+DEFUN(cfg_stats_reporter_mtu, cfg_stats_reporter_mtu_cmd,
+	"mtu <100-65535>",
+	"Set the maximum packet size\n"
+	"Size in byte\n")
+{
+	return set_srep_parameter_int(vty, osmo_stats_reporter_set_mtu,
+		argv[0], "mtu");
+}
+
+DEFUN(cfg_no_stats_reporter_mtu, cfg_no_stats_reporter_mtu_cmd,
+	"no mtu",
+	NO_STR "Set the maximum packet size\n")
+{
+	return set_srep_parameter_int(vty, osmo_stats_reporter_set_mtu,
+		"0", "mtu");
+}
+
+DEFUN(cfg_stats_reporter_prefix, cfg_stats_reporter_prefix_cmd,
+	"prefix PREFIX",
+	"Set the item name prefix\n"
+	"The prefix string\n")
+{
+	return set_srep_parameter_str(vty, osmo_stats_reporter_set_name_prefix,
+		argv[0], "prefix string");
+}
+
+DEFUN(cfg_no_stats_reporter_prefix, cfg_no_stats_reporter_prefix_cmd,
+	"no prefix",
+	NO_STR
+	"Set the item name prefix\n")
+{
+	return set_srep_parameter_str(vty, osmo_stats_reporter_set_name_prefix,
+		"", "prefix string");
+}
+
+DEFUN(cfg_stats_reporter_level, cfg_stats_reporter_level_cmd,
+	"level (global|peer|subscriber)",
+	"Set the maximum group level\n"
+	"Report global groups only\n"
+	"Report global and network peer related groups\n"
+	"Report global, peer, and subscriber groups\n")
+{
+	int level = get_string_value(stats_class_strs, argv[0]);
+	int rc;
+	struct osmo_stats_reporter *srep = osmo_stats_vty2srep(vty);
+
+	OSMO_ASSERT(srep);
+	rc = osmo_stats_reporter_set_max_class(srep, level);
+	if (rc < 0) {
+		vty_out(vty, "%% Unable to set level: %s%s",
+			strerror(-rc), VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	return 0;
+}
+
+DEFUN(cfg_stats_reporter_enable, cfg_stats_reporter_enable_cmd,
+	"enable",
+	"Enable the reporter\n")
+{
+	int rc;
+	struct osmo_stats_reporter *srep = osmo_stats_vty2srep(vty);
+	OSMO_ASSERT(srep);
+
+	rc = osmo_stats_reporter_enable(srep);
+	if (rc < 0) {
+		vty_out(vty, "%% Unable to enable the reporter: %s%s",
+			strerror(-rc), VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_stats_reporter_disable, cfg_stats_reporter_disable_cmd,
+	"disable",
+	"Disable the reporter\n")
+{
+	int rc;
+	struct osmo_stats_reporter *srep = osmo_stats_vty2srep(vty);
+	OSMO_ASSERT(srep);
+
+	rc = osmo_stats_reporter_disable(srep);
+	if (rc < 0) {
+		vty_out(vty, "%% Unable to disable the reporter: %s%s",
+			strerror(-rc), VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_stats_reporter_statsd, cfg_stats_reporter_statsd_cmd,
+	"stats reporter statsd",
+	CFG_STATS_STR CFG_REPORTER_STR "Report to a STATSD server\n")
+{
+	struct osmo_stats_reporter *srep;
+
+	srep = osmo_stats_reporter_find(OSMO_STATS_REPORTER_STATSD, NULL);
+	if (!srep) {
+		srep = osmo_stats_reporter_create_statsd(NULL);
+		if (!srep) {
+			vty_out(vty, "%% Unable to create statsd reporter%s",
+				VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		srep->max_class = OSMO_STATS_CLASS_GLOBAL;
+		/* TODO: if needed, add osmo_stats_add_reporter(srep); */
+	}
+
+	vty->index = srep;
+	vty->node = CFG_STATS_NODE;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_stats_interval, cfg_stats_interval_cmd,
+	"stats interval <1-65535>",
+	CFG_STATS_STR "Set the reporting interval\n"
+	"Interval in seconds\n")
+{
+	int rc;
+	int interval = atoi(argv[0]);
+	rc = osmo_stats_set_interval(interval);
+	if (rc < 0) {
+		vty_out(vty, "%% Unable to set interval: %s%s",
+			strerror(-rc), VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	return CMD_SUCCESS;
+}
+
+
+DEFUN(cfg_no_stats_reporter_statsd, cfg_no_stats_reporter_statsd_cmd,
+	"no stats reporter statsd",
+	NO_STR CFG_STATS_STR CFG_REPORTER_STR "Report to a STATSD server\n")
+{
+	struct osmo_stats_reporter *srep;
+
+	srep = osmo_stats_reporter_find(OSMO_STATS_REPORTER_STATSD, NULL);
+	if (!srep) {
+		vty_out(vty, "%% No statsd logging active%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	osmo_stats_reporter_free(srep);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_stats_reporter_log, cfg_stats_reporter_log_cmd,
+	"stats reporter log",
+	CFG_STATS_STR CFG_REPORTER_STR "Report to the logger\n")
+{
+	struct osmo_stats_reporter *srep;
+
+	srep = osmo_stats_reporter_find(OSMO_STATS_REPORTER_LOG, NULL);
+	if (!srep) {
+		srep = osmo_stats_reporter_create_log(NULL);
+		if (!srep) {
+			vty_out(vty, "%% Unable to create log reporter%s",
+				VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		srep->max_class = OSMO_STATS_CLASS_GLOBAL;
+		/* TODO: if needed, add osmo_stats_add_reporter(srep); */
+	}
+
+	vty->index = srep;
+	vty->node = CFG_STATS_NODE;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_stats_reporter_log, cfg_no_stats_reporter_log_cmd,
+	"no stats reporter log",
+	NO_STR CFG_STATS_STR CFG_REPORTER_STR "Report to the logger\n")
+{
+	struct osmo_stats_reporter *srep;
+
+	srep = osmo_stats_reporter_find(OSMO_STATS_REPORTER_LOG, NULL);
+	if (!srep) {
+		vty_out(vty, "%% No log reporting active%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	osmo_stats_reporter_free(srep);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(show_stats,
+      show_stats_cmd,
+      "show stats",
+      SHOW_STR SHOW_STATS_STR)
+{
+	vty_out_statistics_full(vty, "");
+
+	return CMD_SUCCESS;
+}
+
+static int config_write_stats_reporter(struct vty *vty, struct osmo_stats_reporter *srep)
+{
+	if (srep == NULL)
+		return 0;
+
+	switch (srep->type) {
+	case OSMO_STATS_REPORTER_STATSD:
+		vty_out(vty, "stats reporter statsd%s", VTY_NEWLINE);
+		break;
+	case OSMO_STATS_REPORTER_LOG:
+		vty_out(vty, "stats reporter log%s", VTY_NEWLINE);
+		break;
+	}
+
+	vty_out(vty, "  disable%s", VTY_NEWLINE);
+
+	if (srep->have_net_config) {
+		if (srep->dest_addr_str)
+			vty_out(vty, "  remote-ip %s%s",
+				srep->dest_addr_str, VTY_NEWLINE);
+		if (srep->dest_port)
+			vty_out(vty, "  remote-port %d%s",
+				srep->dest_port, VTY_NEWLINE);
+		if (srep->bind_addr_str)
+			vty_out(vty, "  local-ip %s%s",
+				srep->bind_addr_str, VTY_NEWLINE);
+		if (srep->mtu)
+			vty_out(vty, "  mtu %d%s",
+				srep->mtu, VTY_NEWLINE);
+	}
+
+	if (srep->max_class)
+		vty_out(vty, "  level %s%s",
+			get_value_string(stats_class_strs, srep->max_class),
+			VTY_NEWLINE);
+
+	if (srep->name_prefix && *srep->name_prefix)
+		vty_out(vty, "  prefix %s%s",
+			srep->name_prefix, VTY_NEWLINE);
+	else
+		vty_out(vty, "  no prefix%s", VTY_NEWLINE);
+
+	if (srep->enabled)
+		vty_out(vty, "  enable%s", VTY_NEWLINE);
+
+	return 1;
+}
+
+static int config_write_stats(struct vty *vty)
+{
+	struct osmo_stats_reporter *srep;
+
+	/* TODO: loop through all reporters */
+	srep = osmo_stats_reporter_find(OSMO_STATS_REPORTER_STATSD, NULL);
+	config_write_stats_reporter(vty, srep);
+	srep = osmo_stats_reporter_find(OSMO_STATS_REPORTER_LOG, NULL);
+	config_write_stats_reporter(vty, srep);
+
+	vty_out(vty, "stats interval %d%s", osmo_stats_config->interval, VTY_NEWLINE);
+
+	return 1;
+}
+
+void osmo_stats_vty_add_cmds()
+{
+	install_element_ve(&show_stats_cmd);
+
+	install_element(CONFIG_NODE, &cfg_stats_reporter_statsd_cmd);
+	install_element(CONFIG_NODE, &cfg_no_stats_reporter_statsd_cmd);
+	install_element(CONFIG_NODE, &cfg_stats_reporter_log_cmd);
+	install_element(CONFIG_NODE, &cfg_no_stats_reporter_log_cmd);
+	install_element(CONFIG_NODE, &cfg_stats_interval_cmd);
+
+	install_node(&cfg_stats_node, config_write_stats);
+	vty_install_default(CFG_STATS_NODE);
+
+	install_element(CFG_STATS_NODE, &cfg_stats_reporter_local_ip_cmd);
+	install_element(CFG_STATS_NODE, &cfg_no_stats_reporter_local_ip_cmd);
+	install_element(CFG_STATS_NODE, &cfg_stats_reporter_remote_ip_cmd);
+	install_element(CFG_STATS_NODE, &cfg_stats_reporter_remote_port_cmd);
+	install_element(CFG_STATS_NODE, &cfg_stats_reporter_mtu_cmd);
+	install_element(CFG_STATS_NODE, &cfg_no_stats_reporter_mtu_cmd);
+	install_element(CFG_STATS_NODE, &cfg_stats_reporter_prefix_cmd);
+	install_element(CFG_STATS_NODE, &cfg_no_stats_reporter_prefix_cmd);
+	install_element(CFG_STATS_NODE, &cfg_stats_reporter_level_cmd);
+	install_element(CFG_STATS_NODE, &cfg_stats_reporter_enable_cmd);
+	install_element(CFG_STATS_NODE, &cfg_stats_reporter_disable_cmd);
+}
diff --git a/src/vty/utils.c b/src/vty/utils.c
index d0ad431..8df44ae 100644
--- a/src/vty/utils.c
+++ b/src/vty/utils.c
@@ -29,7 +29,9 @@
 #include <osmocom/core/talloc.h>
 #include <osmocom/core/timer.h>
 #include <osmocom/core/rate_ctr.h>
+#include <osmocom/core/stat_item.h>
 #include <osmocom/core/utils.h>
+#include <osmocom/core/statistics.h>
 
 #include <osmocom/vty/vty.h>
 
@@ -39,6 +41,30 @@
  *  @{
  */
 
+struct vty_out_context {
+	struct vty *vty;
+	const char *prefix;
+};
+
+static int rate_ctr_handler(
+	struct rate_ctr_group *ctrg, struct rate_ctr *ctr,
+	const struct rate_ctr_desc *desc, void *vctx_)
+{
+	struct vty_out_context *vctx = vctx_;
+	struct vty *vty = vctx->vty;
+
+	vty_out(vty, " %s%s: %8" PRIu64 " "
+		"(%" PRIu64 "/s %" PRIu64 "/m %" PRIu64 "/h %" PRIu64 "/d)%s",
+		vctx->prefix, desc->description, ctr->current,
+		ctr->intv[RATE_CTR_INTV_SEC].rate,
+		ctr->intv[RATE_CTR_INTV_MIN].rate,
+		ctr->intv[RATE_CTR_INTV_HOUR].rate,
+		ctr->intv[RATE_CTR_INTV_DAY].rate,
+		VTY_NEWLINE);
+
+	return 0;
+}
+
 /*! \brief print a rate counter group to given VTY
  *  \param[in] vty The VTY to which it should be printed
  *  \param[in] prefix Any additional log prefix ahead of each line
@@ -47,20 +73,97 @@
 void vty_out_rate_ctr_group(struct vty *vty, const char *prefix,
 			    struct rate_ctr_group *ctrg)
 {
-	unsigned int i;
+	struct vty_out_context vctx = {vty, prefix};
 
 	vty_out(vty, "%s%s:%s", prefix, ctrg->desc->group_description, VTY_NEWLINE);
-	for (i = 0; i < ctrg->desc->num_ctr; i++) {
-		struct rate_ctr *ctr = &ctrg->ctr[i];
-		vty_out(vty, " %s%s: %8" PRIu64 " "
-			"(%" PRIu64 "/s %" PRIu64 "/m %" PRIu64 "/h %" PRIu64 "/d)%s",
-			prefix, ctrg->desc->ctr_desc[i].description, ctr->current,
-			ctr->intv[RATE_CTR_INTV_SEC].rate,
-			ctr->intv[RATE_CTR_INTV_MIN].rate,
-			ctr->intv[RATE_CTR_INTV_HOUR].rate,
-			ctr->intv[RATE_CTR_INTV_DAY].rate,
+
+	rate_ctr_for_each_counter(ctrg, rate_ctr_handler, &vctx);
+}
+
+static int osmo_stat_item_handler(
+	struct osmo_stat_item_group *statg, struct osmo_stat_item *item, void *vctx_)
+{
+	struct vty_out_context *vctx = vctx_;
+	struct vty *vty = vctx->vty;
+
+	vty_out(vty, " %s%s: %8" PRIi32 " %s%s",
+		vctx->prefix, item->desc->description,
+		osmo_stat_item_get_last(item),
+		item->desc->unit, VTY_NEWLINE);
+
+	return 0;
+}
+
+/*! \brief print a stat item group to given VTY
+ *  \param[in] vty The VTY to which it should be printed
+ *  \param[in] prefix Any additional log prefix ahead of each line
+ *  \param[in] statg Stat item group to be printed
+ */
+void vty_out_stat_item_group(struct vty *vty, const char *prefix,
+			     struct osmo_stat_item_group *statg)
+{
+	struct vty_out_context vctx = {vty, prefix};
+
+	vty_out(vty, "%s%s:%s", prefix, statg->desc->group_description,
+		VTY_NEWLINE);
+	osmo_stat_item_for_each_item(statg, osmo_stat_item_handler, &vctx);
+}
+
+static int osmo_stat_item_group_handler(struct osmo_stat_item_group *statg, void *vctx_)
+{
+	struct vty_out_context *vctx = vctx_;
+	struct vty *vty = vctx->vty;
+
+	if (statg->idx)
+		vty_out(vty, "%s%s (%d):%s", vctx->prefix,
+			statg->desc->group_description, statg->idx,
 			VTY_NEWLINE);
-	};
+	else
+		vty_out(vty, "%s%s:%s", vctx->prefix,
+			statg->desc->group_description, VTY_NEWLINE);
+
+	osmo_stat_item_for_each_item(statg, osmo_stat_item_handler, vctx);
+
+	return 0;
+}
+
+static int rate_ctr_group_handler(struct rate_ctr_group *ctrg, void *vctx_)
+{
+	struct vty_out_context *vctx = vctx_;
+	struct vty *vty = vctx->vty;
+
+	if (ctrg->idx)
+		vty_out(vty, "%s%s (%d):%s", vctx->prefix,
+			ctrg->desc->group_description, ctrg->idx, VTY_NEWLINE);
+	else
+		vty_out(vty, "%s%s:%s", vctx->prefix,
+			ctrg->desc->group_description, VTY_NEWLINE);
+
+	rate_ctr_for_each_counter(ctrg, rate_ctr_handler, vctx);
+
+	return 0;
+}
+
+static int handle_counter(struct osmo_counter *counter, void *vctx_)
+{
+	struct vty_out_context *vctx = vctx_;
+	struct vty *vty = vctx->vty;
+
+	vty_out(vty, " %s%s: %8lu%s",
+		vctx->prefix, counter->description,
+		osmo_counter_get(counter), VTY_NEWLINE);
+
+	return 0;
+}
+
+void vty_out_statistics_full(struct vty *vty, const char *prefix)
+{
+	struct vty_out_context vctx = {vty, prefix};
+
+	vty_out(vty, "%sUngrouped counters:%s", prefix, VTY_NEWLINE);
+	osmo_counters_for_each(handle_counter, &vctx);
+	rate_ctr_for_each_group(rate_ctr_group_handler, &vctx);
+	osmo_stat_item_for_each_group(osmo_stat_item_group_handler, &vctx);
 }
 
 /*! \brief Generate a VTY command string from value_string */
diff --git a/tests/Makefile.am b/tests/Makefile.am
index cf0977d..6065c0d 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -9,7 +9,7 @@
 		 kasumi/kasumi_test logging/logging_test fr/fr_test	\
 		 loggingrb/loggingrb_test strrb/strrb_test              \
 		 vty/vty_test comp128/comp128_test utils/utils_test	\
-		 smscb/gsm0341_test
+		 smscb/gsm0341_test stats/stats_test
 
 if ENABLE_MSGFILE
 check_PROGRAMS += msgfile/msgfile_test
@@ -18,6 +18,9 @@
 utils_utils_test_SOURCES = utils/utils_test.c
 utils_utils_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gsm/libosmogsm.la
 
+stats_stats_test_SOURCES = stats/stats_test.c
+stats_stats_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gsm/libosmogsm.la
+
 a5_a5_test_SOURCES = a5/a5_test.c
 a5_a5_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gsm/libgsmint.la
 
@@ -64,19 +67,19 @@
 ussd_ussd_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gsm/libosmogsm.la
 
 gb_bssgp_fc_test_SOURCES = gb/bssgp_fc_test.c
-gb_bssgp_fc_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gb/libosmogb.la
+gb_bssgp_fc_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gb/libosmogb.la $(top_builddir)/src/vty/libosmovty.la
 
 gb_gprs_bssgp_test_SOURCES = gb/gprs_bssgp_test.c
-gb_gprs_bssgp_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gb/libosmogb.la $(LIBRARY_DL)
+gb_gprs_bssgp_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gb/libosmogb.la $(top_builddir)/src/vty/libosmovty.la $(LIBRARY_DL)
 
 gb_gprs_ns_test_SOURCES = gb/gprs_ns_test.c
-gb_gprs_ns_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gb/libosmogb.la $(LIBRARY_DL)
+gb_gprs_ns_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gb/libosmogb.la $(top_builddir)/src/vty/libosmovty.la $(LIBRARY_DL)
 
 logging_logging_test_SOURCES = logging/logging_test.c
 logging_logging_test_LDADD = $(top_builddir)/src/libosmocore.la
 
 fr_fr_test_SOURCES = fr/fr_test.c
-fr_fr_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gb/libosmogb.la $(LIBRARY_DL)
+fr_fr_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/gb/libosmogb.la $(top_builddir)/src/vty/libosmovty.la $(LIBRARY_DL)
 
 loggingrb_loggingrb_test_SOURCES = loggingrb/loggingrb_test.c
 loggingrb_loggingrb_test_LDADD = $(top_builddir)/src/libosmocore.la $(top_builddir)/src/vty/libosmovty.la
@@ -120,7 +123,7 @@
              fr/fr_test.ok loggingrb/logging_test.ok			\
              loggingrb/logging_test.err	strrb/strrb_test.ok		\
 	     vty/vty_test.ok comp128/comp128_test.ok			\
-	     utils/utils_test.ok
+	     utils/utils_test.ok stats/stats_test.ok
 
 DISTCLEANFILES = atconfig
 
diff --git a/tests/stats/stats_test.c b/tests/stats/stats_test.c
new file mode 100644
index 0000000..f8c7dc0
--- /dev/null
+++ b/tests/stats/stats_test.c
@@ -0,0 +1,213 @@
+/* tests for statistics */
+/*
+ * (C) 2015 Sysmocom s.m.f.c. GmbH
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include <osmocom/core/logging.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/stat_item.h>
+
+#include <stdio.h>
+
+static void stat_test(void)
+{
+	enum test_items {
+		TEST_A_ITEM,
+		TEST_B_ITEM,
+	};
+
+	static const struct osmo_stat_item_desc item_description[] = {
+		{ "item.a", "The A value", "ma", 4, -1 },
+		{ "item.b", "The B value", "kb", 7, -1 },
+	};
+
+	static const struct osmo_stat_item_group_desc statg_desc = {
+		.group_name_prefix = "test.one",
+		.group_description = "Test number 1",
+		.num_items = ARRAY_SIZE(item_description),
+		.item_desc = item_description,
+	};
+
+	struct osmo_stat_item_group *statg =
+		osmo_stat_item_group_alloc(NULL, &statg_desc, 0);
+
+	struct osmo_stat_item_group *sgrp2;
+	const struct osmo_stat_item *sitem1, *sitem2;
+	int rc;
+	int32_t value;
+	int32_t rd_a = 0;
+	int32_t rd_b = 0;
+	int i;
+
+	OSMO_ASSERT(statg != NULL);
+
+	sgrp2 = osmo_stat_item_get_group_by_name_idx("test.one", 0);
+	OSMO_ASSERT(sgrp2 == statg);
+
+	sgrp2 = osmo_stat_item_get_group_by_name_idx("test.one", 1);
+	OSMO_ASSERT(sgrp2 == NULL);
+
+	sgrp2 = osmo_stat_item_get_group_by_name_idx("test.two", 0);
+	OSMO_ASSERT(sgrp2 == NULL);
+
+	sitem1 = osmo_stat_item_get_by_name(statg, "item.c");
+	OSMO_ASSERT(sitem1 == NULL);
+
+	sitem1 = osmo_stat_item_get_by_name(statg, "item.a");
+	OSMO_ASSERT(sitem1 != NULL);
+	OSMO_ASSERT(sitem1 == statg->items[TEST_A_ITEM]);
+
+	sitem2 = osmo_stat_item_get_by_name(statg, "item.b");
+	OSMO_ASSERT(sitem2 != NULL);
+	OSMO_ASSERT(sitem2 != sitem1);
+	OSMO_ASSERT(sitem2 == statg->items[TEST_B_ITEM]);
+
+	value = osmo_stat_item_get_last(statg->items[TEST_A_ITEM]);
+	OSMO_ASSERT(value == -1);
+
+	rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+	OSMO_ASSERT(rc == 0);
+
+	osmo_stat_item_set(statg->items[TEST_A_ITEM], 1);
+
+	value = osmo_stat_item_get_last(statg->items[TEST_A_ITEM]);
+	OSMO_ASSERT(value == 1);
+
+	rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+	OSMO_ASSERT(rc > 0);
+	OSMO_ASSERT(value == 1);
+
+	rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+	OSMO_ASSERT(rc == 0);
+
+	for (i = 2; i <= 32; i++) {
+		osmo_stat_item_set(statg->items[TEST_A_ITEM], i);
+		osmo_stat_item_set(statg->items[TEST_B_ITEM], 1000 + i);
+
+		rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+		OSMO_ASSERT(rc > 0);
+		OSMO_ASSERT(value == i);
+
+		rc = osmo_stat_item_get_next(statg->items[TEST_B_ITEM], &rd_b, &value);
+		OSMO_ASSERT(rc > 0);
+		OSMO_ASSERT(value == 1000 + i);
+	}
+
+	/* Keep 2 in FIFO */
+	osmo_stat_item_set(statg->items[TEST_A_ITEM], 33);
+	osmo_stat_item_set(statg->items[TEST_B_ITEM], 1000 + 33);
+
+	for (i = 34; i <= 64; i++) {
+		osmo_stat_item_set(statg->items[TEST_A_ITEM], i);
+		osmo_stat_item_set(statg->items[TEST_B_ITEM], 1000 + i);
+
+		rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+		OSMO_ASSERT(rc > 0);
+		OSMO_ASSERT(value == i-1);
+
+		rc = osmo_stat_item_get_next(statg->items[TEST_B_ITEM], &rd_b, &value);
+		OSMO_ASSERT(rc > 0);
+		OSMO_ASSERT(value == 1000 + i-1);
+	}
+
+	rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+	OSMO_ASSERT(rc > 0);
+	OSMO_ASSERT(value == 64);
+
+	rc = osmo_stat_item_get_next(statg->items[TEST_B_ITEM], &rd_b, &value);
+	OSMO_ASSERT(rc > 0);
+	OSMO_ASSERT(value == 1000 + 64);
+
+	/* Overrun FIFOs */
+	for (i = 65; i <= 96; i++) {
+		osmo_stat_item_set(statg->items[TEST_A_ITEM], i);
+		osmo_stat_item_set(statg->items[TEST_B_ITEM], 1000 + i);
+	}
+
+	rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+	OSMO_ASSERT(rc > 0);
+	OSMO_ASSERT(value == 93);
+
+	for (i = 94; i <= 96; i++) {
+		rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+		OSMO_ASSERT(rc > 0);
+		OSMO_ASSERT(value == i);
+	}
+
+	rc = osmo_stat_item_get_next(statg->items[TEST_B_ITEM], &rd_b, &value);
+	OSMO_ASSERT(rc > 0);
+	OSMO_ASSERT(value == 1000 + 90);
+
+	for (i = 91; i <= 96; i++) {
+		rc = osmo_stat_item_get_next(statg->items[TEST_B_ITEM], &rd_b, &value);
+		OSMO_ASSERT(rc > 0);
+		OSMO_ASSERT(value == 1000 + i);
+	}
+
+	/* Test Discard (single item) */
+	osmo_stat_item_set(statg->items[TEST_A_ITEM], 97);
+	rc = osmo_stat_item_discard(statg->items[TEST_A_ITEM], &rd_a);
+	OSMO_ASSERT(rc > 0);
+
+	rc = osmo_stat_item_discard(statg->items[TEST_A_ITEM], &rd_a);
+	OSMO_ASSERT(rc == 0);
+
+	rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+	OSMO_ASSERT(rc == 0);
+
+	osmo_stat_item_set(statg->items[TEST_A_ITEM], 98);
+	rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+	OSMO_ASSERT(rc > 0);
+	OSMO_ASSERT(value == 98);
+
+	rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+	OSMO_ASSERT(rc == 0);
+
+	/* Test Discard (all items) */
+	osmo_stat_item_set(statg->items[TEST_A_ITEM], 99);
+	osmo_stat_item_set(statg->items[TEST_A_ITEM], 100);
+	osmo_stat_item_set(statg->items[TEST_A_ITEM], 101);
+	osmo_stat_item_set(statg->items[TEST_B_ITEM], 99);
+	osmo_stat_item_set(statg->items[TEST_B_ITEM], 100);
+
+	rc = osmo_stat_item_discard_all(&rd_a);
+	rc = osmo_stat_item_discard_all(&rd_b);
+
+	rc = osmo_stat_item_get_next(statg->items[TEST_A_ITEM], &rd_a, &value);
+	OSMO_ASSERT(rc == 0);
+	rc = osmo_stat_item_get_next(statg->items[TEST_B_ITEM], &rd_b, &value);
+	OSMO_ASSERT(rc == 0);
+
+	osmo_stat_item_group_free(statg);
+
+	sgrp2 = osmo_stat_item_get_group_by_name_idx("test.one", 0);
+	OSMO_ASSERT(sgrp2 == NULL);
+}
+
+int main(int argc, char **argv)
+{
+	static const struct log_info log_info = {};
+	log_init(&log_info, NULL);
+
+	osmo_stat_item_init(NULL);
+
+	stat_test();
+	return 0;
+}
diff --git a/tests/stats/stats_test.ok b/tests/stats/stats_test.ok
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/stats/stats_test.ok
diff --git a/tests/testsuite.at b/tests/testsuite.at
index fe30363..a542798 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -136,6 +136,12 @@
 AT_CHECK([$abs_top_builddir/tests/utils/utils_test], [0], [expout], [ignore])
 AT_CLEANUP
 
+AT_SETUP([stats])
+AT_KEYWORDS([stats])
+cat $abs_srcdir/stats/stats_test.ok > expout
+AT_CHECK([$abs_top_builddir/tests/stats/stats_test], [0], [expout], [ignore])
+AT_CLEANUP
+
 AT_SETUP([bssgp-fc])
 AT_KEYWORDS([bssgp-fc])
 cat $abs_srcdir/gb/bssgp_fc_tests.ok > expout
diff --git a/tests/vty/vty_test.ok b/tests/vty/vty_test.ok
index 0ea2dab..c636590 100644
--- a/tests/vty/vty_test.ok
+++ b/tests/vty/vty_test.ok
@@ -24,11 +24,11 @@
 Going to execute 'configure terminal'
 Returned: 0, Current node: 4 '%s(config)# '
 Going to execute 'line vty'
-Returned: 0, Current node: 8 '%s(config-line)# '
+Returned: 0, Current node: 9 '%s(config-line)# '
 Going to execute 'exit'
 Returned: 0, Current node: 4 '%s(config)# '
 Going to execute 'line vty'
-Returned: 0, Current node: 8 '%s(config-line)# '
+Returned: 0, Current node: 9 '%s(config-line)# '
 Going to execute 'end'
 Returned: 0, Current node: 3 '%s# '
 Going to execute 'configure terminal'
@@ -36,7 +36,7 @@
 Going to execute 'log stderr'
 Returned: 0, Current node: 7 '%s(config-log)# '
 Going to execute 'line vty'
-Returned: 0, Current node: 8 '%s(config-line)# '
+Returned: 0, Current node: 9 '%s(config-line)# '
 Going to execute 'log stderr'
 Returned: 0, Current node: 7 '%s(config-log)# '
 Going to execute 'end'