From 9bf81f5c5ea3e0788e59408e9202fb9c77596216 Mon Sep 17 00:00:00 2001 From: Cyrill Troxler Date: Fri, 19 Jan 2024 15:46:13 +0100 Subject: [PATCH] feat: output build logs during app create this adds a log view during the initial build so you can see how the build is progressing without having to open a second terminal. --- create/application.go | 67 ++++++++++++++++--- create/application_test.go | 7 ++ create/create.go | 33 +++++++--- go.mod | 13 +++- go.sum | 28 +++++++- internal/logbox/logbox.go | 132 +++++++++++++++++++++++++++++++++++++ logs/build.go | 4 ++ 7 files changed, 265 insertions(+), 19 deletions(-) create mode 100644 internal/logbox/logbox.go diff --git a/create/application.go b/create/application.go index e883eb4..e439fed 100644 --- a/create/application.go +++ b/create/application.go @@ -11,13 +11,16 @@ import ( "time" "github.com/alecthomas/kong" + tea "github.com/charmbracelet/bubbletea" "github.com/grafana/loki/pkg/logproto" + "github.com/mattn/go-isatty" apps "github.com/ninech/apis/apps/v1alpha1" meta "github.com/ninech/apis/meta/v1alpha1" "github.com/ninech/nctl/api" "github.com/ninech/nctl/api/log" "github.com/ninech/nctl/api/util" "github.com/ninech/nctl/internal/format" + "github.com/ninech/nctl/internal/logbox" "github.com/ninech/nctl/logs" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -154,7 +157,7 @@ func (app *applicationCmd) Run(ctx context.Context, client *api.Client) error { if err := c.wait( appWaitCtx, waitForBuildStart(newApp), - waitForBuildFinish(newApp), + waitForBuildFinish(appWaitCtx, cancel, newApp, client.Log), waitForRelease(newApp), ); err != nil { if buildErr, ok := err.(buildError); ok { @@ -305,21 +308,64 @@ func waitForBuildStart(app *apps.Application) waitStage { } } -func waitForBuildFinish(app *apps.Application) waitStage { +func waitForBuildFinish(ctx context.Context, cancel context.CancelFunc, app *apps.Application, logClient *log.Client) waitStage { + msg := message{icon: "📦", text: "building application"} + interrupt := make(chan bool, 1) + lb := logbox.New(15, msg.progress(), interrupt) + opts := []tea.ProgramOption{tea.WithoutSignalHandler()} + + // disable input if we are not in a terminal + if !isatty.IsTerminal(os.Stdout.Fd()) { + opts = append(opts, tea.WithInput(nil)) + } + + p := tea.NewProgram(lb, opts...) + return waitStage{ - kind: strings.ToLower(apps.BuildKind), - objectList: &apps.BuildList{}, + disableSpinner: true, + kind: strings.ToLower(apps.BuildKind), + objectList: &apps.BuildList{}, listOpts: []runtimeclient.ListOption{ runtimeclient.InNamespace(app.GetNamespace()), runtimeclient.MatchingLabels{util.ApplicationNameLabel: app.GetName()}, }, - waitMessage: &message{ - text: "building application", - icon: "📦", - }, + waitMessage: nil, doneMessage: &message{ disabled: true, }, + beforeWait: func() { + // setup the log tailing and send it to the logbox. Run in the + // background until the context is cancelled. + go func() { + if err := logClient.TailQuery( + ctx, 0, &logbox.Output{Program: p}, + log.Query{ + QueryString: logs.BuildsOfAppQuery(app.Name, app.Namespace), + Limit: 10, + Start: time.Now(), + End: time.Now(), + Direction: logproto.BACKWARD, + Quiet: true, + }, + ); err != nil { + fmt.Fprintf(os.Stderr, "error tailing the build log: %s", err) + } + }() + + go func() { + if _, err := p.Run(); err != nil { + fmt.Fprintf(os.Stderr, "error running tea program: %s", err) + return + } + p.Wait() + if <-interrupt { + // as the tea program intercepts ctrl+c/d while it's + // running, we need to cancel the context when we get an + // interrupt signal. + cancel() + } + }() + }, onResult: func(e watch.Event) (bool, error) { build, ok := e.Object.(*apps.Build) if !ok { @@ -328,10 +374,15 @@ func waitForBuildFinish(app *apps.Application) waitStage { switch build.Status.AtProvider.BuildStatus { case buildStatusSuccess: + p.Send(logbox.Msg{Done: true}) + p.Quit() + p.Wait() return true, nil case buildStatusError: fallthrough case buildStatusUnknown: + p.Quit() + p.Wait() return false, buildError{build: build} } diff --git a/create/application_test.go b/create/application_test.go index 1cd3568..81052e4 100644 --- a/create/application_test.go +++ b/create/application_test.go @@ -384,6 +384,13 @@ func TestApplicationWait(t *testing.T) { t.Fatal(err) } + out, err := log.StdOut("default") + if err != nil { + t.Fatal(err) + } + + apiClient.Log = &log.Client{Client: log.NewFake(t, time.Now(), "one", "two"), StdOut: out} + ctx := context.Background() // to test the wait we create a ticker that continously updates our diff --git a/create/create.go b/create/create.go index 419df95..e99303f 100644 --- a/create/create.go +++ b/create/create.go @@ -40,13 +40,16 @@ type creator struct { } type waitStage struct { - kind string - waitMessage *message - doneMessage *message - objectList runtimeclient.ObjectList - listOpts []runtimeclient.ListOption - onResult resultFunc - spinner *yacspin.Spinner + kind string + waitMessage *message + doneMessage *message + objectList runtimeclient.ObjectList + listOpts []runtimeclient.ListOption + onResult resultFunc + spinner *yacspin.Spinner + disableSpinner bool + // beforeWait is a hook that is called just before the wait is being run. + beforeWait func() } type message struct { @@ -104,6 +107,10 @@ func (c *creator) wait(ctx context.Context, stages ...waitStage) error { } stage.spinner = spinner + if stage.beforeWait != nil { + stage.beforeWait() + } + if err := retry.OnError(watchBackoff, isWatchError, func() error { return stage.wait(ctx, c.client) }); err != nil { @@ -157,10 +164,20 @@ func isWatchError(err error) bool { } func (w *waitStage) wait(ctx context.Context, client *api.Client) error { - _ = w.spinner.Start() + if !w.disableSpinner { + _ = w.spinner.Start() + } + + return w.watch(ctx, client) +} + +func (w *waitStage) watch(ctx context.Context, client *api.Client) error { wa, err := client.Watch(ctx, w.objectList, w.listOpts...) if err != nil { + if err == context.Canceled { + return err + } return watchError{kind: w.kind} } diff --git a/go.mod b/go.mod index 27d9d2f..fd89f2d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,9 @@ go 1.21 require ( github.com/alecthomas/kong v0.7.0 + github.com/charmbracelet/bubbles v0.16.1 + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.9.1 github.com/crossplane/crossplane-runtime v1.14.3 github.com/docker/docker v24.0.7+incompatible github.com/fatih/color v1.15.0 @@ -54,11 +57,13 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go v1.44.321 // indirect github.com/axiomhq/hyperloglog v0.0.0-20230201085229-3ddf4bad03dc // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/coreos/go-oidc/v3 v3.2.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect @@ -134,9 +139,11 @@ require ( github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/klauspost/compress v1.17.3 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/miekg/dns v1.1.55 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -146,6 +153,10 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/oklog/ulid v1.3.1 // indirect diff --git a/go.sum b/go.sum index 4259732..6a18085 100644 --- a/go.sum +++ b/go.sum @@ -154,6 +154,8 @@ github.com/aws/aws-sdk-go v1.44.321 h1:iXwFLxWjZPjYqjPq0EcCs46xX7oDLEELte1+BzgpK github.com/aws/aws-sdk-go v1.44.321/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/axiomhq/hyperloglog v0.0.0-20230201085229-3ddf4bad03dc h1:Keo7wQ7UODUaHcEi7ltENhbAK2VgZjfat6mLy03tQzo= github.com/axiomhq/hyperloglog v0.0.0-20230201085229-3ddf4bad03dc/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -169,6 +171,12 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/chromedp/cdproto v0.0.0-20220827030233-358ed4af73cf/go.mod h1:5Y4sD/eXpwrChIuxhSr/G20n9CdbCmoerOHnuAf0Zr0= github.com/chromedp/chromedp v0.8.5/go.mod h1:xal2XY5Di7m/bzlGwtoYpmgIOfDqCakOIVg5OfdkPZ4= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= @@ -189,6 +197,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/coreos/go-oidc/v3 v3.2.0 h1:2eR2MGR7thBXSQ2YbODlF0fcmgtliLCfr9iX6RW11fc= github.com/coreos/go-oidc/v3 v3.2.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= @@ -665,6 +675,8 @@ github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/linode/linodego v1.19.0 h1:n4WJrcr9+30e9JGZ6DI0nZbm5SdAj1kSwvvt/998YUw= github.com/linode/linodego v1.19.0/go.mod h1:XZFR+yJ9mm2kwf6itZ6SCpu+6w3KnIevV0Uu5HNWJgQ= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasepe/codename v0.2.0 h1:zkW9mKWSO8jjVIYFyZWE9FPvBtFVJxgMpQcMkf4Vv20= github.com/lucasepe/codename v0.2.0/go.mod h1:RDcExRuZPWp5Uz+BosvpROFTrxpt5r1vSzBObHdBdDM= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -689,8 +701,11 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= @@ -730,6 +745,14 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -828,6 +851,7 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/prometheus v0.47.2-0.20231010075449-4b9c19fe5510 h1:6ksZ7t1hNOzGPPs8DK7SvXQf6UfWzi+W5Z7PCBl8gx4= github.com/prometheus/prometheus v0.47.2-0.20231010075449-4b9c19fe5510/go.mod h1:UC0TwJiF90m2T3iYPQBKnGu8gv3s55dF/EgpTq8gyvo= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab h1:ZjX6I48eZSFetPb41dHudEyVr5v953N15TsNZXlkcWY= diff --git a/internal/logbox/logbox.go b/internal/logbox/logbox.go new file mode 100644 index 0000000..334cfe8 --- /dev/null +++ b/internal/logbox/logbox.go @@ -0,0 +1,132 @@ +package logbox + +import ( + "fmt" + "io" + "strings" + "time" + + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/fatih/color" + "github.com/grafana/loki/pkg/logcli/output" + "github.com/grafana/loki/pkg/loghttp" +) + +var appStyle = lipgloss.NewStyle().Margin(0, 2, 1, 1) + +type Msg struct { + Line string + Done bool +} + +func (r Msg) String() string { + return r.Line +} + +// LogBox renders a scrolling box in a terminal for outputting logs without +// filling up the whole scrollback. On the first line it shows the waitMessage +// with a spinner in front. +type LogBox struct { + height int + waitMessage string + results []Msg + quitting bool + spinner spinner.Model + interrupt chan bool +} + +// New initializes a LogBox. The interrupt channel will be written to on +// ctrl+c/ctrl+d since it intercepts these. +func New(height int, waitMessage string, interrupt chan bool) LogBox { + s := spinner.New(spinner.WithSpinner(spinner.MiniDot), spinner.WithStyle(lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{}))) + return LogBox{ + height: height, + waitMessage: waitMessage, + spinner: s, + results: []Msg{}, + interrupt: interrupt, + } +} + +func (lb LogBox) Init() tea.Cmd { + return lb.spinner.Tick +} + +func (lb LogBox) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "ctrl+d": + lb.interrupt <- true + return lb, tea.Quit + default: + return lb, nil + } + case Msg: + if msg.Done { + lb.quitting = true + return lb, tea.Quit + } + if len(lb.results) == lb.height { + lb.results = append(lb.results[1:], msg) + } else { + lb.results = append(lb.results, msg) + } + return lb, nil + case spinner.TickMsg: + var cmd tea.Cmd + lb.spinner, cmd = lb.spinner.Update(msg) + return lb, cmd + default: + return lb, nil + } +} + +func (lb LogBox) View() string { + var s string + if !lb.quitting { + s += lb.spinner.View() + lb.waitMessage + } + + if lb.quitting { + s += "✓" + lb.waitMessage + return appStyle.Render(s) + } + + if len(lb.results) > 0 { + s += "\n\n" + } + + for _, res := range lb.results { + s += res.String() + "\n" + } + + return appStyle.Render(s) +} + +// Output implements output.LogOutput to send log lines to the tea.Program. +type Output struct { + *tea.Program +} + +func (f *Output) FormatAndPrintln(ts time.Time, lbls loghttp.LabelSet, maxLabelsLen int, line string) { + timestamp := ts.In(time.Local).Format(time.RFC3339) + line = strings.TrimSpace(line) + + // we delay the send to the terminal slightly to make the log output look + // a little smoother. Since our log forwarder only sends at a certain + // interval (1s), we often receive 10+ log lines at once. + f.delaySend(time.Millisecond*10, Msg{Line: fmt.Sprintf("%s %s", color.BlueString(timestamp), line)}) +} + +func (f *Output) WithWriter(w io.Writer) output.LogOutput { + return f +} + +// delaySend delays the sending of the message by the specified duration. +func (f *Output) delaySend(d time.Duration, msg Msg) { + time.Sleep(d) + f.Send(msg) +} diff --git a/logs/build.go b/logs/build.go index 8724100..21087f3 100644 --- a/logs/build.go +++ b/logs/build.go @@ -42,3 +42,7 @@ const ( func BuildQuery(name, project string) string { return queryString(map[string]string{buildLabel: name, phaseLabel: buildPhase}, project) } + +func BuildsOfAppQuery(name, project string) string { + return queryString(map[string]string{appLabel: name, phaseLabel: buildPhase}, project) +}