reporter/text/text.go
// 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
}
}