add scope stats api endpoint

This commit is contained in:
Enot (ded) Skelly
2026-06-04 11:17:58 -07:00
parent 1e5ccc6e50
commit 594cd7281e
8 changed files with 227 additions and 0 deletions
+13
View File
@@ -651,6 +651,19 @@ WHERE ($1::text = '' OR preset = $1::text)
AND ($2::text = '' OR iata = $2::text)
ORDER BY preset, iata, source_type;
-- name: GetScopeStats :many
SELECT
ts.name,
COUNT(DISTINCT p.packet_hash) AS packet_count,
COUNT(DISTINCT os.observer_id) AS observer_count,
COUNT(DISTINCT n.id) AS node_count
FROM transport_scopes ts
LEFT JOIN packets p ON p.scope_id = ts.id
LEFT JOIN observer_scopes os ON os.scope_id = ts.id
LEFT JOIN nodes n ON n.default_scope_id = ts.id
GROUP BY ts.name
ORDER BY ts.name;
-- ============================================================
-- REGIONS
-- ============================================================
+46
View File
@@ -780,6 +780,52 @@ func (q *Queries) GetRegionIATAs(ctx context.Context, regionID int32) ([]string,
return items, nil
}
const getScopeStats = `-- name: GetScopeStats :many
SELECT
ts.name,
COUNT(DISTINCT p.packet_hash) AS packet_count,
COUNT(DISTINCT os.observer_id) AS observer_count,
COUNT(DISTINCT n.id) AS node_count
FROM transport_scopes ts
LEFT JOIN packets p ON p.scope_id = ts.id
LEFT JOIN observer_scopes os ON os.scope_id = ts.id
LEFT JOIN nodes n ON n.default_scope_id = ts.id
GROUP BY ts.name
ORDER BY ts.name
`
type GetScopeStatsRow struct {
Name string `json:"name"`
PacketCount int64 `json:"packet_count"`
ObserverCount int64 `json:"observer_count"`
NodeCount int64 `json:"node_count"`
}
func (q *Queries) GetScopeStats(ctx context.Context) ([]GetScopeStatsRow, error) {
rows, err := q.db.Query(ctx, getScopeStats)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetScopeStatsRow{}
for rows.Next() {
var i GetScopeStatsRow
if err := rows.Scan(
&i.Name,
&i.PacketCount,
&i.ObserverCount,
&i.NodeCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getStatsOverview = `-- name: GetStatsOverview :one
SELECT
+18
View File
@@ -1433,6 +1433,24 @@ func (s *Store) GetRadioPresets(ctx context.Context, preset, iata string) ([]api
return items, nil
}
// GetScopeStats returns aggregate packet, observer and node counts per transport scope.
func (s *Store) GetScopeStats(ctx context.Context) ([]api.ScopeStats, error) {
rows, err := s.q.GetScopeStats(ctx)
if err != nil {
return nil, err
}
items := make([]api.ScopeStats, 0, len(rows))
for _, r := range rows {
items = append(items, api.ScopeStats{
Name: r.Name,
PacketCount: r.PacketCount,
ObserverCount: r.ObserverCount,
NodeCount: r.NodeCount,
})
}
return items, nil
}
func nullableUUID(id uuid.UUID) *uuid.UUID {
if id == (uuid.UUID{}) {
return nil
+45
View File
@@ -1202,6 +1202,34 @@ const docTemplate = `{
}
}
},
"/stats/scopes": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Stats"
],
"summary": "Scope statistics",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/github_com_MeshCore-Tower_tower-server_internal_api.ScopeStats"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/internal_api_handlers.APIError"
}
}
}
}
},
"/stats/top-nodes": {
"get": {
"produces": [
@@ -2180,6 +2208,23 @@ const docTemplate = `{
}
}
},
"github_com_MeshCore-Tower_tower-server_internal_api.ScopeStats": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"nodeCount": {
"type": "integer"
},
"observerCount": {
"type": "integer"
},
"packetCount": {
"type": "integer"
}
}
},
"github_com_MeshCore-Tower_tower-server_internal_api.StatsOverview": {
"type": "object",
"properties": {
+45
View File
@@ -1200,6 +1200,34 @@
}
}
},
"/stats/scopes": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Stats"
],
"summary": "Scope statistics",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/github_com_MeshCore-Tower_tower-server_internal_api.ScopeStats"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/internal_api_handlers.APIError"
}
}
}
}
},
"/stats/top-nodes": {
"get": {
"produces": [
@@ -2178,6 +2206,23 @@
}
}
},
"github_com_MeshCore-Tower_tower-server_internal_api.ScopeStats": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"nodeCount": {
"type": "integer"
},
"observerCount": {
"type": "integer"
},
"packetCount": {
"type": "integer"
}
}
},
"github_com_MeshCore-Tower_tower-server_internal_api.StatsOverview": {
"type": "object",
"properties": {
+29
View File
@@ -603,6 +603,17 @@ definitions:
description: hex-encoded prefix
type: string
type: object
github_com_MeshCore-Tower_tower-server_internal_api.ScopeStats:
properties:
name:
type: string
nodeCount:
type: integer
observerCount:
type: integer
packetCount:
type: integer
type: object
github_com_MeshCore-Tower_tower-server_internal_api.StatsOverview:
properties:
activeIatas:
@@ -1448,6 +1459,24 @@ paths:
summary: Radio preset usage by IATA
tags:
- Stats
/stats/scopes:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/github_com_MeshCore-Tower_tower-server_internal_api.ScopeStats'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/internal_api_handlers.APIError'
summary: Scope statistics
tags:
- Stats
/stats/top-nodes:
get:
parameters:
+21
View File
@@ -18,6 +18,7 @@ import (
// GET /stats/top-nodes → getStatsTopNodes
// GET /stats/top-observers → getStatsTopObservers
// GET /stats/radio-presets → getStatsRadioPresets
// GET /stats/scopes → GetStatsScopes
//
// All endpoints accept an optional iata= filter (case-insensitive).
func StatsRouter(reader api.Reader) http.Handler {
@@ -28,6 +29,7 @@ func StatsRouter(reader api.Reader) http.Handler {
r.Get("/top-nodes", getStatsTopNodes(reader))
r.Get("/top-observers", getStatsTopObservers(reader))
r.Get("/radio-presets", getStatsRadioPresets(reader))
r.Get("/scopes", getStatsScopes(reader))
return r
}
@@ -213,3 +215,22 @@ func getStatsRadioPresets(reader api.Reader) http.HandlerFunc {
respond(w, http.StatusOK, presets)
}
}
// getStatsScopes godoc
//
// @Summary Scope statistics
// @Tags Stats
// @Produce json
// @Success 200 {object} []api.ScopeStats
// @Failure 500 {object} handlers.APIError
// @Router /stats/scopes [get]
func getStatsScopes(reader api.Reader) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
stats, err := reader.GetScopeStats(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, "internal server error")
return
}
respond(w, http.StatusOK, stats)
}
}
+10
View File
@@ -42,6 +42,14 @@ type PayloadBreakdownItem struct {
Count int64 `json:"count"`
}
// ScopeStats represents aggregate statistics for a single transport scope.
type ScopeStats struct {
Name string `json:"name"`
PacketCount int64 `json:"packetCount"`
ObserverCount int64 `json:"observerCount"`
NodeCount int64 `json:"nodeCount"`
}
// TopNode is a node ranked by observation count.
type TopNode struct {
NodeID uuid.UUID `json:"nodeId"`
@@ -459,4 +467,6 @@ type Reader interface {
// Pass empty string iata to return stats across all IATAs.
// since defines the start of the window; pass zero time for default (last 24h).
GetStatsTopObservers(ctx context.Context, iata string, since time.Time, limit int32) ([]TopObserver, error)
// GetScopeStats returns aggregate packet, observer and node counts per transport scope.
GetScopeStats(ctx context.Context) ([]ScopeStats, error)
}