diff --git a/plugins/outputs/cloudwatch/cloudwatch.go b/plugins/outputs/cloudwatch/cloudwatch.go index 92cc6e5a27..af93a14202 100644 --- a/plugins/outputs/cloudwatch/cloudwatch.go +++ b/plugins/outputs/cloudwatch/cloudwatch.go @@ -47,9 +47,8 @@ const ( ) const ( - opPutLogEvents = "PutLogEvents" - opPutMetricData = "PutMetricData" - dropOriginalWildcard = "*" + opPutLogEvents = "PutLogEvents" + opPutMetricData = "PutMetricData" ) type CloudWatch struct { @@ -392,9 +391,12 @@ func (c *CloudWatch) BuildMetricDatum(metric *aggregationDatum) []*cloudwatch.Me } dimensionsList := c.ProcessRollup(metric.Dimensions) - for _, dimensions := range dimensionsList { + for index, dimensions := range dimensionsList { //index == 0 means it's the original metrics, and if the metric name and dimension matches, skip creating //metric datum + if index == 0 && c.IsDropping(*metric.MetricDatum.MetricName) { + continue + } if len(distList) == 0 { // Not a distribution. datum := &cloudwatch.MetricDatum{ @@ -434,6 +436,17 @@ func (c *CloudWatch) BuildMetricDatum(metric *aggregationDatum) []*cloudwatch.Me return datums } +func (c *CloudWatch) IsDropping(metricName string) bool { + // Check if any metrics are provided in drop_original_metrics + if len(c.config.DropOriginalConfigs) == 0 { + return false + } + if _, ok := c.config.DropOriginalConfigs[metricName]; ok { + return true + } + return false +} + // sortedTagKeys returns a sorted list of keys in the map. // Necessary for comparing a metric-name and its dimensions to determine // if 2 metrics are actually the same. diff --git a/plugins/outputs/cloudwatch/cloudwatch_test.go b/plugins/outputs/cloudwatch/cloudwatch_test.go index 8a395be198..fcecd85286 100644 --- a/plugins/outputs/cloudwatch/cloudwatch_test.go +++ b/plugins/outputs/cloudwatch/cloudwatch_test.go @@ -19,6 +19,7 @@ import ( "github.com/influxdata/telegraf/metric" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/aws/private-amazon-cloudwatch-agent-staging/handlers/agentinfo" "github.com/aws/private-amazon-cloudwatch-agent-staging/internal/publisher" @@ -255,6 +256,66 @@ func TestGetUniqueRollupList(t *testing.T) { assert.EqualValues(t, expectedLists, actualLists, "Unique list result should be empty") } +func TestIsDropping(t *testing.T) { + svc := new(mockCloudWatchClient) + cw := newCloudWatchClient(svc, time.Second) + + testCases := map[string]struct { + dropMetricsConfig map[string]bool + expectMetricsDropped map[string]bool + }{ + "TestIsDroppingWithMultipleCategoryLinux": { + dropMetricsConfig: map[string]bool{ + "cpu_usage_idle": true, + "cpu_time_active": true, + "nvidia_smi_utilization_gpu": true, + }, + expectMetricsDropped: map[string]bool{ + "cpu_usage_idle": true, + "cpu_time_active": true, + "nvidia_smi": false, + "cpu_usage_guest": false, + }, + }, + "TestIsDroppingWithMultipleCategoryWindows": { + dropMetricsConfig: map[string]bool{ + "cpu usage_idle": true, + "cpu time_active": true, + "nvidia_smi utilization_gpu": true, + }, + expectMetricsDropped: map[string]bool{ + "cpu usage_idle": true, + "cpu time_active": true, + "nvidia_smi": false, + "cpu usage_guest": false, + }, + }, + "TestIsDroppingWithMetricDecoration": { + dropMetricsConfig: map[string]bool{ + "CPU_USAGE_IDLE": true, + "cpu_time_active": true, + "nvidia_smi_utilization_gpu": true, + }, + expectMetricsDropped: map[string]bool{ + "cpu_usage_idle": false, + "CPU_USAGE_IDLE": true, + "nvidia_smi": false, + "nvidia_smi_utilization_gpu": true, + "cpu": false, + }, + }, + } + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + cw.config.DropOriginalConfigs = testCase.dropMetricsConfig + for metricName, expectMetricDropped := range testCase.expectMetricsDropped { + actualMetricDropped := cw.IsDropping(metricName) + require.Equal(t, expectMetricDropped, actualMetricDropped) + } + }) + } +} + func TestIsFlushable(t *testing.T) { svc := new(mockCloudWatchClient) res := cloudwatch.PutMetricDataOutput{} diff --git a/plugins/outputs/cloudwatch/config.go b/plugins/outputs/cloudwatch/config.go index 466d50e704..0790b54216 100644 --- a/plugins/outputs/cloudwatch/config.go +++ b/plugins/outputs/cloudwatch/config.go @@ -13,20 +13,20 @@ import ( // Config represent a configuration for the CloudWatch logs exporter. type Config struct { - Region string `mapstructure:"region"` - EndpointOverride string `mapstructure:"endpoint_override,omitempty"` - AccessKey string `mapstructure:"access_key,omitempty"` - SecretKey string `mapstructure:"secret_key,omitempty"` - RoleARN string `mapstructure:"role_arn,omitempty"` - Profile string `mapstructure:"profile,omitempty"` - SharedCredentialFilename string `mapstructure:"shared_credential_file,omitempty"` - Token string `mapstructure:"token,omitempty"` - ForceFlushInterval time.Duration `mapstructure:"force_flush_interval"` - MaxDatumsPerCall int `mapstructure:"max_datums_per_call"` - MaxValuesPerDatum int `mapstructure:"max_values_per_datum"` - RollupDimensions [][]string `mapstructure:"rollup_dimensions,omitempty"` - DropOriginConfigs map[string][]string `mapstructure:"drop_original_metrics,omitempty"` - Namespace string `mapstructure:"namespace"` + Region string `mapstructure:"region"` + EndpointOverride string `mapstructure:"endpoint_override,omitempty"` + AccessKey string `mapstructure:"access_key,omitempty"` + SecretKey string `mapstructure:"secret_key,omitempty"` + RoleARN string `mapstructure:"role_arn,omitempty"` + Profile string `mapstructure:"profile,omitempty"` + SharedCredentialFilename string `mapstructure:"shared_credential_file,omitempty"` + Token string `mapstructure:"token,omitempty"` + ForceFlushInterval time.Duration `mapstructure:"force_flush_interval"` + MaxDatumsPerCall int `mapstructure:"max_datums_per_call"` + MaxValuesPerDatum int `mapstructure:"max_values_per_datum"` + RollupDimensions [][]string `mapstructure:"rollup_dimensions,omitempty"` + DropOriginalConfigs map[string]bool `mapstructure:"drop_original_metrics,omitempty"` + Namespace string `mapstructure:"namespace"` // ResourceToTelemetrySettings is the option for converting resource // attributes to telemetry attributes. diff --git a/plugins/outputs/cloudwatch/config_test.go b/plugins/outputs/cloudwatch/config_test.go index 828d39bca2..dedf2c1c32 100644 --- a/plugins/outputs/cloudwatch/config_test.go +++ b/plugins/outputs/cloudwatch/config_test.go @@ -104,12 +104,10 @@ func TestConfigDropOriginConfigs(t *testing.T) { assert.Equal(t, 1, len(c.Exporters)) c2, ok := c.Exporters[component.NewID(TypeStr)].(*Config) assert.True(t, ok) - drop := c2.DropOriginConfigs + drop := c2.DropOriginalConfigs assert.NotEmpty(t, drop) - assert.Len(t, drop, 2) - assert.NotNil(t, drop["cpu"]) - assert.Equal(t, []string{"time", "usage"}, drop["cpu"]) - assert.NotNil(t, drop["foo"]) - assert.Nil(t, drop["bar"]) - assert.Equal(t, []string{"bar"}, drop["foo"]) + assert.Len(t, drop, 3) + assert.True(t, drop["cpu_time"]) + assert.True(t, drop["cpu_usage"]) + assert.True(t, drop["foo_bar"]) } diff --git a/plugins/outputs/cloudwatch/testdata/drop_original.yaml b/plugins/outputs/cloudwatch/testdata/drop_original.yaml index 1a397dc5e7..764d008557 100644 --- a/plugins/outputs/cloudwatch/testdata/drop_original.yaml +++ b/plugins/outputs/cloudwatch/testdata/drop_original.yaml @@ -5,11 +5,9 @@ exporters: awscloudwatch: region: us-yeast-99 drop_original_metrics: - cpu: - - time - - usage - foo: - - bar + cpu_time: true + cpu_usage: true + foo_bar: true service: pipelines: diff --git a/translator/tocwconfig/sampleConfig/complete_linux_config.yaml b/translator/tocwconfig/sampleConfig/complete_linux_config.yaml index 57ad1d1cb7..30c840cae4 100644 --- a/translator/tocwconfig/sampleConfig/complete_linux_config.yaml +++ b/translator/tocwconfig/sampleConfig/complete_linux_config.yaml @@ -2,9 +2,8 @@ connectors: {} exporters: awscloudwatch: drop_original_metrics: - cpu: - - cpu_usage_idle - - time_active + CPU_USAGE_IDLE: true + cpu_time_active: true endpoint_override: https://monitoring-fips.us-west-2.amazonaws.com force_flush_interval: 1m0s max_datums_per_call: 1000 diff --git a/translator/tocwconfig/sampleConfig/drop_origin_linux.yaml b/translator/tocwconfig/sampleConfig/drop_origin_linux.yaml index 665bd05913..641056436c 100644 --- a/translator/tocwconfig/sampleConfig/drop_origin_linux.yaml +++ b/translator/tocwconfig/sampleConfig/drop_origin_linux.yaml @@ -2,12 +2,10 @@ connectors: {} exporters: awscloudwatch: drop_original_metrics: - cpu: - - cpu_usage_idle - - time_active - nvidia_smi: - - utilization_gpu - - temperature_gpu + CPU_USAGE_IDLE: true + cpu_time_active: true + nvidia_smi_utilization_gpu: true + nvidia_smi_temperature_gpu: true force_flush_interval: 1m0s max_datums_per_call: 1000 max_values_per_datum: 150 diff --git a/translator/tocwconfig/tocwconfig_test.go b/translator/tocwconfig/tocwconfig_test.go index 4e4e00db48..b6fc86beb3 100644 --- a/translator/tocwconfig/tocwconfig_test.go +++ b/translator/tocwconfig/tocwconfig_test.go @@ -160,12 +160,6 @@ func TestAdvancedConfig(t *testing.T) { checkTranslation(t, "advanced_config_windows", "windows", expectedEnvVars, "") } -func TestDropOriginConfig(t *testing.T) { - resetContext(t) - expectedEnvVars := map[string]string{} - checkTranslation(t, "drop_origin_linux", "linux", expectedEnvVars, "") -} - func TestLogOnlyConfig(t *testing.T) { resetContext(t) expectedEnvVars := map[string]string{} diff --git a/translator/tocwconfig/tocwconfig_unix_test.go b/translator/tocwconfig/tocwconfig_unix_test.go index 3ab008a24e..3adc1fa120 100644 --- a/translator/tocwconfig/tocwconfig_unix_test.go +++ b/translator/tocwconfig/tocwconfig_unix_test.go @@ -21,3 +21,9 @@ func TestCompleteConfigUnix(t *testing.T) { checkTranslation(t, "complete_linux_config", "linux", expectedEnvVars, "") checkTranslation(t, "complete_darwin_config", "darwin", nil, "") } + +func TestDropOriginConfig(t *testing.T) { + resetContext(t) + expectedEnvVars := map[string]string{} + checkTranslation(t, "drop_origin_linux", "linux", expectedEnvVars, "") +} diff --git a/translator/translate/otel/common/common.go b/translator/translate/otel/common/common.go index 845ccb7f4c..fca64b6d1b 100644 --- a/translator/translate/otel/common/common.go +++ b/translator/translate/otel/common/common.go @@ -38,6 +38,8 @@ const ( CredentialsKey = "credentials" RoleARNKey = "role_arn" MetricsCollectionIntervalKey = "metrics_collection_interval" + MeasurementKey = "measurement" + DropOriginalMetricsKey = "drop_original_metrics" ForceFlushIntervalKey = "force_flush_interval" ContainerInsightsMetricGranularity = "metric_granularity" PreferFullPodName = "prefer_full_pod_name" diff --git a/translator/translate/otel/exporter/awscloudwatch/translator.go b/translator/translate/otel/exporter/awscloudwatch/translator.go index 54ea4e5cda..10154a10be 100644 --- a/translator/translate/otel/exporter/awscloudwatch/translator.go +++ b/translator/translate/otel/exporter/awscloudwatch/translator.go @@ -4,13 +4,16 @@ package awscloudwatch import ( + "strings" + "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/confmap" "go.opentelemetry.io/collector/exporter" + "github.com/aws/private-amazon-cloudwatch-agent-staging/internal/metric" "github.com/aws/private-amazon-cloudwatch-agent-staging/plugins/outputs/cloudwatch" "github.com/aws/private-amazon-cloudwatch-agent-staging/translator/translate/agent" - "github.com/aws/private-amazon-cloudwatch-agent-staging/translator/translate/metrics/drop_origin" + "github.com/aws/private-amazon-cloudwatch-agent-staging/translator/translate/metrics/config" "github.com/aws/private-amazon-cloudwatch-agent-staging/translator/translate/metrics/rollup_dimensions" "github.com/aws/private-amazon-cloudwatch-agent-staging/translator/translate/otel/common" ) @@ -18,6 +21,7 @@ import ( const ( namespaceKey = "namespace" forceFlushIntervalKey = "force_flush_interval" + dropOriginalWildcard = "*" internalMaxValuesPerDatum = 5000 ) @@ -65,8 +69,12 @@ func (t *translator) Translate(conf *confmap.Conf) (component.Config, error) { if agent.Global_Config.Internal { cfg.MaxValuesPerDatum = internalMaxValuesPerDatum } - cfg.RollupDimensions = getRollupDimensions(conf) - cfg.DropOriginConfigs = getDropOriginalMetrics(conf) + if rollupDimensions := getRollupDimensions(conf); rollupDimensions != nil { + cfg.RollupDimensions = rollupDimensions + } + if dropOriginalMetrics := getDropOriginalMetrics(conf); len(dropOriginalMetrics) != 0 { + cfg.DropOriginalConfigs = dropOriginalMetrics + } return cfg, nil } @@ -83,10 +91,13 @@ func getRoleARN(conf *confmap.Conf) string { func getRollupDimensions(conf *confmap.Conf) [][]string { key := common.ConfigKey(common.MetricsKey, rollup_dimensions.SectionKey) value := conf.Get(key) - if value == nil || !rollup_dimensions.IsValidRollupList(value) { + if value == nil { + return nil + } + aggregates, ok := value.([]interface{}) + if !ok || !isValidRollupList(aggregates) { return nil } - aggregates := value.([]interface{}) rollup := make([][]string, len(aggregates)) for i, aggregate := range aggregates { dimensions := aggregate.([]interface{}) @@ -98,14 +109,129 @@ func getRollupDimensions(conf *confmap.Conf) [][]string { return rollup } -// TODO: remove dependency on rule. -func getDropOriginalMetrics(conf *confmap.Conf) map[string][]string { - _, result := new(drop_origin.DropOrigin).ApplyRule(conf.Get(common.MetricsKey)) - dom, ok := result.(map[string][]string) - if ok { - return dom +// isValidRollupList confirms whether the supplied aggregate_dimension is a valid type ([][]string) +func isValidRollupList(aggregates []interface{}) bool { + if len(aggregates) == 0 { + return false } - return nil + for _, aggregate := range aggregates { + if dimensions, ok := aggregate.([]interface{}); ok { + if len(dimensions) != 0 { + for _, dimension := range dimensions { + if _, ok := dimension.(string); !ok { + return false + } + } + } + } else { + return false + } + } + + return true } -// TODO: remove dependency on rule. +func getDropOriginalMetrics(conf *confmap.Conf) map[string]bool { + key := common.ConfigKey(common.MetricsKey, common.MetricsCollectedKey) + value := conf.Get(key) + if value == nil { + return nil + } + categories := value.(map[string]interface{}) + dropOriginalMetrics := make(map[string]bool) + for category := range categories { + realCategoryName := config.GetRealPluginName(category) + measurementCfgKey := common.ConfigKey(common.MetricsKey, common.MetricsCollectedKey, category, common.MeasurementKey) + dropOriginalCfgKey := common.ConfigKey(common.MetricsKey, common.MetricsCollectedKey, category, common.DropOriginalMetricsKey) + /* Drop original metrics does not support procstat since procstat can monitor multiple process + "procstat": [ + { + "exe": "W3SVC", + "measurement": [ + "pid_count" + ] + }, + { + "exe": "IISADMIN", + "measurement": [ + "pid_count" + ] + }] + Therefore, dropping the original metrics can conflict between these two processes (e.g customers can drop pid_count with the first + process but not the second process) + */ + if dropMetrics := common.GetArray[any](conf, dropOriginalCfgKey); dropMetrics != nil { + for _, dropMetric := range dropMetrics { + measurements := common.GetArray[any](conf, measurementCfgKey) + if measurements == nil { + continue + } + + dropMetric, ok := dropMetric.(string) + if !ok { + continue + } + + if !strings.Contains(dropMetric, category) && dropMetric != dropOriginalWildcard { + dropMetric = metric.DecorateMetricName(realCategoryName, dropMetric) + } + isMetricDecoration := false + for _, measurement := range measurements { + switch val := measurement.(type) { + /* + "disk": { + "measurement": [ + { + "name": "free", + "rename": "DISK_FREE", + "unit": "unit" + } + ] + } + */ + case map[string]interface{}: + metricName, ok := val["name"].(string) + if !ok { + continue + } + if !strings.Contains(metricName, category) { + metricName = metric.DecorateMetricName(realCategoryName, metricName) + } + // If customers provides drop_original_metrics with a wildcard (*), adding the renamed metric or add the original metric + // if customers only re-unit the metric + if strings.Contains(dropMetric, metricName) || dropMetric == dropOriginalWildcard { + isMetricDecoration = true + if newMetricName, ok := val["rename"].(string); ok { + dropOriginalMetrics[newMetricName] = true + } else { + dropOriginalMetrics[metricName] = true + } + } + + /* + "measurement": ["free"] + */ + case string: + if dropMetric != dropOriginalWildcard { + continue + } + metricName := val + if !strings.Contains(metricName, category) { + metricName = metric.DecorateMetricName(realCategoryName, metricName) + } + + dropOriginalMetrics[metricName] = true + default: + continue + } + } + + if !isMetricDecoration && dropMetric != dropOriginalWildcard { + dropOriginalMetrics[dropMetric] = true + } + + } + } + } + return dropOriginalMetrics +} diff --git a/translator/translate/otel/exporter/awscloudwatch/translator_test.go b/translator/translate/otel/exporter/awscloudwatch/translator_test.go index 3655b0e03b..f6798e4b5d 100644 --- a/translator/translate/otel/exporter/awscloudwatch/translator_test.go +++ b/translator/translate/otel/exporter/awscloudwatch/translator_test.go @@ -7,6 +7,7 @@ import ( "encoding/json" "os" "path/filepath" + "runtime" "testing" "time" @@ -28,6 +29,7 @@ func TestTranslator(t *testing.T) { internal bool credentials map[string]interface{} want *cloudwatch.Config + wantWindows *cloudwatch.Config wantErr error }{ "WithMissingKey": { @@ -113,9 +115,22 @@ func TestTranslator(t *testing.T) { EndpointOverride: "https://monitoring-fips.us-west-2.amazonaws.com", RoleARN: "metrics_role_arn_value_test", RollupDimensions: [][]string{{"ImageId"}, {"InstanceId", "InstanceType"}, {"d1"}, {}}, - DropOriginConfigs: map[string][]string{ - "cpu": {"cpu_usage_idle", "time_active"}, - "nvidia_smi": {"utilization_gpu", "temperature_gpu"}, + DropOriginalConfigs: map[string]bool{ + "CPU_USAGE_IDLE": true, + "cpu_time_active": true, + }, + }, + wantWindows: &cloudwatch.Config{ + Namespace: "namespace", + Region: "us-east-1", + ForceFlushInterval: 30 * time.Second, + MaxValuesPerDatum: 5000, + EndpointOverride: "https://monitoring-fips.us-west-2.amazonaws.com", + RoleARN: "metrics_role_arn_value_test", + RollupDimensions: [][]string{{"ImageId"}, {"InstanceId", "InstanceType"}, {"d1"}, {}}, + DropOriginalConfigs: map[string]bool{ + "CPU_USAGE_IDLE": true, + "cpu time_active": true, }, }, }, @@ -143,7 +158,11 @@ func TestTranslator(t *testing.T) { require.Equal(t, testCase.want.SharedCredentialFilename, gotCfg.SharedCredentialFilename) require.Equal(t, testCase.want.MaxValuesPerDatum, gotCfg.MaxValuesPerDatum) require.Equal(t, testCase.want.RollupDimensions, gotCfg.RollupDimensions) - require.Equal(t, testCase.want.DropOriginConfigs, gotCfg.DropOriginConfigs) + if testCase.wantWindows != nil && runtime.GOOS == "windows" { + require.Equal(t, testCase.wantWindows.DropOriginalConfigs, gotCfg.DropOriginalConfigs) + } else { + require.Equal(t, testCase.want.DropOriginalConfigs, gotCfg.DropOriginalConfigs) + } } }) } diff --git a/translator/translate/otel/processor/metricsdecorator/translator.go b/translator/translate/otel/processor/metricsdecorator/translator.go index 9da7d0ea50..d16cd9afa1 100644 --- a/translator/translate/otel/processor/metricsdecorator/translator.go +++ b/translator/translate/otel/processor/metricsdecorator/translator.go @@ -127,7 +127,7 @@ func getMeasurementMaps(conf *confmap.Conf) map[string][]interface{} { measurementMap := make(map[string][]interface{}) metricsList := append(common.LinuxPluginKeys, common.WindowsPluginKeys...) for _, metric := range metricsList { - path := common.ConfigKey(metricsKey, metric, "measurement") + path := common.ConfigKey(metricsKey, metric, common.MeasurementKey) if conf.IsSet(path) { m := conf.Get(path).([]interface{}) measurementMap[metric] = m