From d5841907d83525da3ddfd3a86d76f2764345994d Mon Sep 17 00:00:00 2001 From: Chris Rohr <51920+chrisrohr@users.noreply.github.com> Date: Mon, 4 Sep 2023 13:06:30 -0400 Subject: [PATCH] Add endpoint to retrieve build stats (#339) Update UI to use new endpoint for build stats --- .../java/org/kiwiproject/champagne/App.java | 2 + .../kiwiproject/champagne/dao/BuildDao.java | 4 + .../champagne/resource/MetricsResource.java | 50 ++++++++++++ .../champagne/dao/BuildDaoTest.java | 20 +++++ .../resource/MetricsResourceTest.java | 79 +++++++++++++++++++ ui/src/stores/stats.js | 8 +- 6 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 service/src/main/java/org/kiwiproject/champagne/resource/MetricsResource.java create mode 100644 service/src/test/java/org/kiwiproject/champagne/resource/MetricsResourceTest.java diff --git a/service/src/main/java/org/kiwiproject/champagne/App.java b/service/src/main/java/org/kiwiproject/champagne/App.java index 66da465b..cc5e18ca 100644 --- a/service/src/main/java/org/kiwiproject/champagne/App.java +++ b/service/src/main/java/org/kiwiproject/champagne/App.java @@ -40,6 +40,7 @@ import org.kiwiproject.champagne.resource.DeployableSystemResource; import org.kiwiproject.champagne.resource.DeploymentEnvironmentResource; import org.kiwiproject.champagne.resource.HostConfigurationResource; +import org.kiwiproject.champagne.resource.MetricsResource; import org.kiwiproject.champagne.resource.TagResource; import org.kiwiproject.champagne.resource.TaskResource; import org.kiwiproject.champagne.resource.UserResource; @@ -122,6 +123,7 @@ public void run(AppConfig configuration, Environment environment) { environment.jersey().register(new ApplicationErrorResource(errorDao)); environment.jersey().register(new DeployableSystemResource(deployableSystemDao, userDao, auditRecordDao, errorDao)); environment.jersey().register(new TagResource(tagDao, auditRecordDao, errorDao)); + environment.jersey().register(new MetricsResource(buildDao)); configureCors(environment); diff --git a/service/src/main/java/org/kiwiproject/champagne/dao/BuildDao.java b/service/src/main/java/org/kiwiproject/champagne/dao/BuildDao.java index 1300f455..dc988f6c 100644 --- a/service/src/main/java/org/kiwiproject/champagne/dao/BuildDao.java +++ b/service/src/main/java/org/kiwiproject/champagne/dao/BuildDao.java @@ -4,6 +4,7 @@ import static org.kiwiproject.base.KiwiStrings.f; import static org.kiwiproject.champagne.dao.DaoHelper.LIKE_QUERY_FORMAT; +import java.time.Instant; import java.util.List; import org.jdbi.v3.sqlobject.customizer.Bind; @@ -53,4 +54,7 @@ default long countBuilds(long systemId, String componentFilter) { + "(:repoNamespace, :repoName, :commitRef, :commitUser, :sourceBranch, :componentIdentifier, :componentVersion, :distributionLocation, :extraData, :changeLog, :gitProvider, :deployableSystemId)") @GetGeneratedKeys long insertBuild(@BindBean Build build, @Bind("extraData") String extraDataJson); + + @SqlQuery("select count(*) from builds where deployable_system_id = :systemId and created_at >= :start and created_at <= :end") + long countBuildsInSystemInRange(@Bind("systemId") long systemId, @Bind("start") Instant start, @Bind("end") Instant end); } diff --git a/service/src/main/java/org/kiwiproject/champagne/resource/MetricsResource.java b/service/src/main/java/org/kiwiproject/champagne/resource/MetricsResource.java new file mode 100644 index 00000000..0882a0bd --- /dev/null +++ b/service/src/main/java/org/kiwiproject/champagne/resource/MetricsResource.java @@ -0,0 +1,50 @@ +package org.kiwiproject.champagne.resource; + +import static org.kiwiproject.champagne.util.DeployableSystems.getSystemIdOrThrowBadRequest; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +import com.codahale.metrics.annotation.ExceptionMetered; +import com.codahale.metrics.annotation.Timed; +import jakarta.annotation.security.PermitAll; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.kiwiproject.champagne.dao.BuildDao; + +@Path("/metrics") +@Produces(MediaType.APPLICATION_JSON) +public class MetricsResource { + + private final BuildDao buildDao; + + public MetricsResource(BuildDao buildDao) { + this.buildDao = buildDao; + } + + @GET + @Timed + @ExceptionMetered + @PermitAll + public Response getMetrics() { + var systemId = getSystemIdOrThrowBadRequest(); + + var endOfToday = LocalDate.now().atTime(LocalTime.MAX); + var oneMonthAgo = endOfToday.minusDays(30).truncatedTo(ChronoUnit.DAYS); + + long thisMonthCount = buildDao.countBuildsInSystemInRange(systemId, oneMonthAgo.toInstant(ZoneOffset.UTC), endOfToday.toInstant(ZoneOffset.UTC)); + + var endOfLastMonth = oneMonthAgo.minusSeconds(1); + var startOfPreviousMonth = endOfLastMonth.minusDays(30).truncatedTo(ChronoUnit.DAYS); + + long lastMonthCount = buildDao.countBuildsInSystemInRange(systemId, startOfPreviousMonth.toInstant(ZoneOffset.UTC), endOfLastMonth.toInstant(ZoneOffset.UTC)); + + return Response.ok(Map.of("builds", Map.of("current", thisMonthCount, "previous", lastMonthCount))).build(); + } +} diff --git a/service/src/test/java/org/kiwiproject/champagne/dao/BuildDaoTest.java b/service/src/test/java/org/kiwiproject/champagne/dao/BuildDaoTest.java index a0d112c1..bdcd34b7 100644 --- a/service/src/test/java/org/kiwiproject/champagne/dao/BuildDaoTest.java +++ b/service/src/test/java/org/kiwiproject/champagne/dao/BuildDaoTest.java @@ -8,6 +8,7 @@ import static org.kiwiproject.test.constants.KiwiTestConstants.JSON_HELPER; import static org.kiwiproject.test.util.DateTimeTestHelper.assertTimeDifferenceWithinTolerance; +import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Map; @@ -143,5 +144,24 @@ void shouldReturnEmptyListWhenNoBuildsFound() { assertThat(builds).isZero(); } } + + @Nested + class CountBuildsInSystemInRange { + + @Test + void shouldReturnCountOfBuildsInRange() { + var systemId = insertDeployableSystem(handle, "kiwi"); + insertBuildRecord(handle, "champagne-service", "42.0", systemId); + + var builds = dao.countBuildsInSystemInRange(systemId, Instant.now().minusSeconds(60), Instant.now()); + assertThat(builds).isOne(); + } + + @Test + void shouldReturnEmptyListWhenNoBuildsFound() { + var builds = dao.countBuildsInSystemInRange(1L, Instant.now(), Instant.now()); + assertThat(builds).isZero(); + } + } } diff --git a/service/src/test/java/org/kiwiproject/champagne/resource/MetricsResourceTest.java b/service/src/test/java/org/kiwiproject/champagne/resource/MetricsResourceTest.java new file mode 100644 index 00000000..7e9fb036 --- /dev/null +++ b/service/src/test/java/org/kiwiproject/champagne/resource/MetricsResourceTest.java @@ -0,0 +1,79 @@ +package org.kiwiproject.champagne.resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.kiwiproject.test.jaxrs.JaxrsTestHelper.assertOkResponse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.Map; + +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import jakarta.ws.rs.core.GenericType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.kiwiproject.champagne.dao.BuildDao; +import org.kiwiproject.champagne.junit.jupiter.DeployableSystemExtension; +import org.kiwiproject.champagne.junit.jupiter.JwtExtension; +import org.kiwiproject.jaxrs.exception.JaxrsExceptionMapper; + +@DisplayName("MetricsResource") +@ExtendWith({DropwizardExtensionsSupport.class}) +class MetricsResourceTest { + + private static final BuildDao BUILD_DAO = mock(BuildDao.class); + + private static final MetricsResource RESOURCE = new MetricsResource(BUILD_DAO); + private static final ResourceExtension RESOURCES = ResourceExtension.builder() + .bootstrapLogging(false) + .addResource(RESOURCE) + .addProvider(JaxrsExceptionMapper.class) + .build(); + + @RegisterExtension + public final JwtExtension jwtExtension = new JwtExtension("bob"); + + @RegisterExtension + public final DeployableSystemExtension deployableSystemExtension = new DeployableSystemExtension(1L, true); + + @AfterEach + void cleanup() { + reset(BUILD_DAO); + } + + @Nested + class GetMetrics { + + @Test + void shouldReturnMetrics() { + when(BUILD_DAO.countBuildsInSystemInRange(eq(1L), any(Instant.class), any(Instant.class))) + .thenReturn(2L) + .thenReturn(1L); + + var response = RESOURCES + .target("/metrics") + .request() + .get(); + + assertOkResponse(response); + + var result = response.readEntity(new GenericType>>() {}); + + assertThat(result).isEqualTo(Map.of("builds", Map.of("current", 2L, "previous", 1L))); + + verify(BUILD_DAO, times(2)).countBuildsInSystemInRange(eq(1L), any(Instant.class), any(Instant.class)); + verifyNoMoreInteractions(BUILD_DAO); + } + } +} diff --git a/ui/src/stores/stats.js b/ui/src/stores/stats.js index 5fb8b55d..654b126d 100644 --- a/ui/src/stores/stats.js +++ b/ui/src/stores/stats.js @@ -1,5 +1,6 @@ import {computed, ref} from 'vue' import {defineStore} from 'pinia' +import {api} from "@/plugins/axios"; export const useStatsStore = defineStore('stats', () => { const currentMonthBuilds = ref(0); @@ -28,9 +29,10 @@ export const useStatsStore = defineStore('stats', () => { return ((currentMonthFailures.value - previousMonthFailures.value) / (divisor * 1.0) * 100).toFixed(2); }); - function loadStats() { - currentMonthBuilds.value = 50; - previousMonthBuilds.value = 45; + async function loadStats() { + const response = await api.get('/metrics'); + currentMonthBuilds.value = response.data.builds.current; + previousMonthBuilds.value = response.data.builds.previous; currentMonthDeployments.value = 38; previousMonthDeployments.value = 45;