feat: improve BGP protocol summary handling by introducing isBGPProtocolSummaryRow function. Update SummarizeProtocolsOutput and related tests to ensure evobgp_* static names are not counted as BGP sessions, enhancing accuracy in protocol summaries.
CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 23s
CI / bird2 (push) Has been cancelled
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been cancelled
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been cancelled
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has started running
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been cancelled
CI / docker-bird (push) Has been cancelled

This commit is contained in:
Denozordec
2026-04-06 00:41:59 +07:00
parent 94a4c6acd4
commit 399622cc70
6 changed files with 129 additions and 39 deletions
+22 -6
View File
@@ -11,6 +11,24 @@ type ProtocolsSummary struct {
RawLineCount int
}
// isBGPProtocolSummaryRow is true for BIRD "show protocols" summary rows where the
// second column (Proto) is BGP. Substring checks are unsafe: names like evobgp_* contain "bgp".
func isBGPProtocolSummaryRow(line string) bool {
line = strings.TrimSpace(line)
if line == "" {
return false
}
low := strings.ToLower(line)
if strings.HasPrefix(low, "name") || strings.HasPrefix(low, "table") {
return false
}
fields := strings.Fields(line)
if len(fields) < 2 {
return false
}
return strings.EqualFold(fields[1], "BGP")
}
// SummarizeProtocolsOutput extracts BGP session heuristics from birdc output.
func SummarizeProtocolsOutput(output string) ProtocolsSummary {
var s ProtocolsSummary
@@ -22,14 +40,12 @@ func SummarizeProtocolsOutput(output string) ProtocolsSummary {
continue
}
low := strings.ToLower(line)
if strings.HasPrefix(low, "name") || strings.HasPrefix(low, "table") {
if !isBGPProtocolSummaryRow(line) {
continue
}
if strings.Contains(low, "bgp") {
s.BGPSessionsTotal++
if strings.Contains(low, "established") {
s.BGPEstablished++
}
s.BGPSessionsTotal++
if strings.Contains(low, "established") {
s.BGPEstablished++
}
}
return s
+11
View File
@@ -12,3 +12,14 @@ uplink BGP --- start 10:00:01 Established
t.Fatalf("got %+v", s)
}
}
func TestSummarizeProtocolsOutput_evoBGPNameNotCountedAsBGP(t *testing.T) {
sample := `Name Proto Table State Since Info
evobgp_prefixes_v4 Static master4 up 17:32:14.631
evobgp_prefixes_v6 Static master6 up 17:32:14.631
`
s := SummarizeProtocolsOutput(sample)
if s.BGPSessionsTotal != 0 || s.BGPEstablished != 0 {
t.Fatalf("evobgp_* static names must not match substring bgp: got %+v", s)
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ func CountEstablishedBGPSessions(showProtocolsOutput string) int {
if line == "" || strings.HasPrefix(line, "name") || strings.HasPrefix(strings.ToLower(line), "table") {
continue
}
if !strings.Contains(strings.ToLower(line), "bgp") {
if !isBGPProtocolSummaryRow(line) {
continue
}
if strings.Contains(strings.ToLower(line), "established") {
@@ -0,0 +1,29 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
/** preserve — вывод CLI (колонки birdc); wrap — конфиги и JSON с длинными строками */
type Variant = 'preserve' | 'wrap';
let {
variant = 'preserve',
text,
class: className = ''
}: {
variant?: Variant;
text: string;
class?: string;
} = $props();
const shell =
'border-border bg-muted/40 relative isolate min-w-0 overflow-auto rounded-lg border [scrollbar-gutter:stable] overscroll-contain';
const prePreserve =
'text-foreground m-0 block w-max min-w-full p-4 font-mono text-[0.8125rem] leading-normal whitespace-pre select-text';
const preWrap =
'text-foreground m-0 block min-w-0 w-full max-w-none p-4 font-mono text-[0.8125rem] leading-relaxed whitespace-pre-wrap break-words select-text';
</script>
<div class={cn(shell, className)} data-slot="scroll-pre-block">
<pre class={variant === 'preserve' ? prePreserve : preWrap}>{text}</pre>
</div>
+21
View File
@@ -0,0 +1,21 @@
/**
* Общая разметка для модалок с прокручиваемым контентом (превью BIRD, вывод birdc, логи).
* DialogContent по умолчанию — grid + p-4; здесь переопределяем на flex-колонку без внешних отступов.
*/
export const dialogContentDocument =
'!flex w-[min(100vw-2rem,56rem)] max-h-[min(92vh,880px)] flex-col gap-0 overflow-hidden !p-0 sm:max-w-4xl';
export const dialogHeaderDocument =
'shrink-0 space-y-1.5 border-b border-border/70 px-6 pt-5 pb-3 pr-14 text-left';
export const dialogBodyDocument =
'flex min-h-0 min-w-0 flex-1 flex-col gap-3 overflow-hidden px-6 py-4';
/** Компактные модалки (детали задачи, формы): единая шапка и тело */
export const dialogContentPanel =
'!flex max-h-[min(90vh,40rem)] w-full max-w-lg flex-col gap-0 overflow-hidden !p-0 sm:max-w-lg';
export const dialogHeaderPanel =
'shrink-0 space-y-1.5 border-b border-border/70 px-6 pt-5 pb-3 pr-14 text-left';
export const dialogBodyPanel = 'min-h-0 flex-1 overflow-y-auto px-6 py-4';
+45 -32
View File
@@ -40,6 +40,16 @@
TableRow
} from '$lib/components/ui/table/index.js';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import ScrollPreBlock from '$lib/components/app/scroll-pre-block.svelte';
import {
dialogBodyDocument,
dialogBodyPanel,
dialogContentDocument,
dialogContentPanel,
dialogHeaderDocument,
dialogHeaderPanel
} from '$lib/dialog-layout.js';
import { cn } from '$lib/utils.js';
import { toast } from 'svelte-sonner';
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import Play from '@lucide/svelte/icons/play';
@@ -631,25 +641,23 @@
<!-- Preview dialog -->
<Dialog bind:open={previewDialog}>
<DialogContent
class="!flex max-h-[min(96vh,880px)] w-[min(100vw-2rem,56rem)] flex-col gap-4 overflow-hidden sm:max-w-4xl"
>
<DialogHeader class="shrink-0 space-y-1.5 pr-8">
<DialogContent class={dialogContentDocument}>
<DialogHeader class={dialogHeaderDocument}>
<DialogTitle>Ревизия {previewRevision?.id.slice(0, 8)}</DialogTitle>
<DialogDescription>
Срендеренный конфиг BIRD 2 (фрагменты из control plane) и материализованные префиксы.
</DialogDescription>
</DialogHeader>
{#if previewLoading}
<p class="text-muted-foreground py-4 text-center text-sm">Загрузка…</p>
<div class="text-muted-foreground px-6 py-10 text-center text-sm">Загрузка…</div>
{:else}
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
<Tabs bind:value={previewSubTab} class="flex min-h-0 flex-1 flex-col gap-3">
<div class={cn(dialogBodyDocument, 'min-h-[min(44vh,400px)]')}>
<Tabs bind:value={previewSubTab} class="flex min-h-0 min-w-0 flex-1 flex-col gap-3">
<TabsList class="grid w-full max-w-md shrink-0 grid-cols-2">
<TabsTrigger value="bird">Конфиг BIRD</TabsTrigger>
<TabsTrigger value="prefixes">Префиксы</TabsTrigger>
</TabsList>
<TabsContent value="bird" class="mt-0 flex min-h-0 flex-1 flex-col gap-2 outline-none">
<TabsContent value="bird" class="mt-0 flex min-h-0 min-w-0 flex-1 flex-col gap-2 outline-none">
{@const frags = asPreviewFragments(previewData)}
{#if Object.keys(frags).length === 0}
<p class="text-muted-foreground text-sm">Нет фрагментов превью (старая ревизия или пустой render).</p>
@@ -670,22 +678,20 @@
Совет: откройте <code class="bg-muted rounded px-1">_bird_full_expanded.conf</code> — один текст с
<code class="bg-muted rounded px-1">bird.conf</code> и содержимым всех include.
</p>
<div
class="border-border bg-muted/40 text-foreground relative isolate min-h-[12rem] min-w-0 flex-1 overflow-x-auto overflow-y-auto rounded-lg border [scrollbar-gutter:stable] overscroll-contain"
>
<pre
class="text-foreground m-0 block min-w-0 w-full max-w-none p-3 font-mono text-xs leading-relaxed whitespace-pre-wrap break-words select-text"
>{frags[birdPreviewPath] ?? ''}</pre>
</div>
<ScrollPreBlock
variant="wrap"
text={frags[birdPreviewPath] ?? ''}
class="min-h-[12rem] min-w-0 flex-1"
/>
{/if}
</TabsContent>
<TabsContent value="prefixes" class="mt-0 flex min-h-0 flex-1 flex-col gap-2 outline-none">
<TabsContent value="prefixes" class="mt-0 flex min-h-0 min-w-0 flex-1 flex-col gap-2 outline-none">
<p class="text-sm shrink-0">
<span class="font-medium">Префиксов:</span>
{prefixesData.length}
</p>
<div
class="border-border bg-muted/40 min-h-[10rem] min-w-0 flex-1 overflow-x-auto overflow-y-auto rounded-lg border p-3 [scrollbar-gutter:stable] overscroll-contain"
class="border-border bg-muted/40 flex min-h-[10rem] min-w-0 flex-1 flex-col overflow-auto rounded-lg border p-3 [scrollbar-gutter:stable] overscroll-contain"
>
{#each prefixesData as pfx}
<p class="font-mono text-xs">{pfx}</p>
@@ -702,39 +708,46 @@
<!-- BIRD protocols excerpt -->
<Dialog bind:open={birdProtocolsOpen}>
<DialogContent class="flex max-h-[min(90vh,720px)] flex-col gap-3 sm:max-w-3xl">
<DialogHeader>
<DialogContent class={dialogContentDocument}>
<DialogHeader class={dialogHeaderDocument}>
<DialogTitle>Вывод birdc (протоколы)</DialogTitle>
<DialogDescription>Фрагмент ответа на этом API-хосте; при длинном выводе обрезан на сервере.</DialogDescription>
<DialogDescription>
Фрагмент ответа на этом API-хосте; при длинном выводе обрезан на сервере. Таблица сохраняет выравнивание
колонок (горизонтальная прокрутка).
</DialogDescription>
</DialogHeader>
<ScrollArea class="border-border bg-muted/40 max-h-[min(60vh,480px)] rounded-md border">
<pre class="m-0 p-3 font-mono text-xs leading-relaxed whitespace-pre-wrap break-words select-text">{birdStatus?.protocols_excerpt ?? ''}</pre>
</ScrollArea>
<div class="px-6 pb-6 pt-0">
<ScrollPreBlock
variant="preserve"
text={birdStatus?.protocols_excerpt ?? ''}
class="h-[min(65vh,560px)] w-full"
/>
</div>
</DialogContent>
</Dialog>
<!-- Job detail dialog -->
<Dialog bind:open={jobDetailDialog}>
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogContent class={dialogContentPanel}>
<DialogHeader class={dialogHeaderPanel}>
<DialogTitle>Задача: {jobDetail?.kind}</DialogTitle>
</DialogHeader>
{#if jobDetail}
<div class="space-y-2 text-sm">
<div class="grid grid-cols-2 gap-x-4 gap-y-1">
<span class="text-muted-foreground">ID</span><span class="font-mono">{jobDetail.job_id}</span>
<div class={dialogBodyPanel}>
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
<span class="text-muted-foreground">ID</span><span class="font-mono break-all">{jobDetail.job_id}</span>
<span class="text-muted-foreground">Статус</span><span><Badge variant={jobStatusVariant(jobDetail.status)}>{jobDetail.status}</Badge></span>
<span class="text-muted-foreground">Создана</span><span>{formatDate(jobDetail.created_at)}</span>
<span class="text-muted-foreground">Начата</span><span>{formatDate(jobDetail.started_at)}</span>
<span class="text-muted-foreground">Завершена</span><span>{formatDate(jobDetail.finished_at)}</span>
{#if jobDetail.error}
<span class="text-muted-foreground">Ошибка</span><span class="text-destructive">{jobDetail.error}</span>
<span class="text-muted-foreground">Ошибка</span><span class="text-destructive break-words">{jobDetail.error}</span>
{/if}
</div>
{#if jobDetail.meta && Object.keys(jobDetail.meta).length > 0}
<div>
<p class="text-muted-foreground mb-1">Meta</p>
<pre class="bg-muted rounded p-2 text-xs overflow-auto max-h-40">{JSON.stringify(jobDetail.meta, null, 2)}</pre>
<div class="mt-4 space-y-2">
<p class="text-muted-foreground text-sm font-medium">Meta</p>
<ScrollPreBlock variant="wrap" text={JSON.stringify(jobDetail.meta, null, 2)} class="max-h-52 w-full" />
</div>
{/if}
</div>