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}