github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/replyfmt/results.go (about) 1 // Copyright (c) 2020-2022, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package replyfmt 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "io" 12 "sort" 13 "strconv" 14 "strings" 15 "text/tabwriter" 16 "time" 17 18 "github.com/fatih/color" 19 "github.com/tidwall/gjson" 20 "github.com/tidwall/pretty" 21 "golang.org/x/text/cases" 22 "golang.org/x/text/language" 23 24 "github.com/choria-io/go-choria/internal/util" 25 "github.com/choria-io/go-choria/providers/agent/mcorpc" 26 rpc "github.com/choria-io/go-choria/providers/agent/mcorpc/client" 27 "github.com/choria-io/go-choria/providers/agent/mcorpc/ddl/common" 28 ) 29 30 type RPCStats struct { 31 RequestID string `json:"requestid"` 32 NoResponses []string `json:"no_responses"` 33 UnexpectedResponses []string `json:"unexpected_responses"` 34 DiscoveredCount int `json:"discovered"` 35 FailCount int `json:"failed"` 36 OKCount int `json:"ok"` 37 ResponseCount int `json:"responses"` 38 PublishTime time.Duration `json:"publish_time"` 39 RequestTime time.Duration `json:"request_time"` 40 DiscoverTime time.Duration `json:"discover_time"` 41 StartTime time.Time `json:"start_time_utc"` 42 } 43 44 type RPCReply struct { 45 Sender string `json:"sender"` 46 *rpc.RPCReply 47 } 48 49 type RPCResults struct { 50 Agent string `json:"agent"` 51 Action string `json:"action"` 52 Replies []*RPCReply `json:"replies"` 53 Stats *rpc.Stats `json:"-"` 54 ParsedStats *RPCStats `json:"request_stats"` 55 Summaries json.RawMessage `json:"summaries"` 56 } 57 58 type ActionDDL interface { 59 SetOutputDefaults(results map[string]any) 60 AggregateResult(result map[string]any) error 61 AggregateResultJSON(jres []byte) error 62 AggregateSummaryJSON() ([]byte, error) 63 GetOutput(string) (*common.OutputItem, bool) 64 AggregateSummaryFormattedStrings() (map[string][]string, error) 65 DisplayMode() string 66 OutputNames() []string 67 } 68 69 type Logger interface { 70 Debugf(format string, args ...any) 71 Infof(format string, args ...any) 72 Warnf(format string, args ...any) 73 Errorf(format string, args ...any) 74 Fatalf(format string, args ...any) 75 Panicf(format string, args ...any) 76 } 77 78 type flusher interface { 79 Flush() 80 } 81 82 func (r *RPCResults) RenderTXTFooter(w io.Writer, verbose bool) { 83 stats := statsFromClient(r.Stats) 84 85 if verbose { 86 fmt.Fprintln(w, color.YellowString("---- request stats ----")) 87 fmt.Fprintf(w, " Nodes: %d / %d\n", stats.ResponseCount, stats.DiscoveredCount) 88 fmt.Fprintf(w, " Pass / Fail: %d / %d\n", stats.OKCount, stats.FailCount) 89 fmt.Fprintf(w, " No Responses: %d\n", len(stats.NoResponses)) 90 fmt.Fprintf(w, "Unexpected Responses: %d\n", len(stats.UnexpectedResponses)) 91 fmt.Fprintf(w, " Start Time: %s\n", stats.StartTime.Format("2006-01-02T15:04:05-0700")) 92 fmt.Fprintf(w, " Discovery Time: %v\n", stats.DiscoverTime) 93 fmt.Fprintf(w, " Publish Time: %v\n", stats.PublishTime) 94 fmt.Fprintf(w, " Agent Time: %v\n", stats.RequestTime-stats.PublishTime) 95 fmt.Fprintf(w, " Total Time: %v\n", stats.RequestTime+stats.DiscoverTime) 96 } else { 97 var rcnt, dcnt string 98 99 switch { 100 case stats.ResponseCount == 0: 101 dcnt = color.RedString(strconv.Itoa(stats.DiscoveredCount)) 102 rcnt = color.RedString(strconv.Itoa(stats.ResponseCount)) 103 case stats.ResponseCount != stats.DiscoveredCount: 104 dcnt = color.YellowString(strconv.Itoa(stats.DiscoveredCount)) 105 rcnt = color.YellowString(strconv.Itoa(stats.ResponseCount)) 106 default: 107 dcnt = color.GreenString(strconv.Itoa(stats.DiscoveredCount)) 108 rcnt = color.GreenString(strconv.Itoa(stats.ResponseCount)) 109 } 110 111 fmt.Fprintf(w, "Finished processing %s / %s hosts in %s\n", rcnt, dcnt, (stats.RequestTime + stats.DiscoverTime).Round(time.Millisecond)) 112 } 113 114 nodeListPrinter := func(nodes []string, message string) { 115 if len(nodes) > 0 { 116 sort.Strings(nodes) 117 118 if !verbose && len(nodes) > 200 { 119 fmt.Fprintf(w, "\n%s (showing first 200): %d\n\n", message, len(nodes)) 120 nodes = nodes[0:200] 121 } else { 122 fmt.Fprintf(w, "\n%s: %d\n\n", message, len(nodes)) 123 } 124 125 out := bytes.NewBuffer([]byte{}) 126 127 wr := new(tabwriter.Writer) 128 wr.Init(out, 0, 0, 4, ' ', 0) 129 util.SliceGroups(nodes, 3, func(g []string) { 130 fmt.Fprintf(wr, " %s\t\n", strings.Join(g, "\t")) 131 }) 132 wr.Flush() 133 134 fmt.Fprint(w, out.String()) 135 } 136 } 137 138 nodeListPrinter(stats.NoResponses, "No Responses from") 139 nodeListPrinter(stats.UnexpectedResponses, "Unexpected Responses from") 140 } 141 142 func (r *RPCResults) RenderTXT(w io.Writer, action ActionDDL, verbose bool, silent bool, display DisplayMode, colorize bool, log Logger) (err error) { 143 fmtopts := []Option{ 144 Display(display), 145 } 146 147 if verbose { 148 fmtopts = append(fmtopts, Verbose()) 149 } 150 151 if silent { 152 fmtopts = append(fmtopts, Silent()) 153 } 154 155 if !colorize { 156 fmtopts = append(fmtopts, ConsoleNoColor()) 157 } 158 159 for _, reply := range r.Replies { 160 err := FormatReply(w, ConsoleFormat, action, reply.Sender, reply.RPCReply, fmtopts...) 161 if err != nil { 162 fmt.Fprintf(w, "Could not render reply from %s: %v", reply.Sender, err) 163 } 164 165 err = action.AggregateResultJSON(reply.Data) 166 if err != nil { 167 log.Warnf("could not aggregate data in reply: %v", err) 168 } 169 } 170 171 if silent { 172 return nil 173 } 174 175 FormatAggregates(w, ConsoleFormat, action, fmtopts...) 176 177 fmt.Fprintln(w) 178 179 r.RenderTXTFooter(w, verbose) 180 181 f, ok := w.(flusher) 182 if ok { 183 f.Flush() 184 } 185 186 return nil 187 } 188 189 // RenderNames renders a list of names of successful senders 190 // TODO: should become a reply format formatter maybe 191 func (r *RPCResults) RenderNames(w io.Writer, jsonFormat bool, sortNames bool) error { 192 var names []string 193 194 for _, reply := range r.Replies { 195 if reply.Statuscode == mcorpc.OK { 196 names = append(names, reply.Sender) 197 } 198 } 199 200 if sortNames { 201 sort.Strings(names) 202 } 203 204 if jsonFormat { 205 j, err := json.MarshalIndent(names, "", " ") 206 if err != nil { 207 return err 208 } 209 210 fmt.Fprintln(w, string(j)) 211 212 return nil 213 } 214 215 for _, name := range names { 216 fmt.Fprintln(w, name) 217 } 218 219 return nil 220 } 221 222 // RenderTable renders a table of outputs 223 // TODO: should become a reply format formatter, but those lack a prepare phase to print headers etc 224 func (r *RPCResults) RenderTable(w io.Writer, action ActionDDL) (err error) { 225 var ( 226 headers = []any{"Sender"} 227 outputs = action.OutputNames() 228 ) 229 230 for _, o := range outputs { 231 output, ok := action.GetOutput(o) 232 if ok { 233 headers = append(headers, output.DisplayAs) 234 } else { 235 headers = append(headers, cases.Title(language.AmericanEnglish).String(o)) 236 } 237 } 238 239 table := util.NewUTF8Table(headers...) 240 241 for _, reply := range r.Replies { 242 if reply.Statuscode != mcorpc.OK { 243 continue 244 } 245 246 parsedResult := gjson.ParseBytes(reply.RPCReply.Data) 247 if parsedResult.Exists() { 248 row := []any{reply.Sender} 249 for _, o := range outputs { 250 val := parsedResult.Get(o) 251 switch { 252 case val.IsArray(), val.IsObject(): 253 row = append(row, string(pretty.PrettyOptions([]byte(val.String()), &pretty.Options{ 254 SortKeys: true, 255 }))) 256 default: 257 row = append(row, val.String()) 258 } 259 } 260 table.AddRow(row...) 261 } 262 } 263 264 fmt.Fprintln(w, table.Render()) 265 266 return nil 267 } 268 269 func (r *RPCResults) CalculateAggregates(action ActionDDL) error { 270 for _, reply := range r.Replies { 271 parsed, ok := gjson.ParseBytes(reply.RPCReply.Data).Value().(map[string]any) 272 if ok { 273 action.SetOutputDefaults(parsed) 274 action.AggregateResult(parsed) 275 } 276 } 277 278 // silently failing as this is optional 279 r.Summaries, _ = action.AggregateSummaryJSON() 280 r.ParsedStats = statsFromClient(r.Stats) 281 282 return nil 283 } 284 285 func (r *RPCResults) RenderJSON(w io.Writer, action ActionDDL) (err error) { 286 r.CalculateAggregates(action) 287 288 j, err := json.MarshalIndent(r, "", " ") 289 if err != nil { 290 return fmt.Errorf("could not prepare display: %s", err) 291 } 292 293 _, err = fmt.Fprintln(w, string(j)) 294 295 return err 296 } 297 298 func statsFromClient(cs *rpc.Stats) *RPCStats { 299 s := &RPCStats{} 300 301 s.RequestID = cs.RequestID 302 s.NoResponses = cs.NoResponseFrom() 303 s.UnexpectedResponses = cs.UnexpectedResponseFrom() 304 s.DiscoveredCount = cs.DiscoveredCount() 305 s.FailCount = cs.FailCount() 306 s.OKCount = cs.OKCount() 307 s.ResponseCount = cs.ResponsesCount() 308 s.StartTime = cs.Started().UTC() 309 310 d, err := cs.DiscoveryDuration() 311 if err == nil { 312 s.DiscoverTime = d 313 } 314 315 d, err = cs.PublishDuration() 316 if err == nil { 317 s.PublishTime = d 318 } 319 320 d, err = cs.RequestDuration() 321 if err == nil { 322 s.RequestTime = d 323 } 324 325 return s 326 }