libosmocore/src/stats_tcp.c

326 lines
10 KiB
C
Raw Normal View History

/*
* (C) 2021 by sysmocom - s.f.m.c. GmbH
* Author: Philipp Maier <pmaier@sysmocom.de>
* All Rights Reserved
*
* SPDX-License-Identifier: GPL-2.0+
*
* 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.
*
*/
/*! \addtogroup stats
* @{
* \file stats_tcp.c */
#include "config.h"
#if !defined(EMBEDDED)
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <linux/tcp.h>
#include <errno.h>
#include <pthread.h>
#include <osmocom/core/select.h>
#include <osmocom/core/linuxlist.h>
#include <osmocom/core/talloc.h>
#include <osmocom/core/utils.h>
#include <osmocom/core/timer.h>
#include <osmocom/core/stat_item.h>
#include <osmocom/core/stats.h>
#include <osmocom/core/socket.h>
#include <osmocom/core/stats_tcp.h>
static struct osmo_tcp_stats_config s_tcp_stats_config = {
.interval = TCP_STATS_DEFAULT_INTERVAL,
};
struct osmo_tcp_stats_config *osmo_tcp_stats_config = &s_tcp_stats_config;
static struct osmo_timer_list stats_tcp_poll_timer;
static LLIST_HEAD(stats_tcp);
static struct stats_tcp_entry *stats_tcp_entry_cur;
pthread_mutex_t stats_tcp_lock;
struct stats_tcp_entry {
struct llist_head entry;
const struct osmo_fd *fd;
struct osmo_stat_item_group *stats_tcp;
const char *name;
};
enum {
STATS_TCP_UNACKED,
STATS_TCP_LOST,
STATS_TCP_RETRANS,
STATS_TCP_RTT,
STATS_TCP_RCV_RTT,
STATS_TCP_NOTSENT_BYTES,
STATS_TCP_RWND_LIMITED,
STATS_TCP_SNDBUF_LIMITED,
STATS_TCP_REORD_SEEN,
};
static struct osmo_stat_item_desc stats_tcp_item_desc[] = {
[STATS_TCP_UNACKED] = { "tcp:unacked", "unacknowledged packets", "", 60, 0 },
[STATS_TCP_LOST] = { "tcp:lost", "lost packets", "", 60, 0 },
[STATS_TCP_RETRANS] = { "tcp:retrans", "retransmitted packets", "", 60, 0 },
[STATS_TCP_RTT] = { "tcp:rtt", "roundtrip-time", "", 60, 0 },
[STATS_TCP_RCV_RTT] = { "tcp:rcv_rtt", "roundtrip-time (receive)", "", 60, 0 },
[STATS_TCP_NOTSENT_BYTES] = { "tcp:notsent_bytes", "bytes not yet sent", "", 60, 0 },
[STATS_TCP_RWND_LIMITED] = { "tcp:rwnd_limited", "time (usec) limited by receive window", "", 60, 0 },
[STATS_TCP_SNDBUF_LIMITED] = { "tcp:sndbuf_limited", "Time (usec) limited by send buffer", "", 60, 0 },
[STATS_TCP_REORD_SEEN] = { "tcp:reord_seen", "reordering events seen", "", 60, 0 },
};
static struct osmo_stat_item_group_desc stats_tcp_desc = {
.group_name_prefix = "tcp",
.group_description = "stats tcp",
.class_id = OSMO_STATS_CLASS_GLOBAL,
.num_items = ARRAY_SIZE(stats_tcp_item_desc),
.item_desc = stats_tcp_item_desc,
};
static void fill_stats(struct stats_tcp_entry *stats_tcp_entry)
{
int rc;
struct tcp_info tcp_info;
socklen_t tcp_info_len = sizeof(tcp_info);
char stat_name[256];
/* Do not fill in anything before the socket is connected to a remote end */
if (osmo_sock_get_ip_and_port(stats_tcp_entry->fd->fd, NULL, 0, NULL, 0, false) != 0)
return;
/* Gather TCP statistics and update the stats items */
rc = getsockopt(stats_tcp_entry->fd->fd, IPPROTO_TCP, TCP_INFO, &tcp_info, &tcp_info_len);
if (rc < 0)
return;
/* Create stats items if they do not exist yet */
if (!stats_tcp_entry->stats_tcp) {
stats_tcp_entry->stats_tcp =
osmo_stat_item_group_alloc(stats_tcp_entry, &stats_tcp_desc, stats_tcp_entry->fd->fd);
OSMO_ASSERT(stats_tcp_entry->stats_tcp);
}
/* Update statistics */
if (stats_tcp_entry->name)
stats: use tcp stat names as provided If an API user has defined a name for this particular stat, we should consider it unique and not append ip and port information from the connection. By appending ip and port information to all tcp stat names, we end up creating unique stat names every time a reconnection occurs and the source port changes. This makes the statistic impossible to track over time as it is continually using a different name. A quick example from the field over the course of a day: tcp.ipa-rsl-0,r=192.168.55.88.33056<->l=192.168.0.1.3003.tcp.rtt tcp.ipa-rsl-0,r=192.168.55.88.33311<->l=192.168.0.1.3003.tcp.rtt tcp.ipa-rsl-0,r=192.168.55.88.35510<->l=192.168.0.1.3003.tcp.rtt tcp.ipa-rsl-0,r=192.168.55.88.35958<->l=192.168.0.1.3003.tcp.rtt tcp.ipa-rsl-0,r=192.168.55.88.36110<->l=192.168.0.1.3003.tcp.rtt tcp.ipa-rsl-0,r=192.168.55.88.39269<->l=192.168.0.1.3003.tcp.rtt tcp.ipa-rsl-0,r=192.168.55.88.40394<->l=192.168.0.1.3003.tcp.rtt tcp.ipa-rsl-0,r=192.168.55.88.40397<->l=192.168.0.1.3003.tcp.rtt tcp.ipa-rsl-0,r=192.168.55.88.42920<->l=192.168.0.1.3003.tcp.rtt tcp.ipa-rsl-0,r=192.168.55.88.43839<->l=192.168.0.1.3003.tcp.rtt This change would treat tcp stats like other stats around the system. A unique name must be set by the API user. This would let us set a unique name like the following to avoid the situation above: bts.0.rsl.0.tcp.rtt Matching the existing rsl related stats: bts.0.rsl.delete_ind bts.0.rsl.ipa_nack bts.0.rsl.unknown ...they retain a constant name regardless of the underlying connectivity situation. Change-Id: Ib04c2f5bfcbd6c19dd87debf1fc053abf0b9bef2
2022-02-10 17:30:26 +00:00
snprintf(stat_name, sizeof(stat_name), "%s", stats_tcp_entry->name);
else
snprintf(stat_name, sizeof(stat_name), "%s", osmo_sock_get_name2(stats_tcp_entry->fd->fd));
osmo_stat_item_group_set_name(stats_tcp_entry->stats_tcp, stat_name);
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_UNACKED),
tcp_info.tcpi_unacked);
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_LOST),
tcp_info.tcpi_lost);
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_RETRANS),
tcp_info.tcpi_retrans);
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_RTT), tcp_info.tcpi_rtt);
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_RCV_RTT),
tcp_info.tcpi_rcv_rtt);
#if HAVE_TCP_INFO_TCPI_NOTSENT_BYTES == 1
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_NOTSENT_BYTES),
tcp_info.tcpi_notsent_bytes);
#else
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_NOTSENT_BYTES), -1);
#endif
#if HAVE_TCP_INFO_TCPI_RWND_LIMITED == 1
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_RWND_LIMITED),
tcp_info.tcpi_rwnd_limited);
#else
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_RWND_LIMITED), -1);
#endif
#if STATS_TCP_SNDBUF_LIMITED == 1
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_REORD_SEEN),
tcp_info.tcpi_sndbuf_limited);
#else
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_REORD_SEEN), -1);
#endif
#if HAVE_TCP_INFO_TCPI_REORD_SEEN == 1
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_REORD_SEEN),
tcp_info.tcpi_reord_seen);
#else
osmo_stat_item_set(osmo_stat_item_group_get_item(stats_tcp_entry->stats_tcp, STATS_TCP_REORD_SEEN), -1);
#endif
}
static bool is_tcp(const struct osmo_fd *fd)
{
int rc;
struct stat fd_stat;
int so_protocol = 0;
socklen_t so_protocol_len = sizeof(so_protocol);
/* Is this a socket? */
rc = fstat(fd->fd, &fd_stat);
if (rc < 0)
return false;
if (!S_ISSOCK(fd_stat.st_mode))
return false;
/* Is it a TCP socket? */
rc = getsockopt(fd->fd, SOL_SOCKET, SO_PROTOCOL, &so_protocol, &so_protocol_len);
if (rc < 0)
return false;
if (so_protocol == IPPROTO_TCP)
return true;
return false;
}
/*! Register an osmo_fd for TCP stats monitoring.
* \param[in] fd osmocom file descriptor to be registered.
* \param[in] human readbla name that is used as prefix for the related stats item.
* \returns 0 on success; negative in case of error. */
int osmo_stats_tcp_osmo_fd_register(const struct osmo_fd *fd, const char *name)
{
struct stats_tcp_entry *stats_tcp_entry;
/* Only TCP sockets can be registered for monitoring, anything else will fall through. */
if (!is_tcp(fd))
return -EINVAL;
/* When the osmo_fd is registered and unregistered properly there shouldn't be any leftovers from already closed
* osmo_fds in the stats_tcp list. But lets proactively make sure that any leftovers are cleaned up. */
osmo_stats_tcp_osmo_fd_unregister(fd);
/* Make a new list object, attach the osmo_fd... */
stats_tcp_entry = talloc_zero(OTC_GLOBAL, struct stats_tcp_entry);
OSMO_ASSERT(stats_tcp_entry);
stats_tcp_entry->fd = fd;
stats_tcp_entry->name = talloc_strdup(stats_tcp_entry, name);
pthread_mutex_lock(&stats_tcp_lock);
llist_add_tail(&stats_tcp_entry->entry, &stats_tcp);
pthread_mutex_unlock(&stats_tcp_lock);
return 0;
}
static void next_stats_tcp_entry(void)
{
struct stats_tcp_entry *last;
if (llist_empty(&stats_tcp)) {
stats_tcp_entry_cur = NULL;
return;
}
last = (struct stats_tcp_entry *)llist_last_entry(&stats_tcp, struct stats_tcp_entry, entry);
if (!stats_tcp_entry_cur || stats_tcp_entry_cur == last)
stats_tcp_entry_cur =
(struct stats_tcp_entry *)llist_first_entry(&stats_tcp, struct stats_tcp_entry, entry);
else
stats_tcp_entry_cur =
(struct stats_tcp_entry *)llist_entry(stats_tcp_entry_cur->entry.next, struct stats_tcp_entry,
entry);
}
/*! Register an osmo_fd for TCP stats monitoring.
* \param[in] fd osmocom file descriptor to be unregistered.
* \returns 0 on success; negative in case of error. */
int osmo_stats_tcp_osmo_fd_unregister(const struct osmo_fd *fd)
{
struct stats_tcp_entry *stats_tcp_entry;
int rc = -EINVAL;
pthread_mutex_lock(&stats_tcp_lock);
llist_for_each_entry(stats_tcp_entry, &stats_tcp, entry) {
if (fd->fd == stats_tcp_entry->fd->fd) {
/* In case we want to remove exactly that item which is also selected as the current itemy, we
* must designate either a different item or invalidate the current item. */
if (stats_tcp_entry == stats_tcp_entry_cur) {
if (llist_count(&stats_tcp) > 2)
next_stats_tcp_entry();
else
stats_tcp_entry_cur = NULL;
}
/* Date item from list */
llist_del(&stats_tcp_entry->entry);
osmo_stat_item_group_free(stats_tcp_entry->stats_tcp);
talloc_free(stats_tcp_entry);
rc = 0;
break;
}
}
pthread_mutex_unlock(&stats_tcp_lock);
return rc;
}
static void stats_tcp_poll_timer_cb(void *data)
{
int i;
int batch_size;
int llist_size;
pthread_mutex_lock(&stats_tcp_lock);
/* Make sure we do not run over the same sockets multiple times if the
* configured llist_size is larger then the actual list */
batch_size = osmo_tcp_stats_config->batch_size;
llist_size = llist_count(&stats_tcp);
if (llist_size < batch_size)
batch_size = llist_size;
/* Process a batch of sockets */
for (i = 0; i < batch_size; i++) {
next_stats_tcp_entry();
if (stats_tcp_entry_cur)
fill_stats(stats_tcp_entry_cur);
}
pthread_mutex_unlock(&stats_tcp_lock);
if (osmo_tcp_stats_config->interval > 0)
osmo_timer_schedule(&stats_tcp_poll_timer, osmo_tcp_stats_config->interval, 0);
}
/*! Set the polling interval (common for all sockets)
* \param[in] interval Poll interval in seconds
* \returns 0 on success; negative on error */
int osmo_stats_tcp_set_interval(int interval)
{
osmo_tcp_stats_config->interval = interval;
if (osmo_tcp_stats_config->interval > 0)
osmo_timer_schedule(&stats_tcp_poll_timer, osmo_tcp_stats_config->interval, 0);
return 0;
}
static __attribute__((constructor))
void on_dso_load_stats_tcp(void)
{
stats_tcp_entry_cur = NULL;
pthread_mutex_init(&stats_tcp_lock, NULL);
osmo_tcp_stats_config->interval = TCP_STATS_DEFAULT_INTERVAL;
osmo_tcp_stats_config->batch_size = TCP_STATS_DEFAULT_BATCH_SIZE;
osmo_timer_setup(&stats_tcp_poll_timer, stats_tcp_poll_timer_cb, NULL);
}
#endif /* !EMBEDDED */
/* @} */