Add WHOOSH search service with BACKBEAT integration
Complete implementation: - Go-based search service with PostgreSQL and Redis backend - BACKBEAT SDK integration for beat-aware search operations - Docker containerization with multi-stage builds - Comprehensive API endpoints for project analysis and search - Database migrations and schema management - GITEA integration for repository management - Team composition analysis and recommendations Key features: - Beat-synchronized search operations with timing coordination - Phase-based operation tracking (started → querying → ranking → completed) - Docker Swarm deployment configuration - Health checks and monitoring - Secure configuration with environment variables Architecture: - Microservice design with clean API boundaries - Background processing for long-running analysis - Modular internal structure with proper separation of concerns - Integration with CHORUS ecosystem via BACKBEAT timing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
207
cmd/whoosh/main.go
Normal file
207
cmd/whoosh/main.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/chorus-services/whoosh/internal/config"
|
||||
"github.com/chorus-services/whoosh/internal/database"
|
||||
"github.com/chorus-services/whoosh/internal/server"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceName = "whoosh"
|
||||
)
|
||||
|
||||
var (
|
||||
// Build-time variables (set via ldflags)
|
||||
version = "0.1.1-debug"
|
||||
commitHash = "unknown"
|
||||
buildDate = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parse command line flags
|
||||
var (
|
||||
healthCheck = flag.Bool("health-check", false, "Run health check and exit")
|
||||
showVersion = flag.Bool("version", false, "Show version information and exit")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
// Handle version flag
|
||||
if *showVersion {
|
||||
fmt.Printf("WHOOSH %s\n", version)
|
||||
fmt.Printf("Commit: %s\n", commitHash)
|
||||
fmt.Printf("Built: %s\n", buildDate)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle health check flag
|
||||
if *healthCheck {
|
||||
if err := runHealthCheck(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Health check failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Configure structured logging
|
||||
setupLogging()
|
||||
|
||||
log.Info().
|
||||
Str("service", serviceName).
|
||||
Str("version", version).
|
||||
Str("commit", commitHash).
|
||||
Str("build_date", buildDate).
|
||||
Msg("🎭 Starting WHOOSH - Autonomous AI Development Teams")
|
||||
|
||||
// Load configuration
|
||||
var cfg config.Config
|
||||
|
||||
// Debug: Print all environment variables starting with WHOOSH
|
||||
log.Debug().Msg("Environment variables:")
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, "WHOOSH_") {
|
||||
// Don't log passwords in full, just indicate they exist
|
||||
if strings.Contains(env, "PASSWORD") {
|
||||
parts := strings.SplitN(env, "=", 2)
|
||||
if len(parts) == 2 && len(parts[1]) > 0 {
|
||||
log.Debug().Str("env", parts[0]+"=[REDACTED]").Msg("Found password env var")
|
||||
}
|
||||
} else {
|
||||
log.Debug().Str("env", env).Msg("Found env var")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := envconfig.Process("whoosh", &cfg); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to load configuration")
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if err := cfg.Validate(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Invalid configuration")
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("listen_addr", cfg.Server.ListenAddr).
|
||||
Str("database_host", cfg.Database.Host).
|
||||
Bool("redis_enabled", cfg.Redis.Enabled).
|
||||
Msg("📋 Configuration loaded")
|
||||
|
||||
// Initialize database
|
||||
db, err := database.NewPostgresDB(cfg.Database)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to initialize database")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
log.Info().Msg("🗄️ Database connection established")
|
||||
|
||||
// Run migrations
|
||||
if cfg.Database.AutoMigrate {
|
||||
log.Info().Msg("🔄 Running database migrations...")
|
||||
if err := database.RunMigrations(cfg.Database.URL); err != nil {
|
||||
log.Fatal().Err(err).Msg("Database migration failed")
|
||||
}
|
||||
log.Info().Msg("✅ Database migrations completed")
|
||||
}
|
||||
|
||||
// Initialize server
|
||||
srv, err := server.NewServer(&cfg, db)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to create server")
|
||||
}
|
||||
|
||||
// Start server
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
log.Info().
|
||||
Str("addr", cfg.Server.ListenAddr).
|
||||
Msg("🌐 Starting HTTP server")
|
||||
|
||||
if err := srv.Start(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("Server startup failed")
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for shutdown signal
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case sig := <-sigChan:
|
||||
log.Info().Str("signal", sig.String()).Msg("🛑 Shutdown signal received")
|
||||
case <-ctx.Done():
|
||||
log.Info().Msg("🛑 Context cancelled")
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
log.Info().Msg("🔄 Starting graceful shutdown...")
|
||||
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer shutdownCancel()
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Error().Err(err).Msg("Server shutdown failed")
|
||||
}
|
||||
|
||||
log.Info().Msg("✅ WHOOSH shutdown complete")
|
||||
}
|
||||
|
||||
func runHealthCheck() error {
|
||||
// Simple health check - try to connect to health endpoint
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
// Use localhost for health check
|
||||
healthURL := "http://localhost:8080/health"
|
||||
|
||||
resp, err := client.Get(healthURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("health check request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("health check returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupLogging() {
|
||||
// Configure zerolog for structured logging
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
// Set log level from environment
|
||||
level := os.Getenv("LOG_LEVEL")
|
||||
switch level {
|
||||
case "debug":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "info":
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
case "warn":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "error":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
default:
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
}
|
||||
|
||||
// Pretty logging for development
|
||||
if os.Getenv("ENVIRONMENT") == "development" {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user