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  }