feat: abra app logs shows task errors #395
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 {
|
||||
decentral1se marked this conversation as resolved
Outdated
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Slightly confusing that this function does
return err
and alsologrus.Fatal(err)
. Maybe this is handier?Good catch! Changed it to
return err
. I did not change thelogrus.Fatal(err)
in the goroutine below because otherwise I would need to create an error channel.