001package io.prometheus.cloudwatch; 002 003import static io.prometheus.cloudwatch.CachingDimensionSource.DimensionCacheConfig; 004 005import io.prometheus.client.Collector; 006import io.prometheus.client.Collector.Describable; 007import io.prometheus.client.Counter; 008import io.prometheus.cloudwatch.DataGetter.MetricRuleData; 009import java.io.FileReader; 010import java.io.IOException; 011import java.io.Reader; 012import java.time.Duration; 013import java.util.ArrayList; 014import java.util.Arrays; 015import java.util.Collections; 016import java.util.HashMap; 017import java.util.HashSet; 018import java.util.List; 019import java.util.Map; 020import java.util.Map.Entry; 021import java.util.Set; 022import java.util.logging.Level; 023import java.util.logging.Logger; 024import org.yaml.snakeyaml.LoaderOptions; 025import org.yaml.snakeyaml.Yaml; 026import org.yaml.snakeyaml.constructor.SafeConstructor; 027import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 028import software.amazon.awssdk.regions.Region; 029import software.amazon.awssdk.services.cloudwatch.CloudWatchClient; 030import software.amazon.awssdk.services.cloudwatch.CloudWatchClientBuilder; 031import software.amazon.awssdk.services.cloudwatch.model.Dimension; 032import software.amazon.awssdk.services.cloudwatch.model.Statistic; 033import software.amazon.awssdk.services.resourcegroupstaggingapi.ResourceGroupsTaggingApiClient; 034import software.amazon.awssdk.services.resourcegroupstaggingapi.ResourceGroupsTaggingApiClientBuilder; 035import software.amazon.awssdk.services.resourcegroupstaggingapi.model.GetResourcesRequest; 036import software.amazon.awssdk.services.resourcegroupstaggingapi.model.GetResourcesResponse; 037import software.amazon.awssdk.services.resourcegroupstaggingapi.model.ResourceTagMapping; 038import software.amazon.awssdk.services.resourcegroupstaggingapi.model.Tag; 039import software.amazon.awssdk.services.resourcegroupstaggingapi.model.TagFilter; 040import software.amazon.awssdk.services.sts.StsClient; 041import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider; 042import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; 043 044public class CloudWatchCollector extends Collector implements Describable { 045 private static final Logger LOGGER = Logger.getLogger(CloudWatchCollector.class.getName()); 046 047 static class ActiveConfig { 048 ArrayList<MetricRule> rules; 049 CloudWatchClient cloudWatchClient; 050 ResourceGroupsTaggingApiClient taggingClient; 051 DimensionSource dimensionSource; 052 053 public ActiveConfig(ActiveConfig cfg) { 054 this.rules = new ArrayList<>(cfg.rules); 055 this.cloudWatchClient = cfg.cloudWatchClient; 056 this.taggingClient = cfg.taggingClient; 057 this.dimensionSource = cfg.dimensionSource; 058 } 059 060 public ActiveConfig() {} 061 } 062 063 static class AWSTagSelect { 064 String resourceTypeSelection; 065 String resourceIdDimension; 066 Map<String, List<String>> tagSelections; 067 } 068 069 ActiveConfig activeConfig = new ActiveConfig(); 070 071 private static final Counter cloudwatchRequests = 072 Counter.build() 073 .labelNames("action", "namespace") 074 .name("cloudwatch_requests_total") 075 .help("API requests made to CloudWatch") 076 .register(); 077 078 private static final Counter cloudwatchMetricsRequested = 079 Counter.build() 080 .labelNames("metric_name", "namespace") 081 .name("cloudwatch_metrics_requested_total") 082 .help("Metrics requested by either GetMetricStatistics or GetMetricData") 083 .register(); 084 085 private static final Counter taggingApiRequests = 086 Counter.build() 087 .labelNames("action", "resource_type") 088 .name("tagging_api_requests_total") 089 .help("API requests made to the Resource Groups Tagging API") 090 .register(); 091 092 private static final List<String> brokenDynamoMetrics = 093 Arrays.asList( 094 "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", 095 "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", 096 "ReadThrottleEvents", "WriteThrottleEvents"); 097 098 public CloudWatchCollector(Reader in) { 099 loadConfig(in, null, null); 100 } 101 102 public CloudWatchCollector(String yamlConfig) { 103 this(yamlConfig, null, null); 104 } 105 106 /* For unittests. */ 107 protected CloudWatchCollector( 108 String jsonConfig, 109 CloudWatchClient cloudWatchClient, 110 ResourceGroupsTaggingApiClient taggingClient) { 111 this( 112 (Map<String, Object>) new Yaml(new SafeConstructor(new LoaderOptions())).load(jsonConfig), 113 cloudWatchClient, 114 taggingClient); 115 } 116 117 private CloudWatchCollector( 118 Map<String, Object> config, 119 CloudWatchClient cloudWatchClient, 120 ResourceGroupsTaggingApiClient taggingClient) { 121 loadConfig(config, cloudWatchClient, taggingClient); 122 } 123 124 @Override 125 public List<MetricFamilySamples> describe() { 126 return Collections.emptyList(); 127 } 128 129 protected void reloadConfig() throws IOException { 130 LOGGER.log(Level.INFO, "Reloading configuration"); 131 try (FileReader reader = new FileReader(WebServer.configFilePath); ) { 132 loadConfig(reader, activeConfig.cloudWatchClient, activeConfig.taggingClient); 133 } 134 } 135 136 protected void loadConfig( 137 Reader in, CloudWatchClient cloudWatchClient, ResourceGroupsTaggingApiClient taggingClient) { 138 loadConfig( 139 (Map<String, Object>) new Yaml(new SafeConstructor(new LoaderOptions())).load(in), 140 cloudWatchClient, 141 taggingClient); 142 } 143 144 private void loadConfig( 145 Map<String, Object> config, 146 CloudWatchClient cloudWatchClient, 147 ResourceGroupsTaggingApiClient taggingClient) { 148 if (config == null) { // Yaml config empty, set config to empty map. 149 config = new HashMap<>(); 150 } 151 152 int defaultPeriod = 60; 153 if (config.containsKey("period_seconds")) { 154 defaultPeriod = ((Number) config.get("period_seconds")).intValue(); 155 } 156 int defaultRange = 600; 157 if (config.containsKey("range_seconds")) { 158 defaultRange = ((Number) config.get("range_seconds")).intValue(); 159 } 160 int defaultDelay = 600; 161 if (config.containsKey("delay_seconds")) { 162 defaultDelay = ((Number) config.get("delay_seconds")).intValue(); 163 } 164 165 boolean defaultCloudwatchTimestamp = true; 166 if (config.containsKey("set_timestamp")) { 167 defaultCloudwatchTimestamp = (Boolean) config.get("set_timestamp"); 168 } 169 170 boolean defaultUseGetMetricData = false; 171 if (config.containsKey("use_get_metric_data")) { 172 defaultUseGetMetricData = (Boolean) config.get("use_get_metric_data"); 173 } 174 175 Duration defaultMetricCacheSeconds = Duration.ofSeconds(0); 176 if (config.containsKey("list_metrics_cache_ttl")) { 177 defaultMetricCacheSeconds = 178 Duration.ofSeconds(((Number) config.get("list_metrics_cache_ttl")).intValue()); 179 } 180 181 boolean defaultWarnOnMissingDimensions = false; 182 if (config.containsKey("warn_on_empty_list_dimensions")) { 183 defaultWarnOnMissingDimensions = (Boolean) config.get("warn_on_empty_list_dimensions"); 184 } 185 186 String region = (String) config.get("region"); 187 188 if (cloudWatchClient == null) { 189 CloudWatchClientBuilder clientBuilder = CloudWatchClient.builder(); 190 191 if (config.containsKey("role_arn")) { 192 clientBuilder.credentialsProvider(getRoleCredentialProvider(config)); 193 } 194 195 if (region != null) { 196 clientBuilder.region(Region.of(region)); 197 } 198 199 cloudWatchClient = clientBuilder.build(); 200 } 201 202 if (taggingClient == null) { 203 ResourceGroupsTaggingApiClientBuilder clientBuilder = 204 ResourceGroupsTaggingApiClient.builder(); 205 206 if (config.containsKey("role_arn")) { 207 clientBuilder.credentialsProvider(getRoleCredentialProvider(config)); 208 } 209 if (region != null) { 210 clientBuilder.region(Region.of(region)); 211 } 212 taggingClient = clientBuilder.build(); 213 } 214 215 if (!config.containsKey("metrics")) { 216 throw new IllegalArgumentException("Must provide metrics"); 217 } 218 219 DimensionCacheConfig metricCacheConfig = new DimensionCacheConfig(defaultMetricCacheSeconds); 220 ArrayList<MetricRule> rules = new ArrayList<>(); 221 222 for (Object ruleObject : (List<Map<String, Object>>) config.get("metrics")) { 223 Map<String, Object> yamlMetricRule = (Map<String, Object>) ruleObject; 224 MetricRule rule = new MetricRule(); 225 rules.add(rule); 226 if (!yamlMetricRule.containsKey("aws_namespace") 227 || !yamlMetricRule.containsKey("aws_metric_name")) { 228 throw new IllegalArgumentException("Must provide aws_namespace and aws_metric_name"); 229 } 230 rule.awsNamespace = (String) yamlMetricRule.get("aws_namespace"); 231 rule.awsMetricName = (String) yamlMetricRule.get("aws_metric_name"); 232 if (yamlMetricRule.containsKey("help")) { 233 rule.help = (String) yamlMetricRule.get("help"); 234 } 235 if (yamlMetricRule.containsKey("aws_dimensions")) { 236 rule.awsDimensions = (List<String>) yamlMetricRule.get("aws_dimensions"); 237 } 238 if (yamlMetricRule.containsKey("aws_dimension_select") 239 && yamlMetricRule.containsKey("aws_dimension_select_regex")) { 240 throw new IllegalArgumentException( 241 "Must not provide aws_dimension_select and aws_dimension_select_regex at the same time"); 242 } 243 if (yamlMetricRule.containsKey("aws_dimension_select")) { 244 rule.awsDimensionSelect = 245 (Map<String, List<String>>) yamlMetricRule.get("aws_dimension_select"); 246 } 247 if (yamlMetricRule.containsKey("aws_dimension_select_regex")) { 248 rule.awsDimensionSelectRegex = 249 (Map<String, List<String>>) yamlMetricRule.get("aws_dimension_select_regex"); 250 } 251 if (yamlMetricRule.containsKey("aws_statistics")) { 252 rule.awsStatistics = new ArrayList<>(); 253 for (String statistic : (List<String>) yamlMetricRule.get("aws_statistics")) { 254 rule.awsStatistics.add(Statistic.fromValue(statistic)); 255 } 256 } else if (!yamlMetricRule.containsKey("aws_extended_statistics")) { 257 rule.awsStatistics = new ArrayList<>(); 258 for (String statistic : 259 Arrays.asList("Sum", "SampleCount", "Minimum", "Maximum", "Average")) { 260 rule.awsStatistics.add(Statistic.fromValue(statistic)); 261 } 262 } 263 if (yamlMetricRule.containsKey("aws_extended_statistics")) { 264 rule.awsExtendedStatistics = (List<String>) yamlMetricRule.get("aws_extended_statistics"); 265 } 266 if (yamlMetricRule.containsKey("period_seconds")) { 267 rule.periodSeconds = ((Number) yamlMetricRule.get("period_seconds")).intValue(); 268 } else { 269 rule.periodSeconds = defaultPeriod; 270 } 271 if (yamlMetricRule.containsKey("range_seconds")) { 272 rule.rangeSeconds = ((Number) yamlMetricRule.get("range_seconds")).intValue(); 273 } else { 274 rule.rangeSeconds = defaultRange; 275 } 276 if (yamlMetricRule.containsKey("delay_seconds")) { 277 rule.delaySeconds = ((Number) yamlMetricRule.get("delay_seconds")).intValue(); 278 } else { 279 rule.delaySeconds = defaultDelay; 280 } 281 if (yamlMetricRule.containsKey("set_timestamp")) { 282 rule.cloudwatchTimestamp = (Boolean) yamlMetricRule.get("set_timestamp"); 283 } else { 284 rule.cloudwatchTimestamp = defaultCloudwatchTimestamp; 285 } 286 if (yamlMetricRule.containsKey("use_get_metric_data")) { 287 rule.useGetMetricData = (Boolean) yamlMetricRule.get("use_get_metric_data"); 288 } else { 289 rule.useGetMetricData = defaultUseGetMetricData; 290 } 291 if (yamlMetricRule.containsKey("warn_on_empty_list_dimensions")) { 292 rule.warnOnEmptyListDimensions = 293 (Boolean) yamlMetricRule.get("warn_on_empty_list_dimensions"); 294 } else { 295 rule.warnOnEmptyListDimensions = defaultWarnOnMissingDimensions; 296 } 297 298 if (yamlMetricRule.containsKey("aws_tag_select")) { 299 Map<String, Object> yamlAwsTagSelect = 300 (Map<String, Object>) yamlMetricRule.get("aws_tag_select"); 301 if (!yamlAwsTagSelect.containsKey("resource_type_selection") 302 || !yamlAwsTagSelect.containsKey("resource_id_dimension")) { 303 throw new IllegalArgumentException( 304 "Must provide resource_type_selection and resource_id_dimension"); 305 } 306 AWSTagSelect awsTagSelect = new AWSTagSelect(); 307 rule.awsTagSelect = awsTagSelect; 308 309 awsTagSelect.resourceTypeSelection = 310 (String) yamlAwsTagSelect.get("resource_type_selection"); 311 awsTagSelect.resourceIdDimension = (String) yamlAwsTagSelect.get("resource_id_dimension"); 312 313 if (yamlAwsTagSelect.containsKey("tag_selections")) { 314 awsTagSelect.tagSelections = 315 (Map<String, List<String>>) yamlAwsTagSelect.get("tag_selections"); 316 } 317 } 318 319 if (yamlMetricRule.containsKey("list_metrics_cache_ttl")) { 320 rule.listMetricsCacheTtl = 321 Duration.ofSeconds(((Number) yamlMetricRule.get("list_metrics_cache_ttl")).intValue()); 322 metricCacheConfig.addOverride(rule); 323 } else { 324 rule.listMetricsCacheTtl = defaultMetricCacheSeconds; 325 } 326 } 327 328 DimensionSource dimensionSource = 329 new DefaultDimensionSource(cloudWatchClient, cloudwatchRequests); 330 if (defaultMetricCacheSeconds.toSeconds() > 0 || !metricCacheConfig.metricConfig.isEmpty()) { 331 dimensionSource = new CachingDimensionSource(dimensionSource, metricCacheConfig); 332 } 333 334 loadConfig(rules, cloudWatchClient, taggingClient, dimensionSource); 335 } 336 337 private void loadConfig( 338 ArrayList<MetricRule> rules, 339 CloudWatchClient cloudWatchClient, 340 ResourceGroupsTaggingApiClient taggingClient, 341 DimensionSource dimensionSource) { 342 synchronized (activeConfig) { 343 activeConfig.cloudWatchClient = cloudWatchClient; 344 activeConfig.taggingClient = taggingClient; 345 activeConfig.rules = rules; 346 activeConfig.dimensionSource = dimensionSource; 347 } 348 } 349 350 private AwsCredentialsProvider getRoleCredentialProvider(Map<String, Object> config) { 351 StsClient stsClient = 352 StsClient.builder().region(Region.of((String) config.get("region"))).build(); 353 AssumeRoleRequest assumeRoleRequest = 354 AssumeRoleRequest.builder() 355 .roleArn((String) config.get("role_arn")) 356 .roleSessionName("cloudwatch_exporter") 357 .build(); 358 return StsAssumeRoleCredentialsProvider.builder() 359 .stsClient(stsClient) 360 .refreshRequest(assumeRoleRequest) 361 .build(); 362 } 363 364 private List<ResourceTagMapping> getResourceTagMappings( 365 MetricRule rule, ResourceGroupsTaggingApiClient taggingClient) { 366 if (rule.awsTagSelect == null) { 367 return Collections.emptyList(); 368 } 369 370 List<TagFilter> tagFilters = new ArrayList<>(); 371 if (rule.awsTagSelect.tagSelections != null) { 372 for (Entry<String, List<String>> entry : rule.awsTagSelect.tagSelections.entrySet()) { 373 tagFilters.add(TagFilter.builder().key(entry.getKey()).values(entry.getValue()).build()); 374 } 375 } 376 377 List<ResourceTagMapping> resourceTagMappings = new ArrayList<>(); 378 GetResourcesRequest.Builder requestBuilder = 379 GetResourcesRequest.builder() 380 .tagFilters(tagFilters) 381 .resourceTypeFilters(rule.awsTagSelect.resourceTypeSelection); 382 String paginationToken = ""; 383 do { 384 requestBuilder.paginationToken(paginationToken); 385 386 GetResourcesResponse response = taggingClient.getResources(requestBuilder.build()); 387 taggingApiRequests.labels("getResources", rule.awsTagSelect.resourceTypeSelection).inc(); 388 389 resourceTagMappings.addAll(response.resourceTagMappingList()); 390 391 paginationToken = response.paginationToken(); 392 } while (paginationToken != null && !paginationToken.isEmpty()); 393 394 return resourceTagMappings; 395 } 396 397 private List<String> extractResourceIds(List<ResourceTagMapping> resourceTagMappings) { 398 List<String> resourceIds = new ArrayList<>(); 399 for (ResourceTagMapping resourceTagMapping : resourceTagMappings) { 400 resourceIds.add(extractResourceIdFromArn(resourceTagMapping.resourceARN())); 401 } 402 return resourceIds; 403 } 404 405 private String toSnakeCase(String str) { 406 return str.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase(); 407 } 408 409 private String safeName(String s) { 410 // Change invalid chars to underscore, and merge underscores. 411 return s.replaceAll("[^a-zA-Z0-9:_]", "_").replaceAll("__+", "_"); 412 } 413 414 private String safeLabelName(String s) { 415 // Change invalid chars to underscore, and merge underscores. 416 return s.replaceAll("[^a-zA-Z0-9_]", "_").replaceAll("__+", "_"); 417 } 418 419 private String help(MetricRule rule, String unit, String statistic) { 420 if (rule.help != null) { 421 return rule.help; 422 } 423 return "CloudWatch metric " 424 + rule.awsNamespace 425 + " " 426 + rule.awsMetricName 427 + " Dimensions: " 428 + rule.awsDimensions 429 + " Statistic: " 430 + statistic 431 + " Unit: " 432 + unit; 433 } 434 435 private String sampleLabelSuffixBy(Statistic s) { 436 switch (s) { 437 case SUM: 438 return "_sum"; 439 case SAMPLE_COUNT: 440 return "_sample_count"; 441 case MINIMUM: 442 return "_minimum"; 443 case MAXIMUM: 444 return "_maximum"; 445 case AVERAGE: 446 return "_average"; 447 default: 448 throw new RuntimeException("I did not expect this stats!"); 449 } 450 } 451 452 private void scrape(List<MetricFamilySamples> mfs) { 453 ActiveConfig config = new ActiveConfig(activeConfig); 454 Set<String> publishedResourceInfo = new HashSet<>(); 455 456 long start = System.currentTimeMillis(); 457 List<MetricFamilySamples.Sample> infoSamples = new ArrayList<>(); 458 459 for (MetricRule rule : config.rules) { 460 String baseName = 461 safeName(rule.awsNamespace.toLowerCase() + "_" + toSnakeCase(rule.awsMetricName)); 462 String jobName = safeName(rule.awsNamespace.toLowerCase()); 463 Map<Statistic, List<MetricFamilySamples.Sample>> baseSamples = new HashMap<>(); 464 for (Statistic s : Statistic.values()) { 465 baseSamples.put(s, new ArrayList<>()); 466 } 467 HashMap<String, List<MetricFamilySamples.Sample>> extendedSamples = new HashMap<>(); 468 469 String unit = null; 470 471 if (rule.awsNamespace.equals("AWS/DynamoDB") 472 && rule.awsDimensions != null 473 && rule.awsDimensions.contains("GlobalSecondaryIndexName") 474 && brokenDynamoMetrics.contains(rule.awsMetricName)) { 475 baseName += "_index"; 476 } 477 478 List<ResourceTagMapping> resourceTagMappings = 479 getResourceTagMappings(rule, config.taggingClient); 480 List<String> tagBasedResourceIds = extractResourceIds(resourceTagMappings); 481 482 List<List<Dimension>> dimensionList = 483 config.dimensionSource.getDimensions(rule, tagBasedResourceIds).getDimensions(); 484 DataGetter dataGetter = null; 485 if (rule.useGetMetricData) { 486 dataGetter = 487 new GetMetricDataDataGetter( 488 config.cloudWatchClient, 489 start, 490 rule, 491 cloudwatchRequests, 492 cloudwatchMetricsRequested, 493 dimensionList); 494 } else { 495 dataGetter = 496 new GetMetricStatisticsDataGetter( 497 config.cloudWatchClient, 498 start, 499 rule, 500 cloudwatchRequests, 501 cloudwatchMetricsRequested); 502 } 503 504 for (List<Dimension> dimensions : dimensionList) { 505 MetricRuleData values = dataGetter.metricRuleDataFor(dimensions); 506 if (values == null) { 507 continue; 508 } 509 unit = values.unit; 510 List<String> labelNames = new ArrayList<>(); 511 List<String> labelValues = new ArrayList<>(); 512 labelNames.add("job"); 513 labelValues.add(jobName); 514 labelNames.add("instance"); 515 labelValues.add(""); 516 for (Dimension d : dimensions) { 517 labelNames.add(safeLabelName(toSnakeCase(d.name()))); 518 labelValues.add(d.value()); 519 } 520 521 Long timestamp = null; 522 if (rule.cloudwatchTimestamp) { 523 timestamp = values.timestamp.toEpochMilli(); 524 } 525 526 // iterate over aws statistics 527 for (Entry<Statistic, Double> e : values.statisticValues.entrySet()) { 528 String suffix = sampleLabelSuffixBy(e.getKey()); 529 baseSamples 530 .get(e.getKey()) 531 .add( 532 new MetricFamilySamples.Sample( 533 baseName + suffix, labelNames, labelValues, e.getValue(), timestamp)); 534 } 535 536 // iterate over extended values 537 for (Entry<String, Double> entry : values.extendedValues.entrySet()) { 538 List<MetricFamilySamples.Sample> samples = 539 extendedSamples.getOrDefault(entry.getKey(), new ArrayList<>()); 540 samples.add( 541 new MetricFamilySamples.Sample( 542 baseName + "_" + safeName(toSnakeCase(entry.getKey())), 543 labelNames, 544 labelValues, 545 entry.getValue(), 546 timestamp)); 547 extendedSamples.put(entry.getKey(), samples); 548 } 549 } 550 551 if (!baseSamples.get(Statistic.SUM).isEmpty()) { 552 mfs.add( 553 new MetricFamilySamples( 554 baseName + "_sum", 555 Type.GAUGE, 556 help(rule, unit, "Sum"), 557 baseSamples.get(Statistic.SUM))); 558 } 559 if (!baseSamples.get(Statistic.SAMPLE_COUNT).isEmpty()) { 560 mfs.add( 561 new MetricFamilySamples( 562 baseName + "_sample_count", 563 Type.GAUGE, 564 help(rule, unit, "SampleCount"), 565 baseSamples.get(Statistic.SAMPLE_COUNT))); 566 } 567 if (!baseSamples.get(Statistic.MINIMUM).isEmpty()) { 568 mfs.add( 569 new MetricFamilySamples( 570 baseName + "_minimum", 571 Type.GAUGE, 572 help(rule, unit, "Minimum"), 573 baseSamples.get(Statistic.MINIMUM))); 574 } 575 if (!baseSamples.get(Statistic.MAXIMUM).isEmpty()) { 576 mfs.add( 577 new MetricFamilySamples( 578 baseName + "_maximum", 579 Type.GAUGE, 580 help(rule, unit, "Maximum"), 581 baseSamples.get(Statistic.MAXIMUM))); 582 } 583 if (!baseSamples.get(Statistic.AVERAGE).isEmpty()) { 584 mfs.add( 585 new MetricFamilySamples( 586 baseName + "_average", 587 Type.GAUGE, 588 help(rule, unit, "Average"), 589 baseSamples.get(Statistic.AVERAGE))); 590 } 591 for (Entry<String, List<MetricFamilySamples.Sample>> entry : extendedSamples.entrySet()) { 592 mfs.add( 593 new MetricFamilySamples( 594 baseName + "_" + safeName(toSnakeCase(entry.getKey())), 595 Type.GAUGE, 596 help(rule, unit, entry.getKey()), 597 entry.getValue())); 598 } 599 600 // Add the "aws_resource_info" metric for existing tag mappings 601 for (ResourceTagMapping resourceTagMapping : resourceTagMappings) { 602 if (!publishedResourceInfo.contains(resourceTagMapping.resourceARN())) { 603 List<String> labelNames = new ArrayList<>(); 604 List<String> labelValues = new ArrayList<>(); 605 labelNames.add("job"); 606 labelValues.add(jobName); 607 labelNames.add("instance"); 608 labelValues.add(""); 609 labelNames.add("arn"); 610 labelValues.add(resourceTagMapping.resourceARN()); 611 labelNames.add(safeLabelName(toSnakeCase(rule.awsTagSelect.resourceIdDimension))); 612 labelValues.add(extractResourceIdFromArn(resourceTagMapping.resourceARN())); 613 for (Tag tag : resourceTagMapping.tags()) { 614 // Avoid potential collision between resource tags and other metric labels by adding the 615 // "tag_" prefix 616 // The AWS tags are case sensitive, so to avoid loosing information and label 617 // collisions, tag keys are not snaked cased 618 labelNames.add("tag_" + safeLabelName(tag.key())); 619 labelValues.add(tag.value()); 620 } 621 622 infoSamples.add( 623 new MetricFamilySamples.Sample("aws_resource_info", labelNames, labelValues, 1)); 624 625 publishedResourceInfo.add(resourceTagMapping.resourceARN()); 626 } 627 } 628 } 629 mfs.add( 630 new MetricFamilySamples( 631 "aws_resource_info", 632 Type.GAUGE, 633 "AWS information available for resource", 634 infoSamples)); 635 } 636 637 public List<MetricFamilySamples> collect() { 638 long start = System.nanoTime(); 639 double error = 0; 640 List<MetricFamilySamples> mfs = new ArrayList<>(); 641 try { 642 scrape(mfs); 643 } catch (Exception e) { 644 error = 1; 645 LOGGER.log(Level.WARNING, "CloudWatch scrape failed", e); 646 } 647 List<MetricFamilySamples.Sample> samples = new ArrayList<>(); 648 samples.add( 649 new MetricFamilySamples.Sample( 650 "cloudwatch_exporter_scrape_duration_seconds", 651 new ArrayList<>(), 652 new ArrayList<>(), 653 (System.nanoTime() - start) / 1.0E9)); 654 mfs.add( 655 new MetricFamilySamples( 656 "cloudwatch_exporter_scrape_duration_seconds", 657 Type.GAUGE, 658 "Time this CloudWatch scrape took, in seconds.", 659 samples)); 660 661 samples = new ArrayList<>(); 662 samples.add( 663 new MetricFamilySamples.Sample( 664 "cloudwatch_exporter_scrape_error", new ArrayList<>(), new ArrayList<>(), error)); 665 mfs.add( 666 new MetricFamilySamples( 667 "cloudwatch_exporter_scrape_error", 668 Type.GAUGE, 669 "Non-zero if this scrape failed.", 670 samples)); 671 return mfs; 672 } 673 674 private String extractResourceIdFromArn(String arn) { 675 // ARN parsing is based on 676 // https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html 677 String[] arnArray = arn.split(":"); 678 String resourceId = arnArray[arnArray.length - 1]; 679 if (resourceId.contains("/")) { 680 String[] resourceArray = resourceId.split("/", 2); 681 resourceId = resourceArray[resourceArray.length - 1]; 682 } 683 return resourceId; 684 } 685 686 /** Convenience function to run standalone. */ 687 public static void main(String[] args) { 688 String region = "eu-west-1"; 689 if (args.length > 0) { 690 region = args[0]; 691 } 692 new BuildInfoCollector().register(); 693 CloudWatchCollector jc = 694 new CloudWatchCollector( 695 ("{" 696 + "`region`: `" 697 + region 698 + "`," 699 + "`metrics`: [{`aws_namespace`: `AWS/ELB`, `aws_metric_name`: `RequestCount`, `aws_dimensions`: [`AvailabilityZone`, `LoadBalancerName`]}] ," 700 + "}") 701 .replace('`', '"')); 702 for (MetricFamilySamples mfs : jc.collect()) { 703 System.out.println(mfs); 704 } 705 } 706}