plugins/outputs/cloudwatch/util.go (149 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT
package cloudwatch
import (
"fmt"
"log"
"math/rand"
"sort"
"strings"
"time"
"github.com/aws/amazon-cloudwatch-agent/metric/distribution"
"github.com/aws/amazon-cloudwatch-agent/metric/distribution/regular"
"github.com/aws/amazon-cloudwatch-agent/metric/distribution/seh1"
"github.com/aws/amazon-cloudwatch-agent/sdk/service/cloudwatch"
)
const (
maxValuesPerDatum = 5000
// Constant for estimate encoded metric size
// Action=PutMetricData
pmdActionSize = 20
// &Version=2010-08-01
versionSize = 19
// &MetricData.member.100.StatisticValues.Maximum=1558.3086995967291&MetricData.member.100.StatisticValues.Minimum=1558.3086995967291&MetricData.member.100.StatisticValues.SampleCount=1000&MetricData.member.100.StatisticValues.Sum=1558.3086995967291
statisticsSize = 246
// &MetricData.member.100.Timestamp=2018-05-29T21%3A14%3A00Z
timestampSize = 57
overallConstPerRequestSize = pmdActionSize + versionSize
// &Namespace=, this is per request
namespaceOverheads = 11
// &MetricData.member.100.Dimensions.member.1.Name= &MetricData.member.100.Dimensions.member.1.Value=
dimensionOverheads = 48 + 49
// &MetricData.member.100.MetricName=
metricNameOverheads = 34
// &MetricData.member.100.StorageResolution=1
highResolutionOverheads = 42
// &MetricData.member.100.Values.member.100=1558.3086995967291 &MetricData.member.100.Counts.member.100=1000
valuesCountsOverheads = 59 + 45
// &MetricData.member.100.Value=1558.3086995967291
valueOverheads = 47
// &MetricData.member.1.Unit=Kilobytes/Second
unitOverheads = 42
/* Entity overheads - these would be used to calculate entity size if we decide to include it as a part of the payload.
The three main components are the KeyAttributes key/value pair, Attributes key/value pair, and StrictEntityValidation
// &StrictEntityValidation=false
strictEntityValidationSize = 29
// &EntityMetricData.member.100.Entity.KeyAttributes.entry.1.key= &EntityMetricData.member.100.Entity.KeyAttributes.entry.1.value=
entityKeyAttributesOverhead = 62 + 64
// &EntityMetricData.member.100.Entity.Attributes.entry.1.key= &EntityMetricData.member.100.Entity.Attributes.entry.1.value=
entityAttributesOverhead = 59 + 61
// EntityMetricData.member.100.
entityMetricDataPrefixOverhead = 28
*/
)
// Set seed once.
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
// publishJitter returns a random duration between 0 and the given publishInterval.
func publishJitter(publishInterval time.Duration) time.Duration {
jitter := seededRand.Int63n(int64(publishInterval))
return time.Duration(jitter)
}
func setNewDistributionFunc(maxValuesPerDatumLimit int) {
if maxValuesPerDatumLimit >= maxValuesPerDatum {
distribution.NewDistribution = seh1.NewSEH1Distribution
} else {
distribution.NewDistribution = regular.NewRegularDistribution
}
}
func resize(dist distribution.Distribution, listMaxSize int) (distList []distribution.Distribution) {
var ok bool
// If this is SEH1 distribution, it has already considered the list max size.
if _, ok = dist.(*seh1.SEH1Distribution); ok {
distList = append(distList, dist)
return
}
var regularDist *regular.RegularDistribution
if regularDist, ok = dist.(*regular.RegularDistribution); !ok {
log.Printf("E! The distribution type %T is not supported for resizing.", dist)
return
}
values, _ := regularDist.ValuesAndCounts()
sort.Float64s(values)
newSEH1Dist := seh1.NewSEH1Distribution().(*seh1.SEH1Distribution)
for i := 0; i < len(values); i++ {
if !newSEH1Dist.CanAdd(values[i], listMaxSize) {
distList = append(distList, newSEH1Dist)
newSEH1Dist = seh1.NewSEH1Distribution().(*seh1.SEH1Distribution)
}
newSEH1Dist.AddEntry(values[i], regularDist.GetCount(values[i]))
}
if newSEH1Dist.Size() > 0 {
distList = append(distList, newSEH1Dist)
}
return
}
func payload(datum *cloudwatch.MetricDatum) (size int) {
size += timestampSize
for _, dimension := range datum.Dimensions {
size += len(*dimension.Name) + len(*dimension.Value) + dimensionOverheads
}
if datum.MetricName != nil {
// The metric name won't be nil, but it should fail in the validation instead of panic here.
size += len(*datum.MetricName) + metricNameOverheads
}
if datum.StorageResolution != nil {
size += highResolutionOverheads
}
valuesCountsLen := len(datum.Values)
if valuesCountsLen != 0 {
size += valuesCountsLen*valuesCountsOverheads + statisticsSize
} else {
size += valueOverheads
}
if datum.Unit != nil {
size += unitOverheads
}
return
}
func entityToString(entity cloudwatch.Entity) string {
var attributes, keyAttributes, data string
if entity.Attributes != nil {
attributes = entityAttributesToString(entity.Attributes)
}
if entity.KeyAttributes != nil {
keyAttributes = entityAttributesToString(entity.KeyAttributes)
}
if attributes != "" || keyAttributes != "" {
data = fmt.Sprintf(
"%s|%s",
attributes,
keyAttributes,
)
}
return data
}
// Helper function to convert a map of entityAttributes to a consistent string representation
func entityAttributesToString(m map[string]*string) string {
if m == nil {
return ""
}
pairs := make([]string, 0, len(m))
for k, v := range m {
if v == nil {
pairs = append(pairs, k+":")
} else {
pairs = append(pairs, k+":"+*v)
}
}
sort.Strings(pairs) // Ensure a consistent order
return strings.Join(pairs, ";")
}
func stringToEntity(data string) cloudwatch.Entity {
parts := strings.Split(data, "|")
if len(parts) < 2 {
// Handle error: invalid input string
return cloudwatch.Entity{}
}
entity := cloudwatch.Entity{
Attributes: make(map[string]*string),
KeyAttributes: make(map[string]*string),
}
if parts[0] != "" {
entity.Attributes = stringToEntityAttributes(parts[0])
}
if parts[1] != "" {
entity.KeyAttributes = stringToEntityAttributes(parts[1])
}
return entity
}
func stringToEntityAttributes(s string) map[string]*string {
result := make(map[string]*string)
pairs := strings.Split(s, ";")
for _, pair := range pairs {
kv := strings.SplitN(pair, ":", 2)
if len(kv) == 2 {
value := kv[1]
result[kv[0]] = &value
}
}
return result
}