Skip to content

Commit

Permalink
Add endpoint to retrieve build stats (#339)
Browse files Browse the repository at this point in the history
Update UI to use new endpoint for build stats
  • Loading branch information
chrisrohr authored Sep 4, 2023
1 parent dc61281 commit d584190
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 3 deletions.
2 changes: 2 additions & 0 deletions service/src/main/java/org/kiwiproject/champagne/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}

}
Original file line number Diff line number Diff line change
@@ -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<Map<String, Map<String, Long>>>() {});

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);
}
}
}
8 changes: 5 additions & 3 deletions ui/src/stores/stats.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit d584190

Please sign in to comment.