diff --git a/db/queries/queries.sql b/db/queries/queries.sql index 5ef8e78..74e269f 100644 --- a/db/queries/queries.sql +++ b/db/queries/queries.sql @@ -812,7 +812,8 @@ SELECT MIN(p.first_heard_at)::timestamptz AS first_heard_at, MAX(p.last_heard_at)::timestamptz AS last_heard_at, COUNT(DISTINCT p.packet_hash) AS packet_count, - COUNT(DISTINCT po.iata) AS iata_count + COUNT(DISTINCT po.iata) AS iata_count, + MAX(p.parsed_payload->>'type')::text AS trace_type FROM packets p LEFT JOIN packet_observations po ON po.packet_hash = p.packet_hash WHERE p.trace_tag IS NOT NULL @@ -821,6 +822,7 @@ WHERE p.trace_tag IS NOT NULL AND ($3::timestamptz IS NULL OR p.first_heard_at >= $3) AND ($4::timestamptz IS NULL OR p.first_heard_at <= $4) AND ($5::timestamptz IS NULL OR p.last_heard_at < $5) + AND ($7::text = '' OR p.parsed_payload->>'type' = $7) GROUP BY p.trace_tag ORDER BY MAX(p.last_heard_at) DESC LIMIT $6; diff --git a/db/sqlc/queries.sql.go b/db/sqlc/queries.sql.go index cad26cc..4442dd9 100644 --- a/db/sqlc/queries.sql.go +++ b/db/sqlc/queries.sql.go @@ -2620,7 +2620,8 @@ SELECT MIN(p.first_heard_at)::timestamptz AS first_heard_at, MAX(p.last_heard_at)::timestamptz AS last_heard_at, COUNT(DISTINCT p.packet_hash) AS packet_count, - COUNT(DISTINCT po.iata) AS iata_count + COUNT(DISTINCT po.iata) AS iata_count, + MAX(p.parsed_payload->>'type')::text AS trace_type FROM packets p LEFT JOIN packet_observations po ON po.packet_hash = p.packet_hash WHERE p.trace_tag IS NOT NULL @@ -2629,6 +2630,7 @@ WHERE p.trace_tag IS NOT NULL AND ($3::timestamptz IS NULL OR p.first_heard_at >= $3) AND ($4::timestamptz IS NULL OR p.first_heard_at <= $4) AND ($5::timestamptz IS NULL OR p.last_heard_at < $5) + AND ($7::text = '' OR p.parsed_payload->>'type' = $7) GROUP BY p.trace_tag ORDER BY MAX(p.last_heard_at) DESC LIMIT $6 @@ -2641,6 +2643,7 @@ type ListTraceTagsParams struct { Column4 pgtype.Timestamptz `json:"column_4"` Column5 pgtype.Timestamptz `json:"column_5"` Limit int32 `json:"limit"` + Column7 string `json:"column_7"` } type ListTraceTagsRow struct { @@ -2649,6 +2652,7 @@ type ListTraceTagsRow struct { LastHeardAt pgtype.Timestamptz `json:"last_heard_at"` PacketCount int64 `json:"packet_count"` IataCount int64 `json:"iata_count"` + TraceType string `json:"trace_type"` } // ============================================================ @@ -2663,6 +2667,7 @@ func (q *Queries) ListTraceTags(ctx context.Context, arg ListTraceTagsParams) ([ arg.Column4, arg.Column5, arg.Limit, + arg.Column7, ) if err != nil { return nil, err @@ -2677,6 +2682,7 @@ func (q *Queries) ListTraceTags(ctx context.Context, arg ListTraceTagsParams) ([ &i.LastHeardAt, &i.PacketCount, &i.IataCount, + &i.TraceType, ); err != nil { return nil, err } diff --git a/db/traces.go b/db/traces.go index 3072a3c..1931713 100644 --- a/db/traces.go +++ b/db/traces.go @@ -21,7 +21,7 @@ type tracePayload struct { SNRValues []float32 `json:"snrValues"` } -func (s *Store) ListTraceTags(ctx context.Context, iatas []string, scope string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) { +func (s *Store) ListTraceTags(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) { iataFilter := strings.Join(iatas, ",") var sinceTS, untilTS, cursorTS pgtype.Timestamptz if !since.IsZero() { @@ -40,6 +40,7 @@ func (s *Store) ListTraceTags(ctx context.Context, iatas []string, scope string, Column4: untilTS, Column5: cursorTS, Limit: limit, + Column7: traceType, }) if err != nil { return nil, err @@ -52,6 +53,7 @@ func (s *Store) ListTraceTags(ctx context.Context, iatas []string, scope string, LastHeardAt: r.LastHeardAt.Time.UnixMilli(), PacketCount: r.PacketCount, IATACount: r.IataCount, + TraceType: r.TraceType, }) } return items, nil diff --git a/docs/docs.go b/docs/docs.go index 8a95cd9..93c5c46 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1941,6 +1941,12 @@ const docTemplate = `{ "name": "scope", "in": "query" }, + { + "type": "string", + "description": "Filter by type: TRACE or PING (default: all)", + "name": "type", + "in": "query" + }, { "type": "integer", "description": "Filter by first_heard_at \u003e= since (epoch ms)", @@ -3123,6 +3129,7 @@ const docTemplate = `{ "type": "string" }, "snr": { + "description": "signal-to-noise ratio in dB, if available", "type": "number" } } @@ -3358,6 +3365,7 @@ const docTemplate = `{ "type": "object", "properties": { "packets": { + "description": "all packets observed for this trace", "type": "array", "items": { "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.TracePacket" @@ -3381,27 +3389,33 @@ const docTemplate = `{ "type": "integer" }, "packetHash": { + "description": "hex-encoded packet hash", "type": "string" }, "rawPath": { + "description": "hops as received in the packet", "type": "array", "items": { "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.RawHop" } }, "resolvedRoute": { + "description": "hops resolved to known nodes", "type": "array", "items": { "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ResolvedHop" } }, "routeType": { + "description": "numeric route type", "type": "integer" }, "routeTypeName": { + "description": "human-readable route type", "type": "string" }, "scope": { + "description": "transport scope name, if known", "type": "string" } } @@ -3428,6 +3442,10 @@ const docTemplate = `{ "traceTag": { "description": "hex-encoded 4-byte tag", "type": "string" + }, + "traceType": { + "description": "TRACE or PING", + "type": "string" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 775bdc5..cb422a6 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1939,6 +1939,12 @@ "name": "scope", "in": "query" }, + { + "type": "string", + "description": "Filter by type: TRACE or PING (default: all)", + "name": "type", + "in": "query" + }, { "type": "integer", "description": "Filter by first_heard_at \u003e= since (epoch ms)", @@ -3121,6 +3127,7 @@ "type": "string" }, "snr": { + "description": "signal-to-noise ratio in dB, if available", "type": "number" } } @@ -3356,6 +3363,7 @@ "type": "object", "properties": { "packets": { + "description": "all packets observed for this trace", "type": "array", "items": { "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.TracePacket" @@ -3379,27 +3387,33 @@ "type": "integer" }, "packetHash": { + "description": "hex-encoded packet hash", "type": "string" }, "rawPath": { + "description": "hops as received in the packet", "type": "array", "items": { "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.RawHop" } }, "resolvedRoute": { + "description": "hops resolved to known nodes", "type": "array", "items": { "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ResolvedHop" } }, "routeType": { + "description": "numeric route type", "type": "integer" }, "routeTypeName": { + "description": "human-readable route type", "type": "string" }, "scope": { + "description": "transport scope name, if known", "type": "string" } } @@ -3426,6 +3440,10 @@ "traceTag": { "description": "hex-encoded 4-byte tag", "type": "string" + }, + "traceType": { + "description": "TRACE or PING", + "type": "string" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index df915ed..3080a4a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -755,6 +755,7 @@ definitions: description: hex-encoded path hash type: string snr: + description: signal-to-noise ratio in dB, if available type: number type: object github_com_MeshCore-Beacon_beacon-server_internal_api.Region: @@ -912,6 +913,7 @@ definitions: github_com_MeshCore-Beacon_beacon-server_internal_api.TraceDetail: properties: packets: + description: all packets observed for this trace items: $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.TracePacket' type: array @@ -928,20 +930,26 @@ definitions: description: epoch ms type: integer packetHash: + description: hex-encoded packet hash type: string rawPath: + description: hops as received in the packet items: $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.RawHop' type: array resolvedRoute: + description: hops resolved to known nodes items: $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ResolvedHop' type: array routeType: + description: numeric route type type: integer routeTypeName: + description: human-readable route type type: string scope: + description: transport scope name, if known type: string type: object github_com_MeshCore-Beacon_beacon-server_internal_api.TraceTagSummary: @@ -961,6 +969,9 @@ definitions: traceTag: description: hex-encoded 4-byte tag type: string + traceType: + description: TRACE or PING + type: string type: object internal_api_handlers.APIError: properties: @@ -2246,6 +2257,10 @@ paths: in: query name: scope type: string + - description: 'Filter by type: TRACE or PING (default: all)' + in: query + name: type + type: string - description: Filter by first_heard_at >= since (epoch ms) in: query name: since diff --git a/internal/api/handlers/stub_reader_test.go b/internal/api/handlers/stub_reader_test.go index c551900..76a7fd3 100644 --- a/internal/api/handlers/stub_reader_test.go +++ b/internal/api/handlers/stub_reader_test.go @@ -148,7 +148,7 @@ func (stubReader) GetScopeByName(ctx context.Context, name string) (*api.ScopeDe return nil, nil } -func (stubReader) ListTraceTags(ctx context.Context, iatas []string, scope string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) { +func (stubReader) ListTraceTags(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) { return nil, nil } diff --git a/internal/api/handlers/traces.go b/internal/api/handlers/traces.go index 9c25a1f..3fd39ab 100644 --- a/internal/api/handlers/traces.go +++ b/internal/api/handlers/traces.go @@ -6,6 +6,7 @@ package handlers import ( "net/http" "strconv" + "strings" "time" "github.com/MeshCore-Beacon/beacon-server/internal/api" @@ -32,6 +33,7 @@ func TracesRouter(reader api.Reader) http.Handler { // @Param region query string false "Filter by region slug" // @Param regionId query int false "Filter by region ID" // @Param scope query string false "Filter by transport scope name" +// @Param type query string false "Filter by type: TRACE or PING (default: all)" // @Param since query int false "Filter by first_heard_at >= since (epoch ms)" // @Param until query int false "Filter by first_heard_at <= until (epoch ms)" // @Param cursor query int false "last_heard_at epoch ms of last item for pagination" @@ -73,7 +75,8 @@ func listTraceTags(reader api.Reader) http.HandlerFunc { iatas = append(iatas, regionIATAs...) } scope := r.URL.Query().Get("scope") - tags, err := reader.ListTraceTags(r.Context(), iatas, scope, since, until, cursor, limit) + traceType := strings.ToUpper(r.URL.Query().Get("type")) + tags, err := reader.ListTraceTags(r.Context(), iatas, scope, traceType, since, until, cursor, limit) if err != nil { respondError(w, http.StatusInternalServerError, "internal server error") return diff --git a/internal/api/reader.go b/internal/api/reader.go index df6c0ad..0eaaa92 100644 --- a/internal/api/reader.go +++ b/internal/api/reader.go @@ -172,7 +172,7 @@ type Reader interface { GetScopeByName(ctx context.Context, name string) (*ScopeDetail, error) // ListTraceTags returns a paginated list of trace tags with aggregate metadata. - ListTraceTags(ctx context.Context, iatas []string, scope string, since, until time.Time, cursor time.Time, limit int32) ([]TraceTagSummary, error) + ListTraceTags(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]TraceTagSummary, error) // GetTraceByTag returns all packets for a given trace tag with resolved routes. GetTraceByTag(ctx context.Context, tag string) (*TraceDetail, error) diff --git a/internal/api/traces.go b/internal/api/traces.go index e7db15c..e34bbbe 100644 --- a/internal/api/traces.go +++ b/internal/api/traces.go @@ -10,28 +10,30 @@ type TraceTagSummary struct { LastHeardAt int64 `json:"lastHeardAt"` // epoch ms PacketCount int64 `json:"packetCount"` // number of packets with this trace tag IATACount int64 `json:"iataCount"` // number of distinct IATAs where heard + TraceType string `json:"traceType"` // TRACE or PING } // TracePacket is a single packet within a trace series, including its // resolved route derived from the trace path hashes. type TracePacket struct { - PacketHash string `json:"packetHash"` - RouteType int16 `json:"routeType"` - RouteTypeName string `json:"routeTypeName"` - Scope *string `json:"scope,omitempty"` - FirstHeardAt int64 `json:"firstHeardAt"` // epoch ms - LastHeardAt int64 `json:"lastHeardAt"` // epoch ms - RawPath []RawHop `json:"rawPath"` - ResolvedRoute []ResolvedHop `json:"resolvedRoute"` + PacketHash string `json:"packetHash"` // hex-encoded packet hash + RouteType int16 `json:"routeType"` // numeric route type + RouteTypeName string `json:"routeTypeName"` // human-readable route type + Scope *string `json:"scope,omitempty"` // transport scope name, if known + FirstHeardAt int64 `json:"firstHeardAt"` // epoch ms + LastHeardAt int64 `json:"lastHeardAt"` // epoch ms + RawPath []RawHop `json:"rawPath"` // hops as received in the packet + ResolvedRoute []ResolvedHop `json:"resolvedRoute"` // hops resolved to known nodes } +// RawHop is a single hop in a trace path as received in the packet. type RawHop struct { - Hash string `json:"hash"` // hex-encoded path hash - SNR *float32 `json:"snr,omitempty"` + Hash string `json:"hash"` // hex-encoded path hash + SNR *float32 `json:"snr,omitempty"` // signal-to-noise ratio in dB, if available } // TraceDetail is the full trace series for a given trace tag. type TraceDetail struct { TraceTag string `json:"traceTag"` // hex-encoded 4-byte tag - Packets []TracePacket `json:"packets"` + Packets []TracePacket `json:"packets"` // all packets observed for this trace } diff --git a/internal/cache/reader.go b/internal/cache/reader.go index c14627a..eb98240 100644 --- a/internal/cache/reader.go +++ b/internal/cache/reader.go @@ -389,6 +389,6 @@ func (cr *CachedReader) SearchCrossIATARoutes(ctx context.Context, fromHash, fro } // ListTraceTags implements [api.Reader]. -func (cr *CachedReader) ListTraceTags(ctx context.Context, iatas []string, scope string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) { - return cr.inner.ListTraceTags(ctx, iatas, scope, since, until, cursor, limit) +func (cr *CachedReader) ListTraceTags(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) { + return cr.inner.ListTraceTags(ctx, iatas, scope, traceType, since, until, cursor, limit) } diff --git a/internal/ingest/packet.go b/internal/ingest/packet.go index f4d3774..b568de4 100644 --- a/internal/ingest/packet.go +++ b/internal/ingest/packet.go @@ -447,9 +447,13 @@ func (w *Worker) handlePacket(ctx context.Context, iata, pubkeyHex string, raw [ for _, b := range packet.Path { snrValues = append(snrValues, float32(int8(b))/4.0) } + traceType := "TRACE" + if len(hashes) == 1 { + traceType = "PING" + } pt := parsedTrace{ Raw: hex.EncodeToString(packet.Payload), - Type: "TRACE", + Type: traceType, TraceTag: hex.EncodeToString(uint32ToBytes(trace.Tag)), AuthCode: trace.AuthCode, Flags: trace.Flags,