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 }