github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/aggregate/summary.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 aggregate 6 7 import ( 8 "encoding/json" 9 "fmt" 10 "sort" 11 "sync" 12 ) 13 14 // SummaryAggregator keeps track of seen values and summarize how many times each were seen 15 type SummaryAggregator struct { 16 items map[any]int 17 args []any 18 format string 19 20 jsonTrue string 21 jsonFalse string 22 23 mapping map[string]string 24 25 sync.Mutex 26 } 27 28 // NewSummaryAggregator creates a new SummaryAggregator with the specific options supplied 29 func NewSummaryAggregator(args []any) (*SummaryAggregator, error) { 30 agg := &SummaryAggregator{ 31 items: make(map[any]int), 32 args: args, 33 format: parseFormatFromArgs(args), 34 mapping: make(map[string]string), 35 } 36 37 s, _ := json.Marshal(true) 38 agg.jsonTrue = string(s) 39 s, _ = json.Marshal(false) 40 agg.jsonFalse = string(s) 41 42 err := agg.parseBoolMapsFromArgs() 43 if err != nil { 44 return nil, err 45 } 46 47 return agg, nil 48 } 49 50 // Type is the type of Aggregator 51 func (s *SummaryAggregator) Type() string { 52 return "summary" 53 } 54 55 // ProcessValue processes and tracks a specified value 56 func (s *SummaryAggregator) ProcessValue(v any) error { 57 s.Lock() 58 defer s.Unlock() 59 60 item := v 61 62 switch val := v.(type) { 63 case bool: 64 var tm string 65 var ok bool 66 67 if val { 68 tm, ok = s.mapping[s.jsonTrue] 69 } else { 70 tm, ok = s.mapping[s.jsonFalse] 71 } 72 if ok { 73 item = tm 74 } 75 76 case string: 77 tm, ok := s.mapping[val] 78 if ok { 79 item = tm 80 } 81 default: 82 // we'll almost never get to this cpu intensive default as 83 // the ddl always send string values in reality but I want to support 84 // different types in the plugins for future uses 85 vs, err := json.Marshal(v) 86 if err == nil { 87 tm, ok := s.mapping[string(vs)] 88 if ok { 89 item = tm 90 } 91 } 92 } 93 94 _, ok := s.items[item] 95 if !ok { 96 s.items[item] = 0 97 } 98 99 s.items[item]++ 100 101 return nil 102 } 103 104 // ResultStrings returns a map of results in string format 105 func (s *SummaryAggregator) ResultStrings() (map[string]string, error) { 106 s.Lock() 107 defer s.Unlock() 108 109 result := map[string]string{} 110 111 if len(s.items) == 0 { 112 return result, nil 113 } 114 115 for k, v := range s.items { 116 result[fmt.Sprintf("%v", k)] = fmt.Sprintf("%d", v) 117 } 118 119 return result, nil 120 } 121 122 // ResultJSON return the results in JSON format preserving types 123 func (s *SummaryAggregator) ResultJSON() ([]byte, error) { 124 s.Lock() 125 defer s.Unlock() 126 127 result := map[string]int{} 128 for k, v := range s.items { 129 result[fmt.Sprintf("%v", k)] = v 130 } 131 132 return json.Marshal(result) 133 } 134 135 // ResultFormattedStrings return the results in a formatted way, if no format is given a calculated value is used 136 func (s *SummaryAggregator) ResultFormattedStrings(format string) ([]string, error) { 137 s.Lock() 138 defer s.Unlock() 139 140 output := []string{} 141 142 if len(s.items) == 0 { 143 return output, nil 144 } 145 146 type kv struct { 147 Key string 148 Val int 149 } 150 151 var sortable []kv 152 for k, v := range s.items { 153 sortable = append(sortable, kv{fmt.Sprintf("%v", k), v}) 154 } 155 156 max := 0 157 for _, k := range sortable { 158 l := len(k.Key) 159 if l > max { 160 max = l 161 } 162 } 163 164 if format == "" { 165 if s.format != "" { 166 format = s.format 167 } else { 168 format = fmt.Sprintf("%%%ds: %%d", max) 169 } 170 } 171 172 sort.Slice(sortable, func(i int, j int) bool { 173 return sortable[i].Val > sortable[j].Val 174 }) 175 176 for _, k := range sortable { 177 output = append(output, fmt.Sprintf(format, k.Key, k.Val)) 178 } 179 180 return output, nil 181 } 182 183 func (a *SummaryAggregator) parseBoolMapsFromArgs() error { 184 if len(a.args) == 2 { 185 cfg, ok := a.args[1].(map[string]any) 186 if !ok { 187 return nil 188 } 189 190 for k, v := range cfg { 191 switch k { 192 case "true": 193 a.mapping[a.jsonTrue] = fmt.Sprintf("%v", v) 194 case "false": 195 a.mapping[a.jsonFalse] = fmt.Sprintf("%v", v) 196 case "format": 197 // nothing its reserved 198 default: 199 a.mapping[k] = fmt.Sprintf("%v", v) 200 } 201 } 202 } 203 204 return nil 205 }