diff --git a/README.md b/README.md index a416d59..d3b5133 100644 --- a/README.md +++ b/README.md @@ -912,6 +912,17 @@ How it works: | `month` | 6 hours | hourly | | `year` | 1 month | monthly | +- **Site-to-site availability:** `site_to_site_up` of the same answer divides the view into slots of its own, `slot`, and gives for each slot and connection the percentage of the samples in which the connection was up, 0 to 100, as `points` of `{"time", "connections": {name: percent}}`: + + | Range | Slot | Source | + |---|---|---| + | `hour` | 5 minutes | details | + | `day` | 1 hour | details | + | `month` | 1 day | hourly | + | `year` | 1 month | monthly | + + A sample is taken every minute, so an interruption shorter than a minute may be missed; a slot without samples has no value, and the year view, like the others of its level, has the complete months only. + ### Scraping `GET /metrics` of the metrics service serves the last sample in the Prometheus text format, written by `prometheus_client`. diff --git a/debian-package/debian/changelog b/debian-package/debian/changelog index e42cdfd..0fcdbcc 100644 --- a/debian-package/debian/changelog +++ b/debian-package/debian/changelog @@ -1,3 +1,14 @@ +vpn-router (0.19.1-1) unstable; urgency=medium + + fix: /metrics/history gives the availability of each site-to-site connection as the percentage of time it was up in slots of the view. + + * site_to_site_up of /metrics/history divides the view into slots of 5 + minutes for an hour, 1 hour for a day, 1 day for a month and 1 month + for a year, and gives for each slot and connection the percentage of + the samples in which the connection was up. + + -- Slawomir Koszewski Mon, 26 Oct 2026 21:00:00 +0100 + vpn-router (0.19.0-1) unstable; urgency=medium feature: service accounts have a display name and a description, which the router sets for its internal ones. diff --git a/debian-package/src/usr/lib/vpn-router/vpnrouter_metrics.py b/debian-package/src/usr/lib/vpn-router/vpnrouter_metrics.py index 2bc733f..fdbb051 100644 --- a/debian-package/src/usr/lib/vpn-router/vpnrouter_metrics.py +++ b/debian-package/src/usr/lib/vpn-router/vpnrouter_metrics.py @@ -44,6 +44,15 @@ LEVELS = ( #: The kinds of VPN clients of metrics_vpn. VPN_KINDS = ('p2s', 'openvpn', 'wireguard') +#: The time slots of the site-to-site availability of each view of history(): the summary level read (None: the +#: details), the slot as PostgreSQL starts it from time, and its length. +UP_SLOTS = { + 'hour': (None, "date_bin('5 minutes', time, timestamptz '2000-01-01 00:00:00+00')", '5 minutes'), + 'day': (None, "date_trunc('hour', time, 'UTC')", '1 hour'), + 'month': ('hourly', "date_trunc('day', time, 'UTC')", '1 day'), + 'year': ('monthly', "date_trunc('month', time, 'UTC')", '1 month'), +} + #: The views of history(): the summary level read (None: the details), the bucket and the span. RANGES = { 'hour': (None, '1 minute', '1 hour'), @@ -148,8 +157,13 @@ def history(connection, view): each interface to its rx and tx, clients, which maps each kind of VPN client to its clients, and site_to_site, which maps each site-to-site connection to the values of metrics_site_to_site; a value without data is None. + + site_to_site_up is the availability of each site-to-site connection in the time slots of UP_SLOTS, oldest first: + for each slot its start in Unix seconds and, per connection, the percentage of its samples in which the connection + was up, 0 to 100; the summaries' averages of up give it for the month and the year. """ level, bucket, span = RANGES[view] + up_level, up_start, up_slot = UP_SLOTS[view] def rows(table): keys, values = TABLES[table] @@ -182,4 +196,12 @@ def history(connection, view): for row in rows('metrics_site_to_site'): point(row[0])['site_to_site'][row[1]] = pairs(row[2:], TABLES['metrics_site_to_site'][1]) empty = {value: {'avg': None, 'max': None} for value in TABLES['metrics_system'][1]} - return {'range': view, 'bucket': bucket, 'points': [{**empty, **points[start]} for start in sorted(points)]} + + slots = {} + for start, name, percent in connection.execute( + f"SELECT extract(epoch FROM {up_start})::bigint AS slot, connection, avg({'up' if up_level is None else 'up_avg'}) * 100 " + f"FROM {'metrics_site_to_site' if up_level is None else f'metrics_site_to_site_{up_level}'} " + f"WHERE time >= now() - interval '{span}' GROUP BY slot, connection ORDER BY slot").fetchall(): + slots.setdefault(start, {'time': start, 'connections': {}})['connections'][name] = percent + return {'range': view, 'bucket': bucket, 'points': [{**empty, **points[start]} for start in sorted(points)], + 'site_to_site_up': {'slot': up_slot, 'points': [slots[start] for start in sorted(slots)]}}