diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index e469c6756..aa1dc6566 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -133,6 +133,7 @@ func Provider() terraform.ResourceProvider { "aws_cloudwatch_event_rule": resourceAwsCloudWatchEventRule(), "aws_cloudwatch_event_target": resourceAwsCloudWatchEventTarget(), "aws_cloudwatch_log_group": resourceAwsCloudWatchLogGroup(), + "aws_cloudwatch_log_metric_filter": resourceAwsCloudWatchLogMetricFilter(), "aws_autoscaling_lifecycle_hook": resourceAwsAutoscalingLifecycleHook(), "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), "aws_codedeploy_app": resourceAwsCodeDeployApp(), diff --git a/builtin/providers/aws/resource_aws_cloudwatch_log_group.go b/builtin/providers/aws/resource_aws_cloudwatch_log_group.go index e4f7236b2..66416b7df 100644 --- a/builtin/providers/aws/resource_aws_cloudwatch_log_group.go +++ b/builtin/providers/aws/resource_aws_cloudwatch_log_group.go @@ -19,9 +19,10 @@ func resourceAwsCloudWatchLogGroup() *schema.Resource { Schema: map[string]*schema.Schema{ "name": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateLogGroupName, }, "retention_in_days": &schema.Schema{ diff --git a/builtin/providers/aws/resource_aws_cloudwatch_log_metric_filter.go b/builtin/providers/aws/resource_aws_cloudwatch_log_metric_filter.go new file mode 100644 index 000000000..943472f85 --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudwatch_log_metric_filter.go @@ -0,0 +1,187 @@ +package aws + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" +) + +func resourceAwsCloudWatchLogMetricFilter() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsCloudWatchLogMetricFilterUpdate, + Read: resourceAwsCloudWatchLogMetricFilterRead, + Update: resourceAwsCloudWatchLogMetricFilterUpdate, + Delete: resourceAwsCloudWatchLogMetricFilterDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateLogMetricFilterName, + }, + + "pattern": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateMaxLength(512), + StateFunc: func(v interface{}) string { + s, ok := v.(string) + if !ok { + return "" + } + return strings.TrimSpace(s) + }, + }, + + "log_group_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateLogGroupName, + }, + + "metric_transformation": &schema.Schema{ + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateLogMetricFilterTransformationName, + }, + "namespace": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateLogMetricFilterTransformationName, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateMaxLength(100), + }, + }, + }, + }, + }, + } +} + +func resourceAwsCloudWatchLogMetricFilterUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatchlogsconn + + input := cloudwatchlogs.PutMetricFilterInput{ + FilterName: aws.String(d.Get("name").(string)), + FilterPattern: aws.String(strings.TrimSpace(d.Get("pattern").(string))), + LogGroupName: aws.String(d.Get("log_group_name").(string)), + } + + transformations := d.Get("metric_transformation").([]interface{}) + o := transformations[0].(map[string]interface{}) + input.MetricTransformations = expandCloudWachLogMetricTransformations(o) + + log.Printf("[DEBUG] Creating/Updating CloudWatch Log Metric Filter: %s", input) + _, err := conn.PutMetricFilter(&input) + if err != nil { + return fmt.Errorf("Creating/Updating CloudWatch Log Metric Filter failed: %s", err) + } + + d.SetId(d.Get("name").(string)) + + log.Println("[INFO] CloudWatch Log Metric Filter created/updated") + + return resourceAwsCloudWatchLogMetricFilterRead(d, meta) +} + +func resourceAwsCloudWatchLogMetricFilterRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatchlogsconn + + mf, err := lookupCloudWatchLogMetricFilter(conn, d.Get("name").(string), + d.Get("log_group_name").(string), nil) + if err != nil { + if _, ok := err.(*resource.NotFoundError); ok { + log.Printf("[WARN] Removing CloudWatch Log Metric Filter as it is gone") + d.SetId("") + return nil + } + + return fmt.Errorf("Failed reading CloudWatch Log Metric Filter: %s", err) + } + + log.Printf("[DEBUG] Found CloudWatch Log Metric Filter: %s", mf) + + d.Set("name", mf.FilterName) + d.Set("pattern", mf.FilterPattern) + d.Set("metric_transformation", flattenCloudWachLogMetricTransformations(mf.MetricTransformations)) + + return nil +} + +func lookupCloudWatchLogMetricFilter(conn *cloudwatchlogs.CloudWatchLogs, + name, logGroupName string, nextToken *string) (*cloudwatchlogs.MetricFilter, error) { + + input := cloudwatchlogs.DescribeMetricFiltersInput{ + FilterNamePrefix: aws.String(name), + LogGroupName: aws.String(logGroupName), + NextToken: nextToken, + } + log.Printf("[DEBUG] Reading CloudWatch Log Metric Filter: %s", input) + resp, err := conn.DescribeMetricFilters(&input) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" { + return nil, &resource.NotFoundError{ + Message: fmt.Sprintf("CloudWatch Log Metric Filter %q / %q not found via"+ + " initial DescribeMetricFilters call", name, logGroupName), + LastError: err, + LastRequest: input, + } + } + + return nil, fmt.Errorf("Failed describing CloudWatch Log Metric Filter: %s", err) + } + + for _, mf := range resp.MetricFilters { + if *mf.FilterName == name { + return mf, nil + } + } + + if resp.NextToken != nil { + return lookupCloudWatchLogMetricFilter(conn, name, logGroupName, resp.NextToken) + } + + return nil, &resource.NotFoundError{ + Message: fmt.Sprintf("CloudWatch Log Metric Filter %q / %q not found "+ + "in given results from DescribeMetricFilters", name, logGroupName), + LastResponse: resp, + LastRequest: input, + } +} + +func resourceAwsCloudWatchLogMetricFilterDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatchlogsconn + + input := cloudwatchlogs.DeleteMetricFilterInput{ + FilterName: aws.String(d.Get("name").(string)), + LogGroupName: aws.String(d.Get("log_group_name").(string)), + } + log.Printf("[INFO] Deleting CloudWatch Log Metric Filter: %s", d.Id()) + _, err := conn.DeleteMetricFilter(&input) + if err != nil { + return fmt.Errorf("Error deleting CloudWatch Log Metric Filter: %s", err) + } + log.Println("[INFO] CloudWatch Log Metric Filter deleted") + + d.SetId("") + + return nil +} diff --git a/builtin/providers/aws/resource_aws_cloudwatch_log_metric_filter_test.go b/builtin/providers/aws/resource_aws_cloudwatch_log_metric_filter_test.go new file mode 100644 index 000000000..27de163ec --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudwatch_log_metric_filter_test.go @@ -0,0 +1,185 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSCloudWatchLogMetricFilter_basic(t *testing.T) { + var mf cloudwatchlogs.MetricFilter + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchLogMetricFilterDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSCloudWatchLogMetricFilterConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchLogMetricFilterExists("aws_cloudwatch_log_metric_filter.foobar", &mf), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "name", "MyAppAccessCount"), + testAccCheckCloudWatchLogMetricFilterName(&mf, "MyAppAccessCount"), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "pattern", ""), + testAccCheckCloudWatchLogMetricFilterPattern(&mf, ""), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "log_group_name", "MyApp/access.log"), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "metric_transformation.0.name", "EventCount"), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "metric_transformation.0.namespace", "YourNamespace"), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "metric_transformation.0.value", "1"), + testAccCheckCloudWatchLogMetricFilterTransformation(&mf, &cloudwatchlogs.MetricTransformation{ + MetricName: aws.String("EventCount"), + MetricNamespace: aws.String("YourNamespace"), + MetricValue: aws.String("1"), + }), + ), + }, + resource.TestStep{ + Config: testAccAWSCloudWatchLogMetricFilterConfigModified, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchLogMetricFilterExists("aws_cloudwatch_log_metric_filter.foobar", &mf), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "name", "MyAppAccessCount"), + testAccCheckCloudWatchLogMetricFilterName(&mf, "MyAppAccessCount"), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "pattern", "{ $.errorCode = \"AccessDenied\" }"), + testAccCheckCloudWatchLogMetricFilterPattern(&mf, "{ $.errorCode = \"AccessDenied\" }"), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "log_group_name", "MyApp/access.log"), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "metric_transformation.0.name", "AccessDeniedCount"), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "metric_transformation.0.namespace", "MyNamespace"), + resource.TestCheckResourceAttr("aws_cloudwatch_log_metric_filter.foobar", "metric_transformation.0.value", "2"), + testAccCheckCloudWatchLogMetricFilterTransformation(&mf, &cloudwatchlogs.MetricTransformation{ + MetricName: aws.String("AccessDeniedCount"), + MetricNamespace: aws.String("MyNamespace"), + MetricValue: aws.String("2"), + }), + ), + }, + }, + }) +} + +func testAccCheckCloudWatchLogMetricFilterName(mf *cloudwatchlogs.MetricFilter, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if name != *mf.FilterName { + return fmt.Errorf("Expected filter name: %q, given: %q", name, *mf.FilterName) + } + return nil + } +} + +func testAccCheckCloudWatchLogMetricFilterPattern(mf *cloudwatchlogs.MetricFilter, pattern string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if mf.FilterPattern == nil { + if pattern != "" { + return fmt.Errorf("Received empty filter pattern, expected: %q", pattern) + } + return nil + } + + if pattern != *mf.FilterPattern { + return fmt.Errorf("Expected filter pattern: %q, given: %q", pattern, *mf.FilterPattern) + } + return nil + } +} + +func testAccCheckCloudWatchLogMetricFilterTransformation(mf *cloudwatchlogs.MetricFilter, + t *cloudwatchlogs.MetricTransformation) resource.TestCheckFunc { + return func(s *terraform.State) error { + given := mf.MetricTransformations[0] + expected := t + + if *given.MetricName != *expected.MetricName { + return fmt.Errorf("Expected metric name: %q, received: %q", + *expected.MetricName, *given.MetricName) + } + + if *given.MetricNamespace != *expected.MetricNamespace { + return fmt.Errorf("Expected metric namespace: %q, received: %q", + *expected.MetricNamespace, *given.MetricNamespace) + } + + if *given.MetricValue != *expected.MetricValue { + return fmt.Errorf("Expected metric value: %q, received: %q", + *expected.MetricValue, *given.MetricValue) + } + + return nil + } +} + +func testAccCheckCloudWatchLogMetricFilterExists(n string, mf *cloudwatchlogs.MetricFilter) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).cloudwatchlogsconn + metricFilter, err := lookupCloudWatchLogMetricFilter(conn, rs.Primary.ID, rs.Primary.Attributes["log_group_name"], nil) + if err != nil { + return err + } + + *mf = *metricFilter + + return nil + } +} + +func testAccCheckAWSCloudWatchLogMetricFilterDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudwatchlogsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudwatch_log_metric_filter" { + continue + } + + _, err := lookupCloudWatchLogMetricFilter(conn, rs.Primary.ID, rs.Primary.Attributes["log_group_name"], nil) + if err == nil { + return fmt.Errorf("MetricFilter Still Exists: %s", rs.Primary.ID) + } + } + + return nil +} + +var testAccAWSCloudWatchLogMetricFilterConfig = ` +resource "aws_cloudwatch_log_metric_filter" "foobar" { + name = "MyAppAccessCount" + pattern = "" + log_group_name = "${aws_cloudwatch_log_group.dada.name}" + + metric_transformation { + name = "EventCount" + namespace = "YourNamespace" + value = "1" + } +} + +resource "aws_cloudwatch_log_group" "dada" { + name = "MyApp/access.log" +} +` + +var testAccAWSCloudWatchLogMetricFilterConfigModified = ` +resource "aws_cloudwatch_log_metric_filter" "foobar" { + name = "MyAppAccessCount" + pattern = < 512 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 512 characters: %q", k, value)) + } + + // http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutMetricFilter.html + pattern := `^[^:*]+$` + if !regexp.MustCompile(pattern).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q isn't a valid log metric name (must not contain colon nor asterisk): %q", + k, value)) + } + + return +} + +func validateLogMetricFilterTransformationName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if len(value) > 255 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 255 characters: %q", k, value)) + } + + // http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_MetricTransformation.html + pattern := `^[^:*$]*$` + if !regexp.MustCompile(pattern).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q isn't a valid log metric transformation name (must not contain"+ + " colon, asterisk nor dollar sign): %q", + k, value)) + } + + return +} + +func validateLogGroupName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if len(value) > 512 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 512 characters: %q", k, value)) + } + + // http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_CreateLogGroup.html + pattern := `^[\.\-_/#A-Za-z0-9]+$` + if !regexp.MustCompile(pattern).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q isn't a valid log group name (alphanumeric characters, underscores,"+ + " hyphens, slashes, hash signs and dots are allowed): %q", + k, value)) + } + + return +} diff --git a/builtin/providers/aws/validators_test.go b/builtin/providers/aws/validators_test.go index 2d4028fd5..972a9cbf2 100644 --- a/builtin/providers/aws/validators_test.go +++ b/builtin/providers/aws/validators_test.go @@ -285,3 +285,100 @@ func TestValidateHTTPMethod(t *testing.T) { } } } + +func TestValidateLogMetricFilterName(t *testing.T) { + validNames := []string{ + "YadaHereAndThere", + "Valid-5Metric_Name", + "This . is also %% valid@!)+(", + "1234", + strings.Repeat("W", 512), + } + for _, v := range validNames { + _, errors := validateLogMetricFilterName(v, "name") + if len(errors) != 0 { + t.Fatalf("%q should be a valid Log Metric Filter Name: %q", v, errors) + } + } + + invalidNames := []string{ + "Here is a name with: colon", + "and here is another * invalid name", + "*", + // length > 512 + strings.Repeat("W", 513), + } + for _, v := range invalidNames { + _, errors := validateLogMetricFilterName(v, "name") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid Log Metric Filter Name", v) + } + } +} + +func TestValidateLogMetricTransformationName(t *testing.T) { + validNames := []string{ + "YadaHereAndThere", + "Valid-5Metric_Name", + "This . is also %% valid@!)+(", + "1234", + "", + strings.Repeat("W", 255), + } + for _, v := range validNames { + _, errors := validateLogMetricFilterTransformationName(v, "name") + if len(errors) != 0 { + t.Fatalf("%q should be a valid Log Metric Filter Transformation Name: %q", v, errors) + } + } + + invalidNames := []string{ + "Here is a name with: colon", + "and here is another * invalid name", + "also $ invalid", + "*", + // length > 255 + strings.Repeat("W", 256), + } + for _, v := range invalidNames { + _, errors := validateLogMetricFilterTransformationName(v, "name") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid Log Metric Filter Transformation Name", v) + } + } +} + +func TestValidateLogGroupName(t *testing.T) { + validNames := []string{ + "ValidLogGroupName", + "ValidLogGroup.Name", + "valid/Log-group", + "1234", + "YadaValid#0123", + "Also_valid-name", + strings.Repeat("W", 512), + } + for _, v := range validNames { + _, errors := validateLogGroupName(v, "name") + if len(errors) != 0 { + t.Fatalf("%q should be a valid Log Metric Filter Transformation Name: %q", v, errors) + } + } + + invalidNames := []string{ + "Here is a name with: colon", + "and here is another * invalid name", + "also $ invalid", + "This . is also %% invalid@!)+(", + "*", + "", + // length > 512 + strings.Repeat("W", 513), + } + for _, v := range invalidNames { + _, errors := validateLogGroupName(v, "name") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid Log Metric Filter Transformation Name", v) + } + } +} diff --git a/helper/resource/error.go b/helper/resource/error.go new file mode 100644 index 000000000..58bd127ad --- /dev/null +++ b/helper/resource/error.go @@ -0,0 +1,21 @@ +package resource + +type NotFoundError struct { + LastError error + LastRequest interface{} + LastResponse interface{} + Message string + Retries int +} + +func (e *NotFoundError) Error() string { + if e.Message != "" { + return e.Message + } + + return "couldn't find resource" +} + +func NewNotFoundError(err string) *NotFoundError { + return &NotFoundError{Message: err} +} diff --git a/website/source/docs/providers/aws/r/cloudwatch_log_metric_filter.html.markdown b/website/source/docs/providers/aws/r/cloudwatch_log_metric_filter.html.markdown new file mode 100644 index 000000000..bd60bf082 --- /dev/null +++ b/website/source/docs/providers/aws/r/cloudwatch_log_metric_filter.html.markdown @@ -0,0 +1,54 @@ +--- +layout: "aws" +page_title: "AWS: aws_cloudwatch_log_metric_filter" +sidebar_current: "docs-aws-resource-cloudwatch-log-metric-filter" +description: |- + Provides a CloudWatch Log Metric Filter resource. +--- + +# aws\_cloudwatch\_log\_metric\_filter + +Provides a CloudWatch Log Metric Filter resource. + +## Example Usage + +``` +resource "aws_cloudwatch_log_metric_filter" "yada" { + name = "MyAppAccessCount" + pattern = "" + log_group_name = "${aws_cloudwatch_log_group.dada.name}" + + metric_transformation { + name = "EventCount" + namespace = "YourNamespace" + value = "1" + } +} + +resource "aws_cloudwatch_log_group" "dada" { + name = "MyApp/access.log" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A name for the metric filter. +* `pattern` - (Required) A valid [CloudWatch Logs filter pattern](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/FilterAndPatternSyntax.html) + for extracting metric data out of ingested log events. +* `log_group_name` - (Required) The name of the log group to associate the metric filter with. +* `metric_transformation` - (Required) A block defining collection of information + needed to define how metric data gets emitted. See below. + +The `metric_transformation` block supports the following arguments: + +* `name` - (Required) The name of the CloudWatch metric to which the monitored log information should be published (e.g. `ErrorCount`) +* `namespace` - (Required) The destination namespace of the CloudWatch metric. +* `value` - (Required) What to publish to the metric. For example, if you're counting the occurrences of a particular term like "Error", the value will be "1" for each occurrence. If you're counting the bytes transferred the published value will be the value in the log event. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The name of the metric filter. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 6887917c9..10f3dd7b2 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -76,6 +76,10 @@ aws_cloudwatch_log_group + > + aws_cloudwatch_log_metric_filter + + > aws_cloudwatch_metric_alarm