// Command silod is the Silo HTTP API server. package main import ( "context" "flag" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/kindredsystems/silo/internal/api" "github.com/kindredsystems/silo/internal/config" "github.com/kindredsystems/silo/internal/db" "github.com/kindredsystems/silo/internal/schema" "github.com/kindredsystems/silo/internal/storage" "github.com/rs/zerolog" ) func main() { // Parse flags configPath := flag.String("config", "config.yaml", "Path to configuration file") flag.Parse() // Setup logger logger := zerolog.New(os.Stdout).With().Timestamp().Logger() // Load configuration cfg, err := config.Load(*configPath) if err != nil { logger.Fatal().Err(err).Msg("failed to load configuration") } logger.Info(). Str("host", cfg.Server.Host). Int("port", cfg.Server.Port). Str("database", cfg.Database.Host). Str("storage", cfg.Storage.Endpoint). Msg("starting silo server") // Connect to database ctx := context.Background() database, err := db.Connect(ctx, db.Config{ Host: cfg.Database.Host, Port: cfg.Database.Port, Name: cfg.Database.Name, User: cfg.Database.User, Password: cfg.Database.Password, SSLMode: cfg.Database.SSLMode, MaxConnections: cfg.Database.MaxConnections, }) if err != nil { logger.Fatal().Err(err).Msg("failed to connect to database") } defer database.Close() logger.Info().Msg("connected to database") // Connect to storage (optional - may be externally managed) var store *storage.Storage if cfg.Storage.Endpoint != "" { store, err = storage.Connect(ctx, storage.Config{ Endpoint: cfg.Storage.Endpoint, AccessKey: cfg.Storage.AccessKey, SecretKey: cfg.Storage.SecretKey, Bucket: cfg.Storage.Bucket, UseSSL: cfg.Storage.UseSSL, Region: cfg.Storage.Region, }) if err != nil { logger.Warn().Err(err).Msg("failed to connect to storage - file operations disabled") store = nil } else { logger.Info().Msg("connected to storage") } } else { logger.Info().Msg("storage not configured - file operations disabled") } // Load schemas schemas, err := schema.LoadAll(cfg.Schemas.Directory) if err != nil { logger.Fatal().Err(err).Str("directory", cfg.Schemas.Directory).Msg("failed to load schemas") } logger.Info().Int("count", len(schemas)).Msg("loaded schemas") // Create API server server := api.NewServer(logger, database, schemas, cfg.Schemas.Directory, store) router := api.NewRouter(server, logger) // Create HTTP server addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) httpServer := &http.Server{ Addr: addr, Handler: router, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } // Start server in goroutine go func() { logger.Info().Str("addr", addr).Msg("listening") if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Fatal().Err(err).Msg("server error") } }() // Wait for interrupt signal quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info().Msg("shutting down server") // Graceful shutdown with timeout shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := httpServer.Shutdown(shutdownCtx); err != nil { logger.Fatal().Err(err).Msg("server forced to shutdown") } logger.Info().Msg("server stopped") }