diff --git a/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java b/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java index cb34407f..71ff755c 100644 --- a/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java +++ b/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java @@ -21,6 +21,8 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -44,6 +46,11 @@ public class CloudWatchCollector extends Collector implements Describable { private static final Logger LOGGER = Logger.getLogger(CloudWatchCollector.class.getName()); + // ARN parsing is based on + // https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html + private static final Pattern DEFAULT_ARN_RESOURCE_ID_REGEXP = + Pattern.compile("(?:([^:/]+)|[^:/]+/([^:]+))$"); + static class ActiveConfig { ArrayList rules; CloudWatchClient cloudWatchClient; @@ -64,6 +71,7 @@ static class AWSTagSelect { String resourceTypeSelection; String resourceIdDimension; Map> tagSelections; + Pattern arnResourceIdRegexp; } ActiveConfig activeConfig = new ActiveConfig(); @@ -314,6 +322,10 @@ private void loadConfig( awsTagSelect.tagSelections = (Map>) yamlAwsTagSelect.get("tag_selections"); } + if (yamlAwsTagSelect.containsKey("arn_resource_id_regexp")) { + awsTagSelect.arnResourceIdRegexp = + Pattern.compile((String) yamlAwsTagSelect.get("arn_resource_id_regexp")); + } } if (yamlMetricRule.containsKey("list_metrics_cache_ttl")) { @@ -394,14 +406,23 @@ private List getResourceTagMappings( return resourceTagMappings; } - private List extractResourceIds(List resourceTagMappings) { + private List extractResourceIds( + Pattern arnResourceIdRegexp, List resourceTagMappings) { List resourceIds = new ArrayList<>(); for (ResourceTagMapping resourceTagMapping : resourceTagMappings) { - resourceIds.add(extractResourceIdFromArn(resourceTagMapping.resourceARN())); + resourceIds.add( + extractResourceIdFromArn(resourceTagMapping.resourceARN(), arnResourceIdRegexp)); } return resourceIds; } + private static Pattern getArnResourceIdRegexp(MetricRule rule) { + if (rule.awsTagSelect != null && rule.awsTagSelect.arnResourceIdRegexp != null) { + return rule.awsTagSelect.arnResourceIdRegexp; + } + return DEFAULT_ARN_RESOURCE_ID_REGEXP; + } + private String toSnakeCase(String str) { return str.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase(); } @@ -477,7 +498,9 @@ private void scrape(List mfs) { List resourceTagMappings = getResourceTagMappings(rule, config.taggingClient); - List tagBasedResourceIds = extractResourceIds(resourceTagMappings); + Pattern arnResourceIdRegexp = getArnResourceIdRegexp(rule); + List tagBasedResourceIds = + extractResourceIds(arnResourceIdRegexp, resourceTagMappings); List> dimensionList = config.dimensionSource.getDimensions(rule, tagBasedResourceIds).getDimensions(); @@ -609,7 +632,8 @@ private void scrape(List mfs) { labelNames.add("arn"); labelValues.add(resourceTagMapping.resourceARN()); labelNames.add(safeLabelName(toSnakeCase(rule.awsTagSelect.resourceIdDimension))); - labelValues.add(extractResourceIdFromArn(resourceTagMapping.resourceARN())); + labelValues.add( + extractResourceIdFromArn(resourceTagMapping.resourceARN(), arnResourceIdRegexp)); for (Tag tag : resourceTagMapping.tags()) { // Avoid potential collision between resource tags and other metric labels by adding the // "tag_" prefix @@ -671,16 +695,17 @@ public List collect() { return mfs; } - private String extractResourceIdFromArn(String arn) { - // ARN parsing is based on - // https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html - String[] arnArray = arn.split(":"); - String resourceId = arnArray[arnArray.length - 1]; - if (resourceId.contains("/")) { - String[] resourceArray = resourceId.split("/", 2); - resourceId = resourceArray[resourceArray.length - 1]; + private String extractResourceIdFromArn(String arn, Pattern arnResourceIdRegexp) { + final Matcher matcher = arnResourceIdRegexp.matcher(arn); + if (matcher.find()) { + for (int i = 1; i <= matcher.groupCount(); i++) { + final String group = matcher.group(i); + if (group != null && !group.isEmpty()) { + return group; + } + } } - return resourceId; + return ""; } /** Convenience function to run standalone. */ diff --git a/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java b/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java index 1ae8f286..796e32e6 100644 --- a/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java +++ b/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java @@ -1136,6 +1136,206 @@ public void testTagSelectEC2() throws Exception { .01); } + @Test + public void testTagSelectWebACL() { + // Testing "aws_tag_select" with an WAF WebAcl, which have a non-standard resource id in + // metrics. + // The regexp to get the resource id from the arn is specified in the rule + new CloudWatchCollector( + "---\nregion: eu-west-1\nmetrics:\n- aws_namespace: AWS/WAFV2\n aws_metric_name: CountedRequests\n aws_dimensions: [Region, Rule, WebACL]\n aws_tag_select:\n resource_type_selection: \"wafv2:regional/webacl\"\n resource_id_dimension: WebACL\n arn_resource_id_regexp: \"([^/]+)/[^/]+$\"\n", + cloudWatchClient, + taggingClient) + .register(registry); + + Mockito.when( + taggingClient.getResources( + argThat( + new GetResourcesRequestMatcher().ResourceTypeFilter("wafv2:regional/webacl")))) + .thenReturn( + GetResourcesResponse.builder() + .resourceTagMappingList( + ResourceTagMapping.builder() + .tags(Tag.builder().key("Monitoring").value("enabled").build()) + .resourceARN( + "arn:aws:wafv2:eu-west-1:123456789:regional/webacl/svc-integration-xxxx/d177aaf1-b18f-4f84-aa8e-f1c5c40fc426") + .build()) + .build()); + + Mockito.when( + cloudWatchClient.listMetrics( + argThat( + new ListMetricsRequestMatcher() + .Namespace("AWS/WAFV2") + .MetricName("CountedRequests") + .Dimensions("Region", "Rule", "WebACL")))) + .thenReturn( + ListMetricsResponse.builder() + .metrics( + Metric.builder() + .dimensions( + Dimension.builder().name("Region").value("eu-west-1").build(), + Dimension.builder().name("Rule").value("WebAclLog").build(), + Dimension.builder() + .name("WebACL") + .value("svc-integration-xxxx") + .build()) + .build()) + .build()); + + Mockito.when( + cloudWatchClient.getMetricStatistics( + argThat( + new GetMetricStatisticsRequestMatcher() + .Namespace("AWS/WAFV2") + .MetricName("CountedRequests") + .Dimension("Region", "eu-west-1") + .Dimension("Rule", "WebAclLog") + .Dimension("WebACL", "svc-integration-xxxx")))) + .thenReturn( + GetMetricStatisticsResponse.builder() + .datapoints( + Datapoint.builder().timestamp(new Date().toInstant()).sum(200.0).build()) + .build()); + + assertEquals( + 200.0, + registry.getSampleValue( + "aws_wafv2_counted_requests_sum", + new String[] {"job", "instance", "region", "rule", "web_acl"}, + new String[] {"aws_wafv2", "", "eu-west-1", "WebAclLog", "svc-integration-xxxx"}), + .01); + assertEquals( + 1.0, + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "web_acl", "tag_Monitoring"}, + new String[] { + "aws_wafv2", + "", + "arn:aws:wafv2:eu-west-1:123456789:regional/webacl/svc-integration-xxxx/d177aaf1-b18f-4f84-aa8e-f1c5c40fc426", + "svc-integration-xxxx", + "enabled" + }), + .01); + } + + @Test + public void testTagSelectTargetGroup() { + // Testing "aws_tag_select" with an ALB target group, which have a non-standard resource id in + // metrics + // The regexp to get the resource id from the arn is specified in the rule + new CloudWatchCollector( + "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ApplicationELB\n aws_metric_name: UnHealthyHostCount\n aws_dimensions:\n - TargetGroup\n - LoadBalancer\n aws_tag_select:\n resource_type_selection: \"elasticloadbalancing:targetgroup\"\n resource_id_dimension: TargetGroup\n tag_selections:\n Monitoring: [enabled]\n arn_resource_id_regexp: \"(targetgroup/.*)$\"\n", + cloudWatchClient, + taggingClient) + .register(registry); + + Mockito.when( + taggingClient.getResources( + argThat( + new GetResourcesRequestMatcher() + .ResourceTypeFilter("elasticloadbalancing:targetgroup") + .TagFilter("Monitoring", List.of("enabled"))))) + .thenReturn( + GetResourcesResponse.builder() + .resourceTagMappingList( + ResourceTagMapping.builder() + .tags(Tag.builder().key("Monitoring").value("enabled").build()) + .resourceARN( + "arn:aws:elasticloadbalancing:us-east-1:121212121212:targetgroup/abc-123") + .build(), + ResourceTagMapping.builder() + .tags(Tag.builder().key("Monitoring").value("enabled").build()) + .resourceARN( + "arn:aws:elasticloadbalancing:us-east-1:121212121212:targetgroup/abc-234") + .build()) + .build()); + + Mockito.when( + cloudWatchClient.listMetrics( + argThat( + new ListMetricsRequestMatcher() + .Namespace("AWS/ApplicationELB") + .MetricName("UnHealthyHostCount") + .Dimensions("TargetGroup", "LoadBalancer")))) + .thenReturn( + ListMetricsResponse.builder() + .metrics( + Metric.builder() + .dimensions( + Dimension.builder() + .name("TargetGroup") + .value("targetgroup/abc-123") + .build(), + Dimension.builder().name("LoadBalancer").value("app/myLB/123").build()) + .build(), + Metric.builder() + .dimensions( + Dimension.builder() + .name("TargetGroup") + .value("targetgroup/abc-234") + .build(), + Dimension.builder().name("LoadBalancer").value("app/myLB/123").build()) + .build()) + .build()); + + Mockito.when( + cloudWatchClient.getMetricStatistics( + argThat( + new GetMetricStatisticsRequestMatcher() + .Namespace("AWS/ApplicationELB") + .MetricName("UnHealthyHostCount") + .Dimension("TargetGroup", "targetgroup/abc-123") + .Dimension("LoadBalancer", "app/myLB/123")))) + .thenReturn( + GetMetricStatisticsResponse.builder() + .datapoints( + Datapoint.builder().timestamp(new Date().toInstant()).average(2.0).build()) + .build()); + + Mockito.when( + cloudWatchClient.getMetricStatistics( + argThat( + new GetMetricStatisticsRequestMatcher() + .Namespace("AWS/ApplicationELB") + .MetricName("UnHealthyHostCount") + .Dimension("TargetGroup", "targetgroup/abc-234") + .Dimension("LoadBalancer", "app/myLB/123")))) + .thenReturn( + GetMetricStatisticsResponse.builder() + .datapoints( + Datapoint.builder().timestamp(new Date().toInstant()).average(3.0).build()) + .build()); + + assertEquals( + 2.0, + registry.getSampleValue( + "aws_applicationelb_un_healthy_host_count_average", + new String[] {"job", "instance", "target_group", "load_balancer"}, + new String[] {"aws_applicationelb", "", "targetgroup/abc-123", "app/myLB/123"}), + .01); + assertEquals( + 3.0, + registry.getSampleValue( + "aws_applicationelb_un_healthy_host_count_average", + new String[] {"job", "instance", "target_group", "load_balancer"}, + new String[] {"aws_applicationelb", "", "targetgroup/abc-234", "app/myLB/123"}), + .01); + assertEquals( + 1.0, + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "target_group", "tag_Monitoring"}, + new String[] { + "aws_applicationelb", + "", + "arn:aws:elasticloadbalancing:us-east-1:121212121212:targetgroup/abc-123", + "targetgroup/abc-123", + "enabled" + }), + .01); + } + @Test public void testTagSelectALB() throws Exception { // Testing "aws_tag_select" with an ALB, which have a fairly complex ARN