mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-09-01 16:48:19 +00:00
add scope stats api endpoint
This commit is contained in:
@@ -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
|
||||
-- ============================================================
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user