mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 00:31:35 +00:00
* docs: Update TASKS.md and PRODUCTION-TASKS.md to reflect current codebase state (December 2024 audit) * refactor: Unify all commands into a single Cobra CLI - Refactor cmd/api/main.go into 'tercul serve' command - Refactor cmd/worker/main.go into 'tercul worker' command - Refactor cmd/tools/enrich/main.go into 'tercul enrich' command - Add 'tercul bleve-migrate' command for Bleve index migration - Extract common initialization logic into cmd/cli/internal/bootstrap - Update Dockerfile to build unified CLI - Update README with new CLI usage This consolidates all entry points into a single, maintainable CLI structure. * fix: Fix CodeQL workflow and add comprehensive test coverage - Fix Go version mismatch by setting up Go before CodeQL init - Add Go version verification step - Improve error handling for code scanning upload - Add comprehensive test suite for CLI commands: - Bleve migration tests with in-memory indexes - Edge case tests (empty data, large batches, errors) - Command-level integration tests - Bootstrap initialization tests - Optimize tests to use in-memory Bleve indexes for speed - Add test tags for skipping slow tests in short mode - Update workflow documentation Test coverage: 18.1% with 806 lines of test code All tests passing in short mode * fix: Fix test workflow and Bleve test double-close panic - Add POSTGRES_USER to PostgreSQL service configuration in test workflow - Fix TestInitBleveIndex double-close panic by removing defer before explicit close - Test now passes successfully Fixes failing Unit Tests workflow in PR #64
195 lines
5.6 KiB
Go
195 lines
5.6 KiB
Go
package commands
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"tercul/cmd/cli/internal/bootstrap"
|
|
"tercul/internal/adapters/graphql"
|
|
"tercul/internal/observability"
|
|
platform_auth "tercul/internal/platform/auth"
|
|
"tercul/internal/platform/config"
|
|
"tercul/internal/platform/db"
|
|
app_log "tercul/internal/platform/log"
|
|
|
|
"github.com/99designs/gqlgen/graphql/handler"
|
|
"github.com/99designs/gqlgen/graphql/playground"
|
|
"github.com/pressly/goose/v3"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/spf13/cobra"
|
|
"github.com/weaviate/weaviate-go-client/v5/weaviate"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// NewServeCommand creates a new Cobra command for serving the API
|
|
func NewServeCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "serve",
|
|
Short: "Start the Tercul API server",
|
|
Long: `Start the Tercul GraphQL API server with all endpoints including:
|
|
- GraphQL query endpoint (/query)
|
|
- GraphQL Playground (/playground)
|
|
- Prometheus metrics (/metrics)`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// Load configuration
|
|
cfg, err := config.LoadConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load config: %w", err)
|
|
}
|
|
|
|
// Initialize logger
|
|
app_log.Init("tercul-api", cfg.Environment)
|
|
obsLogger := observability.NewLogger("tercul-api", cfg.Environment)
|
|
|
|
// Initialize OpenTelemetry Tracer Provider
|
|
tp, err := observability.TracerProvider("tercul-api", cfg.Environment)
|
|
if err != nil {
|
|
app_log.Fatal(err, "Failed to initialize OpenTelemetry tracer")
|
|
}
|
|
defer func() {
|
|
if err := tp.Shutdown(context.Background()); err != nil {
|
|
app_log.Error(err, "Error shutting down tracer provider")
|
|
}
|
|
}()
|
|
|
|
// Initialize Prometheus metrics
|
|
reg := prometheus.NewRegistry()
|
|
metrics := observability.NewMetrics(reg)
|
|
|
|
app_log.Info(fmt.Sprintf("Starting Tercul application in %s environment, version 1.0.0", cfg.Environment))
|
|
|
|
// Initialize database connection
|
|
database, err := db.InitDB(cfg, metrics)
|
|
if err != nil {
|
|
app_log.Fatal(err, "Failed to initialize database")
|
|
}
|
|
defer func() {
|
|
if err := db.Close(database); err != nil {
|
|
app_log.Error(err, "Error closing database")
|
|
}
|
|
}()
|
|
|
|
// Run migrations
|
|
if err := runMigrations(database, cfg.MigrationPath); err != nil {
|
|
app_log.Fatal(err, "Failed to apply database migrations")
|
|
}
|
|
|
|
// Initialize Weaviate client
|
|
weaviateCfg := weaviate.Config{
|
|
Host: cfg.WeaviateHost,
|
|
Scheme: cfg.WeaviateScheme,
|
|
}
|
|
weaviateClient, err := weaviate.NewClient(weaviateCfg)
|
|
if err != nil {
|
|
app_log.Fatal(err, "Failed to create weaviate client")
|
|
}
|
|
|
|
// Bootstrap application dependencies
|
|
deps, err := bootstrap.BootstrapWithMetrics(cfg, database, weaviateClient)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to bootstrap application: %w", err)
|
|
}
|
|
|
|
// Create GraphQL server
|
|
resolver := &graphql.Resolver{
|
|
App: deps.Application,
|
|
}
|
|
|
|
// Create the API server
|
|
apiHandler := newAPIServer(resolver, deps.JWTManager, metrics, obsLogger, reg)
|
|
|
|
// Create the main HTTP server
|
|
mainServer := &http.Server{
|
|
Addr: cfg.ServerPort,
|
|
Handler: apiHandler,
|
|
}
|
|
app_log.Info(fmt.Sprintf("API server listening on port %s", cfg.ServerPort))
|
|
|
|
// Start the main server in a goroutine
|
|
go func() {
|
|
if err := mainServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
app_log.Fatal(err, "Failed to start server")
|
|
}
|
|
}()
|
|
|
|
// Wait for interrupt signal to gracefully shutdown the server
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
app_log.Info("Shutting down server...")
|
|
|
|
// Graceful shutdown
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
if err := mainServer.Shutdown(ctx); err != nil {
|
|
app_log.Error(err, "Server forced to shutdown")
|
|
}
|
|
|
|
app_log.Info("Server shut down successfully")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
// runMigrations applies database migrations using goose.
|
|
func runMigrations(gormDB *gorm.DB, migrationPath string) error {
|
|
sqlDB, err := gormDB.DB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := goose.SetDialect("postgres"); err != nil {
|
|
return err
|
|
}
|
|
|
|
app_log.Info(fmt.Sprintf("Applying database migrations from %s", migrationPath))
|
|
if err := goose.Up(sqlDB, migrationPath); err != nil {
|
|
return err
|
|
}
|
|
app_log.Info("Database migrations applied successfully")
|
|
return nil
|
|
}
|
|
|
|
// newAPIServer creates a new http.ServeMux and configures it with all the API routes
|
|
func newAPIServer(
|
|
resolver *graphql.Resolver,
|
|
jwtManager *platform_auth.JWTManager,
|
|
metrics *observability.Metrics,
|
|
logger *observability.Logger,
|
|
reg *prometheus.Registry,
|
|
) *http.ServeMux {
|
|
// Configure the GraphQL server
|
|
c := graphql.Config{Resolvers: resolver}
|
|
c.Directives.Binding = graphql.Binding
|
|
|
|
// Create the core GraphQL handler
|
|
graphqlHandler := handler.New(graphql.NewExecutableSchema(c))
|
|
graphqlHandler.SetErrorPresenter(graphql.NewErrorPresenter())
|
|
|
|
// Create the middleware chain for the GraphQL endpoint.
|
|
// Middlewares are applied from bottom to top.
|
|
var chain http.Handler
|
|
chain = graphqlHandler
|
|
chain = metrics.PrometheusMiddleware(chain)
|
|
chain = observability.LoggingMiddleware(logger)(chain) // Must run after auth and tracing
|
|
chain = platform_auth.GraphQLAuthMiddleware(jwtManager)(chain)
|
|
chain = observability.TracingMiddleware(chain)
|
|
chain = observability.RequestIDMiddleware(chain)
|
|
|
|
// Create a new ServeMux and register all handlers
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/query", chain)
|
|
mux.Handle("/playground", playground.Handler("GraphQL Playground", "/query"))
|
|
mux.Handle("/metrics", observability.PrometheusHandler(reg))
|
|
|
|
return mux
|
|
}
|