@@ -21,10 +21,13 @@ import (
2121 "github.com/aws/aws-sdk-go/aws/session"
2222 "github.com/influxdata/telegraf"
2323 "github.com/influxdata/telegraf/metric"
24+ "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/aws/cloudwatch/histograms"
2425 "github.com/stretchr/testify/assert"
2526 "github.com/stretchr/testify/mock"
2627 "github.com/stretchr/testify/require"
2728 "go.opentelemetry.io/collector/component"
29+ "go.opentelemetry.io/collector/pdata/pcommon"
30+ "go.opentelemetry.io/collector/pdata/pmetric"
2831 "go.uber.org/zap"
2932
3033 "github.com/aws/amazon-cloudwatch-agent/extension/agenthealth/handler/useragent"
@@ -778,3 +781,270 @@ func TestUserAgentFeatureFlags(t *testing.T) {
778781 })
779782 }
780783}
784+
785+ func TestBuildMetricDatumHist (t * testing.T ) {
786+ testCases := histograms .TestCases ()
787+
788+ for _ , tc := range testCases {
789+ t .Run (tc .Name , func (t * testing.T ) {
790+ // Create CloudWatch client
791+ svc := new (mockCloudWatchClient )
792+ cw := newCloudWatchClient (svc , time .Second )
793+ cw .config .MaxValuesPerDatum = 150
794+
795+ // Create histogram datapoint from test case
796+ dp := createHistogramDataPoint (tc .Input )
797+
798+ // Create aggregation datum
799+ metric := & aggregationDatum {
800+ MetricDatum : cloudwatch.MetricDatum {
801+ MetricName : aws .String ("test_histogram" ),
802+ Unit : aws .String ("ms" ),
803+ Timestamp : aws .Time (time .Now ()),
804+ StorageResolution : aws .Int64 (60 ),
805+ },
806+ histogram : & dp ,
807+ }
808+
809+ // Create dimensions list
810+ dimensions := []* cloudwatch.Dimension {
811+ {Name : aws .String ("service" ), Value : aws .String ("test" )},
812+ }
813+ dimensionsList := [][]* cloudwatch.Dimension {dimensions }
814+
815+ // Call buildMetricDatumHist
816+ datums := cw .buildMetricDatumHist (metric , dimensionsList )
817+
818+ // Verify results
819+ assert .NotEmpty (t , datums , "Should produce at least one datum" )
820+
821+ // Verify all datums have required fields
822+ totalCount := uint64 (0 )
823+ for i , datum := range datums {
824+ assert .NotNil (t , datum .MetricName )
825+ assert .Equal (t , "test_histogram" , * datum .MetricName )
826+ assert .NotNil (t , datum .Unit )
827+ assert .Equal (t , "ms" , * datum .Unit )
828+ assert .NotNil (t , datum .Timestamp )
829+ assert .NotNil (t , datum .StorageResolution )
830+ assert .Equal (t , int64 (60 ), * datum .StorageResolution )
831+ assert .NotNil (t , datum .Dimensions )
832+ assert .NotNil (t , datum .StatisticValues )
833+ assert .NotNil (t , datum .Values )
834+ assert .NotNil (t , datum .Counts )
835+
836+ // Verify values and counts have same length
837+ assert .Equal (t , len (datum .Values ), len (datum .Counts ))
838+ assert .LessOrEqual (t , len (datum .Values ), cw .config .MaxValuesPerDatum )
839+
840+ // Verify statistic values
841+ if i == 0 {
842+ // Sum is only assigned on the first datum
843+ assert .NotNil (t , datum .StatisticValues .Sum )
844+ assert .InDelta (t , tc .Expected .Sum , * datum .StatisticValues .Sum , 0.01 )
845+ } else {
846+ assert .NotNil (t , datum .StatisticValues .Sum )
847+ assert .Zero (t , * datum .StatisticValues .Sum )
848+ }
849+ // Count can vary based on splitting
850+ totalCount += uint64 (* datum .StatisticValues .SampleCount )
851+ if tc .Expected .Min != nil {
852+ assert .InDelta (t , * tc .Expected .Min , * datum .StatisticValues .Minimum , 0.01 )
853+ }
854+ if tc .Expected .Max != nil {
855+ assert .InDelta (t , * tc .Expected .Max , * datum .StatisticValues .Maximum , 0.01 )
856+ }
857+ }
858+ assert .Equal (t , tc .Expected .Count , totalCount )
859+ })
860+ }
861+ }
862+
863+ func TestBuildMetricDatumHistEmptyHistogram (t * testing.T ) {
864+ svc := new (mockCloudWatchClient )
865+ cw := newCloudWatchClient (svc , time .Second )
866+
867+ // Create empty histogram
868+ dp := pmetric .NewHistogramDataPoint ()
869+ dp .SetCount (0 )
870+ dp .SetSum (0 )
871+ dp .SetTimestamp (pcommon .NewTimestampFromTime (time .Now ()))
872+
873+ metric := & aggregationDatum {
874+ MetricDatum : cloudwatch.MetricDatum {
875+ MetricName : aws .String ("empty_histogram" ),
876+ },
877+ histogram : & dp ,
878+ }
879+
880+ dimensions := [][]* cloudwatch.Dimension {{}}
881+ datums := cw .buildMetricDatumHist (metric , dimensions )
882+
883+ assert .Empty (t , datums , "Empty histogram should produce no datums" )
884+ }
885+
886+ func TestBuildMetricDatumHistMaxValuesPerDatum (t * testing.T ) {
887+ svc := new (mockCloudWatchClient )
888+ cw := newCloudWatchClient (svc , time .Second )
889+ cw .config .MaxValuesPerDatum = 10 // Small limit to test splitting
890+
891+ // Create histogram with many buckets
892+ input := histograms.HistogramInput {
893+ Count : 220 , // 22 buckets * 10 items each
894+ Sum : 11000 ,
895+ Min : aws .Float64 (5.0 ),
896+ Max : aws .Float64 (225.0 ),
897+ Boundaries : make ([]float64 , 21 ),
898+ Counts : make ([]uint64 , 22 ),
899+ }
900+
901+ for i := 0 ; i < 21 ; i ++ {
902+ input .Boundaries [i ] = float64 (i + 1 ) * 10
903+ input .Counts [i ] = 10
904+ }
905+ input .Counts [21 ] = 10
906+
907+ dp := createHistogramDataPoint (input )
908+
909+ metric := & aggregationDatum {
910+ MetricDatum : cloudwatch.MetricDatum {
911+ MetricName : aws .String ("large_histogram" ),
912+ },
913+ histogram : & dp ,
914+ }
915+
916+ dimensions := [][]* cloudwatch.Dimension {{}}
917+ datums := cw .buildMetricDatumHist (metric , dimensions )
918+
919+ // Should split into multiple datums due to MaxValuesPerDatum limit
920+ assert .Greater (t , len (datums ), 1 , "Large histogram should be split into multiple datums" )
921+
922+ // Verify each datum respects the limit
923+ for _ , datum := range datums {
924+ assert .LessOrEqual (t , len (datum .Values ), cw .config .MaxValuesPerDatum )
925+ assert .LessOrEqual (t , len (datum .Counts ), cw .config .MaxValuesPerDatum )
926+ }
927+
928+ // Verify only first datum has sum
929+ assert .Greater (t , * datums [0 ].StatisticValues .Sum , 0.0 )
930+ for i := 1 ; i < len (datums ); i ++ {
931+ assert .Zero (t , * datums [i ].StatisticValues .Sum )
932+ }
933+ }
934+
935+ func TestBuildMetricDatumHistDropOriginalMetrics (t * testing.T ) {
936+ svc := new (mockCloudWatchClient )
937+ cw := newCloudWatchClient (svc , time .Second )
938+ cw .config .DropOriginalConfigs = map [string ]bool {
939+ "dropped_histogram" : true ,
940+ }
941+
942+ // Create test histogram
943+ input := histograms.HistogramInput {
944+ Count : 100 ,
945+ Sum : 5000 ,
946+ Min : aws .Float64 (10.0 ),
947+ Max : aws .Float64 (200.0 ),
948+ Boundaries : []float64 {25 , 50 , 75 , 100 , 150 },
949+ Counts : []uint64 {20 , 30 , 25 , 15 , 8 , 2 },
950+ }
951+
952+ dp := createHistogramDataPoint (input )
953+
954+ metric := & aggregationDatum {
955+ MetricDatum : cloudwatch.MetricDatum {
956+ MetricName : aws .String ("dropped_histogram" ),
957+ },
958+ histogram : & dp ,
959+ }
960+
961+ // First dimension set (index 0) should be dropped
962+ dimensions := [][]* cloudwatch.Dimension {
963+ {{Name : aws .String ("original" ), Value : aws .String ("true" )}},
964+ {{Name : aws .String ("rollup" ), Value : aws .String ("true" )}},
965+ }
966+
967+ datums := cw .buildMetricDatumHist (metric , dimensions )
968+
969+ // Should only have datums for rollup dimensions (not original)
970+ assert .Equal (t , 1 , len (datums ))
971+ assert .Equal (t , "true" , * datums [0 ].Dimensions [0 ].Value )
972+ }
973+
974+ func TestBuildMetricDatumHistMultipleDimensions (t * testing.T ) {
975+ svc := new (mockCloudWatchClient )
976+ cw := newCloudWatchClient (svc , time .Second )
977+
978+ // Create test histogram
979+ input := histograms.HistogramInput {
980+ Count : 50 ,
981+ Sum : 2500 ,
982+ Min : aws .Float64 (10.0 ),
983+ Max : aws .Float64 (100.0 ),
984+ Boundaries : []float64 {25 , 50 , 75 },
985+ Counts : []uint64 {10 , 15 , 15 , 10 },
986+ }
987+
988+ dp := createHistogramDataPoint (input )
989+
990+ metric := & aggregationDatum {
991+ MetricDatum : cloudwatch.MetricDatum {
992+ MetricName : aws .String ("multi_dim_histogram" ),
993+ },
994+ histogram : & dp ,
995+ }
996+
997+ // Multiple dimension sets
998+ dimensions := [][]* cloudwatch.Dimension {
999+ {{Name : aws .String ("service" ), Value : aws .String ("api" )}},
1000+ {{Name : aws .String ("service" ), Value : aws .String ("api" )}, {Name : aws .String ("env" ), Value : aws .String ("prod" )}},
1001+ {},
1002+ }
1003+
1004+ datums := cw .buildMetricDatumHist (metric , dimensions )
1005+
1006+ // Should have one datum per dimension set
1007+ assert .Equal (t , 3 , len (datums ))
1008+
1009+ // Verify dimensions
1010+ assert .Equal (t , 1 , len (datums [0 ].Dimensions ))
1011+ assert .Equal (t , 2 , len (datums [1 ].Dimensions ))
1012+ assert .Equal (t , 0 , len (datums [2 ].Dimensions ))
1013+ }
1014+
1015+ // Helper function to create histogram datapoint from test input
1016+ func createHistogramDataPoint (input histograms.HistogramInput ) pmetric.HistogramDataPoint {
1017+ dp := pmetric .NewHistogramDataPoint ()
1018+ dp .SetCount (input .Count )
1019+ dp .SetSum (input .Sum )
1020+ dp .SetTimestamp (pcommon .NewTimestampFromTime (time .Now ()))
1021+
1022+ if input .Min != nil {
1023+ dp .SetMin (* input .Min )
1024+ }
1025+ if input .Max != nil {
1026+ dp .SetMax (* input .Max )
1027+ }
1028+
1029+ // Set boundaries
1030+ bounds := dp .ExplicitBounds ()
1031+ bounds .EnsureCapacity (len (input .Boundaries ))
1032+ for _ , boundary := range input .Boundaries {
1033+ bounds .Append (boundary )
1034+ }
1035+
1036+ // Set bucket counts
1037+ bucketCounts := dp .BucketCounts ()
1038+ bucketCounts .EnsureCapacity (len (input .Counts ))
1039+ for _ , count := range input .Counts {
1040+ bucketCounts .Append (count )
1041+ }
1042+
1043+ // Set attributes
1044+ attrs := dp .Attributes ()
1045+ for k , v := range input .Attributes {
1046+ attrs .PutStr (k , v )
1047+ }
1048+
1049+ return dp
1050+ }
0 commit comments