github.com/skabbes/up@v0.2.1/reporter/text.go (about)

     1  // Package reporter provides event-based reporting for the CLI,
     2  // aka this is what the user sees.
     3  package reporter
     4  
     5  import (
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"time"
    10  
    11  	"github.com/aws/aws-sdk-go/service/cloudformation"
    12  	"github.com/dustin/go-humanize"
    13  	"github.com/tj/go-progress"
    14  	"github.com/tj/go-spin"
    15  	"github.com/tj/go/term"
    16  
    17  	"github.com/apex/up/internal/colors"
    18  	"github.com/apex/up/internal/util"
    19  	"github.com/apex/up/platform/event"
    20  	"github.com/apex/up/platform/lambda/cost"
    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  // Text outputs human friendly textual reporting.
    29  func Text(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  // pending log with spinner.
    57  func (r *reporter) pending(name, value string) {
    58  	r.pendingName = name
    59  	r.pendingValue = value
    60  	term.ClearLine()
    61  	fmt.Printf("\r %30s %s", colors.Purple(r.spinner.Next()+" "+name+":"), value)
    62  }
    63  
    64  // complete log with duration.
    65  func (r *reporter) complete(name, value string, d time.Duration) {
    66  	r.pendingName = ""
    67  	r.pendingValue = ""
    68  	term.ClearLine()
    69  	duration := fmt.Sprintf("(%s)", d.Round(time.Millisecond))
    70  	fmt.Printf("\r %30s %s %s\n", colors.Purple(name+":"), value, colors.Gray(duration))
    71  }
    72  
    73  // log line
    74  func (r *reporter) log(name, value string) {
    75  	fmt.Printf("\r %35s %s\n", colors.Purple(name+":"), value)
    76  }
    77  
    78  // Start handling events.
    79  func (r *reporter) Start() {
    80  	tick := time.NewTicker(150 * time.Millisecond)
    81  	defer tick.Stop()
    82  
    83  	for {
    84  		select {
    85  		case <-tick.C:
    86  			r.spin()
    87  		case e := <-r.events:
    88  			switch e.Name {
    89  			case "hook":
    90  				r.pending("hook", e.String("name"))
    91  			case "hook.complete":
    92  				r.complete("hook", e.String("name"), e.Duration("duration"))
    93  			case "deploy", "stack.delete", "platform.stack.apply":
    94  				term.HideCursor()
    95  			case "deploy.complete", "stack.delete.complete", "platform.stack.apply.complete":
    96  				term.ShowCursor()
    97  			case "platform.build":
    98  				r.pending("build", "")
    99  			case "platform.build.zip":
   100  				s := fmt.Sprintf("%s files, %s", humanize.Comma(e.Int64("files")), humanize.Bytes(uint64(e.Int("size_compressed"))))
   101  				r.complete("build", s, e.Duration("duration"))
   102  			case "platform.deploy":
   103  				r.pending("deploy", "")
   104  			case "platform.deploy.complete":
   105  				r.complete("deploy", "complete", e.Duration("duration"))
   106  			case "platform.function.create":
   107  				r.inlineProgress = true
   108  			case "stack.create":
   109  				r.inlineProgress = true
   110  			case "platform.stack.report":
   111  				if r.inlineProgress {
   112  					r.bar = util.NewInlineProgressInt(e.Int("total"))
   113  					r.pending("stack", r.bar.String())
   114  				} else {
   115  					r.bar = util.NewProgressInt(e.Int("total"))
   116  					io.WriteString(os.Stdout, term.CenterLine(r.bar.String()))
   117  				}
   118  			case "platform.stack.report.event":
   119  				if r.inlineProgress {
   120  					r.bar.ValueInt(e.Int("complete"))
   121  					r.pending("stack", r.bar.String())
   122  				} else {
   123  					r.bar.ValueInt(e.Int("complete"))
   124  					io.WriteString(os.Stdout, term.CenterLine(r.bar.String()))
   125  				}
   126  			case "platform.stack.report.complete":
   127  				if r.inlineProgress {
   128  					r.complete("stack", "complete", e.Duration("duration"))
   129  				} else {
   130  					term.ClearAll()
   131  					term.ShowCursor()
   132  				}
   133  			case "platform.stack.show", "platform.stack.show.complete":
   134  				fmt.Printf("\n")
   135  			case "platform.stack.show.stack":
   136  				s := e.Fields["stack"].(*cloudformation.Stack)
   137  				fmt.Printf("  %s: %s\n", colors.Purple("status"), stack.Status(*s.StackStatus))
   138  				if reason := s.StackStatusReason; reason != nil {
   139  					fmt.Printf("  %s: %s\n", colors.Purple("reason"), *reason)
   140  				}
   141  				fmt.Printf("\n")
   142  			case "platform.stack.show.event":
   143  				event := e.Fields["event"].(*cloudformation.StackEvent)
   144  				kind := *event.ResourceType
   145  				status := stack.Status(*event.ResourceStatus)
   146  				color := colors.Purple
   147  				if status.State() == stack.Failure {
   148  					color = colors.Red
   149  				}
   150  				fmt.Printf("  %s\n", color(kind))
   151  				fmt.Printf("    %s: %v\n", color("id"), *event.LogicalResourceId)
   152  				fmt.Printf("    %s: %s\n", color("status"), status)
   153  				if reason := event.ResourceStatusReason; reason != nil {
   154  					fmt.Printf("    %s: %s\n", color("reason"), *reason)
   155  				}
   156  				fmt.Printf("\n")
   157  			case "stack.plan":
   158  				fmt.Printf("\n")
   159  			case "platform.stack.plan.change":
   160  				c := e.Fields["change"].(*cloudformation.Change).ResourceChange
   161  				color := actionColor(*c.Action)
   162  				fmt.Printf("  %s %s\n", color(*c.Action), *c.ResourceType)
   163  				fmt.Printf("    %s: %s\n", color("id"), *c.LogicalResourceId)
   164  				if c.Replacement != nil {
   165  					fmt.Printf("    %s: %s\n", color("replace"), *c.Replacement)
   166  				}
   167  				fmt.Printf("\n")
   168  			case "metrics", "metrics.complete":
   169  				fmt.Printf("\n")
   170  			case "metrics.value":
   171  				switch n := e.String("name"); n {
   172  				case "Duration (min)", "Duration (avg)", "Duration (max)":
   173  					r.log(n, fmt.Sprintf("%dms", e.Int("value")))
   174  				case "Requests":
   175  					v := humanize.Comma(int64(e.Int("value")))
   176  					c := cost.Requests(e.Int("value"))
   177  					r.log(n, fmt.Sprintf("%s %s", v, currency(c)))
   178  				case "Duration (sum)":
   179  					d := time.Millisecond * time.Duration(e.Int("value"))
   180  					c := cost.Duration(e.Int("value"), e.Int("memory"))
   181  					r.log(n, fmt.Sprintf("%s %s", d, currency(c)))
   182  				case "Invocations":
   183  					d := time.Millisecond * time.Duration(e.Int("value"))
   184  					c := cost.Invocations(e.Int("value"))
   185  					r.log(n, fmt.Sprintf("%s %s", d, currency(c)))
   186  				default:
   187  					r.log(n, fmt.Sprintf("%s", humanize.Comma(int64(e.Int("value")))))
   188  				}
   189  			}
   190  
   191  			r.prevTime = time.Now()
   192  		}
   193  	}
   194  }
   195  
   196  // currency format.
   197  func currency(n float64) string {
   198  	return colors.Gray(fmt.Sprintf("($%0.2f)", n))
   199  }
   200  
   201  // countEventsByStatus returns the number of events with the given state.
   202  func countEventsByStatus(events []*cloudformation.StackEvent, desired stack.Status) (n int) {
   203  	for _, e := range events {
   204  		status := stack.Status(*e.ResourceStatus)
   205  
   206  		if *e.ResourceType == "AWS::CloudFormation::Stack" {
   207  			continue
   208  		}
   209  
   210  		if status == desired {
   211  			n++
   212  		}
   213  	}
   214  
   215  	return
   216  }
   217  
   218  // countEventsComplete returns the number of completed or failed events.
   219  func countEventsComplete(events []*cloudformation.StackEvent) (n int) {
   220  	for _, e := range events {
   221  		status := stack.Status(*e.ResourceStatus)
   222  
   223  		if *e.ResourceType == "AWS::CloudFormation::Stack" {
   224  			continue
   225  		}
   226  
   227  		if status.IsDone() {
   228  			n++
   229  		}
   230  	}
   231  
   232  	return
   233  }
   234  
   235  // actionColor returns a color func by action.
   236  func actionColor(s string) colors.Func {
   237  	switch s {
   238  	case "Add":
   239  		return colors.Purple
   240  	case "Remove":
   241  		return colors.Red
   242  	case "Modify":
   243  		return colors.Blue
   244  	default:
   245  		return colors.Gray
   246  	}
   247  }