diff --git a/cli/app/logs.go b/cli/app/logs.go index b1566f3f..b06634a2 100644 --- a/cli/app/logs.go +++ b/cli/app/logs.go @@ -2,75 +2,26 @@ package app import ( "context" - "fmt" "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/service" "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 logOpts = types.ContainerLogsOptions{ - ShowStderr: true, - ShowStdout: true, - Since: "", - Until: "", - Timestamps: true, - Follow: true, - Tail: "20", - Details: false, -} - -// stackLogs lists logs for all stack services -func stackLogs(c *cli.Context, app config.App, client *dockerClient.Client) { - filters, err := app.Filters(true, false) - if err != nil { - logrus.Fatal(err) - } - - serviceOpts := types.ServiceListOptions{Filters: filters} - services, err := client.ServiceList(context.Background(), serviceOpts) - if err != nil { - logrus.Fatal(err) - } - - var wg sync.WaitGroup - for _, service := range services { - wg.Add(1) - go func(s string) { - if internal.StdErrOnly { - logOpts.ShowStdout = false - } - - logs, err := client.ServiceLogs(context.Background(), s, logOpts) - 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) - } - - wg.Wait() - - os.Exit(0) -} - var appLogsCommand = cli.Command{ Name: "logs", Aliases: []string{"l"}, @@ -105,46 +56,84 @@ var appLogsCommand = cli.Command{ logrus.Fatalf("%s is not deployed?", app.Name) } - logOpts.Since = internal.SinceLogs - serviceName := c.Args().Get(1) - if serviceName == "" { - logrus.Debugf("tailing logs for all %s services", app.Recipe) - stackLogs(c, app, cl) - } else { - logrus.Debugf("tailing logs for %s", serviceName) - if err := tailServiceLogs(c, cl, app, serviceName); err != nil { - logrus.Fatal(err) - } + serviceNames := []string{} + if serviceName != "" { + serviceNames = []string{serviceName} + } + err = tailLogs(cl, app, serviceNames) + if err != nil { + logrus.Fatal(err) } return nil }, } -func tailServiceLogs(c *cli.Context, cl *dockerClient.Client, app config.App, serviceName string) error { - filters := filters.NewArgs() - filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName)) - - chosenService, err := service.GetService(context.Background(), cl, filters, internal.NoInput) +// 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 { - logrus.Fatal(err) + return err } - if internal.StdErrOnly { - logOpts.ShowStdout = false - } - - logs, err := cl.ServiceLogs(context.Background(), chosenService.ID, logOpts) + services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: f}) if err != nil { - logrus.Fatal(err) + return err } - defer logs.Close() - _, err = io.Copy(os.Stdout, logs) - if err != nil && err != io.EOF { - logrus.Fatal(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 } diff --git a/pkg/config/app.go b/pkg/config/app.go index b3efed2a..a0d3e869 100644 --- a/pkg/config/app.go +++ b/pkg/config/app.go @@ -77,14 +77,32 @@ func StackName(appName string) string { return stackName } -// Filters retrieves exact app filters for querying the container runtime. Due -// to upstream issues, filtering works different depending on what you're +// Filters retrieves app filters for querying the container runtime. By default +// it filters on all services in the app. It is also possible to pass an +// otional list of service names, which get filtered instead. +// +// Due to upstream issues, filtering works different depending on what you're // querying. So, for example, secrets don't work with regex! The caller needs // to implement their own validation that the right secrets are matched. In // order to handle these cases, we provide the `appendServiceNames` / // `exactMatch` modifiers. -func (a App) Filters(appendServiceNames, exactMatch bool) (filters.Args, error) { +func (a App) Filters(appendServiceNames, exactMatch bool, services ...string) (filters.Args, error) { filters := filters.NewArgs() + if len(services) > 0 { + for _, serviceName := range services { + filters.Add("name", ServiceFilter(a.StackName(), serviceName, exactMatch)) + } + return filters, nil + } + + if appendServiceNames { + f := fmt.Sprintf("%s", a.StackName()) + if exactMatch { + f = fmt.Sprintf("^%s", f) + } + filters.Add("name", f) + return filters, nil + } composeFiles, err := GetComposeFiles(a.Recipe, a.Env) if err != nil { @@ -98,28 +116,23 @@ func (a App) Filters(appendServiceNames, exactMatch bool) (filters.Args, error) } for _, service := range compose.Services { - var filter string - - if appendServiceNames { - if exactMatch { - filter = fmt.Sprintf("^%s_%s", a.StackName(), service.Name) - } else { - filter = fmt.Sprintf("%s_%s", a.StackName(), service.Name) - } - } else { - if exactMatch { - filter = fmt.Sprintf("^%s", a.StackName()) - } else { - filter = fmt.Sprintf("%s", a.StackName()) - } - } - - filters.Add("name", filter) + f := ServiceFilter(a.StackName(), service.Name, exactMatch) + filters.Add("name", f) } return filters, nil } +// ServiceFilter creates a filter string for filtering a service in the docker +// container runtime. When exact match is true, it uses regex to match the +// string exactly. +func ServiceFilter(stack, service string, exact bool) string { + if exact { + return fmt.Sprintf("^%s_%s", stack, service) + } + return fmt.Sprintf("%s_%s", stack, service) +} + // ByServer sort a slice of Apps type ByServer []App @@ -340,7 +353,7 @@ func TemplateAppEnvSample(recipeName, appName, server, domain string) error { return fmt.Errorf("%s already exists?", appEnvPath) } - err = ioutil.WriteFile(appEnvPath, envSample, 0664) + err = ioutil.WriteFile(appEnvPath, envSample, 0o664) if err != nil { return err } @@ -602,7 +615,7 @@ func GetLabel(compose *composetypes.Config, stackName string, label string) stri // GetTimeoutFromLabel reads the timeout value from docker label "coop-cloud.${STACK_NAME}.TIMEOUT" and returns 50 as default value func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) { - var timeout = 50 // Default Timeout + timeout := 50 // Default Timeout var err error = nil if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" { logrus.Debugf("timeout label: %s", timeoutLabel)