Better log viewer in launchpad

This commit is contained in:
2025-12-13 19:47:39 +08:00
parent d830c381d4
commit 95005c0cff
7 changed files with 452 additions and 45 deletions

View File

@@ -15,6 +15,7 @@ import (
"time"
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/config"
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/logview"
"github.com/rs/zerolog/log"
)
@@ -22,20 +23,25 @@ var colors = []string{
"\033[32m", "\033[33m", "\033[34m", "\033[35m", "\033[36m", "\033[31m",
}
const colorReset = "\033[0m"
// RunDev starts all services defined in the config in development mode.
func RunDev(cfg config.LaunchpadConfig) {
// RunDev starts selected services defined in the config in development mode.
func RunDev(cfg config.LaunchpadConfig, servicesToStart []config.Service) {
log.Info().Msg("Starting services in development mode with dependency checks...")
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
defer func() {
log.Info().Msg("Shutting down all services...")
cancel()
wg.Wait()
log.Info().Msg("All services have been shut down.")
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigChan
log.Info().Msgf("Received signal: %v. Shutting down all services...", sig)
<-sigChan
log.Info().Msg("Shutdown signal received.")
cancel()
}()
@@ -48,33 +54,43 @@ func RunDev(cfg config.LaunchpadConfig) {
}
createDockerNetwork(devNetwork)
// --- Dependency-aware startup ---
serviceMap := make(map[string]config.Service)
for _, s := range cfg.Services {
serviceMap[s.Name] = s
}
started := make(map[string]chan bool)
for _, s := range cfg.Services {
serviceNamesToStart := []string{}
for _, s := range servicesToStart {
serviceNamesToStart = append(serviceNamesToStart, s.Name)
}
log.Info().Msgf("Attempting to start: %s", strings.Join(serviceNamesToStart, ", "))
logChan := make(chan logview.LogMessage, 100)
for i, s := range servicesToStart {
if _, exists := started[s.Name]; !exists {
startServiceWithDeps(ctx, &wg, s, serviceMap, started, devNetwork)
color := colors[i%len(colors)]
startServiceWithDeps(ctx, &wg, s, serviceMap, started, devNetwork, color, logChan)
}
}
// Don't start the log viewer if no services were selected to run
if len(servicesToStart) > 0 {
if err := logview.Start(logChan, serviceNamesToStart); err != nil {
log.Fatal().Err(err).Msg("Log viewer failed")
}
}
wg.Wait()
log.Info().Msg("All services have been shut down.")
}
func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Service, serviceMap map[string]config.Service, started map[string]chan bool, network string) {
func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Service, serviceMap map[string]config.Service, started map[string]chan bool, network, color string, logChan chan<- logview.LogMessage) {
if _, exists := started[s.Name]; exists {
return
}
// Create a channel that will be closed when this service is healthy
healthyChan := make(chan bool)
started[s.Name] = healthyChan
// First, recursively start dependencies
var depNames []string
for depName := range s.Prod.DependsOn {
depNames = append(depNames, depName)
@@ -82,11 +98,11 @@ func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Serv
for _, depName := range depNames {
if dep, ok := serviceMap[depName]; ok {
startServiceWithDeps(ctx, wg, dep, serviceMap, started, network)
// Dependencies get a default color for now
startServiceWithDeps(ctx, wg, dep, serviceMap, started, network, "\033[37m", logChan)
}
}
// Wait for dependencies to be healthy
log.Info().Str("service", s.Name).Msgf("Waiting for dependencies to be healthy: %v", depNames)
for _, depName := range depNames {
if depChan, ok := started[depName]; ok {
@@ -101,33 +117,29 @@ func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Serv
}
}
// Now, start the actual service
wg.Add(1)
go func(s config.Service, color string) {
go func() {
defer wg.Done()
var healthCheckPorts []int
if s.Type == "docker" {
// For docker, we use the dev healthcheck ports to also map them
healthCheckPorts = s.Dev.Healthcheck.TcpPorts
startDockerService(ctx, s, color, network, healthCheckPorts)
startDockerService(ctx, s, color, network, healthCheckPorts, logChan)
} else if s.Dev.Command != "" {
healthCheckPorts = s.Dev.Healthcheck.TcpPorts
startSourceService(ctx, s, color)
startSourceService(ctx, s, color, logChan)
} else {
log.Warn().Str("service", s.Name).Msg("No dev.command or docker type, skipping.")
close(healthyChan) // Mark as "healthy" so other things can proceed
close(healthyChan)
return
}
// Perform health check on the service we just started
waitForHealth(ctx, s.Name, healthCheckPorts)
close(healthyChan) // Signal that this service is now healthy
close(healthyChan)
// Block until context is cancelled to keep the service running
// and ensure wg.Done is called at the right time.
<-ctx.Done()
}(s, colors[len(started)%len(colors)])
}(
)
}
func waitForHealth(ctx context.Context, serviceName string, ports []int) {
@@ -158,9 +170,7 @@ func waitForHealth(ctx context.Context, serviceName string, ports []int) {
}
}
// startSourceService runs a service from its source code.
func startSourceService(ctx context.Context, s config.Service, color string) {
prefix := fmt.Sprintf("%s[%-10s]%s ", color, s.Name, colorReset)
func startSourceService(ctx context.Context, s config.Service, color string, logChan chan<- logview.LogMessage) {
log.Info().Str("service", s.Name).Str("command", s.Dev.Command).Msg("Starting from source")
parts := strings.Fields(s.Dev.Command)
@@ -173,12 +183,10 @@ func startSourceService(ctx context.Context, s config.Service, color string) {
}
cmd.Env = env
runAndMonitorCommand(ctx, cmd, s.Name, prefix)
runAndMonitorCommand(ctx, cmd, s.Name, color, logChan)
}
// startDockerService runs a pre-built Docker image.
func startDockerService(ctx context.Context, s config.Service, color string, network string, portsToMap []int) {
prefix := fmt.Sprintf("%s[%-10s]%s ", color, s.Name, colorReset)
func startDockerService(ctx context.Context, s config.Service, color string, network string, portsToMap []int, logChan chan<- logview.LogMessage) {
log.Info().Str("service", s.Name).Str("image", s.Prod.Image).Msg("Starting from Docker image")
containerName := fmt.Sprintf("%s-dev", s.Name)
@@ -218,10 +226,10 @@ func startDockerService(ctx context.Context, s config.Service, color string, net
}()
cmd := exec.Command("docker", args...)
runAndMonitorCommand(ctx, cmd, s.Name, prefix)
runAndMonitorCommand(ctx, cmd, s.Name, color, logChan)
}
func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, prefix string) {
func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, color string, logChan chan<- logview.LogMessage) {
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
@@ -230,14 +238,14 @@ func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, prefi
return
}
go streamOutput(stdout, prefix)
go streamOutput(stderr, prefix)
go streamOutput(stdout, serviceName, color, logChan)
go streamOutput(stderr, serviceName, color, logChan)
go func() {
err := cmd.Wait()
if ctx.Err() != nil {
log.Info().Str("service", serviceName).Msg("Process stopped.")
// This is expected on clean shutdown
} else if err != nil {
log.Error().Err(err).Str("service", serviceName).Msg("Exited with error")
} else {
@@ -246,10 +254,10 @@ func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, prefi
}()
}
func streamOutput(pipe io.ReadCloser, prefix string) {
func streamOutput(pipe io.ReadCloser, serviceName, color string, logChan chan<- logview.LogMessage) {
scanner := bufio.NewScanner(pipe)
for scanner.Scan() {
fmt.Printf("%s%s\n", prefix, scanner.Text())
logChan <- logview.LogMessage{ServiceName: serviceName, Line: scanner.Text(), Color: color}
}
}