From d97d9100c70febaa754dcbb2f7a8664e95cc60c6 Mon Sep 17 00:00:00 2001 From: Victor Amorim Date: Thu, 14 Dec 2023 18:26:33 -0300 Subject: [PATCH 1/3] adding global_cache_ttl feature Signed-off-by: Victor Amorim --- README.md | 1 + .../cloudwatch/CloudWatchCollector.java | 132 +++++++++++---- .../cloudwatch/CloudWatchCollectorTest.java | 159 ++++++++++++++---- 3 files changed, 222 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 336236f8..e2817a0f 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ set_timestamp | Optional. Boolean for whether to set the Prometheus metric times use_get_metric_data | Optional. Boolean (experimental) Use GetMetricData API to get metrics instead of GetMetricStatistics. Can be set globally and per metric. list_metrics_cache_ttl | Optional. Number of seconds to cache the result of calling the ListMetrics API. Defaults to 0 (no cache). Can be set globally and per metric. warn_on_empty_list_dimensions | Optional. Boolean Emit warning if the exporter cannot determine what metrics to request +global_cache_ttl | Optional. Number of seconds to cache the result from /metrics. Any value greater than 0 means that the last result will be returned. Defaults to 0 (no cache). Can be set globally. The above config will export time series such as diff --git a/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java b/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java index cb34407f..03450856 100644 --- a/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java +++ b/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java @@ -1,26 +1,9 @@ package io.prometheus.cloudwatch; -import static io.prometheus.cloudwatch.CachingDimensionSource.DimensionCacheConfig; - import io.prometheus.client.Collector; import io.prometheus.client.Collector.Describable; import io.prometheus.client.Counter; import io.prometheus.cloudwatch.DataGetter.MetricRuleData; -import java.io.FileReader; -import java.io.IOException; -import java.io.Reader; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -32,15 +15,23 @@ import software.amazon.awssdk.services.cloudwatch.model.Statistic; import software.amazon.awssdk.services.resourcegroupstaggingapi.ResourceGroupsTaggingApiClient; import software.amazon.awssdk.services.resourcegroupstaggingapi.ResourceGroupsTaggingApiClientBuilder; -import software.amazon.awssdk.services.resourcegroupstaggingapi.model.GetResourcesRequest; -import software.amazon.awssdk.services.resourcegroupstaggingapi.model.GetResourcesResponse; -import software.amazon.awssdk.services.resourcegroupstaggingapi.model.ResourceTagMapping; -import software.amazon.awssdk.services.resourcegroupstaggingapi.model.Tag; -import software.amazon.awssdk.services.resourcegroupstaggingapi.model.TagFilter; +import software.amazon.awssdk.services.resourcegroupstaggingapi.model.*; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider; import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static io.prometheus.cloudwatch.CachingDimensionSource.DimensionCacheConfig; + public class CloudWatchCollector extends Collector implements Describable { private static final Logger LOGGER = Logger.getLogger(CloudWatchCollector.class.getName()); @@ -50,11 +41,14 @@ static class ActiveConfig { ResourceGroupsTaggingApiClient taggingClient; DimensionSource dimensionSource; + Map globalConfig; + public ActiveConfig(ActiveConfig cfg) { this.rules = new ArrayList<>(cfg.rules); this.cloudWatchClient = cfg.cloudWatchClient; this.taggingClient = cfg.taggingClient; this.dimensionSource = cfg.dimensionSource; + this.globalConfig = cfg.globalConfig; } public ActiveConfig() {} @@ -96,29 +90,32 @@ static class AWSTagSelect { "ReadThrottleEvents", "WriteThrottleEvents"); public CloudWatchCollector(Reader in) { - loadConfig(in, null, null); + loadConfig(in, null, null, null); } public CloudWatchCollector(String yamlConfig) { - this(yamlConfig, null, null); + this(yamlConfig, null, null, null); } /* For unittests. */ protected CloudWatchCollector( String jsonConfig, CloudWatchClient cloudWatchClient, - ResourceGroupsTaggingApiClient taggingClient) { + ResourceGroupsTaggingApiClient taggingClient, + Map globalConfig) { this( (Map) new Yaml(new SafeConstructor(new LoaderOptions())).load(jsonConfig), cloudWatchClient, - taggingClient); + taggingClient, + globalConfig); } private CloudWatchCollector( Map config, CloudWatchClient cloudWatchClient, - ResourceGroupsTaggingApiClient taggingClient) { - loadConfig(config, cloudWatchClient, taggingClient); + ResourceGroupsTaggingApiClient taggingClient, + Map globalConfig) { + loadConfig(config, cloudWatchClient, taggingClient, globalConfig); } @Override @@ -129,26 +126,33 @@ public List describe() { protected void reloadConfig() throws IOException { LOGGER.log(Level.INFO, "Reloading configuration"); try (FileReader reader = new FileReader(WebServer.configFilePath); ) { - loadConfig(reader, activeConfig.cloudWatchClient, activeConfig.taggingClient); + loadConfig(reader, activeConfig.cloudWatchClient, activeConfig.taggingClient, activeConfig.globalConfig); } } protected void loadConfig( - Reader in, CloudWatchClient cloudWatchClient, ResourceGroupsTaggingApiClient taggingClient) { + Reader in, CloudWatchClient cloudWatchClient, ResourceGroupsTaggingApiClient taggingClient, Map globalConfig) { loadConfig( (Map) new Yaml(new SafeConstructor(new LoaderOptions())).load(in), cloudWatchClient, - taggingClient); + taggingClient, + globalConfig); } private void loadConfig( Map config, CloudWatchClient cloudWatchClient, - ResourceGroupsTaggingApiClient taggingClient) { + ResourceGroupsTaggingApiClient taggingClient, + Map globalConfig) { if (config == null) { // Yaml config empty, set config to empty map. config = new HashMap<>(); } + if (globalConfig == null) { // Yaml config empty, set config to empty map. + globalConfig = new HashMap<>(); + } + + int defaultPeriod = 60; if (config.containsKey("period_seconds")) { defaultPeriod = ((Number) config.get("period_seconds")).intValue(); @@ -178,6 +182,12 @@ private void loadConfig( Duration.ofSeconds(((Number) config.get("list_metrics_cache_ttl")).intValue()); } + int defaultGlobalCacheSeconds = 0; + if (config.containsKey("global_cache_ttl")) { + defaultGlobalCacheSeconds = ((Number) config.get("global_cache_ttl")).intValue(); + } + globalConfig.put("globalCacheSeconds", defaultGlobalCacheSeconds); + boolean defaultWarnOnMissingDimensions = false; if (config.containsKey("warn_on_empty_list_dimensions")) { defaultWarnOnMissingDimensions = (Boolean) config.get("warn_on_empty_list_dimensions"); @@ -331,19 +341,21 @@ private void loadConfig( dimensionSource = new CachingDimensionSource(dimensionSource, metricCacheConfig); } - loadConfig(rules, cloudWatchClient, taggingClient, dimensionSource); + loadConfig(rules, cloudWatchClient, taggingClient, dimensionSource, globalConfig); } private void loadConfig( - ArrayList rules, - CloudWatchClient cloudWatchClient, - ResourceGroupsTaggingApiClient taggingClient, - DimensionSource dimensionSource) { + ArrayList rules, + CloudWatchClient cloudWatchClient, + ResourceGroupsTaggingApiClient taggingClient, + DimensionSource dimensionSource, + Map globalConfig) { synchronized (activeConfig) { activeConfig.cloudWatchClient = cloudWatchClient; activeConfig.taggingClient = taggingClient; activeConfig.rules = rules; activeConfig.dimensionSource = dimensionSource; + activeConfig.globalConfig = globalConfig; } } @@ -633,11 +645,42 @@ private void scrape(List mfs) { "AWS information available for resource", infoSamples)); } + private void updateCacheMetric(List mfs, double value){ + List samples = new ArrayList<>(); + MetricFamilySamples cacheMetric = null; + for(MetricFamilySamples metric : mfs){ + if (metric.name.equals("cloudwatch_exporter_cached_answer")){ + cacheMetric = metric; + break; + } + } + + if(cacheMetric == null){ + cacheMetric = new MetricFamilySamples( + "cloudwatch_exporter_cached_answer", + Type.GAUGE, + "Non-zero means this scrape was from cache", + samples); + mfs.add(cacheMetric); + }else{ + cacheMetric.samples.clear(); + } + cacheMetric.samples.add(new MetricFamilySamples.Sample( + "cloudwatch_exporter_cached_answer", new ArrayList<>(), new ArrayList<>(), value)); + } + List cachedMfs = new ArrayList<>(); public List collect() { long start = System.nanoTime(); double error = 0; List mfs = new ArrayList<>(); + + if (shouldCache() && shouldReturnFromCache()){ + LOGGER.log(Level.INFO, "Returning from cache"); + this.updateCacheMetric(this.cachedMfs, 1.0); + return this.cachedMfs; + } + this.updateCacheMetric(mfs, 0.0); try { scrape(mfs); } catch (Exception e) { @@ -668,8 +711,23 @@ public List collect() { Type.GAUGE, "Non-zero if this scrape failed.", samples)); + if (shouldCache()){ + this.cachedMfs = mfs; + } + this.lastCall = Instant.now(); return mfs; } + public Instant lastCall; + private boolean shouldCache() { + return (int) this.activeConfig.globalConfig.get("globalCacheSeconds") > 0; + } + private boolean shouldReturnFromCache() { + if (this.lastCall == null){ + return false; + } + Duration elapsedTime = Duration.between(lastCall, Instant.now()); + return elapsedTime.toSeconds() <= (int) this.activeConfig.globalConfig.get("globalCacheSeconds"); + } private String extractResourceIdFromArn(String arn) { // ARN parsing is based on diff --git a/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java b/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java index 1ae8f286..97643fd8 100644 --- a/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java +++ b/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java @@ -13,14 +13,10 @@ import io.prometheus.client.CollectorRegistry; import io.prometheus.cloudwatch.RequestsMatchers.*; import java.time.Instant; -import java.util.Arrays; -import java.util.Date; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Properties; -import java.util.Set; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; +import java.util.*; + import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -49,7 +45,8 @@ public void testMetricPeriod() { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n period_seconds: 100\n range_seconds: 200\n delay_seconds: 300", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when(cloudWatchClient.getMetricStatistics((GetMetricStatisticsRequest) any())) @@ -72,7 +69,8 @@ public void testMetricPeriodUsingGetMetricData() { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n period_seconds: 100\n range_seconds: 200\n delay_seconds: 300\n use_get_metric_data: true\n", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when(cloudWatchClient.getMetricStatistics((GetMetricStatisticsRequest) any())) @@ -107,7 +105,8 @@ public void testDefaultPeriod() { new CloudWatchCollector( "---\nregion: reg\nperiod_seconds: 100\nrange_seconds: 200\ndelay_seconds: 300\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when(cloudWatchClient.getMetricStatistics((GetMetricStatisticsRequest) any())) @@ -131,7 +130,8 @@ public void testAllStatistics() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -195,7 +195,8 @@ public void testAllStatisticsUsingGetMetricData() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n use_get_metric_data: true\n", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); List timestamps = List.of(new Date().toInstant()); MetricMatcher metricMatcher = @@ -304,7 +305,8 @@ public void testCloudwatchTimestamps() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n set_timestamp: true\n- aws_namespace: AWS/ELB\n aws_metric_name: HTTPCode_Backend_2XX\n set_timestamp: false", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Date timestamp = new Date(); @@ -359,7 +361,8 @@ public void testUsesNewestDatapoint() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -390,7 +393,8 @@ public void testDimensions() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n aws_dimensions:\n - AvailabilityZone\n - LoadBalancerName", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -486,7 +490,8 @@ public void testDimensionSelect() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n aws_dimensions:\n - AvailabilityZone\n - LoadBalancerName\n aws_dimension_select:\n LoadBalancerName:\n - myLB", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( cloudWatchClient.listMetrics( @@ -572,7 +577,8 @@ public void testAllSelectDimensionsKnown() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n aws_dimensions:\n - AvailabilityZone\n - LoadBalancerName\n aws_dimension_select:\n LoadBalancerName:\n - myLB\n AvailabilityZone:\n - a\n - b", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( cloudWatchClient.getMetricStatistics( @@ -630,7 +636,8 @@ public void testAllSelectDimensionsKnownUsingGetMetricData() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n aws_dimensions:\n - AvailabilityZone\n - LoadBalancerName\n aws_dimension_select:\n LoadBalancerName:\n - myLB\n AvailabilityZone:\n - a\n - b\n use_get_metric_data: true\n", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); List timestamps = List.of(new Date().toInstant()); MetricMatcher firstMetric = @@ -704,7 +711,8 @@ public void testDimensionSelectRegex() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n aws_dimensions:\n - AvailabilityZone\n - LoadBalancerName\n aws_dimension_select_regex:\n LoadBalancerName:\n - myLB(.*)", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -791,7 +799,8 @@ public void testGetDimensionsUsesNextToken() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n aws_dimensions:\n - AvailabilityZone\n - LoadBalancerName\n aws_dimension_select:\n LoadBalancerName:\n - myLB", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -851,7 +860,8 @@ public void testExtendedStatistics() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: Latency\n aws_extended_statistics:\n - p95\n - p99.99", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); HashMap extendedStatistics = new HashMap(); @@ -892,7 +902,8 @@ public void testDynamoIndexDimensions() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/DynamoDB\n aws_metric_name: ConsumedReadCapacityUnits\n aws_dimensions:\n - TableName\n - GlobalSecondaryIndexName\n- aws_namespace: AWS/DynamoDB\n aws_metric_name: OnlineIndexConsumedWriteCapacity\n aws_dimensions:\n - TableName\n - GlobalSecondaryIndexName\n- aws_namespace: AWS/DynamoDB\n aws_metric_name: ConsumedReadCapacityUnits\n aws_dimensions:\n - TableName", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( cloudWatchClient.listMetrics( @@ -1017,7 +1028,8 @@ public void testDynamoNoDimensions() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/DynamoDB\n aws_metric_name: AccountProvisionedReadCapacityUtilization\n", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -1047,7 +1059,8 @@ public void testTagSelectEC2() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/EC2\n aws_metric_name: CPUUtilization\n aws_dimensions:\n - InstanceId\n aws_tag_select:\n resource_type_selection: \"ec2:instance\"\n resource_id_dimension: InstanceId\n tag_selections:\n Monitoring: [enabled]\n", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -1142,7 +1155,8 @@ public void testTagSelectALB() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ApplicationELB\n aws_metric_name: RequestCount\n aws_dimensions:\n - AvailabilityZone\n - LoadBalancer\n aws_tag_select:\n resource_type_selection: \"elasticloadbalancing:loadbalancer/app\"\n resource_id_dimension: LoadBalancer\n tag_selections:\n Monitoring: [enabled]\n", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -1278,7 +1292,8 @@ public void testTagSelectUsesPaginationToken() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/EC2\n aws_metric_name: CPUUtilization\n aws_dimensions:\n - InstanceId\n aws_tag_select:\n resource_type_selection: \"ec2:instance\"\n resource_id_dimension: InstanceId\n tag_selections:\n Monitoring: [enabled]\n", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -1402,7 +1417,8 @@ public void testNoSelection() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/EC2\n aws_metric_name: CPUUtilization\n aws_dimensions:\n - InstanceId\n", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -1474,7 +1490,8 @@ public void testMultipleSelection() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/EC2\n aws_metric_name: CPUUtilization\n aws_dimensions:\n - InstanceId\n aws_tag_select:\n resource_type_selection: \"ec2:instance\"\n resource_id_dimension: InstanceId\n tag_selections:\n Monitoring: [enabled]\n aws_dimension_select:\n InstanceId: [\"i-1\"]", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -1583,7 +1600,8 @@ public void testOptionalTagSelection() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/EC2\n aws_metric_name: CPUUtilization\n aws_dimensions:\n - InstanceId\n aws_tag_select:\n resource_type_selection: \"ec2:instance\"\n resource_id_dimension: InstanceId\n aws_dimension_select:\n InstanceId: [\"i-1\", \"i-no-tag\"]", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -1723,7 +1741,8 @@ public void testNotRecentlyActive() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n aws_dimensions:\n - AvailabilityZone\n - LoadBalancerName\n range_seconds: 12000", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -1829,7 +1848,8 @@ public void testDimensionsWithDefaultCache() throws Exception { new CloudWatchCollector( "---\nregion: reg\nlist_metrics_cache_ttl: 500\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n aws_dimensions:\n - AvailabilityZone\n - LoadBalancerName", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -1890,7 +1910,8 @@ public void testDimensionsWithMetricLevelCache() throws Exception { new CloudWatchCollector( "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n list_metrics_cache_ttl: 500\n aws_dimensions:\n - AvailabilityZone\n - LoadBalancerName", cloudWatchClient, - taggingClient) + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( @@ -1945,4 +1966,76 @@ public void testDimensionsWithMetricLevelCache() throws Exception { Mockito.verify(cloudWatchClient, times(2)) .getMetricStatistics(any(GetMetricStatisticsRequest.class)); } + @Test + public void testGlobalCacheCanCache() { + CloudWatchCollector cwc = new CloudWatchCollector( + "---\nregion: reg\nglobal_cache_ttl: 10\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount", + cloudWatchClient, + taggingClient, + new HashMap<>()) + .register(registry); + + Mockito.when( + cloudWatchClient.getMetricStatistics( + (GetMetricStatisticsRequest) + argThat( + new GetMetricStatisticsRequestMatcher() + .Namespace("AWS/ELB").MetricName("RequestCount")))) + .thenReturn( + GetMetricStatisticsResponse.builder() // First + .datapoints( + Datapoint.builder() + .timestamp(new Date().toInstant()) + .average(1.0) + .maximum(2.0) + .build()) + .build(), + GetMetricStatisticsResponse.builder() // Second + .datapoints( + Datapoint.builder() + .timestamp(new Date().toInstant()) + .average(2.0) + .maximum(4.0) + .build()) + .build(), + GetMetricStatisticsResponse.builder() // Third + .datapoints( + Datapoint.builder() + .timestamp(new Date().toInstant()) + .average(4.0) + .maximum(8.0) + .build()) + .build()); + + + for (Collector.MetricFamilySamples it : Collections.list(registry.metricFamilySamples())) { + if (it.name.equals("cloudwatch_exporter_cached_answer")) assertEquals(0.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_average")) assertEquals(1.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_maximum")) assertEquals(2.0, it.samples.get(0).value, .01); + } + + cwc.lastCall = Instant.now().minus(1, ChronoUnit.SECONDS); + + for (Collector.MetricFamilySamples it : Collections.list(registry.metricFamilySamples())) { + if (it.name.equals("cloudwatch_exporter_cached_answer")) assertEquals(1.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_average")) assertEquals(1.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_maximum")) assertEquals(2.0, it.samples.get(0).value, .01); + } + + cwc.lastCall = Instant.now().minus(11, ChronoUnit.SECONDS); + + for (Collector.MetricFamilySamples it : Collections.list(registry.metricFamilySamples())) { + if (it.name.equals("cloudwatch_exporter_cached_answer")) assertEquals(0.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_average")) assertEquals(2.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_maximum")) assertEquals(4.0, it.samples.get(0).value, .01); + } + + cwc.lastCall = Instant.now().minus(11, ChronoUnit.SECONDS); + + for (Collector.MetricFamilySamples it : Collections.list(registry.metricFamilySamples())) { + if (it.name.equals("cloudwatch_exporter_cached_answer")) assertEquals(0.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_average")) assertEquals(4.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_maximum")) assertEquals(8.0, it.samples.get(0).value, .01); + } + } } From 680d018be03e34e1d0fd39250b85026ff78f3ff3 Mon Sep 17 00:00:00 2001 From: Victor Amorim Date: Thu, 14 Dec 2023 18:49:40 -0300 Subject: [PATCH 2/3] formating Signed-off-by: Victor Amorim --- .../cloudwatch/CloudWatchCollector.java | 76 +++++++++++-------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java b/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java index 03450856..06896b9d 100644 --- a/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java +++ b/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java @@ -1,9 +1,20 @@ package io.prometheus.cloudwatch; +import static io.prometheus.cloudwatch.CachingDimensionSource.DimensionCacheConfig; + import io.prometheus.client.Collector; import io.prometheus.client.Collector.Describable; import io.prometheus.client.Counter; import io.prometheus.cloudwatch.DataGetter.MetricRuleData; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -20,18 +31,6 @@ import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider; import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; -import java.io.FileReader; -import java.io.IOException; -import java.io.Reader; -import java.time.Duration; -import java.time.Instant; -import java.util.*; -import java.util.Map.Entry; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static io.prometheus.cloudwatch.CachingDimensionSource.DimensionCacheConfig; - public class CloudWatchCollector extends Collector implements Describable { private static final Logger LOGGER = Logger.getLogger(CloudWatchCollector.class.getName()); @@ -126,12 +125,19 @@ public List describe() { protected void reloadConfig() throws IOException { LOGGER.log(Level.INFO, "Reloading configuration"); try (FileReader reader = new FileReader(WebServer.configFilePath); ) { - loadConfig(reader, activeConfig.cloudWatchClient, activeConfig.taggingClient, activeConfig.globalConfig); + loadConfig( + reader, + activeConfig.cloudWatchClient, + activeConfig.taggingClient, + activeConfig.globalConfig); } } protected void loadConfig( - Reader in, CloudWatchClient cloudWatchClient, ResourceGroupsTaggingApiClient taggingClient, Map globalConfig) { + Reader in, + CloudWatchClient cloudWatchClient, + ResourceGroupsTaggingApiClient taggingClient, + Map globalConfig) { loadConfig( (Map) new Yaml(new SafeConstructor(new LoaderOptions())).load(in), cloudWatchClient, @@ -152,7 +158,6 @@ private void loadConfig( globalConfig = new HashMap<>(); } - int defaultPeriod = 60; if (config.containsKey("period_seconds")) { defaultPeriod = ((Number) config.get("period_seconds")).intValue(); @@ -345,11 +350,11 @@ private void loadConfig( } private void loadConfig( - ArrayList rules, - CloudWatchClient cloudWatchClient, - ResourceGroupsTaggingApiClient taggingClient, - DimensionSource dimensionSource, - Map globalConfig) { + ArrayList rules, + CloudWatchClient cloudWatchClient, + ResourceGroupsTaggingApiClient taggingClient, + DimensionSource dimensionSource, + Map globalConfig) { synchronized (activeConfig) { activeConfig.cloudWatchClient = cloudWatchClient; activeConfig.taggingClient = taggingClient; @@ -645,37 +650,42 @@ private void scrape(List mfs) { "AWS information available for resource", infoSamples)); } - private void updateCacheMetric(List mfs, double value){ + + private void updateCacheMetric(List mfs, double value) { List samples = new ArrayList<>(); MetricFamilySamples cacheMetric = null; - for(MetricFamilySamples metric : mfs){ - if (metric.name.equals("cloudwatch_exporter_cached_answer")){ + for (MetricFamilySamples metric : mfs) { + if (metric.name.equals("cloudwatch_exporter_cached_answer")) { cacheMetric = metric; break; } } - if(cacheMetric == null){ - cacheMetric = new MetricFamilySamples( + if (cacheMetric == null) { + cacheMetric = + new MetricFamilySamples( "cloudwatch_exporter_cached_answer", Type.GAUGE, "Non-zero means this scrape was from cache", samples); mfs.add(cacheMetric); - }else{ + } else { cacheMetric.samples.clear(); } - cacheMetric.samples.add(new MetricFamilySamples.Sample( + cacheMetric.samples.add( + new MetricFamilySamples.Sample( "cloudwatch_exporter_cached_answer", new ArrayList<>(), new ArrayList<>(), value)); } + List cachedMfs = new ArrayList<>(); + public List collect() { long start = System.nanoTime(); double error = 0; List mfs = new ArrayList<>(); - if (shouldCache() && shouldReturnFromCache()){ + if (shouldCache() && shouldReturnFromCache()) { LOGGER.log(Level.INFO, "Returning from cache"); this.updateCacheMetric(this.cachedMfs, 1.0); return this.cachedMfs; @@ -711,22 +721,26 @@ public List collect() { Type.GAUGE, "Non-zero if this scrape failed.", samples)); - if (shouldCache()){ + if (shouldCache()) { this.cachedMfs = mfs; } this.lastCall = Instant.now(); return mfs; } + public Instant lastCall; + private boolean shouldCache() { return (int) this.activeConfig.globalConfig.get("globalCacheSeconds") > 0; } + private boolean shouldReturnFromCache() { - if (this.lastCall == null){ + if (this.lastCall == null) { return false; } Duration elapsedTime = Duration.between(lastCall, Instant.now()); - return elapsedTime.toSeconds() <= (int) this.activeConfig.globalConfig.get("globalCacheSeconds"); + return elapsedTime.toSeconds() + <= (int) this.activeConfig.globalConfig.get("globalCacheSeconds"); } private String extractResourceIdFromArn(String arn) { From c7e2d4ecb699798ce6eeb2ec3033bd111e50d07a Mon Sep 17 00:00:00 2001 From: Victor Amorim Date: Thu, 14 Dec 2023 18:50:16 -0300 Subject: [PATCH 3/3] formating Signed-off-by: Victor Amorim --- .../cloudwatch/CloudWatchCollectorTest.java | 111 ++++++++++-------- 1 file changed, 61 insertions(+), 50 deletions(-) diff --git a/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java b/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java index 97643fd8..269e76ac 100644 --- a/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java +++ b/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java @@ -14,9 +14,7 @@ import io.prometheus.cloudwatch.RequestsMatchers.*; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.time.temporal.TemporalUnit; import java.util.*; - import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -1966,76 +1964,89 @@ public void testDimensionsWithMetricLevelCache() throws Exception { Mockito.verify(cloudWatchClient, times(2)) .getMetricStatistics(any(GetMetricStatisticsRequest.class)); } + @Test public void testGlobalCacheCanCache() { - CloudWatchCollector cwc = new CloudWatchCollector( - "---\nregion: reg\nglobal_cache_ttl: 10\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount", - cloudWatchClient, - taggingClient, - new HashMap<>()) + CloudWatchCollector cwc = + new CloudWatchCollector( + "---\nregion: reg\nglobal_cache_ttl: 10\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount", + cloudWatchClient, + taggingClient, + new HashMap<>()) .register(registry); Mockito.when( - cloudWatchClient.getMetricStatistics( - (GetMetricStatisticsRequest) - argThat( - new GetMetricStatisticsRequestMatcher() - .Namespace("AWS/ELB").MetricName("RequestCount")))) - .thenReturn( - GetMetricStatisticsResponse.builder() // First - .datapoints( - Datapoint.builder() - .timestamp(new Date().toInstant()) - .average(1.0) - .maximum(2.0) - .build()) - .build(), - GetMetricStatisticsResponse.builder() // Second - .datapoints( - Datapoint.builder() - .timestamp(new Date().toInstant()) - .average(2.0) - .maximum(4.0) - .build()) - .build(), - GetMetricStatisticsResponse.builder() // Third - .datapoints( - Datapoint.builder() - .timestamp(new Date().toInstant()) - .average(4.0) - .maximum(8.0) - .build()) - .build()); - + cloudWatchClient.getMetricStatistics( + (GetMetricStatisticsRequest) + argThat( + new GetMetricStatisticsRequestMatcher() + .Namespace("AWS/ELB").MetricName("RequestCount")))) + .thenReturn( + GetMetricStatisticsResponse.builder() // First + .datapoints( + Datapoint.builder() + .timestamp(new Date().toInstant()) + .average(1.0) + .maximum(2.0) + .build()) + .build(), + GetMetricStatisticsResponse.builder() // Second + .datapoints( + Datapoint.builder() + .timestamp(new Date().toInstant()) + .average(2.0) + .maximum(4.0) + .build()) + .build(), + GetMetricStatisticsResponse.builder() // Third + .datapoints( + Datapoint.builder() + .timestamp(new Date().toInstant()) + .average(4.0) + .maximum(8.0) + .build()) + .build()); for (Collector.MetricFamilySamples it : Collections.list(registry.metricFamilySamples())) { - if (it.name.equals("cloudwatch_exporter_cached_answer")) assertEquals(0.0, it.samples.get(0).value, .01); - if (it.name.equals("aws_elb_request_count_average")) assertEquals(1.0, it.samples.get(0).value, .01); - if (it.name.equals("aws_elb_request_count_maximum")) assertEquals(2.0, it.samples.get(0).value, .01); + if (it.name.equals("cloudwatch_exporter_cached_answer")) + assertEquals(0.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_average")) + assertEquals(1.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_maximum")) + assertEquals(2.0, it.samples.get(0).value, .01); } cwc.lastCall = Instant.now().minus(1, ChronoUnit.SECONDS); for (Collector.MetricFamilySamples it : Collections.list(registry.metricFamilySamples())) { - if (it.name.equals("cloudwatch_exporter_cached_answer")) assertEquals(1.0, it.samples.get(0).value, .01); - if (it.name.equals("aws_elb_request_count_average")) assertEquals(1.0, it.samples.get(0).value, .01); - if (it.name.equals("aws_elb_request_count_maximum")) assertEquals(2.0, it.samples.get(0).value, .01); + if (it.name.equals("cloudwatch_exporter_cached_answer")) + assertEquals(1.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_average")) + assertEquals(1.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_maximum")) + assertEquals(2.0, it.samples.get(0).value, .01); } cwc.lastCall = Instant.now().minus(11, ChronoUnit.SECONDS); for (Collector.MetricFamilySamples it : Collections.list(registry.metricFamilySamples())) { - if (it.name.equals("cloudwatch_exporter_cached_answer")) assertEquals(0.0, it.samples.get(0).value, .01); - if (it.name.equals("aws_elb_request_count_average")) assertEquals(2.0, it.samples.get(0).value, .01); - if (it.name.equals("aws_elb_request_count_maximum")) assertEquals(4.0, it.samples.get(0).value, .01); + if (it.name.equals("cloudwatch_exporter_cached_answer")) + assertEquals(0.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_average")) + assertEquals(2.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_maximum")) + assertEquals(4.0, it.samples.get(0).value, .01); } cwc.lastCall = Instant.now().minus(11, ChronoUnit.SECONDS); for (Collector.MetricFamilySamples it : Collections.list(registry.metricFamilySamples())) { - if (it.name.equals("cloudwatch_exporter_cached_answer")) assertEquals(0.0, it.samples.get(0).value, .01); - if (it.name.equals("aws_elb_request_count_average")) assertEquals(4.0, it.samples.get(0).value, .01); - if (it.name.equals("aws_elb_request_count_maximum")) assertEquals(8.0, it.samples.get(0).value, .01); + if (it.name.equals("cloudwatch_exporter_cached_answer")) + assertEquals(0.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_average")) + assertEquals(4.0, it.samples.get(0).value, .01); + if (it.name.equals("aws_elb_request_count_maximum")) + assertEquals(8.0, it.samples.get(0).value, .01); } } }