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  }