reporter/text/text.go

Summary

Maintainability
C
1 day
Test Coverage
// Package text provides a reporter for humanized interactive events.
package text

import (
    "fmt"
    "strings"
    "time"

    "github.com/aws/aws-sdk-go/service/cloudformation"
    "github.com/dustin/go-humanize"
    "github.com/tj/go-progress"
    "github.com/tj/go-spin"
    "github.com/tj/go/term"

    "github.com/apex/up/internal/colors"
    "github.com/apex/up/internal/util"
    "github.com/apex/up/platform/aws/cost"
    "github.com/apex/up/platform/event"
    lambdautil "github.com/apex/up/platform/lambda/reporter"
    "github.com/apex/up/platform/lambda/stack"
)

// TODO: platform-specific reporting should live in the platform
// TODO: typed events would be nicer.. refactor event names
// TODO: refactor, this is a hot mess :D

// Report events.
func Report(events <-chan *event.Event) {
    r := reporter{
        events:  events,
        spinner: spin.New(),
    }

    r.Start()
}

// reporter struct.
type reporter struct {
    events         <-chan *event.Event
    spinner        *spin.Spinner
    prevTime       time.Time
    bar            *progress.Bar
    inlineProgress bool
    pendingName    string
    pendingValue   string
}

// spin the spinner by moving to the start of the line and re-printing.
func (r *reporter) spin() {
    if r.pendingName != "" {
        r.pending(r.pendingName, r.pendingValue)
    }
}

// clear the liner.
func (r *reporter) clear() {
    r.pendingName = ""
    r.pendingValue = ""
    term.ClearLine()
}

// pending log with spinner.
func (r *reporter) pending(name, value string) {
    r.pendingName = name
    r.pendingValue = value
    term.ClearLine()
    fmt.Printf("\r   %s %s", colors.Purple(r.spinner.Next()+" "+name+":"), value)
}

// complete log with duration.
func (r *reporter) complete(name, value string, d time.Duration) {
    r.pendingName = ""
    r.pendingValue = ""
    term.ClearLine()
    duration := fmt.Sprintf("(%s)", d.Round(time.Millisecond))
    fmt.Printf("\r     %s %s %s\n", colors.Purple(name+":"), value, colors.Gray(duration))
}

// completeWithoutDuration log without duration.
func (r *reporter) completeWithoutDuration(name, value string) {
    r.pendingName = ""
    r.pendingValue = ""
    term.ClearLine()
    fmt.Printf("\r     %s %s\n", colors.Purple(name+":"), value)
}

// log line.
func (r *reporter) log(name, value string) {
    fmt.Printf("\r     %s %s\n", colors.Purple(name+":"), value)
}

// error line.
func (r *reporter) error(name, value string) {
    fmt.Printf("\r     %s %s\n", colors.Red(name+":"), value)
}

