237 lines
6.5 KiB
Go
237 lines
6.5 KiB
Go
package reports
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"evobgp/internal/store"
|
|
)
|
|
|
|
// RevisionDiagnosticLog is plain-text diagnostic output for a revision.
|
|
type RevisionDiagnosticLog struct {
|
|
Body []byte
|
|
Filename string
|
|
}
|
|
|
|
// BuildRevisionDiagnosticLog aggregates revision prefixes by source and community.
|
|
func BuildRevisionDiagnosticLog(st store.Backend, tenantID string, rev *store.Revision) (*RevisionDiagnosticLog, error) {
|
|
moduleType := ""
|
|
moduleName := ""
|
|
if strings.TrimSpace(rev.ModuleID) != "" {
|
|
if mod, modErr := st.GetModule(tenantID, rev.ModuleID); modErr == nil && mod != nil {
|
|
moduleType = strings.TrimSpace(mod.Type)
|
|
moduleName = strings.TrimSpace(mod.Name)
|
|
}
|
|
}
|
|
|
|
communityLabels := map[string]string{}
|
|
if communities, listErr := st.ListCommunities(tenantID); listErr == nil {
|
|
for _, c := range communities {
|
|
if c == nil {
|
|
continue
|
|
}
|
|
label := strings.TrimSpace(c.Title)
|
|
if label == "" {
|
|
label = strings.TrimSpace(c.Community)
|
|
}
|
|
if label == "" {
|
|
label = c.ID
|
|
}
|
|
communityLabels[c.ID] = label
|
|
}
|
|
}
|
|
|
|
type rawRow struct {
|
|
Prefix string
|
|
Source string
|
|
Kind string
|
|
SourceName string
|
|
SourceDetail string
|
|
CommunityID string
|
|
CommunityLabel string
|
|
}
|
|
type summary struct {
|
|
Kind string
|
|
Source string
|
|
SourceDetail string
|
|
CommunityID string
|
|
CommunityLabel string
|
|
Count int
|
|
Sample []string
|
|
}
|
|
|
|
cdnSourceURLByID := map[string]string{}
|
|
if moduleType == "CDN_CIDRS" && strings.TrimSpace(rev.ModuleID) != "" {
|
|
if sources, srcErr := st.ListCDNSources(tenantID, rev.ModuleID); srcErr == nil {
|
|
for _, src := range sources {
|
|
if src == nil {
|
|
continue
|
|
}
|
|
id := strings.TrimSpace(src.ID)
|
|
url := strings.TrimSpace(src.URL)
|
|
if id != "" && url != "" {
|
|
cdnSourceURLByID[id] = url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
rows := make([]rawRow, 0, rev.MaterializedPrefixCount)
|
|
byGroup := map[string]*summary{}
|
|
cursor := ""
|
|
for {
|
|
page, next, more := st.ListRevisionPrefixes(tenantID, rev.ID, cursor, 2000)
|
|
for _, p := range page {
|
|
kind, sourceName := classifyRevisionSource(p.Source)
|
|
sourceDetail := sourceName
|
|
if kind == "cdn" {
|
|
if url, ok := cdnSourceURLByID[sourceName]; ok && strings.TrimSpace(url) != "" {
|
|
sourceDetail = url
|
|
}
|
|
}
|
|
communityID := "none"
|
|
if p.CommunityID != nil && strings.TrimSpace(*p.CommunityID) != "" {
|
|
communityID = strings.TrimSpace(*p.CommunityID)
|
|
}
|
|
communityLabel := "без community"
|
|
if communityID != "none" {
|
|
if lbl, ok := communityLabels[communityID]; ok && strings.TrimSpace(lbl) != "" {
|
|
communityLabel = lbl
|
|
} else {
|
|
communityLabel = communityID
|
|
}
|
|
}
|
|
rows = append(rows, rawRow{
|
|
Prefix: p.Prefix,
|
|
Source: p.Source,
|
|
Kind: kind,
|
|
SourceName: sourceName,
|
|
SourceDetail: sourceDetail,
|
|
CommunityID: communityID,
|
|
CommunityLabel: communityLabel,
|
|
})
|
|
groupKey := kind + "|" + sourceName + "|" + communityID
|
|
g, ok := byGroup[groupKey]
|
|
if !ok {
|
|
g = &summary{
|
|
Kind: kind,
|
|
Source: sourceName,
|
|
SourceDetail: sourceDetail,
|
|
CommunityID: communityID,
|
|
CommunityLabel: communityLabel,
|
|
Sample: make([]string, 0, 5),
|
|
}
|
|
byGroup[groupKey] = g
|
|
}
|
|
g.Count++
|
|
if len(g.Sample) < 5 {
|
|
g.Sample = append(g.Sample, p.Prefix)
|
|
}
|
|
}
|
|
if !more || strings.TrimSpace(next) == "" {
|
|
break
|
|
}
|
|
cursor = next
|
|
}
|
|
|
|
summaryKeys := make([]string, 0, len(byGroup))
|
|
for k := range byGroup {
|
|
summaryKeys = append(summaryKeys, k)
|
|
}
|
|
sort.Strings(summaryKeys)
|
|
|
|
sort.Slice(rows, func(i, j int) bool {
|
|
if rows[i].Kind != rows[j].Kind {
|
|
return rows[i].Kind < rows[j].Kind
|
|
}
|
|
if rows[i].SourceName != rows[j].SourceName {
|
|
return rows[i].SourceName < rows[j].SourceName
|
|
}
|
|
if rows[i].CommunityID != rows[j].CommunityID {
|
|
return rows[i].CommunityID < rows[j].CommunityID
|
|
}
|
|
return rows[i].Prefix < rows[j].Prefix
|
|
})
|
|
|
|
var b strings.Builder
|
|
b.WriteString("EvoBGP revision diagnostic log\n")
|
|
b.WriteString("generated_at=" + time.Now().UTC().Format(time.RFC3339Nano) + "\n")
|
|
b.WriteString("tenant_id=" + tenantID + "\n")
|
|
b.WriteString("revision_id=" + rev.ID + "\n")
|
|
b.WriteString("revision_created_at=" + rev.CreatedAt.UTC().Format(time.RFC3339Nano) + "\n")
|
|
b.WriteString("content_hash=" + rev.ContentHash + "\n")
|
|
b.WriteString("materialized_prefix_count=" + strconv.Itoa(rev.MaterializedPrefixCount) + "\n")
|
|
if rev.ModuleID != "" {
|
|
b.WriteString("module_id=" + rev.ModuleID + "\n")
|
|
} else {
|
|
b.WriteString("module_id=\n")
|
|
}
|
|
b.WriteString("module_type=" + moduleType + "\n")
|
|
b.WriteString("module_name=" + moduleName + "\n")
|
|
b.WriteString("fetched_rows=" + strconv.Itoa(len(rows)) + "\n\n")
|
|
|
|
b.WriteString("## Aggregation by source and community\n")
|
|
for _, k := range summaryKeys {
|
|
g := byGroup[k]
|
|
b.WriteString("- kind=" + g.Kind +
|
|
" source=" + g.Source +
|
|
" source_detail=" + g.SourceDetail +
|
|
" community_id=" + g.CommunityID +
|
|
" community_label=" + g.CommunityLabel +
|
|
" count=" + strconv.Itoa(g.Count))
|
|
if len(g.Sample) > 0 {
|
|
b.WriteString(" sample=" + strings.Join(g.Sample, ","))
|
|
}
|
|
b.WriteByte('\n')
|
|
}
|
|
|
|
b.WriteString("\n## Raw rows\n")
|
|
b.WriteString("prefix\tkind\tsource\tsource_detail\tcommunity_id\tcommunity_label\n")
|
|
for _, row := range rows {
|
|
b.WriteString(row.Prefix)
|
|
b.WriteByte('\t')
|
|
b.WriteString(row.Kind)
|
|
b.WriteByte('\t')
|
|
b.WriteString(row.Source)
|
|
b.WriteByte('\t')
|
|
b.WriteString(strings.ReplaceAll(row.SourceDetail, "\t", " "))
|
|
b.WriteByte('\t')
|
|
b.WriteString(row.CommunityID)
|
|
b.WriteByte('\t')
|
|
b.WriteString(strings.ReplaceAll(row.CommunityLabel, "\t", " "))
|
|
b.WriteByte('\n')
|
|
}
|
|
|
|
filename := fmt.Sprintf("revision-%s-diagnostic.log", shortRevisionID(rev.ID))
|
|
return &RevisionDiagnosticLog{Body: []byte(b.String()), Filename: filename}, nil
|
|
}
|
|
|
|
func classifyRevisionSource(src string) (kind, sourceName string) {
|
|
switch {
|
|
case strings.HasPrefix(src, "as:"):
|
|
return "asn", strings.TrimPrefix(src, "as:")
|
|
case strings.HasPrefix(src, "domain:"):
|
|
return "domain", strings.TrimPrefix(src, "domain:")
|
|
case strings.HasPrefix(src, "cdn:"):
|
|
return "cdn", strings.TrimPrefix(src, "cdn:")
|
|
case src == "ip_range":
|
|
return "ip_range", "manual_ranges"
|
|
default:
|
|
if strings.TrimSpace(src) == "" {
|
|
return "unknown", "unknown"
|
|
}
|
|
return "source", strings.TrimSpace(src)
|
|
}
|
|
}
|
|
|
|
func shortRevisionID(id string) string {
|
|
s := strings.TrimSpace(id)
|
|
if len(s) <= 8 {
|
|
return s
|
|
}
|
|
return s[:8]
|
|
}
|