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 }