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