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 }