Initial commit: RouteWatch BGP stream monitor
- Connects to RIPE RIS Live stream to receive real-time BGP updates - Stores BGP data in SQLite database: - ASNs with first/last seen timestamps - Prefixes with IPv4/IPv6 classification - BGP announcements and withdrawals - AS-to-AS peering relationships from AS paths - Live routing table tracking active routes - HTTP server with statistics endpoints - Metrics tracking with go-metrics - Custom JSON unmarshaling to handle nested AS sets in paths - Dependency injection with uber/fx - Pure Go implementation (no CGO) - Includes streamdumper utility for debugging raw messages
This commit is contained in:
158
internal/routewatch/app.go
Normal file
158
internal/routewatch/app.go
Normal file
@@ -0,0 +1,158 @@
|
||||
// Package routewatch contains the primary RouteWatch type that represents a running instance
|
||||
// of the application and contains pointers to its core dependencies, and is responsible for initialization.
|
||||
package routewatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||
"git.eeqj.de/sneak/routewatch/internal/metrics"
|
||||
"git.eeqj.de/sneak/routewatch/internal/server"
|
||||
"git.eeqj.de/sneak/routewatch/internal/streamer"
|
||||
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
// Config contains runtime configuration for RouteWatch
|
||||
type Config struct {
|
||||
MaxRuntime time.Duration // Maximum runtime (0 = run forever)
|
||||
}
|
||||
|
||||
// NewConfig provides default configuration
|
||||
func NewConfig() Config {
|
||||
return Config{
|
||||
MaxRuntime: 0, // Run forever by default
|
||||
}
|
||||
}
|
||||
|
||||
// Dependencies contains all dependencies for RouteWatch
|
||||
type Dependencies struct {
|
||||
fx.In
|
||||
|
||||
DB database.Store
|
||||
Streamer *streamer.Streamer
|
||||
Server *server.Server
|
||||
Logger *slog.Logger
|
||||
Config Config `optional:"true"`
|
||||
}
|
||||
|
||||
// RouteWatch represents the main application instance
|
||||
type RouteWatch struct {
|
||||
db database.Store
|
||||
streamer *streamer.Streamer
|
||||
server *server.Server
|
||||
logger *slog.Logger
|
||||
maxRuntime time.Duration
|
||||
}
|
||||
|
||||
// New creates a new RouteWatch instance
|
||||
func New(deps Dependencies) *RouteWatch {
|
||||
return &RouteWatch{
|
||||
db: deps.DB,
|
||||
streamer: deps.Streamer,
|
||||
server: deps.Server,
|
||||
logger: deps.Logger,
|
||||
maxRuntime: deps.Config.MaxRuntime,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the RouteWatch application
|
||||
func (rw *RouteWatch) Run(ctx context.Context) error {
|
||||
rw.logger.Info("Starting RouteWatch")
|
||||
|
||||
// Apply runtime limit if specified
|
||||
if rw.maxRuntime > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, rw.maxRuntime)
|
||||
defer cancel()
|
||||
rw.logger.Info("Running with time limit", "max_runtime", rw.maxRuntime)
|
||||
}
|
||||
|
||||
// Register database handler to process BGP UPDATE messages
|
||||
dbHandler := NewDatabaseHandler(rw.db, rw.logger)
|
||||
rw.streamer.RegisterHandler(dbHandler)
|
||||
|
||||
// Start streaming
|
||||
if err := rw.streamer.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start HTTP server
|
||||
if err := rw.server.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for context cancellation
|
||||
<-ctx.Done()
|
||||
|
||||
// Stop services
|
||||
rw.streamer.Stop()
|
||||
|
||||
// Stop HTTP server with a timeout
|
||||
const serverStopTimeout = 5 * time.Second
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), serverStopTimeout)
|
||||
defer cancel()
|
||||
if err := rw.server.Stop(stopCtx); err != nil {
|
||||
rw.logger.Error("Failed to stop HTTP server gracefully", "error", err)
|
||||
}
|
||||
|
||||
// Log final metrics
|
||||
metrics := rw.streamer.GetMetrics()
|
||||
rw.logger.Info("Final metrics",
|
||||
"total_messages", metrics.TotalMessages,
|
||||
"total_bytes", metrics.TotalBytes,
|
||||
"messages_per_sec", metrics.MessagesPerSec,
|
||||
"bits_per_sec", metrics.BitsPerSec,
|
||||
"duration", time.Since(metrics.ConnectedSince),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewLogger creates a structured logger
|
||||
func NewLogger() *slog.Logger {
|
||||
level := slog.LevelInfo
|
||||
if debug := os.Getenv("DEBUG"); strings.Contains(debug, "routewatch") {
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
|
||||
opts := &slog.HandlerOptions{
|
||||
Level: level,
|
||||
}
|
||||
|
||||
var handler slog.Handler
|
||||
if os.Stdout.Name() != "/dev/stdout" || os.Getenv("TERM") == "" {
|
||||
// Not a terminal, use JSON
|
||||
handler = slog.NewJSONHandler(os.Stdout, opts)
|
||||
} else {
|
||||
// Terminal, use text
|
||||
handler = slog.NewTextHandler(os.Stdout, opts)
|
||||
}
|
||||
|
||||
return slog.New(handler)
|
||||
}
|
||||
|
||||
// getModule provides all fx dependencies
|
||||
func getModule() fx.Option {
|
||||
return fx.Options(
|
||||
fx.Provide(
|
||||
NewLogger,
|
||||
NewConfig,
|
||||
metrics.New,
|
||||
database.New,
|
||||
fx.Annotate(
|
||||
func(db *database.Database) database.Store {
|
||||
return db
|
||||
},
|
||||
fx.As(new(database.Store)),
|
||||
),
|
||||
streamer.New,
|
||||
server.New,
|
||||
New,
|
||||
),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user