Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,13 @@ channel_keys:

# Regional transport scopes for matching TRANSPORT_FLOOD packets.
# Plain names have # prepended automatically (e.g. "bc" → "#bc").
# region (required) is a configured region slug; region-filtered scope lists
# and scope stats show the scope under that region's IATAs. Matching stays global.
scopes:
- name: bc
region: western-canada
- name: "#west"
region: western-canada

# Observer telemetry storage settings.
telemetry:
Expand Down Expand Up @@ -500,7 +504,7 @@ Not yet implemented — see the Authentication section above.
| `GET` | `/routes` | List known routes (all hops high confidence) |
| `GET` | `/routes/search` | Search routes by source and destination hash |
| `GET` | `/routes/cross` | Search for routes crossing IATA boundaries |
| `GET` | `/scopes` | List transport scopes |
| `GET` | `/scopes` | List transport scope names; IATA/region filters use configured regions and MeshMapper catalogues |
| `GET` | `/scopes/{name}` | Get scope detail |
| `GET` | `/stats/observations` | Hourly observation time series (last 7 days by default) |
| `GET` | `/stats/overview` | Network overview stats |
Expand Down
2 changes: 2 additions & 0 deletions cmd/beacon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ func main() {
os.Exit(1)
}
scopes.Load(scopeEntries)
scopes.SetManualMembers(cfg.ManualScopeMembers())
slog.Info(fmt.Sprintf("loaded %d transport scopes", len(scopeEntries)), "component", "startup")
var scopeImporter *meshmapper.Importer
if cfg.MeshMapper.Scopes.Enabled {
Expand Down Expand Up @@ -371,6 +372,7 @@ func main() {
MaxConnectsPerMinute: resolved.MaxConnectsPerMinute,
WSAllowedOrigins: cfg.WebSocket.AllowedOrigins,
CORS: cfg.CORS, Server: cfg.Server, Auth: cfg.Auth, RateLimit: resolved.RateLimit,
Scopes: scopes,
AdminRoutes: map[string]http.Handler{
"/accounts": handlers.AccountsRouter(store),
"/backup": handlers.BackupRouter(backupOpts, ctx),
Expand Down
6 changes: 6 additions & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,13 @@ channel_keys:

# Regional transport scopes for matching TRANSPORT_FLOOD packets.
# Plain names have # prepended automatically (e.g. "bc" → "#bc").
# region (required) is a configured region slug; region-filtered scope lists
# and scope stats show the scope under that region's IATAs. Matching stays global.
scopes:
- name: bc
region: western-canada
- name: "#west"
region: western-canada

telemetry:
retention: 672h # 4 weeks
Expand Down Expand Up @@ -224,6 +228,8 @@ cache:
# Refresh success/failure/freshness is logged under component=meshmapper.scopes.
# Removing/turning off a source deactivates its imported matching keys on restart;
# historical scope identities and recorded evidence are retained.
# Region-filtered scope lists and scope stats show imported names only under the
# IATAs whose catalogue lists them; regions without a source show manual scopes only.
#meshmapper:
# scopes:
# enabled: false
Expand Down
2 changes: 1 addition & 1 deletion db/paths_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ SELECT int4send(n),'00000000-0000-0000-0000-000000000001',heard_at,iata,payload_
}
}
w := httptest.NewRecorder()
handlers.StatsRouter(store).ServeHTTP(w, httptest.NewRequest("GET", fmt.Sprintf("/paths?since=%d&until=%d&iatas=YVR", since.UnixMilli()+123, until.UnixMilli()+123), nil).WithContext(ctx))
handlers.StatsRouter(store, nil).ServeHTTP(w, httptest.NewRequest("GET", fmt.Sprintf("/paths?since=%d&until=%d&iatas=YVR", since.UnixMilli()+123, until.UnixMilli()+123), nil).WithContext(ctx))
var response api.PathStats
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil || w.Code != 200 || response.Receptions != 14 || response.Hashed != 5 {
t.Fatalf("HTTP %d: %s (%v)", w.Code, w.Body.String(), err)
Expand Down
15 changes: 0 additions & 15 deletions db/queries/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,6 @@ SELECT id FROM transport_scopes WHERE name = $1;
-- name: GetScopeNames :many
SELECT name FROM transport_scopes ORDER BY name;

-- name: GetScopesByIATAs :many
SELECT
ts.name,
COUNT(DISTINCT os.observer_id) AS observer_count,
COUNT(DISTINCT n.id) AS node_count,
COUNT(DISTINCT po.iata) AS iata_count
FROM transport_scopes ts
LEFT JOIN observer_scopes os ON os.scope_id = ts.id
LEFT JOIN observers o ON o.id = os.observer_id
LEFT JOIN packet_observations po ON po.observer_id = o.id
LEFT JOIN nodes n ON n.default_scope_id = ts.id
WHERE (COALESCE(cardinality($1::bpchar[]), 0) = 0 OR po.iata = ANY($1::bpchar[]))
GROUP BY ts.name
ORDER BY ts.name;

-- name: GetScopeByName :one
SELECT
ts.name,
Expand Down
18 changes: 0 additions & 18 deletions db/scopes.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,6 @@ func (s *Store) GetScopeNames(ctx context.Context) ([]string, error) {
return s.q.GetScopeNames(ctx)
}

// GetScopesByIATAs returns scope summaries filtered by the given IATA codes.
func (s *Store) GetScopesByIATAs(ctx context.Context, iatas []string) ([]api.ScopeSummary, error) {
rows, err := s.q.GetScopesByIATAs(ctx, iatas)
if err != nil {
return nil, err
}
items := make([]api.ScopeSummary, 0, len(rows))
for _, r := range rows {
items = append(items, api.ScopeSummary{
Name: r.Name,
ObserverCount: r.ObserverCount,
NodeCount: r.NodeCount,
IATACount: r.IataCount,
})
}
return items, nil
}

// GetScopeByName returns full detail for a single scope by its normalized name.
func (s *Store) GetScopeByName(ctx context.Context, name string) (*api.ScopeDetail, error) {
row, err := s.q.GetScopeByName(ctx, name)
Expand Down
23 changes: 0 additions & 23 deletions db/scopes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,6 @@ func TestGetTransportScopes(t *testing.T) {
}
}

func TestGetScopesByIATAs(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)

mock.EXPECT().
GetScopesByIATAs(gomock.Any(), []string{"YVR", "YYJ"}).
Return([]sqlc.GetScopesByIATAsRow{
{Name: "default", ObserverCount: 3, NodeCount: 10, IataCount: 2},
}, nil)

store := &Store{q: mock}
items, err := store.GetScopesByIATAs(context.Background(), []string{"YVR", "YYJ"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(items) != 1 {
t.Fatalf("expected 1 item, got %d", len(items))
}
if items[0].IATACount != 2 {
t.Errorf("expected IATACount 2, got %d", items[0].IATACount)
}
}

func TestGetScopeByName(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
Expand Down
2 changes: 1 addition & 1 deletion db/signal_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ SELECT int4send(n),'00000000-0000-0000-0000-000000000001',heard_at,iata,snr,rssi
}
w := httptest.NewRecorder()
request := httptest.NewRequest("GET", fmt.Sprintf("/signal?since=%d&until=%d&iatas=YVR", since.UnixMilli()+123, until.UnixMilli()+123), nil).WithContext(ctx)
handlers.StatsRouter(store).ServeHTTP(w, request)
handlers.StatsRouter(store, nil).ServeHTTP(w, request)
var response api.SignalStats
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil || w.Code != 200 || response.Receptions != 13 || response.SNR.Samples != 5 {
t.Fatalf("PostgreSQL HTTP response: status=%d body=%s error=%v", w.Code, w.Body.String(), err)
Expand Down
15 changes: 0 additions & 15 deletions db/sqlc/mock/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion db/sqlc/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 0 additions & 48 deletions db/sqlc/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 12 additions & 3 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2219,7 +2219,7 @@ const docTemplate = `{
},
"/scopes": {
"get": {
"description": "The unfiltered list includes stored imported names after an importer is disabled or a source is removed. Those historical identities are retained indefinitely; listing them does not establish observed traffic or current source membership.",
"description": "Without filters, lists every stored scope name, including imported names retained after an importer is disabled or a source is removed. With IATA or region filters, lists only manual scopes configured for a matching region and imported scopes whose current MeshMapper catalogue includes a matching IATA; observed traffic does not add scopes.",
"produces": [
"application/json"
],
Expand Down Expand Up @@ -2251,7 +2251,16 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"type": "object"
"type": "array",
"items": {
"type": "string"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/internal_api_handlers.APIError"
}
},
"500": {
Expand Down Expand Up @@ -2780,7 +2789,7 @@ const docTemplate = `{
},
"/stats/scopes": {
"get": {
"description": "Counts each packet, observer and node once per scope. IATA filters use retained observations for packets/observers and node IATA memberships for nodes. Without filters, returns global totals. Scopes with zero matching counts remain listed; an empty region returns an empty array.",
"description": "Counts each packet, observer and node once per scope. IATA filters use retained observations for packets/observers and node IATA memberships for nodes. Without filters, returns global totals for every stored scope. With filters, lists only manual scopes configured for a matching region and imported scopes whose current MeshMapper catalogue includes a matching IATA; those remain listed with zero counts. An empty region returns an empty array.",
"produces": [
"application/json"
],
Expand Down
15 changes: 12 additions & 3 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2217,7 +2217,7 @@
},
"/scopes": {
"get": {
"description": "The unfiltered list includes stored imported names after an importer is disabled or a source is removed. Those historical identities are retained indefinitely; listing them does not establish observed traffic or current source membership.",
"description": "Without filters, lists every stored scope name, including imported names retained after an importer is disabled or a source is removed. With IATA or region filters, lists only manual scopes configured for a matching region and imported scopes whose current MeshMapper catalogue includes a matching IATA; observed traffic does not add scopes.",
"produces": [
"application/json"
],
Expand Down Expand Up @@ -2249,7 +2249,16 @@
"200": {
"description": "OK",
"schema": {
"type": "object"
"type": "array",
"items": {
"type": "string"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/internal_api_handlers.APIError"
}
},
"500": {
Expand Down Expand Up @@ -2778,7 +2787,7 @@
},
"/stats/scopes": {
"get": {
"description": "Counts each packet, observer and node once per scope. IATA filters use retained observations for packets/observers and node IATA memberships for nodes. Without filters, returns global totals. Scopes with zero matching counts remain listed; an empty region returns an empty array.",
"description": "Counts each packet, observer and node once per scope. IATA filters use retained observations for packets/observers and node IATA memberships for nodes. Without filters, returns global totals for every stored scope. With filters, lists only manual scopes configured for a matching region and imported scopes whose current MeshMapper catalogue includes a matching IATA; those remain listed with zero counts. An empty region returns an empty array.",
"produces": [
"application/json"
],
Expand Down
23 changes: 16 additions & 7 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3013,10 +3013,11 @@ paths:
- Routes
/scopes:
get:
description: The unfiltered list includes stored imported names after an importer
is disabled or a source is removed. Those historical identities are retained
indefinitely; listing them does not establish observed traffic or current
source membership.
description: Without filters, lists every stored scope name, including imported
names retained after an importer is disabled or a source is removed. With
IATA or region filters, lists only manual scopes configured for a matching
region and imported scopes whose current MeshMapper catalogue includes a matching
IATA; observed traffic does not add scopes.
parameters:
- description: Filter by IATA code(s), comma-separated
in: query
Expand All @@ -3036,7 +3037,13 @@ paths:
"200":
description: OK
schema:
type: object
items:
type: string
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/internal_api_handlers.APIError'
"500":
description: Internal Server Error
schema:
Expand Down Expand Up @@ -3400,8 +3407,10 @@ paths:
get:
description: Counts each packet, observer and node once per scope. IATA filters
use retained observations for packets/observers and node IATA memberships
for nodes. Without filters, returns global totals. Scopes with zero matching
counts remain listed; an empty region returns an empty array.
for nodes. Without filters, returns global totals for every stored scope.
With filters, lists only manual scopes configured for a matching region and
imported scopes whose current MeshMapper catalogue includes a matching IATA;
those remain listed with zero counts. An empty region returns an empty array.
parameters:
- description: Comma-separated IATA codes
in: query
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/limits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func TestListLimits(t *testing.T) {
router.Mount("/messages", MessagesRouter(reader))
router.Mount("/routes", RoutesRouter(reader))
router.Mount("/traces", TracesRouter(reader))
router.Mount("/stats", StatsRouter(reader))
router.Mount("/stats", StatsRouter(reader, nil))
for _, endpoint := range []struct {
path string
defaultLimit int32
Expand Down
6 changes: 3 additions & 3 deletions internal/api/handlers/observer_comparison_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestObserverComparisonRejectsInvalidQuery(t *testing.T) {
} {
t.Run(query, func(t *testing.T) {
w := httptest.NewRecorder()
StatsRouter(nil).ServeHTTP(w, httptest.NewRequest("GET", "/observer-comparison?"+query, nil))
StatsRouter(nil, nil).ServeHTTP(w, httptest.NewRequest("GET", "/observer-comparison?"+query, nil))
if w.Code != 400 {
t.Fatalf("status %d; wanted validation error", w.Code)
}
Expand All @@ -42,7 +42,7 @@ func TestObserverComparisonHTTP(t *testing.T) {
query := "observerA=" + a.String() + "&observerB=" + b.String() + "&since=0&until=1000"
for _, suffix := range []string{"&since=2", "&observerA=" + b.String(), "&until=253402300800000", "&observerB=00000000-0000-0000-0000-000000000000"} {
w := httptest.NewRecorder()
StatsRouter(nil).ServeHTTP(w, httptest.NewRequest("GET", "/observer-comparison?"+query+suffix, nil))
StatsRouter(nil, nil).ServeHTTP(w, httptest.NewRequest("GET", "/observer-comparison?"+query+suffix, nil))
if w.Code != 400 {
t.Fatalf("invalid query returned %d", w.Code)
}
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestObserverComparisonHTTP(t *testing.T) {
},
}
w := httptest.NewRecorder()
StatsRouter(reader).ServeHTTP(w, httptest.NewRequest("GET", "/observer-comparison?"+query+tc.suffix, nil))
StatsRouter(reader, nil).ServeHTTP(w, httptest.NewRequest("GET", "/observer-comparison?"+query+tc.suffix, nil))
if w.Code != tc.status {
t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
}
Expand Down
Loading
Loading