From 039ad2e37acb9b88866c900c79b8f092d6c2d7c0 Mon Sep 17 00:00:00 2001 From: tomellm Date: Wed, 29 May 2024 09:53:58 +0200 Subject: [PATCH] #155 restclient observability All RestClients now expose some metrics on the actuator endpoint. The exposed metrics values have not been modified in any way from the default. The only thing that changed is that the uri path tag has been modified, removing the path variables to decrease the cardinality of the metrics. --- .../ActiveWorkflowRunsAdapter.java | 34 +++--- .../githubadapter/GithubApiUtilities.java | 25 +++- .../githubadapter/JobsAdapter.java | 38 +++--- .../githubadapter/PullRequestsAdapter.java | 35 +++--- .../githubadapter/RepositoriesAdapter.java | 4 +- .../SelfHostedRunnerAdapter.java | 24 ++-- .../WorkflowRunBuildTimesAdapter.java | 31 +++-- .../githubadapter/WorkflowRunsAdapter.java | 44 +++---- .../config/GithubRestClientConfig.java | 11 +- ...estClientRequestObservationConvention.java | 72 ++++++++++++ .../config/ObservabilityConfig.java | 13 +++ .../auth/GithubAuthTokenInterceptor.java | 14 +-- .../mappingclasses/GHActionRunTiming.java | 2 + .../mappingclasses/GHActionRuns.java | 2 + .../GHAppInstallationAccessToken.java | 2 + .../mappingclasses/GHPullRequest.java | 1 + .../mappingclasses/GHRepositories.java | 2 + .../mappingclasses/GHSelfHostedRunners.java | 4 + .../mappingclasses/GHWorkflowRunJobs.java | 2 + .../githubadapter/TestUtility.java | 6 +- .../WorkflowRunBuildTimesAdapterTest.java | 3 +- .../auth/GithubAuthTokenInterceptorTest.java | 6 +- ...lientRequestObservationConventionTest.java | 110 ++++++++++++++++++ src/main/resources/application.yml | 1 + 24 files changed, 382 insertions(+), 104 deletions(-) create mode 100644 github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientRequestObservationConvention.java create mode 100644 github-adapter/src/main/java/be/xplore/githubmetrics/githubadapter/config/ObservabilityConfig.java create mode 100644 github-adapter/src/test/java/be/xplore/githubmetrics/githubadapter/config/GithubRestClientRequestObservationConventionTest.java 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: