// Dev runner for local services. // // dev starts the app, tees its raw JSON logs to a file (the same bytes // production emits), and renders live request traces in the terminal. Run // it from the app directory: // // go run atlas9.dev/c/cmd/dev // // By default it runs "go run ." and writes logs to logs/server.log. Pass a // different command after "--": // // go run atlas9.dev/c/cmd/dev -log /tmp/server.log -- ./demo package main import ( "bufio" "context" "flag" "fmt" "os" "os/exec" "os/signal" "path/filepath" "syscall" "time" "atlas9.dev/c/core/tracelog" ) func main() { logPath := flag.String("log", "logs/server.log", "file to append raw JSON logs to") flag.Parse() err := run(*logPath, flag.Args()) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run(logPath string, args []string) error { if len(args) == 0 { args = []string{"go", "run", "."} } if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil { return err } logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return err } defer logFile.Close() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() // Merge the app's stdout and stderr into one pipe so non-JSON output // (build errors, panics) flows through the same reader. pr, pw, err := os.Pipe() if err != nil { return err } cmd := exec.CommandContext(ctx, args[0], args[1:]...) cmd.Stdout = pw cmd.Stderr = pw cmd.Cancel = func() error { return cmd.Process.Signal(os.Interrupt) } fmt.Printf("dev: running %v, logging to %s\n", args, logPath) if err := cmd.Start(); err != nil { return err } pw.Close() col := &tracelog.Collector{Out: os.Stdout, PassThrough: true, Color: tracelog.IsTerminal(os.Stdout)} // Flush hung traces even when the log stream goes quiet. sweeper := time.NewTicker(time.Second) defer sweeper.Stop() go func() { for range sweeper.C { col.Sweep(time.Now()) } }() scanner := bufio.NewScanner(pr) scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024) for scanner.Scan() { logFile.Write(append(scanner.Bytes(), '\n')) id, entry, ok := tracelog.ParseLine(scanner.Bytes()) if !ok { // Not a JSON log line; pass it through untouched. fmt.Println(scanner.Text()) continue } col.Add(id, entry) } col.Flush() if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, "dev: reading logs:", err) } return cmd.Wait() }