package app import ( "context" "fmt" "io" "os" "slices" "sync" "time" "coopcloud.tech/abra/cli/internal" appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/upstream/stack" "github.com/docker/docker/api/types" containerTypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" dockerClient "github.com/docker/docker/client" "github.com/spf13/cobra" ) var AppLogsCommand = &cobra.Command{ Use: "logs [service] [flags]", Aliases: []string{"l"}, Short: "Tail app logs", Args: cobra.RangeArgs(1, 2), ValidArgsFunction: func( cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { switch l := len(args); l { case 0: return autocomplete.AppNameComplete() case 1: app, err := appPkg.Get(args[0]) if err != nil { errMsg := fmt.Sprintf("autocomplete failed: %s", err) return []string{errMsg}, cobra.ShellCompDirectiveError } return autocomplete.ServiceNameComplete(app.Name) default: return nil, cobra.ShellCompDirectiveDefault } }, Run: func(cmd *cobra.Command, args []string) { app := internal.ValidateApp(args) stackName := app.StackName() if err := app.Recipe.EnsureExists(); err != nil { log.Fatal(err) } cl, err := client.New(app.Server) if err != nil { log.Fatal(err) } deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName) if err != nil { log.Fatal(err) } if !deployMeta.IsDeployed { log.Fatalf("%s is not deployed?", app.Name) } var serviceNames []string if len(args) == 2 { serviceNames = []string{args[1]} } if err = tailLogs(cl, app, serviceNames); err != nil { log.Fatal(err) } }, } // tailLogs prints logs for the given app with optional service names to be // filtered on. It also checks if the latest task is not runnning and then // prints the past tasks. func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) error { f, err := app.Filters(true, false, serviceNames...) if err != nil { return err } services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: f}) if err != nil { return err } var wg sync.WaitGroup for _, service := range services { filters := filters.NewArgs() filters.Add("name", service.Spec.Name) tasks, err := cl.TaskList(context.Background(), types.TaskListOptions{Filters: f}) if err != nil { return err } if len(tasks) > 0 { // Need to sort the tasks by the CreatedAt field in the inverse order. // Otherwise they are in the reversed order and not sorted properly. slices.SortFunc[[]swarm.Task](tasks, func(t1, t2 swarm.Task) int { return int(t2.Meta.CreatedAt.Unix() - t1.Meta.CreatedAt.Unix()) }) lastTask := tasks[0].Status if lastTask.State != swarm.TaskStateRunning { for _, task := range tasks { log.Errorf("[%s] %s State %s: %s", service.Spec.Name, task.Meta.CreatedAt.Format(time.RFC3339), task.Status.State, task.Status.Err) } } } // Collect the logs in a go routine, so the logs from all services are // collected in parallel. wg.Add(1) go func(serviceID string) { logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{ ShowStderr: true, ShowStdout: !stdErr, Since: sinceLogs, Until: "", Timestamps: true, Follow: true, Tail: "20", Details: false, }) if err != nil { log.Fatal(err) } defer logs.Close() _, err = io.Copy(os.Stdout, logs) if err != nil && err != io.EOF { log.Fatal(err) } }(service.ID) } // Wait for all log streams to be closed. wg.Wait() return nil } var ( stdErr bool sinceLogs string ) func init() { AppLogsCommand.Flags().BoolVarP( &stdErr, "stderr", "s", false, "only tail stderr", ) AppLogsCommand.Flags().StringVarP( &sinceLogs, "since", "S", "", "tail logs since YYYY-MM-DDTHH:MM:SSZ", ) }