v1.0.0 — first release
Bump VERSION to 1.0.0 and cut the first tagged release of vpp-maglev. Also in this commit: - maglevc: MAGLEV_SERVER env var as an alternative to the --server flag, matching the MAGLEV_CONFIG / MAGLEV_GRPC_ADDR convention on the other binaries. The flag takes precedence when both are set. - Rename cmd/maglevd -> cmd/server and cmd/maglevc -> cmd/client so the source directory names are decoupled from binary names (the frontend and tester commands already followed this convention). Build outputs and the Debian packages are unchanged.
This commit is contained in:
192
cmd/server/main.go
Normal file
192
cmd/server/main.go
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
grpcprom "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
|
||||
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
|
||||
"git.ipng.ch/ipng/vpp-maglev/internal/checker"
|
||||
"git.ipng.ch/ipng/vpp-maglev/internal/config"
|
||||
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
||||
"git.ipng.ch/ipng/vpp-maglev/internal/metrics"
|
||||
"git.ipng.ch/ipng/vpp-maglev/internal/vpp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
slog.Error("startup-fatal", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
// ---- flags / env --------------------------------------------------------
|
||||
printVersion := flag.Bool("version", false, "print version and exit")
|
||||
checkOnly := flag.Bool("check", false, "check config file and exit (0=ok, 1=parse error, 2=semantic error)")
|
||||
enableReflection := flag.Bool("reflection", true, "enable gRPC server reflection (for grpcurl)")
|
||||
configPath := stringFlag("config", "/etc/vpp-maglev/maglev.yaml", "MAGLEV_CONFIG", "path to maglev.yaml")
|
||||
grpcAddr := stringFlag("grpc-addr", ":9090", "MAGLEV_GRPC_ADDR", "gRPC listen address")
|
||||
metricsAddr := stringFlag("metrics-addr", ":9091", "MAGLEV_METRICS_ADDR", "Prometheus /metrics listen address (empty to disable)")
|
||||
vppAPIAddr := stringFlag("vpp-api-addr", "/run/vpp/api.sock", "MAGLEV_VPP_API_ADDR", "VPP binary API socket path (empty to disable)")
|
||||
vppStatsAddr := stringFlag("vpp-stats-addr", "/run/vpp/stats.sock", "MAGLEV_VPP_STATS_ADDR", "VPP stats socket path")
|
||||
logLevel := stringFlag("log-level", "info", "MAGLEV_LOG_LEVEL", "log level (debug|info|warn|error)")
|
||||
flag.Parse()
|
||||
|
||||
if *printVersion {
|
||||
fmt.Printf("maglevd %s (commit %s, built %s)\n",
|
||||
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
|
||||
return nil
|
||||
}
|
||||
|
||||
if *checkOnly {
|
||||
_, result := config.Check(*configPath)
|
||||
if result.OK() {
|
||||
fmt.Printf("config ok: %s\n", *configPath)
|
||||
return nil
|
||||
}
|
||||
if result.ParseError != "" {
|
||||
fmt.Fprintf(os.Stderr, "parse error: %s\n", result.ParseError)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "semantic error: %s\n", result.SemanticError)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// ---- logging ------------------------------------------------------------
|
||||
var level slog.Level
|
||||
if err := level.UnmarshalText([]byte(*logLevel)); err != nil {
|
||||
return fmt.Errorf("invalid log level %q: %w", *logLevel, err)
|
||||
}
|
||||
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})
|
||||
logBroadcaster := grpcapi.NewLogBroadcaster(jsonHandler)
|
||||
slog.SetDefault(slog.New(logBroadcaster))
|
||||
slog.Info("starting", "version", buildinfo.Version(), "commit", buildinfo.Commit(), "date", buildinfo.Date())
|
||||
|
||||
// ---- config -------------------------------------------------------------
|
||||
cfg, err := config.Load(*configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
slog.Info("config-loaded", "path", *configPath, "frontends", len(cfg.Frontends))
|
||||
|
||||
// ---- checker ------------------------------------------------------------
|
||||
chkr := checker.New(cfg)
|
||||
|
||||
ctx, rootCancel := context.WithCancel(context.Background())
|
||||
defer rootCancel()
|
||||
|
||||
go func() {
|
||||
if err := chkr.Run(ctx); err != nil {
|
||||
slog.Error("checker-exited", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// ---- VPP connection -----------------------------------------------------
|
||||
var vppClient *vpp.Client
|
||||
if *vppAPIAddr != "" {
|
||||
vppClient = vpp.New(*vppAPIAddr, *vppStatsAddr)
|
||||
vppClient.SetStateSource(chkr)
|
||||
go vppClient.Run(ctx)
|
||||
// The reconciler subscribes to checker events and pushes per-VIP
|
||||
// syncs into VPP on every backend state transition. This is the
|
||||
// single place where transitions translate into dataplane changes.
|
||||
reconciler := vpp.NewReconciler(vppClient, chkr, chkr)
|
||||
go reconciler.Run(ctx)
|
||||
}
|
||||
|
||||
// ---- gRPC server --------------------------------------------------------
|
||||
// Server-side metrics for every RPC: call counters, in-flight gauges,
|
||||
// and handler-latency histograms, labelled by method and gRPC code.
|
||||
// Provided by go-grpc-middleware's prometheus adapter; the metric
|
||||
// families are emitted on the /metrics endpoint alongside ours.
|
||||
grpcMetrics := grpcprom.NewServerMetrics(
|
||||
grpcprom.WithServerHandlingTimeHistogram(),
|
||||
)
|
||||
lis, err := net.Listen("tcp", *grpcAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen %s: %w", *grpcAddr, err)
|
||||
}
|
||||
srv := grpc.NewServer(
|
||||
grpc.UnaryInterceptor(grpcMetrics.UnaryServerInterceptor()),
|
||||
grpc.StreamInterceptor(grpcMetrics.StreamServerInterceptor()),
|
||||
)
|
||||
maglevServer := grpcapi.NewServer(ctx, chkr, logBroadcaster, *configPath, vppClient)
|
||||
grpcapi.RegisterMaglevServer(srv, maglevServer)
|
||||
if *enableReflection {
|
||||
reflection.Register(srv)
|
||||
}
|
||||
// Pre-register every method with 0 so the metric shows up on first
|
||||
// scrape even before any RPC has been received.
|
||||
grpcMetrics.InitializeMetrics(srv)
|
||||
slog.Info("grpc-listening", "addr", *grpcAddr, "reflection", *enableReflection)
|
||||
|
||||
go func() {
|
||||
if err := srv.Serve(lis); err != nil {
|
||||
slog.Error("grpc-serve-error", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// ---- Prometheus metrics -------------------------------------------------
|
||||
if *metricsAddr != "" {
|
||||
reg := prometheus.DefaultRegisterer
|
||||
// vppClient may be nil when VPP integration is disabled; the
|
||||
// collector handles that by skipping the vpp_* gauges.
|
||||
var vppSrc metrics.VPPSource
|
||||
if vppClient != nil {
|
||||
vppSrc = vppClient
|
||||
}
|
||||
metrics.Register(reg, chkr, vppSrc)
|
||||
reg.MustRegister(grpcMetrics)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
slog.Info("metrics-listening", "addr", *metricsAddr)
|
||||
go func() {
|
||||
if err := http.ListenAndServe(*metricsAddr, mux); err != nil && err != http.ErrServerClosed {
|
||||
slog.Error("metrics-serve-error", "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ---- signal handling ----------------------------------------------------
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
|
||||
|
||||
for sig := range sigCh {
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
maglevServer.TriggerReload()
|
||||
|
||||
case syscall.SIGTERM, syscall.SIGINT:
|
||||
slog.Info("shutdown", "signal", sig)
|
||||
rootCancel()
|
||||
srv.GracefulStop()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// stringFlag declares a flag that falls back to an environment variable.
|
||||
func stringFlag(name, defaultVal, envKey, usage string) *string {
|
||||
val := defaultVal
|
||||
if v := os.Getenv(envKey); v != "" {
|
||||
val = v
|
||||
}
|
||||
return flag.String(name, val, fmt.Sprintf("%s (env: %s)", usage, envKey))
|
||||
}
|
||||
Reference in New Issue
Block a user