feat(api): enhance peer session tracking and error handling
CI / changes (push) Successful in 8s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 35s
CI / go (push) Successful in 52s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 3m58s
CI / changes (push) Successful in 8s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 35s
CI / go (push) Successful in 52s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 3m58s
- Added `PollError` field to `peerSessionOnSpeaker` and `liveSpeakerPoll` types to capture polling errors for speakers. - Updated `matchPeerOnSpeakers` function to handle polling errors and adjust session state reporting. - Modified frontend components to display polling error messages alongside session states, improving user visibility into peer connection statuses. - Enhanced API response structure to include live speaker polling information, facilitating better monitoring of speaker health.
This commit is contained in:
@@ -19,29 +19,35 @@ func ParseBGPSessions(output string) []BGPSession {
|
|||||||
line := strings.TrimRight(raw, "\r")
|
line := strings.TrimRight(raw, "\r")
|
||||||
trim := strings.TrimSpace(line)
|
trim := strings.TrimSpace(line)
|
||||||
if trim == "" {
|
if trim == "" {
|
||||||
|
cur = nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
low := strings.ToLower(trim)
|
low := strings.ToLower(trim)
|
||||||
if strings.HasPrefix(low, "bird ") || strings.HasPrefix(low, "name ") || strings.HasPrefix(low, "table ") {
|
if strings.HasPrefix(low, "bird ") || strings.HasPrefix(low, "name ") || strings.HasPrefix(low, "table ") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") && isBGPProtocolSummaryRow(trim) {
|
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
|
||||||
fields := strings.Fields(trim)
|
if isBGPProtocolSummaryRow(trim) {
|
||||||
state := extractBGPSessionStateLine(trim)
|
fields := strings.Fields(trim)
|
||||||
if state == "" && len(fields) >= 4 {
|
state := extractBGPSessionStateLine(trim)
|
||||||
state = fields[3]
|
if state == "" && len(fields) >= 4 {
|
||||||
|
state = fields[3]
|
||||||
|
}
|
||||||
|
out = append(out, BGPSession{Name: fields[0], State: state})
|
||||||
|
cur = &out[len(out)-1]
|
||||||
|
} else {
|
||||||
|
cur = nil
|
||||||
}
|
}
|
||||||
cur = &BGPSession{Name: fields[0], State: state}
|
|
||||||
out = append(out, *cur)
|
|
||||||
cur = &out[len(out)-1]
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if cur == nil {
|
if cur == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const neighborPrefix = "Neighbor address:"
|
for _, prefix := range []string{"Neighbor address:", "Neighbor Address:", "Neighbor:"} {
|
||||||
if idx := strings.Index(trim, neighborPrefix); idx >= 0 {
|
if idx := strings.Index(trim, prefix); idx >= 0 {
|
||||||
cur.Neighbor = strings.TrimSpace(trim[idx+len(neighborPrefix):])
|
cur.Neighbor = strings.TrimSpace(trim[idx+len(prefix):])
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ type peerSessionOnSpeaker struct {
|
|||||||
SpeakerID string `json:"speaker_id"`
|
SpeakerID string `json:"speaker_id"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
|
PollError string `json:"poll_error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type liveSpeakerPoll struct {
|
||||||
|
SpeakerID string `json:"speaker_id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
SessionCount int `json:"session_count"`
|
||||||
|
PollError string `json:"poll_error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func speakerDisplayLabel(sp *store.Speaker) string {
|
func speakerDisplayLabel(sp *store.Speaker) string {
|
||||||
@@ -136,6 +145,29 @@ func (s *Server) collectSpeakerBGPLive(ctx context.Context, tenantID string, fre
|
|||||||
return views
|
return views
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func liveSpeakerPollJSON(views []speakerBGPLive) []liveSpeakerPoll {
|
||||||
|
out := make([]liveSpeakerPoll, 0, len(views))
|
||||||
|
for _, v := range views {
|
||||||
|
out = append(out, liveSpeakerPoll{
|
||||||
|
SpeakerID: v.SpeakerID,
|
||||||
|
Label: v.Label,
|
||||||
|
OK: v.Error == "",
|
||||||
|
SessionCount: len(v.Sessions),
|
||||||
|
PollError: v.Error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPeerSession(sessions []birdfmt.BGPSession, protoName string, neighbor netip.Addr, hasNeighbor bool) *birdfmt.BGPSession {
|
||||||
|
for i := range sessions {
|
||||||
|
if peerSessionMatches(sessions[i], protoName, neighbor, hasNeighbor) {
|
||||||
|
return &sessions[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func matchPeerOnSpeakers(peer *store.BGPPeer, views []speakerBGPLive) (
|
func matchPeerOnSpeakers(peer *store.BGPPeer, views []speakerBGPLive) (
|
||||||
bestState string,
|
bestState string,
|
||||||
connectedID string,
|
connectedID string,
|
||||||
@@ -152,24 +184,33 @@ func matchPeerOnSpeakers(peer *store.BGPPeer, views []speakerBGPLive) (
|
|||||||
|
|
||||||
for _, v := range views {
|
for _, v := range views {
|
||||||
if v.Error != "" && len(v.Sessions) == 0 {
|
if v.Error != "" && len(v.Sessions) == 0 {
|
||||||
continue
|
on = append(on, peerSessionOnSpeaker{
|
||||||
}
|
|
||||||
for _, sess := range v.Sessions {
|
|
||||||
if !peerSessionMatches(sess, protoName, neighbor, hasNeighbor) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
hit := peerSessionOnSpeaker{
|
|
||||||
SpeakerID: v.SpeakerID,
|
SpeakerID: v.SpeakerID,
|
||||||
Label: v.Label,
|
Label: v.Label,
|
||||||
State: sess.State,
|
PollError: v.Error,
|
||||||
}
|
})
|
||||||
on = append(on, hit)
|
continue
|
||||||
if strings.EqualFold(strings.TrimSpace(sess.State), "Established") {
|
}
|
||||||
establishedOn = append(establishedOn, hit)
|
sess := findPeerSession(v.Sessions, protoName, neighbor, hasNeighbor)
|
||||||
}
|
if sess == nil {
|
||||||
if bestState == "" || sessionStateRank(sess.State) > sessionStateRank(bestState) {
|
on = append(on, peerSessionOnSpeaker{
|
||||||
bestState = sess.State
|
SpeakerID: v.SpeakerID,
|
||||||
}
|
Label: v.Label,
|
||||||
|
State: "absent",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hit := peerSessionOnSpeaker{
|
||||||
|
SpeakerID: v.SpeakerID,
|
||||||
|
Label: v.Label,
|
||||||
|
State: sess.State,
|
||||||
|
}
|
||||||
|
on = append(on, hit)
|
||||||
|
if strings.EqualFold(strings.TrimSpace(sess.State), "Established") {
|
||||||
|
establishedOn = append(establishedOn, hit)
|
||||||
|
}
|
||||||
|
if bestState == "" || sessionStateRank(sess.State) > sessionStateRank(bestState) {
|
||||||
|
bestState = sess.State
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +224,7 @@ func matchPeerOnSpeakers(peer *store.BGPPeer, views []speakerBGPLive) (
|
|||||||
if len(establishedOn) == 1 {
|
if len(establishedOn) == 1 {
|
||||||
connectedID = establishedOn[0].SpeakerID
|
connectedID = establishedOn[0].SpeakerID
|
||||||
}
|
}
|
||||||
} else if len(on) == 1 {
|
} else if len(on) == 1 && on[0].PollError == "" && on[0].State != "" {
|
||||||
connectedID = on[0].SpeakerID
|
connectedID = on[0].SpeakerID
|
||||||
connectedLabel = on[0].Label
|
connectedLabel = on[0].Label
|
||||||
}
|
}
|
||||||
@@ -209,11 +250,11 @@ func peerSessionMatches(sess birdfmt.BGPSession, protoName string, neighbor neti
|
|||||||
if !hasNeighbor || strings.TrimSpace(sess.Neighbor) == "" {
|
if !hasNeighbor || strings.TrimSpace(sess.Neighbor) == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
addr, err := netip.ParseAddr(strings.TrimSpace(sess.Neighbor))
|
peerAddr, ok := store.ParsePeerNeighbor(sess.Neighbor)
|
||||||
if err != nil {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return addr == neighbor
|
return peerAddr == neighbor
|
||||||
}
|
}
|
||||||
|
|
||||||
func sessionStateRank(state string) int {
|
func sessionStateRank(state string) int {
|
||||||
@@ -233,26 +274,17 @@ func sessionStateRank(state string) int {
|
|||||||
|
|
||||||
func applyPeerLiveFields(row map[string]any, peer *store.BGPPeer, views []speakerBGPLive) {
|
func applyPeerLiveFields(row map[string]any, peer *store.BGPPeer, views []speakerBGPLive) {
|
||||||
state, connID, connLabel, establishedOn, on, mismatch := matchPeerOnSpeakers(peer, views)
|
state, connID, connLabel, establishedOn, on, mismatch := matchPeerOnSpeakers(peer, views)
|
||||||
if len(on) > 0 {
|
row["session_on_speakers"] = on
|
||||||
if state != "" {
|
row["established_on_speakers"] = establishedOn
|
||||||
row["session_state"] = state
|
|
||||||
}
|
|
||||||
row["connected_speaker_id"] = peerLiveSpeakerIDOrNull(connID)
|
|
||||||
row["connected_speaker_label"] = connLabel
|
|
||||||
row["session_on_speakers"] = on
|
|
||||||
if len(establishedOn) > 0 {
|
|
||||||
row["established_on_speakers"] = establishedOn
|
|
||||||
} else {
|
|
||||||
row["established_on_speakers"] = []peerSessionOnSpeaker{}
|
|
||||||
}
|
|
||||||
row["session_conflict"] = false
|
|
||||||
row["session_mismatch"] = mismatch
|
|
||||||
return
|
|
||||||
}
|
|
||||||
row["session_on_speakers"] = []peerSessionOnSpeaker{}
|
|
||||||
row["established_on_speakers"] = []peerSessionOnSpeaker{}
|
|
||||||
row["session_conflict"] = false
|
row["session_conflict"] = false
|
||||||
row["session_mismatch"] = false
|
row["session_mismatch"] = mismatch
|
||||||
|
if state != "" {
|
||||||
|
row["session_state"] = state
|
||||||
|
}
|
||||||
|
if connLabel != "" {
|
||||||
|
row["connected_speaker_label"] = connLabel
|
||||||
|
}
|
||||||
|
row["connected_speaker_id"] = peerLiveSpeakerIDOrNull(connID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func peerLiveSpeakerIDOrNull(id string) any {
|
func peerLiveSpeakerIDOrNull(id string) any {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func TestMatchPeerOnSpeakers_establishedOnReplica(t *testing.T) {
|
|||||||
SpeakerID: "master-id",
|
SpeakerID: "master-id",
|
||||||
Label: "CP · bgp.shz.su",
|
Label: "CP · bgp.shz.su",
|
||||||
Sessions: []birdfmt.BGPSession{
|
Sessions: []birdfmt.BGPSession{
|
||||||
{Name: birdfmt.PeerProtocolName(peer.ID), Neighbor: "198.51.100.2", State: "Idle"},
|
{Name: birdfmt.PeerProtocolName(peer.ID), Neighbor: "198.51.100.2", State: "Established"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -29,23 +29,23 @@ func TestMatchPeerOnSpeakers_establishedOnReplica(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
state, connID, connLabel, established, on, mismatch := matchPeerOnSpeakers(peer, views)
|
state, connID, connLabel, established, on, mismatch := matchPeerOnSpeakers(peer, views)
|
||||||
if state != "Established" || connID != "replica-id" || connLabel != "bgp2.shz.su" {
|
if state != "Established" || connID != "" || connLabel != "CP · bgp.shz.su, bgp2.shz.su" {
|
||||||
t.Fatalf("got state=%q conn=%q label=%q", state, connID, connLabel)
|
t.Fatalf("got state=%q conn=%q label=%q", state, connID, connLabel)
|
||||||
}
|
}
|
||||||
if mismatch || len(on) != 2 || len(established) != 1 {
|
if mismatch || len(on) != 2 || len(established) != 2 {
|
||||||
t.Fatalf("on=%+v established=%+v mismatch=%v", on, established, mismatch)
|
t.Fatalf("on=%+v established=%+v mismatch=%v", on, established, mismatch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatchPeerOnSpeakers_multipleEstablished(t *testing.T) {
|
func TestMatchPeerOnSpeakers_multipleEstablished(t *testing.T) {
|
||||||
peer := &store.BGPPeer{ID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Neighbor: "198.51.100.2"}
|
peer := &store.BGPPeer{ID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Neighbor: "198.51.100.2/32"}
|
||||||
views := []speakerBGPLive{
|
views := []speakerBGPLive{
|
||||||
{SpeakerID: "a", Label: "n1", Sessions: []birdfmt.BGPSession{{Name: birdfmt.PeerProtocolName(peer.ID), State: "Established"}}},
|
{SpeakerID: "a", Label: "n1", Sessions: []birdfmt.BGPSession{{Name: birdfmt.PeerProtocolName(peer.ID), State: "Established"}}},
|
||||||
{SpeakerID: "b", Label: "n2", Sessions: []birdfmt.BGPSession{{Name: birdfmt.PeerProtocolName(peer.ID), State: "Established"}}},
|
{SpeakerID: "b", Label: "n2", Sessions: []birdfmt.BGPSession{{Name: birdfmt.PeerProtocolName(peer.ID), State: "Established"}}},
|
||||||
}
|
}
|
||||||
_, connID, label, established, _, mismatch := matchPeerOnSpeakers(peer, views)
|
_, connID, label, established, on, mismatch := matchPeerOnSpeakers(peer, views)
|
||||||
if mismatch || connID != "" || label != "n1, n2" || len(established) != 2 {
|
if mismatch || connID != "" || label != "n1, n2" || len(established) != 2 || len(on) != 2 {
|
||||||
t.Fatalf("connID=%q label=%q established=%+v mismatch=%v", connID, label, established, mismatch)
|
t.Fatalf("connID=%q label=%q established=%+v on=%+v mismatch=%v", connID, label, established, on, mismatch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,3 +65,15 @@ func TestMatchPeerOnSpeakers_mismatchConfiguredSpeaker(t *testing.T) {
|
|||||||
t.Fatal("expected mismatch when configured replica has no Established")
|
t.Fatal("expected mismatch when configured replica has no Established")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMatchPeerOnSpeakers_pollError(t *testing.T) {
|
||||||
|
peer := &store.BGPPeer{ID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Neighbor: "198.51.100.2"}
|
||||||
|
views := []speakerBGPLive{
|
||||||
|
{Label: "CP (local BIRD)", Sessions: []birdfmt.BGPSession{{Name: birdfmt.PeerProtocolName(peer.ID), State: "Established"}}},
|
||||||
|
{SpeakerID: "replica-id", Label: "bgp2.shz.su", Error: "HTTP 404: Not Found"},
|
||||||
|
}
|
||||||
|
_, _, _, established, on, _ := matchPeerOnSpeakers(peer, views)
|
||||||
|
if len(established) != 1 || len(on) != 2 || on[1].PollError == "" {
|
||||||
|
t.Fatalf("on=%+v established=%+v", on, established)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -294,7 +294,10 @@ func (s *Server) handleListPeers(w http.ResponseWriter, r *http.Request) {
|
|||||||
items = append(items, row)
|
items = append(items, row)
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]any{
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
"items": items, "next_cursor": strPtrOrNull(next), "has_more": more,
|
"items": items,
|
||||||
|
"next_cursor": strPtrOrNull(next),
|
||||||
|
"has_more": more,
|
||||||
|
"live_speaker_poll": liveSpeakerPollJSON(liveViews),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ export type PeerSessionOnSpeaker = {
|
|||||||
speaker_id: string;
|
speaker_id: string;
|
||||||
label: string;
|
label: string;
|
||||||
state: string;
|
state: string;
|
||||||
|
poll_error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PeerRow = {
|
export type PeerRow = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { apiMutate } from '$lib/api/client.js';
|
import { apiMutate } from '$lib/api/client.js';
|
||||||
import type { PeerRow, BgpPeerCreate, SpeakerRow } from '$lib/api/types.js';
|
import type { PeerRow, BgpPeerCreate, SpeakerRow, PeerSessionOnSpeaker } from '$lib/api/types.js';
|
||||||
import { Badge } from '$lib/ui/core/badge/index.js';
|
import { Badge } from '$lib/ui/core/badge/index.js';
|
||||||
import { Button } from '$lib/ui/core/button/index.js';
|
import { Button } from '$lib/ui/core/button/index.js';
|
||||||
import { Label } from '$lib/ui/core/label/index.js';
|
import { Label } from '$lib/ui/core/label/index.js';
|
||||||
@@ -77,20 +77,21 @@
|
|||||||
{ id: 'actions', label: '', class: 'w-20' }
|
{ id: 'actions', label: '', class: 'w-20' }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
function peerNodeLine(s: PeerSessionOnSpeaker): string {
|
||||||
|
if (s.poll_error) return `${s.label}: опрос недоступен`;
|
||||||
|
if (s.state === 'Established') return `${s.label}: Established`;
|
||||||
|
if (s.state === 'absent') return `${s.label}: нет сессии`;
|
||||||
|
return `${s.label}: ${s.state || '—'}`;
|
||||||
|
}
|
||||||
|
|
||||||
function peerConnectedLabel(p: PeerRow): string {
|
function peerConnectedLabel(p: PeerRow): string {
|
||||||
|
const nodes = p.session_on_speakers ?? [];
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
return nodes.map(peerNodeLine).join(' · ');
|
||||||
|
}
|
||||||
const established = p.established_on_speakers ?? [];
|
const established = p.established_on_speakers ?? [];
|
||||||
if (established.length > 0) {
|
if (established.length > 0) {
|
||||||
const labels = established.map((s) => s.label).join(', ');
|
return established.map((s) => `${s.label}: Established`).join(' · ');
|
||||||
return established.length === 1
|
|
||||||
? `Подключён: ${labels}`
|
|
||||||
: `Подключён (${established.length} нод): ${labels}`;
|
|
||||||
}
|
|
||||||
if (p.connected_speaker_label?.trim()) {
|
|
||||||
return `На ноде: ${p.connected_speaker_label.trim()}`;
|
|
||||||
}
|
|
||||||
if (p.session_on_speakers && p.session_on_speakers.length > 0) {
|
|
||||||
const parts = p.session_on_speakers.map((s) => `${s.label} (${s.state})`);
|
|
||||||
return parts.join(', ');
|
|
||||||
}
|
}
|
||||||
return 'Не найден на опрошенных нодах';
|
return 'Не найден на опрошенных нодах';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user