diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/ActiveWorkflowRunsAdapter.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/ActiveWorkflowRunsAdapter.java index d5141f2..b2bb5bd 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/ActiveWorkflowRunsAdapter.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/ActiveWorkflowRunsAdapter.java @@ -14,11 +14,13 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriBuilder; -import java.text.MessageFormat; +import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Objects; +import java.util.function.Function; @Service public class ActiveWorkflowRunsAdapter implements ActiveWorkflowRunsQueryPort, ScheduledCacheEvictionPort { @@ -47,26 +49,14 @@ public ActiveWorkflowRunsAdapter( this.rateLimitState = rateLimitState; } - private String getWorkflowRunsApiPath(String repoName) { - return MessageFormat.format( - "repos/{0}/{1}/actions/runs", - this.githubProperties.org(), - repoName - ); - } - @Override @Cacheable(ACTIVE_WORKFLOW_RUNS_CACHE_NAME) public List getActiveWorkflowRuns(Repository repository) { LOGGER.info("Fetching fresh Active WorkflowRuns for Repository {}.", repository.getId()); - var parameters = new HashMap(); - parameters.put("per_page", "100"); var workflowRuns = Objects.requireNonNull(this.restClient.get() - .uri(utilities.setPathAndParameters( - this.getWorkflowRunsApiPath(repository.getName()), - parameters - )) + .uri(workflowRunsUri(repository)) + .header("path", GHActionRuns.PATH) .retrieve() .body(GHActionRuns.class)).getActiveWorkflowRuns(repository); @@ -78,6 +68,20 @@ public List getActiveWorkflowRuns(Repository repository) { return workflowRuns; } + private Function workflowRunsUri(Repository repository) { + var parameters = new HashMap(); + parameters.put("per_page", "100"); + + List pathVars = List.of( + this.githubProperties.org(), + repository.getName() + ); + return utilities.setPathAndParameters( + GHActionRuns.PATH, + pathVars, parameters + ); + } + @Override public boolean freshDataCanWait() { return this.rateLimitState.shouldDataWait( diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/GithubApiUtilities.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/GithubApiUtilities.java index 508424a..dab5be0 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/GithubApiUtilities.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/GithubApiUtilities.java @@ -23,10 +23,33 @@ public class GithubApiUtilities { private static final Logger LOGGER = LoggerFactory.getLogger(GithubApiUtilities.class); private final RestClient restClient; - public GithubApiUtilities(@Qualifier("defaultRestClient") RestClient restClient) { + public GithubApiUtilities( + @Qualifier("defaultRestClient") RestClient restClient + ) { this.restClient = restClient; } + public Function setPathAndParameters( + String path, + List uriVariables + ) { + return uriBuilder -> + uriBuilder.path(path) + .build(uriVariables.toArray(new Object[0])); + } + + public Function setPathAndParameters( + String path, + List uriVariables, + Map parameters + ) { + return uriBuilder -> { + uriBuilder.path(path); + parameters.forEach(uriBuilder::queryParam); + return uriBuilder.build(uriVariables.toArray(new Object[0])); + }; + } + public Function setPathAndParameters( String path, Map parameters diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/JobsAdapter.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/JobsAdapter.java index c3cb2e0..67b22c2 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/JobsAdapter.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/JobsAdapter.java @@ -15,10 +15,12 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriBuilder; -import java.text.MessageFormat; +import java.net.URI; import java.util.HashMap; import java.util.List; +import java.util.function.Function; @Component public class JobsAdapter implements JobsQueryPort, ScheduledCacheEvictionPort { @@ -45,15 +47,6 @@ public JobsAdapter( this.rateLimitState = rateLimitState; } - private String getJobsApiPath(String repoName, long workflowRunId) { - return MessageFormat.format( - "repos/{0}/{1}/actions/runs/{2,number,#}/jobs", - this.githubProperties.org(), - repoName, - workflowRunId - ); - } - @Cacheable(JOBS_CACHE_NAME) @Override public List getAllJobsForWorkflowRun(WorkflowRun workflowRun) { @@ -62,14 +55,9 @@ public List getAllJobsForWorkflowRun(WorkflowRun workflowRun) { workflowRun.getId(), workflowRun.getName() ); - var parameters = new HashMap(); - parameters.put("per_page", "100"); - ResponseEntity responseEntity = this.restClient.get() - .uri(utilities.setPathAndParameters( - getJobsApiPath(workflowRun.getRepository().getName(), workflowRun.getId()), - parameters - )) + .uri(jobsUri(workflowRun)) + .header("path", GHWorkflowRunJobs.PATH) .retrieve() .toEntity(GHWorkflowRunJobs.class); @@ -87,6 +75,22 @@ public List getAllJobsForWorkflowRun(WorkflowRun workflowRun) { return jobs; } + private Function jobsUri(WorkflowRun workflowRun) { + var parameters = new HashMap(); + parameters.put("per_page", "100"); + + List pathVars = List.of( + this.githubProperties.org(), + workflowRun.getRepository().getName(), + workflowRun.getId() + ); + + return utilities.setPathAndParameters( + GHWorkflowRunJobs.PATH, + pathVars, parameters + ); + } + @Override public boolean freshDataCanWait() { return this.rateLimitState.shouldDataWait( diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/PullRequestsAdapter.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/PullRequestsAdapter.java index fe637b5..7168f38 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/PullRequestsAdapter.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/PullRequestsAdapter.java @@ -15,11 +15,13 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriBuilder; -import java.text.MessageFormat; +import java.net.URI; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.function.Function; @Service public class PullRequestsAdapter implements PullRequestQueryPort, ScheduledCacheEvictionPort { @@ -46,14 +48,6 @@ public PullRequestsAdapter( this.rateLimitState = rateLimitState; } - private String getPullRequestsApiPath(String repoName) { - return MessageFormat.format( - "/repos/{0}/{1}/pulls", - this.githubProperties.org(), - repoName - ); - } - @Cacheable(PULL_REQUESTS_CACHE_NAME) @Override public List getAllPullRequestsForRepository(Repository repository) { @@ -61,15 +55,10 @@ public List getAllPullRequestsForRepository(Repository repository) "Fetching fresh PullRequests for Repository {} {}", repository.getId(), repository.getName() ); - var parameters = new HashMap(); - parameters.put("state", "all"); - parameters.put("per_page", "100"); ResponseEntity responseEntity = this.restClient.get() - .uri(utilities.setPathAndParameters( - this.getPullRequestsApiPath(repository.getName()), - parameters - )) + .uri(pullRequestsUri(repository)) + .header("path", GHPullRequest.PATH_ALL) .retrieve() .toEntity(GHPullRequest[].class); @@ -88,6 +77,20 @@ public List getAllPullRequestsForRepository(Repository repository) return pullRequests; } + private Function pullRequestsUri(Repository repository) { + List pathVars = List.of( + this.githubProperties.org(), repository.getName() + ); + var parameters = new HashMap(); + parameters.put("state", "all"); + parameters.put("per_page", "100"); + + return utilities.setPathAndParameters( + GHPullRequest.PATH_ALL, + pathVars, parameters + ); + } + @Override public boolean freshDataCanWait() { return this.rateLimitState.shouldDataWait( diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/RepositoriesAdapter.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/RepositoriesAdapter.java index 328808b..4293673 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/RepositoriesAdapter.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/RepositoriesAdapter.java @@ -21,6 +21,7 @@ @Service public class RepositoriesAdapter implements RepositoriesQueryPort, ScheduledCacheEvictionPort { private static final String REPOSITORIES_CACHE_NAME = "Repositories"; + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoriesAdapter.class); private final RestClient restClient; @@ -52,9 +53,10 @@ public List getAllRepositories() { ResponseEntity responseEntity = this.restClient.get() .uri(utilities.setPathAndParameters( - "/installation/repositories", + GHRepositories.PATH, parameters )) + .header("path", GHRepositories.PATH) .retrieve() .toEntity(GHRepositories.class); diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/SelfHostedRunnerAdapter.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/SelfHostedRunnerAdapter.java index 1d9e48a..7605f89 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/SelfHostedRunnerAdapter.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/SelfHostedRunnerAdapter.java @@ -16,7 +16,6 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; -import java.text.MessageFormat; import java.util.HashMap; import java.util.List; import java.util.stream.Stream; @@ -56,10 +55,10 @@ public List getAllSelfHostedRunners(List repositor private List getForOrganization() { LOGGER.debug("Fetching fresh SelfHostedRunners for Organization."); - var selfHostedRunners = this.fetchSelfHostedRunners(MessageFormat.format( - "orgs/{0}/actions/runners", - this.githubProperties.org() - )); + var selfHostedRunners = this.fetchSelfHostedRunners( + GHSelfHostedRunners.PATH_ORG, + List.of(this.githubProperties.org()) + ); LOGGER.debug( "Response for the SelfHostedRunner fetch of Organization returned {} SelfHostedRunners.", selfHostedRunners.size() @@ -74,11 +73,10 @@ private List getForRepositories(List repositories) private List getForRepository(Repository repository) { LOGGER.debug("Fetching fresh SelfHostedRunners for Repository {}.", repository.getName()); - var selfHostedRunners = this.fetchSelfHostedRunners(MessageFormat.format( - "repos/{0}/{1}/actions/runners", - this.githubProperties.org(), - repository.getName() - )); + var selfHostedRunners = this.fetchSelfHostedRunners( + GHSelfHostedRunners.PATH_REPO, + List.of(this.githubProperties.org(), repository.getName()) + ); LOGGER.debug( "Response for the SelfHostedRunner fetch of Repository {} returned {} SelfHostedRunners.", repository.getName(), @@ -89,14 +87,16 @@ private List getForRepository(Repository repository) { } private List fetchSelfHostedRunners( - String link + String link, List pathVars ) { var parameters = new HashMap(); parameters.put("per_page", "100"); + ResponseEntity responseEntity = this.restClient.get() .uri(utilities.setPathAndParameters( - link, parameters + link, pathVars, parameters )) + .header("path", link) .retrieve() .toEntity(GHSelfHostedRunners.class); diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/WorkflowRunBuildTimesAdapter.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/WorkflowRunBuildTimesAdapter.java index 1a4e9d2..70c6af6 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/WorkflowRunBuildTimesAdapter.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/WorkflowRunBuildTimesAdapter.java @@ -14,29 +14,36 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriBuilder; -import java.text.MessageFormat; +import java.net.URI; +import java.util.List; import java.util.Objects; +import java.util.function.Function; @Service public class WorkflowRunBuildTimesAdapter implements WorkflowRunBuildTimesQueryPort, ScheduledCacheEvictionPort { + private static final String WORKFLOW_RUN_BUILD_TIMES_CACHE_NAME = "WorkflowRunBuildTimes"; private static final Logger LOGGER = LoggerFactory.getLogger(WorkflowRunBuildTimesAdapter.class); private final GithubProperties githubProperties; private final RestClient restClient; private final CacheEvictionProperties evictionProperties; private final ApiRateLimitState rateLimitState; + private final GithubApiUtilities utilities; public WorkflowRunBuildTimesAdapter( GithubProperties githubProperties, @Qualifier("defaultRestClient") RestClient restClient, CacheEvictionProperties evictionProperties, - ApiRateLimitState rateLimitState + ApiRateLimitState rateLimitState, + GithubApiUtilities utilities ) { this.githubProperties = githubProperties; this.restClient = restClient; this.evictionProperties = evictionProperties; this.rateLimitState = rateLimitState; + this.utilities = utilities; } @Cacheable(WORKFLOW_RUN_BUILD_TIMES_CACHE_NAME) @@ -53,10 +60,13 @@ public int getWorkflowRunBuildTimes(WorkflowRun workflowRun) { return 0; } - var buildTime = Objects.requireNonNull(this.restClient.get() - .uri(this.getBuildTimesApiPath(workflowRun)) - .retrieve() - .body(GHActionRunTiming.class)) + var buildTime = Objects.requireNonNull( + this.restClient.get() + .uri(buildTimesUri(workflowRun)) + .header("path", GHActionRunTiming.PATH) + .retrieve() + .body(GHActionRunTiming.class) + ) .run_duration_ms(); LOGGER.debug( @@ -68,13 +78,16 @@ public int getWorkflowRunBuildTimes(WorkflowRun workflowRun) { return buildTime; } - private String getBuildTimesApiPath(WorkflowRun workflowRun) { - return MessageFormat.format( - "repos/{0}/{1}/actions/runs/{2,number,#}/timing", + private Function buildTimesUri(WorkflowRun workflowRun) { + List uriVars = List.of( this.githubProperties.org(), workflowRun.getRepository().getName(), workflowRun.getId() ); + return this.utilities.setPathAndParameters( + GHActionRunTiming.PATH, + uriVars + ); } @Override diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/WorkflowRunsAdapter.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/WorkflowRunsAdapter.java index 877abb4..b5818a1 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/WorkflowRunsAdapter.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/WorkflowRunsAdapter.java @@ -15,12 +15,14 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriBuilder; -import java.text.MessageFormat; +import java.net.URI; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; +import java.util.function.Function; @Service public class WorkflowRunsAdapter implements WorkflowRunsQueryPort, ScheduledCacheEvictionPort { @@ -47,30 +49,17 @@ public WorkflowRunsAdapter( this.evictionProperties = evictionProperties; } - private String getWorkflowRunsApiPath(String repoName) { - return MessageFormat.format( - "repos/{0}/{1}/actions/runs", - this.githubProperties.org(), - repoName - ); - } - @Cacheable(WORKFLOW_RUNS_CACHE_NAME) @Override public List getLastDaysWorkflowRuns(Repository repository) { - LOGGER.debug("Fetching fresh WorkflowRuns for Repository {} {}.", - repository.getId(), repository.getName()); - var parameters = new HashMap(); - parameters.put("per_page", "100"); - parameters.put("created", - ">=" + LocalDate.now().minusDays(1) - .format(DateTimeFormatter.ISO_LOCAL_DATE)); + LOGGER.debug( + "Fetching fresh WorkflowRuns for Repository {} {}.", + repository.getId(), repository.getName() + ); ResponseEntity responseEntity = this.restClient.get() - .uri(utilities.setPathAndParameters( - this.getWorkflowRunsApiPath(repository.getName()), - parameters - )) + .uri(workflowRunsUri(repository)) + .header("path", GHActionRuns.PATH) .retrieve() .toEntity(GHActionRuns.class); @@ -87,6 +76,21 @@ public List getLastDaysWorkflowRuns(Repository repository) { return workflowRuns; } + private Function workflowRunsUri(Repository repository) { + var parameters = new HashMap(); + parameters.put("per_page", "100"); + parameters.put("created", ">=" + LocalDate.now().minusDays(1).format(DateTimeFormatter.ISO_LOCAL_DATE)); + + List pathVars = List.of( + this.githubProperties.org(), + repository.getName() + ); + return utilities.setPathAndParameters( + GHActionRuns.PATH, + pathVars, parameters + ); + } + @Override public boolean freshDataCanWait() { return this.rateLimitState.shouldDataWait( diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientConfig.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientConfig.java index 6c014d9..e85ad70 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientConfig.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientConfig.java @@ -4,6 +4,7 @@ import be.xplore.githubmetrics.githubadapter.config.auth.GithubJwtTokenInterceptor; import be.xplore.githubmetrics.githubadapter.config.auth.GithubUnauthorizedInterceptor; import be.xplore.githubmetrics.githubadapter.config.ratelimiting.RateLimitingInterceptor; +import io.micrometer.observation.ObservationRegistry; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatusCode; @@ -16,17 +17,23 @@ public class GithubRestClientConfig { private final DebugInterceptor debugInterceptor; private final GithubProperties githubProperties; private final RateLimitingInterceptor rateLimitingInterceptor; + private final ObservationRegistry observationRegistry; + private final GithubRestClientRequestObservationConvention githubRestClientRequestObservationConvention; public GithubRestClientConfig( GithubUnauthorizedInterceptor unauthorizedInterceptor, DebugInterceptor debugInterceptor, GithubProperties githubProperties, - RateLimitingInterceptor rateLimitingInterceptor + RateLimitingInterceptor rateLimitingInterceptor, + ObservationRegistry observationRegistry, + GithubRestClientRequestObservationConvention githubRestClientRequestObservationConvention ) { this.unauthorizedInterceptor = unauthorizedInterceptor; this.debugInterceptor = debugInterceptor; this.githubProperties = githubProperties; this.rateLimitingInterceptor = rateLimitingInterceptor; + this.observationRegistry = observationRegistry; + this.githubRestClientRequestObservationConvention = githubRestClientRequestObservationConvention; } @Bean @@ -55,6 +62,8 @@ public RestClient tokenFetcherRestClient( private RestClient.Builder getBasicRestClient() { return RestClient.builder() .baseUrl(githubProperties.url()) + .observationRegistry(this.observationRegistry) + .observationConvention(this.githubRestClientRequestObservationConvention) .defaultHeaders( httpHeaders -> { httpHeaders.set("X-Github-Api-Version", "2022-11-28"); diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientRequestObservationConvention.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientRequestObservationConvention.java new file mode 100644 index 0000000..f321636 --- /dev/null +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientRequestObservationConvention.java @@ -0,0 +1,72 @@ +package be.xplore.githubmetrics.githubadapter.config; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.client.observation.ClientHttpObservationDocumentation; +import org.springframework.http.client.observation.ClientRequestObservationContext; +import org.springframework.http.client.observation.ClientRequestObservationConvention; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Optional; + +@Component +public class GithubRestClientRequestObservationConvention implements ClientRequestObservationConvention { + private static final Logger LOGGER = LoggerFactory.getLogger(GithubRestClientRequestObservationConvention.class); + + @Override + public String getName() { + return "http.client.requests"; + } + + @Override + public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) { + return KeyValues.of(method(context), status(context), uri(context)); + } + + protected KeyValue uri(ClientRequestObservationContext context) { + String uri = Optional.ofNullable(context.getCarrier().getHeaders().get("path")).map(list -> { + if (!list.isEmpty()) { + return list.getFirst(); + } else { + return "NO HEADER"; + } + }).orElse("NO HEADER"); + return KeyValue.of("uri", uri); + } + + protected KeyValue method(ClientRequestObservationContext context) { + return KeyValue.of(ClientHttpObservationDocumentation.LowCardinalityKeyNames.METHOD, context.getCarrier().getMethod().toString()); + } + + protected KeyValue status(ClientRequestObservationContext context) { + String statusCode = Optional.ofNullable(context.getResponse()).map(response -> { + String out = ""; + try { + HttpStatusCode httpStatusCode = response.getStatusCode(); + if (httpStatusCode.is1xxInformational()) { + out = "1xx"; + } else if (httpStatusCode.is2xxSuccessful()) { + out = "2xx"; + } else if (httpStatusCode.is3xxRedirection()) { + out = "3xx"; + } else if (httpStatusCode.is4xxClientError()) { + out = "4xx"; + } else if (httpStatusCode.is5xxServerError()) { + out = "5xx"; + } + } catch (IOException e) { + LOGGER.trace("ERROR: Status Code was not preset on the context response."); + } + return out; + }).orElse(""); + + var val = KeyValue.of(ClientHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, statusCode); + LOGGER.error("{}", val); + return val; + } + +} diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/ObservabilityConfig.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/ObservabilityConfig.java new file mode 100644 index 0000000..e969b3a --- /dev/null +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/ObservabilityConfig.java @@ -0,0 +1,13 @@ +package be.xplore.githubmetrics.githubadapter.config; + +import io.micrometer.observation.ObservationRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ObservabilityConfig { + @Bean + ObservationRegistry observationRegistry() { + return ObservationRegistry.create(); + } +} diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/auth/GithubAuthTokenInterceptor.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/auth/GithubAuthTokenInterceptor.java index debb8e9..a680520 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/auth/GithubAuthTokenInterceptor.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/auth/GithubAuthTokenInterceptor.java @@ -13,7 +13,6 @@ import org.springframework.web.client.RestClient; import java.io.IOException; -import java.text.MessageFormat; import java.time.ZonedDateTime; @Component @@ -47,22 +46,19 @@ private String getAuthToken() { LOGGER.debug("Access token has expired getting a new one."); var accessToken = this.tokenFetcherRestClient.post() - .uri(this.getAccessTokenApiPath()) + .uri(uriBuilder -> uriBuilder.path(GHAppInstallationAccessToken.PATH) + .build(githubProperties.application().installId()) + ) + .header("path", GHAppInstallationAccessToken.PATH) .retrieve() .body(GHAppInstallationAccessToken.class); assert accessToken != null; + this.currentAccessToken = accessToken.token(); this.tokenExpirationDateTime = accessToken.getActualDate(); return this.currentAccessToken; } - private String getAccessTokenApiPath() { - return MessageFormat.format( - "/app/installations/{0}/access_tokens", - githubProperties.application().installId() - ); - } - private ZonedDateTime now() { return ZonedDateTime.now(); } diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHActionRunTiming.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHActionRunTiming.java index 0addd58..4d8d68d 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHActionRunTiming.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHActionRunTiming.java @@ -3,4 +3,6 @@ public record GHActionRunTiming( int run_duration_ms ) { + + public static final String PATH = "repos/{org}/{repo}/actions/runs/{workflowRunId}/timing"; } diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHActionRuns.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHActionRuns.java index d5649c5..1f7078c 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHActionRuns.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHActionRuns.java @@ -9,6 +9,8 @@ public record GHActionRuns( int total_count, List workflow_runs ) { + public static final String PATH = "repos/{org}/{repo}/actions/runs"; + public List getWorkFlowRuns(Repository repository) { return this.workflow_runs.stream().map(workflowRun -> workflowRun.getWorkFlowRun(repository) diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHAppInstallationAccessToken.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHAppInstallationAccessToken.java index c199fad..b55fefe 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHAppInstallationAccessToken.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHAppInstallationAccessToken.java @@ -7,6 +7,8 @@ public record GHAppInstallationAccessToken( String token, String expires_at ) { + public static final String PATH = "/app/installations/{appId}/access_tokens"; + public ZonedDateTime getActualDate() { return Instant .parse(this.expires_at) diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHPullRequest.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHPullRequest.java index 84aa8b2..c092cb0 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHPullRequest.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHPullRequest.java @@ -12,6 +12,7 @@ public record GHPullRequest( String closed_at, String merged_at ) { + public static final String PATH_ALL = "/repos/{org}/{repo}/pulls"; private ZonedDateTime getZonedDateTime(String date) { return Instant.parse(date).atZone(ZoneId.of("Etc/UTC")); diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHRepositories.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHRepositories.java index db85d00..9911602 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHRepositories.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHRepositories.java @@ -8,6 +8,8 @@ public record GHRepositories( List repositories ) { + public static final String PATH = "/installation/repositories"; + public List getRepositories() { List repositoryList = new ArrayList<>(); repositories.forEach(repository -> repositoryList.add(repository.getRepository())); diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHSelfHostedRunners.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHSelfHostedRunners.java index 3967bbf..45fe853 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHSelfHostedRunners.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHSelfHostedRunners.java @@ -8,6 +8,10 @@ public record GHSelfHostedRunners( long total_count, List runners ) { + + public static final String PATH_ORG = "orgs/{org}/actions/runners"; + public static final String PATH_REPO = "repos/{org}/{repo}/actions/runners"; + public List getRunners() { return this.runners.stream().map(GHSelfHostedRunner::getRunner).toList(); } diff --git a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHWorkflowRunJobs.java b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHWorkflowRunJobs.java index b70e31c..7a179f0 100644 --- a/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHWorkflowRunJobs.java +++ b/github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/mappingclasses/GHWorkflowRunJobs.java @@ -8,6 +8,8 @@ public record GHWorkflowRunJobs( int total_count, List jobs ) { + public static final String PATH = "repos/{org}/{repo}/actions/runs/{workflowRunId}/jobs"; + public List getJobs() { return this.jobs.stream().map( GHJob::getJob diff --git a/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/TestUtility.java b/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/TestUtility.java index e1da2a1..b9e6ecb 100644 --- a/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/TestUtility.java +++ b/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/TestUtility.java @@ -6,12 +6,14 @@ import be.xplore.githubmetrics.githubadapter.config.DebugInterceptor; import be.xplore.githubmetrics.githubadapter.config.GithubProperties; import be.xplore.githubmetrics.githubadapter.config.GithubRestClientConfig; +import be.xplore.githubmetrics.githubadapter.config.GithubRestClientRequestObservationConvention; import be.xplore.githubmetrics.githubadapter.config.auth.GithubAuthTokenInterceptor; import be.xplore.githubmetrics.githubadapter.config.auth.GithubJwtTokenInterceptor; import be.xplore.githubmetrics.githubadapter.config.auth.GithubUnauthorizedInterceptor; import be.xplore.githubmetrics.githubadapter.config.ratelimiting.RateLimitResetAwaitScheduler; import be.xplore.githubmetrics.githubadapter.config.ratelimiting.RateLimitingInterceptor; import com.github.tomakehurst.wiremock.WireMockServer; +import io.micrometer.observation.ObservationRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.client.RestClient; @@ -48,7 +50,9 @@ public static ApiRateLimitState getApiRateLimitState() { public static GithubAuthTokenInterceptor getAuthTokenInterceptor(GithubProperties githubProperties) { var restClientConfig = new GithubRestClientConfig( - new GithubUnauthorizedInterceptor(), DEBUG_INTERCEPTOR, githubProperties, getRateLimitingInterceptor() + new GithubUnauthorizedInterceptor(), DEBUG_INTERCEPTOR, + githubProperties, getRateLimitingInterceptor(), mock(ObservationRegistry.NOOP), + mock(GithubRestClientRequestObservationConvention.class) ); var jwtInterceptor = new GithubJwtTokenInterceptor(githubProperties); var jwtRestClient = restClientConfig.tokenFetcherRestClient( diff --git a/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/WorkflowRunBuildTimesAdapterTest.java b/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/WorkflowRunBuildTimesAdapterTest.java index 959201c..c92ed50 100644 --- a/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/WorkflowRunBuildTimesAdapterTest.java +++ b/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/WorkflowRunBuildTimesAdapterTest.java @@ -68,7 +68,8 @@ void setup() { buildTimesAdapter = new WorkflowRunBuildTimesAdapter( githubProperties, tokenRestClient, TestUtility.getCacheEvictionProperties(), - TestUtility.getApiRateLimitState() + TestUtility.getApiRateLimitState(), + new GithubApiUtilities(tokenRestClient) ); this.workflowRun = new WorkflowRun( diff --git a/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/auth/GithubAuthTokenInterceptorTest.java b/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/auth/GithubAuthTokenInterceptorTest.java index df2bf5e..f8570e0 100644 --- a/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/auth/GithubAuthTokenInterceptorTest.java +++ b/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/auth/GithubAuthTokenInterceptorTest.java @@ -4,11 +4,13 @@ import be.xplore.githubmetrics.githubadapter.config.DebugInterceptor; import be.xplore.githubmetrics.githubadapter.config.GithubProperties; import be.xplore.githubmetrics.githubadapter.config.GithubRestClientConfig; +import be.xplore.githubmetrics.githubadapter.config.GithubRestClientRequestObservationConvention; import be.xplore.githubmetrics.githubadapter.config.auth.GithubAuthTokenInterceptor; import be.xplore.githubmetrics.githubadapter.config.auth.GithubJwtTokenInterceptor; import be.xplore.githubmetrics.githubadapter.config.auth.GithubUnauthorizedInterceptor; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; +import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; @@ -44,7 +46,9 @@ class GithubAuthTokenInterceptorTest { new GithubUnauthorizedInterceptor(), new DebugInterceptor(), githubProperties, - TestUtility.getRateLimitingInterceptor() + TestUtility.getRateLimitingInterceptor(), + ObservationRegistry.NOOP, + mock(GithubRestClientRequestObservationConvention.class) ); private GithubAuthTokenInterceptor authTokenInterceptor; diff --git a/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientRequestObservationConventionTest.java b/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientRequestObservationConventionTest.java new file mode 100644 index 0000000..c0e0167 --- /dev/null +++ b/github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientRequestObservationConventionTest.java @@ -0,0 +1,110 @@ +package be.xplore.githubmetrics.githubadapter.config; + +import io.micrometer.common.KeyValues; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.observation.ClientRequestObservationContext; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class GithubRestClientRequestObservationConventionTest { + private static final String PATH = "path"; + private static final String URI = "uri"; + private static final String STATUS = "status"; + + private GithubRestClientRequestObservationConvention observationConvention; + private ClientRequestObservationContext mockObservationContext; + private HttpHeaders mockHeaders; + + @BeforeEach + void setUp() { + this.observationConvention = new GithubRestClientRequestObservationConvention(); + this.mockObservationContext = mock(ClientRequestObservationContext.class); + ClientHttpRequest mockCarrier = mock(ClientHttpRequest.class); + this.mockHeaders = mock(HttpHeaders.class); + when(mockObservationContext.getCarrier()).thenReturn(mockCarrier); + when(mockCarrier.getHeaders()).thenReturn(mockHeaders); + when(mockCarrier.getMethod()).thenReturn(HttpMethod.GET); + } + + @Test + void conventionShouldParsePathHeaderCorrectly() { + String path = "my/special/path"; + + when(mockHeaders.get(PATH)).thenReturn(List.of(path)); + assertTrue(keyValPresent(URI, path)); + } + + @Test + void conventionShouldReturnNOHEADERifNoValue() { + when(mockHeaders.get(PATH)).thenReturn(List.of()); + assertTrue(keyValPresent(URI, "NO HEADER")); + } + + @Test + void conventionShouldReturnNOHEADERifNoHeader() { + assertTrue(keyValPresent(URI, "NO HEADER")); + } + + @Test + void emptyContextResponseShouldTranslateToEmptyResponseCodeLabel() { + assertTrue(keyValPresent(STATUS, "")); + } + + @Test + void conventionResponseCodesShouldTranslateToCorrectStrings() throws IOException { + try (ClientHttpResponse response = mock(ClientHttpResponse.class)) { + when(mockObservationContext.getResponse()).thenReturn(response); + setupResponse(response); + checkStatusCodes(); + } + } + + @Test + void responseCodeThrowingExceptionShouldStillReturnStatus() throws IOException { + try (ClientHttpResponse response = mock(ClientHttpResponse.class)) { + when(mockObservationContext.getResponse()).thenReturn(response); + when(response.getStatusCode()).thenThrow(IOException.class); + + assertTrue(keyValPresent(STATUS, "")); + } + } + + private void setupResponse(ClientHttpResponse response) throws IOException { + when(response.getStatusCode()) + .thenReturn(HttpStatusCode.valueOf(101)) + .thenReturn(HttpStatusCode.valueOf(201)) + .thenReturn(HttpStatusCode.valueOf(301)) + .thenReturn(HttpStatusCode.valueOf(401)) + .thenReturn(HttpStatusCode.valueOf(501)); + } + + private void checkStatusCodes() { + assertTrue(keyValPresent(STATUS, "1xx")); + assertTrue(keyValPresent(STATUS, "2xx")); + assertTrue(keyValPresent(STATUS, "3xx")); + assertTrue(keyValPresent(STATUS, "4xx")); + assertTrue(keyValPresent(STATUS, "5xx")); + } + + private boolean keyValPresent(String key, String val) { + return get().stream().anyMatch(keyValue -> + keyValue.getKey().equals(key) && keyValue.getValue().equals(val) + ); + } + + private KeyValues get() { + return this.observationConvention.getLowCardinalityKeyValues(this.mockObservationContext); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4d1be4f..13107ef 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,6 +2,7 @@ logging: level: be.xplore.githubmetrics: ROOT: ${LOGGING_LEVEL:INFO} + githubadapter.config: TRACE management: endpoints: