package app import ( "context" "io" "os" "slices" "sync" "time" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/upstream/stack" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" dockerClient "github.com/docker/docker/client" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) var appLogsCommand = cli.Command{ Name: "logs", Aliases: []string{"l"}, ArgsUsage: " []", Usage: "Tail app logs", Flags: []cli.Flag{ internal.StdErrOnlyFlag, internal.SinceLogsFlag, internal.DebugFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) stackName := app.StackName() if err := recipe.EnsureExists(app.Recipe); err != nil { logrus.Fatal(err) } cl, err := client.New(app.Server) if err != nil { logrus.Fatal(err) } isDeployed, _, err := stack.IsDeployed(context.Background(), cl, stackName) if err != nil { logrus.Fatal(err) } if !isDeployed { logrus.Fatalf("%s is not deployed?", app.Name) } serviceName := c.Args().Get(1) serviceNames := []string{} if serviceName != "" { serviceNames = []string{serviceName} } err = tailLogs(cl, app, serviceNames) if err != nil { logrus.Fatal(err) } return nil }, } // 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 config.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 { logrus.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, types.ContainerLogsOptions{ ShowStderr: true, ShowStdout: !internal.StdErrOnly, Since: internal.SinceLogs, Until: "", Timestamps: true, Follow: true, Tail: "20", Details: false, }) if err != nil { logrus.Fatal(err) } defer logs.Close() _, err = io.Copy(os.Stdout, logs) if err != nil && err != io.EOF { logrus.Fatal(err) } }(service.ID) } // Wait for all log streams to be closed. wg.Wait() return nil }