forked from toolshed/abra
feat: abra app logs shows task errors (!395)
The log command now checks for the ready state in the task list. If it is not ready. It shows the task logs. This might look like this: ``` ERRO[0000] Service abra-test-recipe_default_app: State rejected: No such image: ngaaaax:1.21.0 ERRO[0000] Service abra-test-recipe_default_app: State preparing: ERRO[0000] Service abra-test-recipe_default_app: State rejected: No such image: ngaaaax:1.21.0 ERRO[0000] Service abra-test-recipe_default_app: State rejected: No such image: ngaaaax:1.21.0 ERRO[0000] Service abra-test-recipe_default_app: State rejected: No such image: ngaaaax:1.21.0 ``` Closes coop-cloud/organising#518 Reviewed-on: coop-cloud/abra#395 Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech> Co-authored-by: p4u1 <p4u1_f4u1@riseup.net> Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
This commit is contained in:
145
cli/app/logs.go
145
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user