// Start handling events.
func (r *reporter) Start() {
    tick := time.NewTicker(150 * time.Millisecond)
    defer tick.Stop()

    render := term.Renderer()

    for {
        select {
        case <-tick.C:
            r.spin()
        case e := <-r.events:
            switch e.Name {
            case "account.login.verify":
                term.HideCursor()
                r.pending("verify", "Check your email for a confirmation link")
            case "account.login.verified":
                term.ShowCursor()
                r.completeWithoutDuration("verify", "complete")
            case "hook":
                r.pending(e.String("name"), "")
            case "hook.complete":
                name := e.String("name")
                if name != "build" {
                    r.clear()
                }
            case "deploy", "stack.delete", "platform.stack.apply":
                term.HideCursor()
            case "deploy.complete", "stack.delete.complete", "platform.stack.apply.complete":
                term.ShowCursor()
            case "platform.build.zip":
                s := fmt.Sprintf("%s files, %s", humanize.Comma(e.Int64("files")), humanize.Bytes(uint64(e.Int("size_compressed"))))
                r.complete("build", s, e.Duration("duration"))
            case "platform.deploy":
                r.pending("deploy", e.String("stage"))
            case "platform.deploy.complete":
                s := e.String("stage")
                if v := e.String("commit"); v != "" {
                    s += " (commit " + v + ")"
                } else if v := e.String("version"); v != "" {
                    s += " (version " + v + ")"
                }
                r.complete("deploy", s, e.Duration("duration"))
            case "platform.deploy.url":
                r.log("endpoint", e.String("url"))
            case "platform.function.create":
                r.inlineProgress = true
            case "stack.create":
                r.inlineProgress = true
            case "platform.stack.report":
                if r.inlineProgress {
                    r.bar = util.NewInlineProgressInt(e.Int("total"))
                    r.pending("stack", r.bar.String())
                } else {
                    term.ClearAll()
                    r.bar = util.NewProgressInt(e.Int("total"))
                    render(term.CenterLine(r.bar.String()))
                }
            case "platform.stack.report.event":
                if r.inlineProgress {
                    r.bar.ValueInt(e.Int("complete"))
                    r.pending("stack", r.bar.String())
                } else {
                    r.bar.ValueInt(e.Int("complete"))
                    render(term.CenterLine(r.bar.String()))
                }
            case "platform.stack.report.complete":
                if r.inlineProgress {
                    r.complete("stack", "complete", e.Duration("duration"))
                } else {
                    term.ClearAll()
                    term.ShowCursor()
                }
            case "platform.stack.show", "platform.stack.show.complete":
                fmt.Printf("\n")
            case "platform.stack.show.stack":
                s := e.Fields["stack"].(*cloudformation.Stack)
                util.LogName("status", "%s", stack.Status(*s.StackStatus))
                if reason := s.StackStatusReason; reason != nil {
                    util.LogName("reason", *reason)
                }
            case "platform.stack.show.stack.events":
                util.LogTitle("Events")
            case "platform.stack.show.nameservers":
                util.Log("nameservers:")
                for _, ns := range e.Strings("nameservers") {
                    util.LogListItem(ns)
                }
            case "platform.stack.show.stack.event":
                event := e.Fields["event"].(*cloudformation.StackEvent)
                status := stack.Status(*event.ResourceStatus)
                if status.State() == stack.Failure {
                    r.error(*event.LogicalResourceId, *event.ResourceStatusReason)
                } else {
                    r.log(*event.LogicalResourceId, status.String())
                }
            case "platform.stack.show.stage":
                util.LogTitle(strings.Title(e.String("name")))
                if s := e.String("domain"); s != "" {
                    util.LogName("domain", e.String("domain"))
                }
            case "platform.stack.show.domain":
                util.LogName("endpoint", e.String("endpoint"))
            case "platform.stack.show.version":
                util.LogName("version", e.String("version"))
            case "stack.plan":
                fmt.Printf("\n")
            case "platform.stack.plan.change":
                c := e.Fields["change"].(*cloudformation.Change).ResourceChange
                if *c.ResourceType == "AWS::Lambda::Alias" {
                    continue
                }
                color := actionColor(*c.Action)
                fmt.Printf("  %s %s\n", color(*c.Action), lambdautil.ResourceType(*c.ResourceType))
                fmt.Printf("    %s: %s\n", color("id"), *c.LogicalResourceId)
                if c.Replacement != nil {
                    fmt.Printf("    %s: %s\n", color("replace"), *c.Replacement)
                }
                fmt.Printf("\n")
            case "platform.certs.create":
                domains := util.UniqueStrings(e.Fields["domains"].([]string))
                r.log("domains", "Check your email to approve the certificate")
                r.pending("confirm", strings.Join(domains, ", "))
            case "platform.certs.create.complete":
                r.complete("confirm", "complete", e.Duration("duration"))
                fmt.Printf("\n")
            case "metrics", "metrics.complete":
                fmt.Printf("\n")
            case "metrics.value":
                switch n := e.String("name"); n {
                case "Duration min", "Duration avg", "Duration max":
                    r.log(n, fmt.Sprintf("%dms", e.Int("value")))
                case "Requests":
                    v := humanize.Comma(int64(e.Int("value")))
                    c := cost.Requests(e.Int("value"))
                    r.log(n, fmt.Sprintf("%s %s", v, currency(c)))
                case "Duration sum":
                    d := time.Millisecond * time.Duration(e.Int("value"))
                    c := cost.Duration(e.Int("value"), e.Int("memory"))
                    r.log(n, fmt.Sprintf("%s %s", d, currency(c)))
                case "Invocations":
                    d := humanize.Comma(int64(e.Int("value")))
                    c := cost.Invocations(e.Int("value"))
                    r.log(n, fmt.Sprintf("%s %s", d, currency(c)))
                default:
                    r.log(n, humanize.Comma(int64(e.Int("value"))))
                }
            case "prune":
                fmt.Printf("\n")
                r.pending("prune", "removing old releases")
            case "prune.complete":
                n := e.Int("count")
                b := e.Int64("size")
                s := fmt.Sprintf("%d old files removed from S3 (%s)", n, humanize.Bytes(uint64(b)))
                r.complete("prune", s, e.Duration("duration"))
                fmt.Printf("\n")
            }

            r.prevTime = time.Now()
        }
    }
}

// currency format.
func currency(n float64) string {
    return colors.Gray(fmt.Sprintf("($%0.2f)", n))
}

// countEventsByStatus returns the number of events with the given state.
func countEventsByStatus(events []*cloudformation.StackEvent, desired stack.Status) (n int) {
    for _, e := range events {
        status := stack.Status(*e.ResourceStatus)

        if *e.ResourceType == "AWS::CloudFormation::Stack" {
            continue
        }

        if status == desired {
            n++
        }
    }

    return
}

// countEventsComplete returns the number of completed or failed events.
func countEventsComplete(events []*cloudformation.StackEvent) (n int) {
    for _, e := range events {
        status := stack.Status(*e.ResourceStatus)

        if *e.ResourceType == "AWS::CloudFormation::Stack" {
            continue
        }

        if status.IsDone() {
            n++
        }
    }

    return
}

// actionColor returns a color func by action.
func actionColor(s string) colors.Func {
    switch s {
    case "Add":
        return colors.Purple
    case "Remove":
        return colors.Red
    case "Modify":
        return colors.Blue
    default:
        return colors.Gray
    }
}