Skip to content

Commit

Permalink
Merge pull request #648 from msvticket/resourceidmapping
Browse files Browse the repository at this point in the history
fix: support aws_tag_select with non-standard resource id in metric
  • Loading branch information
matthiasr authored Mar 22, 2024
2 parents ac160bd + e528d21 commit 64e52db
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 15 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ aws_tag_select | Optional. A tag configuration to filter on, based on mapping fr
tag_selections | Optional, under `aws_tag_select`. Specify a map from a tag key to a list of tag values to apply [tag filtering](https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/API_GetResources.html#resourcegrouptagging-GetResources-request-TagFilters) on resources from which metrics will be gathered.
resource_type_selection | Required, under `aws_tag_select`. Specify the [resource type](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-aws-service-namesspaces) to filter on. `resource_type_selection` should be comprised as `service:resource_type`, as per the [resource group tagging API](https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/API_GetResources.html#resourcegrouptagging-GetResources-request-TagFilters). Where `resource_type` could be an empty string, like in S3 case: `resource_type_selection: "s3:"`.
resource_id_dimension | Required, under `aws_tag_select`. For the current metric, specify which CloudWatch dimension maps to the ARN [resource ID](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arns-syntax).
arn_resource_id_regexp | If the Cloudwatch dimension specified in `resource_id_dimension` doesn't conform to the convention for resource ID an alternative regular expression to extract the resource ID from the ARN can be given here. The default is `(?:([^:/]+)|[^:/]+/([^:]+))$`. The first non empty match group will be used.
aws_statistics | Optional. A list of statistics to retrieve, values can include Sum, SampleCount, Minimum, Maximum, Average. Defaults to all statistics unless extended statistics are requested.
aws_extended_statistics | Optional. A list of extended statistics to retrieve. Extended statistics currently include percentiles in the form `pN` or `pN.N`.
delay_seconds | Optional. The newest data to request. Used to avoid collecting data that has not fully converged. Defaults to 600s. Can be set globally and per metric.
Expand Down
16 changes: 14 additions & 2 deletions examples/ApplicationELB.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@ metrics:
aws_metric_name: HealthyHostCount
aws_dimensions:
- LoadBalancer
aws_statistics:
- Sum
- TargetGroup
aws_statistics:
- Minimum
# In case you want to use some tag to select target group to monitor, or to have additional `info` metric
# with all target group tags as labels, use `aws_tag_select`.
# Since the TargetGroup dimension doesn't follow the convention for how to extract resource ids from ARN
# `arn_resource_id_regexp` is specified with an alternative regular expression.
aws_tag_select:
resource_type_selection: elasticloadbalancing:targetgroup
resource_id_dimension: TargetGroup
arn_resource_id_regexp: "(targetgroup/.*)$"
tag_selections:
Environment:
- production
- aws_namespace: AWS/ApplicationELB
aws_metric_name: UnHealthyHostCount
aws_dimensions:
Expand Down
11 changes: 11 additions & 0 deletions examples/WAFV2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ metrics:
aws_namespace: AWS/WAFV2
aws_statistics:
- Sum
# In case you want to use some tag to select web acls to monitor, or to have additional `info` metric
# with all web acl tags as labels, use `aws_tag_select`.
# Since the WebACL dimension doesn't follow the convention for how to extract resource ids from ARN
# `arn_resource_id_regexp` is specified with an alternative regular expression.
aws_tag_select:
resource_type_selection: wafv2:regional/webacl
resource_id_dimension: WebACL
arn_resource_id_regexp: "([^/]+)/[^/]+$"
tag_selections:
Environment:
- production
- aws_dimensions:
- Region
- Rule
Expand Down
51 changes: 38 additions & 13 deletions src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<MetricRule> rules;
CloudWatchClient cloudWatchClient;
Expand All @@ -64,6 +71,7 @@ static class AWSTagSelect {
String resourceTypeSelection;
String resourceIdDimension;
Map<String, List<String>> tagSelections;
Pattern arnResourceIdRegexp;
}

ActiveConfig activeConfig = new ActiveConfig();
Expand Down Expand Up @@ -314,6 +322,10 @@ private void loadConfig(
awsTagSelect.tagSelections =
(Map<String, List<String>>) 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")) {
Expand Down Expand Up @@ -394,14 +406,23 @@ private List<ResourceTagMapping> getResourceTagMappings(
return resourceTagMappings;
}

private List<String> extractResourceIds(List<ResourceTagMapping> resourceTagMappings) {
private List<String> extractResourceIds(
Pattern arnResourceIdRegexp, List<ResourceTagMapping> resourceTagMappings) {
List<String> 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();
}
Expand Down Expand Up @@ -477,7 +498,9 @@ private void scrape(List<MetricFamilySamples> mfs) {

List<ResourceTagMapping> resourceTagMappings =
getResourceTagMappings(rule, config.taggingClient);
List<String> tagBasedResourceIds = extractResourceIds(resourceTagMappings);
Pattern arnResourceIdRegexp = getArnResourceIdRegexp(rule);
List<String> tagBasedResourceIds =
extractResourceIds(arnResourceIdRegexp, resourceTagMappings);

List<List<Dimension>> dimensionList =
config.dimensionSource.getDimensions(rule, tagBasedResourceIds).getDimensions();
Expand Down Expand Up @@ -609,7 +632,8 @@ private void scrape(List<MetricFamilySamples> 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
Expand Down Expand Up @@ -671,16 +695,17 @@ public List<MetricFamilySamples> 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. */
Expand Down
200 changes: 200 additions & 0 deletions src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 64e52db

Please sign in to comment.