diff --git a/24882.txt b/24882.txt new file mode 100644 index 00000000000..298f1c888d8 --- /dev/null +++ b/24882.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +r/aws_cloudwatch_metric_stream: Add `statistics_configuration` argument +``` \ No newline at end of file diff --git a/internal/service/cloudwatch/metric_stream.go b/internal/service/cloudwatch/metric_stream.go index dd1f6530fd4..dbe7a7f65e2 100644 --- a/internal/service/cloudwatch/metric_stream.go +++ b/internal/service/cloudwatch/metric_stream.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/flex" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/verify" ) @@ -113,6 +114,55 @@ func ResourceMetricStream() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "statistics_configuration": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "additional_statistics": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.All( + validation.Any( + validation.StringMatch( + regexp.MustCompile(`(^IQM$)|(^(p|tc|tm|ts|wm)(100|\d{1,2})(\.\d{0,10})?$)|(^[ou]\d+(\.\d*)?$)`), + "invalid statistic, see: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Statistics-definitions.html", + ), + validation.StringMatch( + regexp.MustCompile(`^(TM|TC|TS|WM)\(((((\d{1,2})(\.\d{0,10})?|100(\.0{0,10})?)%)?:((\d{1,2})(\.\d{0,10})?|100(\.0{0,10})?)%|((\d{1,2})(\.\d{0,10})?|100(\.0{0,10})?)%:(((\d{1,2})(\.\d{0,10})?|100(\.0{0,10})?)%)?)\)|(TM|TC|TS|WM|PR)\(((\d+(\.\d{0,10})?|(\d+(\.\d{0,10})?[Ee][+-]?\d+)):((\d+(\.\d{0,10})?|(\d+(\.\d{0,10})?[Ee][+-]?\d+)))?|((\d+(\.\d{0,10})?|(\d+(\.\d{0,10})?[Ee][+-]?\d+)))?:(\d+(\.\d{0,10})?|(\d+(\.\d{0,10})?[Ee][+-]?\d+)))\)$`), + "invalid statistic, see: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Statistics-definitions.html", + ), + ), + validation.StringDoesNotMatch( + regexp.MustCompile(`^p0(\.0{0,10})?|p100(\.\d{0,10})?$`), + "invalid statistic, see: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Statistics-definitions.html", + ), + ), + }, + }, + "include_metric": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + "namespace": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + }, + }, + }, + }, + }, + }, "tags": tftags.TagsSchema(), "tags_all": tftags.TagsSchemaComputed(), }, @@ -145,6 +195,10 @@ func resourceMetricStreamCreate(ctx context.Context, d *schema.ResourceData, met params.ExcludeFilters = expandMetricStreamFilters(v.(*schema.Set)) } + if v, ok := d.GetOk("statistics_configuration"); ok && v.(*schema.Set).Len() > 0 { + params.StatisticsConfigurations = expandMetricStreamStatisticsConfigurations(v.(*schema.Set)) + } + log.Printf("[DEBUG] Putting CloudWatch Metric Stream: %#v", params) output, err := conn.PutMetricStreamWithContext(ctx, ¶ms) @@ -224,6 +278,12 @@ func resourceMetricStreamRead(ctx context.Context, d *schema.ResourceData, meta } } + if output.StatisticsConfigurations != nil { + if err := d.Set("statistics_configuration", flattenMetricStreamStatisticsConfigurations(output.StatisticsConfigurations)); err != nil { + return diag.FromErr(fmt.Errorf("error setting statistics_configuration error: %w", err)) + } + } + tags, err := ListTags(conn, aws.StringValue(output.Arn)) // Some partitions (i.e., ISO) may not support tagging, giving error @@ -312,3 +372,88 @@ func flattenMetricStreamFilters(s []*cloudwatch.MetricStreamFilter) []map[string return nil } + +func expandMetricStreamStatisticsConfigurations(s *schema.Set) []*cloudwatch.MetricStreamStatisticsConfiguration { + var configurations []*cloudwatch.MetricStreamStatisticsConfiguration + + for _, configurationRaw := range s.List() { + configuration := &cloudwatch.MetricStreamStatisticsConfiguration{} + mConfiguration := configurationRaw.(map[string]interface{}) + + if v, ok := mConfiguration["additional_statistics"].(*schema.Set); ok && v.Len() > 0 { + log.Printf("[DEBUG] CloudWatch Metric Stream StatisticsConfigurations additional_statistics: %#v", v) + configuration.AdditionalStatistics = flex.ExpandStringSet(v) + } + + if v, ok := mConfiguration["include_metric"].(*schema.Set); ok && v.Len() > 0 { + log.Printf("[DEBUG] CloudWatch Metric Stream StatisticsConfigurations include_metrics: %#v", v) + configuration.IncludeMetrics = expandMetricStreamStatisticsConfigurationsIncludeMetrics(v) + } + + configurations = append(configurations, configuration) + + } + + log.Printf("[DEBUG] statistics_configurations: %#v", configurations) + + if len(configurations) > 0 { + return configurations + } + + return nil +} + +func expandMetricStreamStatisticsConfigurationsIncludeMetrics(metrics *schema.Set) []*cloudwatch.MetricStreamStatisticsMetric { + var includeMetrics []*cloudwatch.MetricStreamStatisticsMetric + + for _, metricRaw := range metrics.List() { + metric := &cloudwatch.MetricStreamStatisticsMetric{} + mMetric := metricRaw.(map[string]interface{}) + + if v, ok := mMetric["metric_name"].(string); ok && v != "" { + metric.MetricName = aws.String(v) + } + + if v, ok := mMetric["namespace"].(string); ok && v != "" { + metric.Namespace = aws.String(v) + } + + includeMetrics = append(includeMetrics, metric) + } + + if len(includeMetrics) > 0 { + return includeMetrics + } + + return nil +} + +func flattenMetricStreamStatisticsConfigurations(configurations []*cloudwatch.MetricStreamStatisticsConfiguration) []map[string]interface{} { + flatConfigurations := make([]map[string]interface{}, len(configurations)) + + for i, configuration := range configurations { + flatConfiguration := map[string]interface{}{ + "additional_statistics": flex.FlattenStringSet(configuration.AdditionalStatistics), + "include_metric": flattenMetricStreamStatisticsConfigurationsIncludeMetrics(configuration.IncludeMetrics), + } + + flatConfigurations[i] = flatConfiguration + } + + return flatConfigurations +} + +func flattenMetricStreamStatisticsConfigurationsIncludeMetrics(metrics []*cloudwatch.MetricStreamStatisticsMetric) []map[string]interface{} { + flatMetrics := make([]map[string]interface{}, len(metrics)) + + for i, metric := range metrics { + flatMetric := map[string]interface{}{ + "metric_name": aws.StringValue(metric.MetricName), + "namespace": aws.StringValue(metric.Namespace), + } + + flatMetrics[i] = flatMetric + } + + return flatMetrics +} diff --git a/internal/service/cloudwatch/metric_stream_test.go b/internal/service/cloudwatch/metric_stream_test.go index 1a3fa5d56da..022ee9bccae 100644 --- a/internal/service/cloudwatch/metric_stream_test.go +++ b/internal/service/cloudwatch/metric_stream_test.go @@ -2,6 +2,7 @@ package cloudwatch_test import ( "fmt" + "regexp" "strings" "testing" @@ -255,6 +256,77 @@ func TestAccCloudWatchMetricStream_tags(t *testing.T) { }) } +func TestAccCloudWatchMetricStream_additional_statistics(t *testing.T) { + resourceName := "aws_cloudwatch_metric_stream.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, cloudwatch.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckMetricStreamDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMetricStreamAdditionalStatisticsConfig(rName, "p0"), + ExpectError: regexp.MustCompile(`invalid statistic, see: https:\/\/docs\.aws\.amazon\.com\/.*`), + }, + { + Config: testAccMetricStreamAdditionalStatisticsConfig(rName, "p100"), + ExpectError: regexp.MustCompile(`invalid statistic, see: https:\/\/docs\.aws\.amazon\.com\/.*`), + }, + { + Config: testAccMetricStreamAdditionalStatisticsConfig(rName, "p"), + ExpectError: regexp.MustCompile(`invalid statistic, see: https:\/\/docs\.aws\.amazon\.com\/.*`), + }, + { + Config: testAccMetricStreamAdditionalStatisticsConfig(rName, "tm"), + ExpectError: regexp.MustCompile(`invalid statistic, see: https:\/\/docs\.aws\.amazon\.com\/.*`), + }, + { + Config: testAccMetricStreamAdditionalStatisticsConfig(rName, "tc()"), + ExpectError: regexp.MustCompile(`invalid statistic, see: https:\/\/docs\.aws\.amazon\.com\/.*`), + }, + { + Config: testAccMetricStreamAdditionalStatisticsConfig(rName, "p99.12345678901"), + ExpectError: regexp.MustCompile(`invalid statistic, see: https:\/\/docs\.aws\.amazon\.com\/.*`), + }, + { + Config: testAccMetricStreamAdditionalStatisticsConfig(rName, "IQM"), + Check: resource.ComposeTestCheckFunc( + testAccCheckMetricStreamExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "statistics_configuration.#", "2"), + ), + }, + { + Config: testAccMetricStreamAdditionalStatisticsConfig(rName, "PR(:50)"), + Check: resource.ComposeTestCheckFunc( + testAccCheckMetricStreamExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "statistics_configuration.#", "2"), + ), + }, + { + Config: testAccMetricStreamAdditionalStatisticsConfig(rName, "TS(50.5:)"), + Check: resource.ComposeTestCheckFunc( + testAccCheckMetricStreamExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "statistics_configuration.#", "2"), + ), + }, + { + Config: testAccMetricStreamAdditionalStatisticsConfig(rName, "TC(1:100)"), + Check: resource.ComposeTestCheckFunc( + testAccCheckMetricStreamExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "statistics_configuration.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccCheckMetricStreamGeneratedNamePrefix(resource, prefix string) resource.TestCheckFunc { return func(s *terraform.State) error { r, ok := s.RootModule().Resources[resource] @@ -568,3 +640,40 @@ resource "aws_cloudwatch_metric_stream" "test" { } `, rName) } + +func testAccMetricStreamAdditionalStatisticsConfig(rName string, stat string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} +data "aws_region" "current" {} +data "aws_caller_identity" "current" {} + +resource "aws_cloudwatch_metric_stream" "test" { + name = %[1]q + role_arn = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:role/MyRole" + firehose_arn = "arn:${data.aws_partition.current.partition}:firehose:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:deliverystream/MyFirehose" + output_format = "json" + + statistics_configuration { + additional_statistics = [ + "p1", "tm99" + ] + + include_metric { + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + } + } + + statistics_configuration { + additional_statistics = [ + %[2]q + ] + + include_metric { + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + } + } +} +`, rName, stat) +} diff --git a/website/docs/r/cloudwatch_metric_stream.html.markdown b/website/docs/r/cloudwatch_metric_stream.html.markdown index 4546cd0483d..3047678115b 100644 --- a/website/docs/r/cloudwatch_metric_stream.html.markdown +++ b/website/docs/r/cloudwatch_metric_stream.html.markdown @@ -12,6 +12,8 @@ Provides a CloudWatch Metric Stream resource. ## Example Usage +### Filters + ```terraform resource "aws_cloudwatch_metric_stream" "main" { name = "my-metric-stream" @@ -137,6 +139,39 @@ resource "aws_kinesis_firehose_delivery_stream" "s3_stream" { } ``` +### Additional Statistics + +```terraform +resource "aws_cloudwatch_metric_stream" "main" { + name = "my-metric-stream" + role_arn = aws_iam_role.metric_stream_to_firehose.arn + firehose_arn = aws_kinesis_firehose_delivery_stream.s3_stream.arn + output_format = "json" + + statistics_configuration { + additional_statistics = [ + "p1", "tm99" + ] + + include_metric { + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + } + } + + statistics_configuration { + additional_statistics = [ + "TS(50.5:)" + ] + + include_metric { + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + } + } +} +``` + ## Argument Reference The following arguments are required: @@ -152,15 +187,28 @@ The following arguments are optional: * `name` - (Optional, Forces new resource) Friendly name of the metric stream. If omitted, Terraform will assign a random, unique name. Conflicts with `name_prefix`. * `name_prefix` - (Optional, Forces new resource) Creates a unique friendly name beginning with the specified prefix. Conflicts with `name`. * `tags` - (Optional) Map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `statistics_configuration` - (Optional) For each entry in this array, you specify one or more metrics and the list of additional statistics to stream for those metrics. The additional statistics that you can stream depend on the stream's `output_format`. If the OutputFormat is `json`, you can stream any additional statistic that is supported by CloudWatch, listed in [CloudWatch statistics definitions](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Statistics-definitions.html.html). If the OutputFormat is `opentelemetry0.7`, you can stream percentile statistics (p99 etc.). See details below. + +### Nested Fields -### `exclude_filter` +#### `exclude_filter` * `namespace` - (Required) Name of the metric namespace in the filter. -### `include_filter` +#### `include_filter` * `namespace` - (Required) Name of the metric namespace in the filter. +#### `statistics_configurations` + +* `additional_statistics` - (Required) The additional statistics to stream for the metrics listed in `include_metrics`. +* `include_metric` - (Required) An array that defines the metrics that are to have additional statistics streamed. See details below. + +#### `include_metrics` + +* `metric_name` - (Required) The name of the metric. +* `namespace` - (Required) The namespace of the metric. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: