github.com/webonyx/up@v0.7.4-0.20180808230834-91b94e551323/reporter/text/text.go (about)

     1  // Package text provides a reporter for humanized interactive events.
     2  package text
     3  
     4  import (
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/aws/aws-sdk-go/service/cloudformation"
    10  	"github.com/aws/aws-sdk-go/service/lambda"
    11  	"github.com/dustin/go-humanize"
    12  	"github.com/tj/go-progress"
    13  	"github.com/tj/go-spin"
    14  	"github.com/tj/go/term"
    15  
    16  	"github.com/apex/up/internal/colors"
    17  	"github.com/apex/up/internal/util"
    18  	"github.com/apex/up/platform/aws/cost"
    19  	"github.com/apex/up/platform/event"
    20  	lambdautil "github.com/apex/up/platform/lambda/reporter"
    21  	"github.com/apex/up/platform/lambda/stack"
    22  )
    23  
    24  // TODO: platform-specific reporting should live in the platform
    25  // TODO: typed events would be nicer.. refactor event names
    26  // TODO: refactor, this is a hot mess :D
    27  
    28  // Report events.
    29  func Report(events <-chan *event.Event) {
    30  	r := reporter{
    31  		events:  events,
    32  		spinner: spin.New(),
    33  	}
    34  
    35  	r.Start()
    36  }
    37  
    38  // reporter struct.
    39  type reporter struct {
    40  	events         <-chan *event.Event
    41  	spinner        *spin.Spinner
    42  	prevTime       time.Time
    43  	bar            *progress.Bar
    44  	inlineProgress bool
    45  	pendingName    string
    46  	pendingValue   string
    47  }
    48  
    49  // spin the spinner by moving to the start of the line and re-printing.
    50  func (r *reporter) spin() {
    51  	if r.pendingName != "" {
    52  		r.pending(r.pendingName, r.pendingValue)
    53  	}
    54  }
    55  
    56  // clear the liner.
    57  func (r *reporter) clear() {
    58  	r.pendingName = ""
    59  	r.pendingValue = ""
    60  	term.ClearLine()
    61  }
    62  
    63  // pending log with spinner.
    64  func (r *reporter) pending(name, value string) {
    65  	r.pendingName = name
    66  	r.pendingValue = value
    67  	term.ClearLine()
    68  	fmt.Printf("\r   %s %s", colors.Purple(r.spinner.Next()+" "+name+":"), value)
    69  }
    70  
    71  // complete log with duration.
    72  func (r *reporter) complete(name, value string, d time.Duration) {
    73  	r.pendingName = ""
    74  	r.pendingValue = ""
    75  	term.ClearLine()
    76  	duration := fmt.Sprintf("(%s)", d.Round(time.Millisecond))
    77  	fmt.Printf("\r     %s %s %s\n", colors.Purple(name+":"), value, colors.Gray(duration))
    78  }
    79  
    80  // completeWithoutDuration log without duration.
    81  func (r *reporter) completeWithoutDuration(name, value string) {
    82  	r.pendingName = ""
    83  	r.pendingValue = ""
    84  	term.ClearLine()
    85  	fmt.Printf("\r     %s %s\n", colors.Purple(name+":"), value)
    86  }
    87  
    88  // log line.
    89  func (r *reporter) log(name, value string) {
    90  	fmt.Printf("\r     %s %s\n", colors.Purple(name+":"), value)
    91  }
    92  
    93  // error line.
    94  func (r *reporter) error(name, value string) {
    95  	fmt.Printf("\r     %s %s\n", colors.Red(name+":"), value)
    96  }
    97  
    98  // Start handling events.
    99  func (r *reporter) Start() {
   100  	tick := time.NewTicker(150 * time.Millisecond)
   101  	defer tick.Stop()
   102  
   103  	render := term.Renderer()
   104  
   105  	for {
   106  		select {
   107  		case <-tick.C:
   108  			r.spin()
   109  		case e := <-r.events:
   110  			switch e.Name {
   111  			case "account.login.verify":
   112  				term.HideCursor()
   113  				r.pending("verify", "Check your email for a confirmation link")
   114  			case "account.login.verified":
   115  				term.ShowCursor()
   116  				r.completeWithoutDuration("verify", "complete")
   117  			case "hook":
   118  				r.pending(e.String("name"), "")
   119  			case "hook.complete":
   120  				name := e.String("name")
   121  				if name != "build" {
   122  					r.clear()
   123  				}
   124  			case "deploy", "stack.delete", "platform.stack.apply":
   125  				term.HideCursor()
   126  			case "deploy.complete", "stack.delete.complete", "platform.stack.apply.complete":
   127  				term.ShowCursor()
   128  			case "platform.build.zip":
   129  				s := fmt.Sprintf("%s files, %s", humanize.Comma(e.Int64("files")), humanize.Bytes(uint64(e.Int("size_compressed"))))
   130  				r.complete("build", s, e.Duration("duration"))
   131  			case "platform.deploy":
   132  				r.pending("deploy", "")
   133  			case "platform.deploy.complete":
   134  				s := "complete"
   135  				if v := e.String("commit"); v != "" {
   136  					s = "commit " + v
   137  				} else if v := e.String("version"); v != "" {
   138  					s = "version " + v
   139  				}
   140  				r.complete("deploy", s, e.Duration("duration"))
   141  			case "platform.function.create":
   142  				r.inlineProgress = true
   143  			case "stack.create":
   144  				r.inlineProgress = true
   145  			case "platform.stack.report":
   146  				if r.inlineProgress {
   147  					r.bar = util.NewInlineProgressInt(e.Int("total"))
   148  					r.pending("stack", r.bar.String())
   149  				} else {
   150  					term.ClearAll()
   151  					r.bar = util.NewProgressInt(e.Int("total"))
   152  					render(term.CenterLine(r.bar.String()))
   153  				}
   154  			case "platform.stack.report.event":
   155  				if r.inlineProgress {
   156  					r.bar.ValueInt(e.Int("complete"))
   157  					r.pending("stack", r.bar.String())
   158  				} else {
   159  					r.bar.ValueInt(e.Int("complete"))
   160  					render(term.CenterLine(r.bar.String()))
   161  				}
   162  			case "platform.stack.report.complete":
   163  				if r.inlineProgress {
   164  					r.complete("stack", "complete", e.Duration("duration"))
   165  				} else {
   166  					term.ClearAll()
   167  					term.ShowCursor()
   168  				}
   169  			case "platform.stack.show", "platform.stack.show.complete":
   170  				fmt.Printf("\n")
   171  			case "platform.stack.show.stack":
   172  				s := e.Fields["stack"].(*cloudformation.Stack)
   173  				util.LogName("status", "%s", stack.Status(*s.StackStatus))
   174  				if reason := s.StackStatusReason; reason != nil {
   175  					util.LogName("reason", *reason)
   176  				}
   177  			case "platform.stack.show.stack.events":
   178  				util.LogTitle("Events")
   179  			case "platform.stack.show.nameservers":
   180  				util.Log("nameservers:")
   181  				for _, ns := range e.Strings("nameservers") {
   182  					util.LogListItem(ns)
   183  				}
   184  			case "platform.stack.show.stack.event":
   185  				event := e.Fields["event"].(*cloudformation.StackEvent)
   186  				status := stack.Status(*event.ResourceStatus)
   187  				if status.State() == stack.Failure {
   188  					r.error(*event.LogicalResourceId, *event.ResourceStatusReason)
   189  				} else {
   190  					r.log(*event.LogicalResourceId, status.String())
   191  				}
   192  			case "platform.stack.show.stage":
   193  				util.LogTitle(strings.Title(e.String("name")))
   194  				if s := e.String("domain"); s != "" {
   195  					util.LogName("domain", e.String("domain"))
   196  				}
   197  			case "platform.stack.show.domain":
   198  				util.LogName("endpoint", e.String("endpoint"))
   199  			case "platform.stack.show.version":
   200  				util.LogName("version", e.String("version"))
   201  			case "platform.deploys":
   202  				aliases := e.Fields["aliases"].([]*lambda.AliasConfiguration)
   203  				for _, a := range aliases {
   204  					fmt.Printf("- %s -> %s\n", *a.Name, *a.FunctionVersion)
   205  				}
   206  			case "stack.plan":
   207  				fmt.Printf("\n")
   208  			case "platform.stack.plan.change":
   209  				c := e.Fields["change"].(*cloudformation.Change).ResourceChange
   210  				if *c.ResourceType == "AWS::Lambda::Alias" {
   211  					continue
   212  				}
   213  				color := actionColor(*c.Action)
   214  				fmt.Printf("  %s %s\n", color(*c.Action), lambdautil.ResourceType(*c.ResourceType))
   215  				fmt.Printf("    %s: %s\n", color("id"), *c.LogicalResourceId)
   216  				if c.Replacement != nil {
   217  					fmt.Printf("    %s: %s\n", color("replace"), *c.Replacement)
   218  				}
   219  				fmt.Printf("\n")
   220  			case "platform.certs.create":
   221  				domains := util.UniqueStrings(e.Fields["domains"].([]string))
   222  				r.log("domains", "Check your email to approve the certificate")
   223  				r.pending("confirm", strings.Join(domains, ", "))
   224  			case "platform.certs.create.complete":
   225  				r.complete("confirm", "complete", e.Duration("duration"))
   226  				fmt.Printf("\n")
   227  			case "metrics", "metrics.complete":
   228  				fmt.Printf("\n")
   229  			case "metrics.value":
   230  				switch n := e.String("name"); n {
   231  				case "Duration min", "Duration avg", "Duration max":
   232  					r.log(n, fmt.Sprintf("%dms", e.Int("value")))
   233  				case "Requests":
   234  					v := humanize.Comma(int64(e.Int("value")))
   235  					c := cost.Requests(e.Int("value"))
   236  					r.log(n, fmt.Sprintf("%s %s", v, currency(c)))
   237  				case "Duration sum":
   238  					d := time.Millisecond * time.Duration(e.Int("value"))
   239  					c := cost.Duration(e.Int("value"), e.Int("memory"))
   240  					r.log(n, fmt.Sprintf("%s %s", d, currency(c)))
   241  				case "Invocations":
   242  					d := humanize.Comma(int64(e.Int("value")))
   243  					c := cost.Invocations(e.Int("value"))
   244  					r.log(n, fmt.Sprintf("%s %s", d, currency(c)))
   245  				default:
   246  					r.log(n, humanize.Comma(int64(e.Int("value"))))
   247  				}
   248  			case "prune":
   249  				fmt.Printf("\n")
   250  				r.pending("prune", "removing old releases")
   251  			case "prune.complete":
   252  				n := e.Int("count")
   253  				b := e.Int64("size")
   254  				s := fmt.Sprintf("%d old files removed from S3 (%s)", n, humanize.Bytes(uint64(b)))
   255  				r.complete("prune", s, e.Duration("duration"))
   256  				fmt.Printf("\n")
   257  			}
   258  
   259  			r.prevTime = time.Now()
   260  		}
   261  	}
   262  }
   263  
   264  // currency format.
   265  func currency(n float64) string {
   266  	return colors.Gray(fmt.Sprintf("($%0.2f)", n))
   267  }
   268  
   269  // countEventsByStatus returns the number of events with the given state.
   270  func countEventsByStatus(events []*cloudformation.StackEvent, desired stack.Status) (n int) {
   271  	for _, e := range events {
   272  		status := stack.Status(*e.ResourceStatus)
   273  
   274  		if *e.ResourceType == "AWS::CloudFormation::Stack" {
   275  			continue
   276  		}
   277  
   278  		if status == desired {
   279  			n++
   280  		}
   281  	}
   282  
   283  	return
   284  }
   285  
   286  // countEventsComplete returns the number of completed or failed events.
   287  func countEventsComplete(events []*cloudformation.StackEvent) (n int) {
   288  	for _, e := range events {
   289  		status := stack.Status(*e.ResourceStatus)
   290  
   291  		if *e.ResourceType == "AWS::CloudFormation::Stack" {
   292  			continue
   293  		}
   294  
   295  		if status.IsDone() {
   296  			n++
   297  		}
   298  	}
   299  
   300  	return
   301  }
   302  
   303  // actionColor returns a color func by action.
   304  func actionColor(s string) colors.Func {
   305  	switch s {
   306  	case "Add":
   307  		return colors.Purple
   308  	case "Remove":
   309  		return colors.Red
   310  	case "Modify":
   311  		return colors.Blue
   312  	default:
   313  		return colors.Gray
   314  	}
   315  }