feat: enhance evobgp with new command-line tools for bundle management, including pull, verify, and apply functionalities. Update go.mod to include necessary dependencies and complete todos in architecture plan for improved observability and deployment practices.
CI / changes (push) Successful in 4s
CI / go (push) Failing after 6s
CI / bird2 (push) Has been skipped
CI / openapi (push) Has been skipped

This commit is contained in:
Denozordec
2026-04-05 14:07:45 +07:00
parent 272542b92a
commit bf52b21150
131 changed files with 9222 additions and 17 deletions
+224
View File
@@ -0,0 +1,224 @@
// Package observability registers Prometheus metrics for the control plane (prefix aggregates, BGP peers, jobs, HTTP).
package observability
import (
"context"
"net/http"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"evobgp/internal/store"
)
const namespace = "evobgp"
var (
metricsMem atomic.Pointer[store.Memory]
registerCollectorOnce sync.Once
)
var (
httpRequests = promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: "http_requests_total",
Help: "HTTP requests handled by the API mux (excludes /metrics).",
},
[]string{"method", "code"},
)
jobsFinished = promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: "jobs_finished_total",
Help: "Async jobs that reached a terminal state.",
},
[]string{"kind", "status"},
)
birdBGPEstablished = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Name: "bird_bgp_sessions_established",
Help: "BGP sessions in Established state from birdc show protocols (0 if scrape disabled or failed).",
})
birdProtocolsScrapeSuccess = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Name: "bird_protocols_scrape_success",
Help: "1 if the last birdc protocols scrape succeeded, else 0.",
})
buildInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace,
Name: "build_info",
Help: "Build metadata (value always 1).",
}, []string{"version", "git_sha"})
)
// RecordJobTerminal increments jobs_finished_total for terminal statuses.
func RecordJobTerminal(kind, status string) {
switch status {
case "succeeded", "failed", "cancelled":
jobsFinished.WithLabelValues(kind, status).Inc()
default:
return
}
}
// SetBuildInfo sets evobgp_build_info gauge (idempotent labels).
func SetBuildInfo(version, gitSHA string) {
if version == "" {
version = "unknown"
}
if gitSHA == "" {
gitSHA = "unknown"
}
buildInfo.WithLabelValues(version, gitSHA).Set(1)
}
type memoryStoreCollector struct {
prefixMaxDesc *prometheus.Desc
prefixSumDesc *prometheus.Desc
peersDesc *prometheus.Desc
peerStateDesc *prometheus.Desc
}
func newMemoryStoreCollector() *memoryStoreCollector {
return &memoryStoreCollector{
prefixMaxDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "materialized_prefixes_max"),
"Maximum materialized_prefix_count among all revisions in the store.",
nil, nil,
),
prefixSumDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "materialized_prefixes_sum"),
"Sum of materialized_prefix_count over revisions (development aggregate).",
nil, nil,
),
peersDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "bgp_peers_configured_total"),
"BGP peers configured in the control-plane store.",
nil, nil,
),
peerStateDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "bgp_peer_sessions"),
"Configured BGP peers in the store by session_state (intent / last known, not live BIRD).",
[]string{"state"}, nil,
),
}
}
func (c *memoryStoreCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.prefixMaxDesc
ch <- c.prefixSumDesc
ch <- c.peersDesc
ch <- c.peerStateDesc
}
func (c *memoryStoreCollector) Collect(ch chan<- prometheus.Metric) {
mem := metricsMem.Load()
if mem == nil {
return
}
maxN, sumN := mem.MaterializedPrefixStats()
ch <- prometheus.MustNewConstMetric(c.prefixMaxDesc, prometheus.GaugeValue, float64(maxN))
ch <- prometheus.MustNewConstMetric(c.prefixSumDesc, prometheus.GaugeValue, float64(sumN))
ch <- prometheus.MustNewConstMetric(c.peersDesc, prometheus.GaugeValue, float64(mem.PeerCount()))
for state, n := range mem.PeerSessionCountsByState() {
if state == "" {
state = "unknown"
}
ch <- prometheus.MustNewConstMetric(c.peerStateDesc, prometheus.GaugeValue, float64(n), state)
}
}
// RegisterStoreMetrics points Prometheus collectors at the given store (last call wins; safe for tests).
func RegisterStoreMetrics(mem *store.Memory) {
if mem == nil {
return
}
metricsMem.Store(mem)
registerCollectorOnce.Do(func() {
prometheus.DefaultRegisterer.MustRegister(newMemoryStoreCollector())
})
}
// SetBirdSessionMetrics updates gauges from an optional birdc scrape.
func SetBirdSessionMetrics(established int, scrapeOK bool) {
birdBGPEstablished.Set(float64(established))
if scrapeOK {
birdProtocolsScrapeSuccess.Set(1)
} else {
birdProtocolsScrapeSuccess.Set(0)
}
}
// MetricsHandler returns the Prometheus scrape handler.
func MetricsHandler() http.Handler {
return promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{})
}
// HTTPMiddleware records method and status code for all wrapped requests.
func HTTPMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sw := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(sw, r)
httpRequests.WithLabelValues(r.Method, strconv.Itoa(sw.status)).Inc()
})
}
type statusRecorder struct {
http.ResponseWriter
status int
}
func (s *statusRecorder) WriteHeader(code int) {
s.status = code
s.ResponseWriter.WriteHeader(code)
}
// StartBirdProtocolsPoller runs birdc "show protocols" on interval when socket is non-empty.
func StartBirdProtocolsPoller(socket string, birdcPath string, interval time.Duration, showFn func(ctx context.Context, socket, birdcBin string) (string, error), countFn func(output string) int) {
socket = trimSpace(socket)
if socket == "" || interval <= 0 || showFn == nil || countFn == nil {
return
}
scrape := func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
out, err := showFn(ctx, socket, birdcPath)
cancel()
if err != nil {
SetBirdSessionMetrics(0, false)
return
}
SetBirdSessionMetrics(countFn(out), true)
}
go func() {
scrape()
t := time.NewTicker(interval)
defer t.Stop()
for range t.C {
scrape()
}
}()
}
func trimSpace(s string) string {
for len(s) > 0 && (s[0] == ' ' || s[0] == '\t') {
s = s[1:]
}
for len(s) > 0 {
last := s[len(s)-1]
if last != ' ' && last != '\t' {
break
}
s = s[:len(s)-1]
}
return s
}