diff --git a/kayenta-stackdriver/kayenta-stackdriver.gradle b/kayenta-stackdriver/kayenta-stackdriver.gradle index 86ec59f9a..b77e6ce6c 100644 --- a/kayenta-stackdriver/kayenta-stackdriver.gradle +++ b/kayenta-stackdriver/kayenta-stackdriver.gradle @@ -2,4 +2,5 @@ dependencies { implementation project(":kayenta-core") implementation project(":kayenta-google") api "com.google.apis:google-api-services-monitoring" + testImplementation 'org.mockito:mockito-inline' } diff --git a/kayenta-stackdriver/src/main/java/com/netflix/kayenta/stackdriver/metrics/StackdriverMetricsService.java b/kayenta-stackdriver/src/main/java/com/netflix/kayenta/stackdriver/metrics/StackdriverMetricsService.java index f7254ff6a..17f10d14d 100644 --- a/kayenta-stackdriver/src/main/java/com/netflix/kayenta/stackdriver/metrics/StackdriverMetricsService.java +++ b/kayenta-stackdriver/src/main/java/com/netflix/kayenta/stackdriver/metrics/StackdriverMetricsService.java @@ -398,11 +398,37 @@ public List queryMetrics( ? Instant.parse(points.get(points.size() - 1).getInterval().getEndTime()) : stackdriverCanaryScope.getEnd(); - // TODO(duftler): What if there are no data points? - List pointValues = - points.stream() - .map(point -> point.getValue().getDoubleValue()) - .collect(Collectors.toList()); + List pointValues; + + if (points.isEmpty()) { + log.warn("No data points available."); + pointValues = Collections.emptyList(); + } else { + if (timeSeries.getValueType() != null) { + if (timeSeries.getValueType().equals("INT64")) { + pointValues = + points.stream() + .map(point -> (double) point.getValue().getInt64Value()) + .collect(Collectors.toList()); + } else if (timeSeries.getValueType().equals("DOUBLE")) { + pointValues = + points.stream() + .map(point -> point.getValue().getDoubleValue()) + .collect(Collectors.toList()); + } else { + log.warn( + "Expected timeSeries value type to be either DOUBLE or INT64. Got {}.", + timeSeries.getValueType()); + pointValues = + points.stream() + .map(point -> point.getValue().getDoubleValue()) + .collect(Collectors.toList()); + } + } else { + log.warn("timeSeries valueType is null."); + pointValues = Collections.emptyList(); // Handle null valueType case as well + } + } MetricSet.MetricSetBuilder metricSetBuilder = MetricSet.builder() diff --git a/kayenta-stackdriver/src/test/java/com/netflix/kayenta/stackdriver/metrics/StackdriverMetricsServiceTest.java b/kayenta-stackdriver/src/test/java/com/netflix/kayenta/stackdriver/metrics/StackdriverMetricsServiceTest.java new file mode 100644 index 000000000..a393e026f --- /dev/null +++ b/kayenta-stackdriver/src/test/java/com/netflix/kayenta/stackdriver/metrics/StackdriverMetricsServiceTest.java @@ -0,0 +1,272 @@ +package com.netflix.kayenta.stackdriver.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.api.services.monitoring.v3.Monitoring; +import com.google.api.services.monitoring.v3.model.ListMetricDescriptorsResponse; +import com.google.api.services.monitoring.v3.model.ListTimeSeriesResponse; +import com.google.api.services.monitoring.v3.model.MetricDescriptor; +import com.google.api.services.monitoring.v3.model.Point; +import com.google.api.services.monitoring.v3.model.TimeInterval; +import com.google.api.services.monitoring.v3.model.TimeSeries; +import com.google.api.services.monitoring.v3.model.TypedValue; +import com.netflix.kayenta.canary.CanaryConfig; +import com.netflix.kayenta.canary.CanaryMetricConfig; +import com.netflix.kayenta.canary.providers.metrics.StackdriverCanaryMetricSetQueryConfig; +import com.netflix.kayenta.google.security.GoogleNamedAccountCredentials; +import com.netflix.kayenta.metrics.MetricSet; +import com.netflix.kayenta.security.AccountCredentials; +import com.netflix.kayenta.security.AccountCredentialsRepository; +import com.netflix.kayenta.stackdriver.canary.StackdriverCanaryScope; +import com.netflix.spectator.api.DefaultRegistry; +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class StackdriverMetricsServiceTest { + + private static final String ACCOUNT = "test-account"; + + private AccountCredentialsRepository accountCredentialsRepoMock; + + private StackdriverMetricsService stackdriverMetricsService; + + @BeforeEach + void setup() { + StackdriverMetricsService.StackdriverMetricsServiceBuilder stackdriverMetricsServiceBuilder = + StackdriverMetricsService.builder(); + accountCredentialsRepoMock = mock(AccountCredentialsRepository.class); + stackdriverMetricsServiceBuilder + .accountCredentialsRepository(accountCredentialsRepoMock) + .registry(new DefaultRegistry()); + stackdriverMetricsService = stackdriverMetricsServiceBuilder.build(); + } + + @Test + void readsInt64Metrics() throws IOException { + GoogleNamedAccountCredentials stackdriverCredentialsMock = + mock(GoogleNamedAccountCredentials.class); + when(accountCredentialsRepoMock.getRequiredOne(ACCOUNT)).thenReturn(stackdriverCredentialsMock); + + Monitoring monitoringMock = mock(Monitoring.class, Mockito.RETURNS_DEEP_STUBS); + when(stackdriverCredentialsMock.getMonitoring()).thenReturn(monitoringMock); + + Monitoring.Projects.TimeSeries.List timeSeriesListMock = + mock(Monitoring.Projects.TimeSeries.List.class); + + when(monitoringMock + .projects() + .timeSeries() + .list(anyString()) + .setAggregationAlignmentPeriod(anyString()) + .setAggregationCrossSeriesReducer(anyString()) + .setAggregationPerSeriesAligner(anyString()) + .setFilter(anyString()) + .setIntervalStartTime(anyString()) + .setIntervalEndTime(anyString())) + .thenReturn(timeSeriesListMock); + + ListTimeSeriesResponse responseMock = mock(ListTimeSeriesResponse.class); + when(timeSeriesListMock.execute()).thenReturn(responseMock); + + List timeSeriesListWithInt64Points = new ArrayList(); + + // Create a time series with INT64 points + List int64Points = new ArrayList(); + int64Points.add( + new Point() + .setValue(new TypedValue().setInt64Value((Long) 64l)) + .setInterval( + new TimeInterval() + .setStartTime("1970-01-01T00:00:00.00Z") + .setEndTime("1970-01-01T00:00:01.00Z"))); + TimeSeries timeSeriesWithInt64Points = + new TimeSeries().setValueType("INT64").setPoints(int64Points); + timeSeriesListWithInt64Points.add(timeSeriesWithInt64Points); + + when(responseMock.getTimeSeries()).thenReturn(timeSeriesListWithInt64Points); + + CanaryConfig canaryConfig = new CanaryConfig(); + CanaryMetricConfig canaryMetricConfig = + CanaryMetricConfig.builder() + .name("metricConfig") + .query( + StackdriverCanaryMetricSetQueryConfig.builder() + .resourceType("global") + .metricType("instance") + .build()) + .build(); + + StackdriverCanaryScope canaryScope = new StackdriverCanaryScope(); + canaryScope.setStart(Instant.EPOCH).setEnd(Instant.EPOCH.plusSeconds(1)).setStep(1l); + canaryScope.setProject("my-project"); + List queriedMetrics = + stackdriverMetricsService.queryMetrics( + ACCOUNT, canaryConfig, canaryMetricConfig, canaryScope); + + assertThat(queriedMetrics.get(0).getValues()).contains(64d); + } + + @Test + void returnsSingleMetricDescriptorInCache() throws IOException { + GoogleNamedAccountCredentials googleAccountCredentialsMock = + mock(GoogleNamedAccountCredentials.class, Mockito.RETURNS_DEEP_STUBS); + + Set accountCredentialsSetMock = new HashSet<>(); + accountCredentialsSetMock.add(googleAccountCredentialsMock); + + when(accountCredentialsRepoMock.getAllOf(AccountCredentials.Type.METRICS_STORE)) + .thenReturn(accountCredentialsSetMock); + + ListMetricDescriptorsResponse listMetricDescriptorsResponseMock = + mock(ListMetricDescriptorsResponse.class); + when(googleAccountCredentialsMock + .getMonitoring() + .projects() + .metricDescriptors() + .list(anyString()) + .execute()) + .thenReturn(listMetricDescriptorsResponseMock); + + List metricDesciprtorMockList = new ArrayList(); + + MetricDescriptor exampleMetricDescriptor = new MetricDescriptor(); + metricDesciprtorMockList.add(exampleMetricDescriptor); + when(listMetricDescriptorsResponseMock.getMetricDescriptors()) + .thenReturn(metricDesciprtorMockList); + + stackdriverMetricsService.updateMetricDescriptorsCache(); + + List metadata = stackdriverMetricsService.getMetadata(ACCOUNT, ""); + + assertThat(metadata).containsOnly(exampleMetricDescriptor); + } + + @Test + void handlesEmptyResponse() throws IOException { + GoogleNamedAccountCredentials stackdriverCredentialsMock = + mock(GoogleNamedAccountCredentials.class); + when(accountCredentialsRepoMock.getRequiredOne(ACCOUNT)).thenReturn(stackdriverCredentialsMock); + + Monitoring monitoringMock = mock(Monitoring.class, Mockito.RETURNS_DEEP_STUBS); + when(stackdriverCredentialsMock.getMonitoring()).thenReturn(monitoringMock); + + Monitoring.Projects.TimeSeries.List timeSeriesListMock = + mock(Monitoring.Projects.TimeSeries.List.class); + when(monitoringMock + .projects() + .timeSeries() + .list(anyString()) + .setAggregationAlignmentPeriod(anyString()) + .setAggregationCrossSeriesReducer(anyString()) + .setAggregationPerSeriesAligner(anyString()) + .setFilter(anyString()) + .setIntervalStartTime(anyString()) + .setIntervalEndTime(anyString())) + .thenReturn(timeSeriesListMock); + + ListTimeSeriesResponse responseMock = mock(ListTimeSeriesResponse.class); + when(timeSeriesListMock.execute()).thenReturn(responseMock); + + // Return an empty list for time series + when(responseMock.getTimeSeries()).thenReturn(Collections.emptyList()); + + CanaryConfig canaryConfig = new CanaryConfig(); + CanaryMetricConfig canaryMetricConfig = + CanaryMetricConfig.builder() + .name("metricConfig") + .query( + StackdriverCanaryMetricSetQueryConfig.builder() + .resourceType("global") + .metricType("instance") + .build()) + .build(); + + StackdriverCanaryScope canaryScope = new StackdriverCanaryScope(); + canaryScope.setStart(Instant.EPOCH).setEnd(Instant.EPOCH.plusSeconds(1)).setStep(1l); + canaryScope.setProject("my-project"); + List queriedMetrics = + stackdriverMetricsService.queryMetrics( + ACCOUNT, canaryConfig, canaryMetricConfig, canaryScope); + + // Verify that an empty metric set is returned + assertThat(queriedMetrics).hasSize(1); + assertThat(queriedMetrics.get(0).getValues()).isEmpty(); + } + + @Test + void handlesInvalidMetricType() throws IOException { + GoogleNamedAccountCredentials stackdriverCredentialsMock = + mock(GoogleNamedAccountCredentials.class); + when(accountCredentialsRepoMock.getRequiredOne(ACCOUNT)).thenReturn(stackdriverCredentialsMock); + + Monitoring monitoringMock = mock(Monitoring.class, Mockito.RETURNS_DEEP_STUBS); + when(stackdriverCredentialsMock.getMonitoring()).thenReturn(monitoringMock); + + Monitoring.Projects.TimeSeries.List timeSeriesListMock = + mock(Monitoring.Projects.TimeSeries.List.class); + when(monitoringMock + .projects() + .timeSeries() + .list(anyString()) + .setAggregationAlignmentPeriod(anyString()) + .setAggregationCrossSeriesReducer(anyString()) + .setAggregationPerSeriesAligner(anyString()) + .setFilter(anyString()) + .setIntervalStartTime(anyString()) + .setIntervalEndTime(anyString())) + .thenReturn(timeSeriesListMock); + + ListTimeSeriesResponse responseMock = mock(ListTimeSeriesResponse.class); + when(timeSeriesListMock.execute()).thenReturn(responseMock); + + List timeSeriesListWithInvalidMetricType = new ArrayList<>(); + + // Create a time series with an invalid metric type + List points = new ArrayList<>(); + points.add( + new Point() + .setValue(new TypedValue().setDoubleValue(3.14)) + .setInterval( + new TimeInterval() + .setStartTime("1970-01-01T00:00:00.00Z") + .setEndTime("1970-01-01T00:00:01.00Z"))); + TimeSeries timeSeriesWithInvalidMetricType = + new TimeSeries().setValueType("STRING").setPoints(points); + timeSeriesListWithInvalidMetricType.add(timeSeriesWithInvalidMetricType); + + when(responseMock.getTimeSeries()).thenReturn(timeSeriesListWithInvalidMetricType); + + CanaryConfig canaryConfig = new CanaryConfig(); + CanaryMetricConfig canaryMetricConfig = + CanaryMetricConfig.builder() + .name("metricConfig") + .query( + StackdriverCanaryMetricSetQueryConfig.builder() + .resourceType("global") + .metricType("instance") + .build()) + .build(); + + StackdriverCanaryScope canaryScope = new StackdriverCanaryScope(); + canaryScope.setStart(Instant.EPOCH).setEnd(Instant.EPOCH.plusSeconds(1)).setStep(1l); + canaryScope.setProject("my-project"); + List queriedMetrics = + stackdriverMetricsService.queryMetrics( + ACCOUNT, canaryConfig, canaryMetricConfig, canaryScope); + + // Verify that the values are extracted as Double + assertThat(queriedMetrics.get(0).getValues()).contains(3.14); + } +}