Repository: bcgov/EDUC-GRAD-TRAX-API
Pull request #433: Grad2-3284 set request headers
Author: login: githubmamatha
Created: 2025-03-07T18:26:37Z | Merged: 2025-03-14T16:24:19Z

Done


Diff:
diff --git a/api/src/main/java/ca/bc/gov/educ/api/trax/config/GradTraxConfig.java b/api/src/main/java/ca/bc/gov/educ/api/trax/config/GradTraxConfig.java
index b83b9967..160f8215 100644
--- a/api/src/main/java/ca/bc/gov/educ/api/trax/config/GradTraxConfig.java
+++ b/api/src/main/java/ca/bc/gov/educ/api/trax/config/GradTraxConfig.java
@@ -6,6 +6,9 @@
 import ca.bc.gov.educ.api.trax.model.entity.DistrictEntity;
 import ca.bc.gov.educ.api.trax.model.entity.PsiEntity;
 import ca.bc.gov.educ.api.trax.model.entity.SchoolEntity;
+import ca.bc.gov.educ.api.trax.util.EducGradTraxApiConstants;
+import ca.bc.gov.educ.api.trax.util.LogHelper;
+import ca.bc.gov.educ.api.trax.util.ThreadLocalStateUtil;
 import net.javacrumbs.shedlock.core.LockProvider;
 import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
 import org.modelmapper.ModelMapper;
@@ -22,14 +25,20 @@
 import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
 import org.springframework.transaction.PlatformTransactionManager;
 import org.springframework.web.client.RestTemplate;
+import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
 import org.springframework.web.reactive.function.client.ExchangeStrategies;
 import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
 import reactor.netty.http.client.HttpClient;
 
 @Configuration
 @Profile("!test")
 public class GradTraxConfig {
 
+	LogHelper logHelper;
+	EducGradTraxApiConstants constants;
+
 	@Bean
 	public ModelMapper modelMapper() {
 
@@ -55,6 +64,7 @@ public WebClient getTraxClientWebClient(OAuth2AuthorizedClientManager authorized
 		ServletOAuth2AuthorizedClientExchangeFilterFunction filter = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
 		filter.setDefaultClientRegistrationId("traxclient");
 		return WebClient.builder()
+				.filter(setRequestHeaders())
 				.exchangeStrategies(ExchangeStrategies
 						.builder()
 						.codecs(codecs -> codecs
@@ -62,6 +72,7 @@ public WebClient getTraxClientWebClient(OAuth2AuthorizedClientManager authorized
 								.maxInMemorySize(50 * 1024 * 1024))
 						.build())
 				.apply(filter.oauth2Configuration())
+				.filter(this.log())
 				.build();
 	}
 
@@ -78,6 +89,7 @@ public WebClient getInstituteWebClient(OAuth2AuthorizedClientManager authorizedC
 								.maxInMemorySize(50 * 1024 * 1024))
 						.build())
 				.apply(filter.oauth2Configuration())
+				.filter(this.log())
 				.build();
 	}
 
@@ -105,12 +117,38 @@ public WebClient webClient() {
 		Integer CODEC_50_MB_SIZE = 50 * 1024 * 1024;
 		HttpClient client = HttpClient.create();
 		client.warmup().block();
-		return WebClient.builder().codecs(clientCodecConfigurer -> {
+		return WebClient.builder()
+				.filter(setRequestHeaders())
+				.codecs(clientCodecConfigurer -> {
 			var codec = new Jackson2JsonDecoder();
 			codec.setMaxInMemorySize(CODEC_50_MB_SIZE);
 			clientCodecConfigurer.customCodecs().register(codec);
-			clientCodecConfigurer.customCodecs().register(new Jackson2JsonEncoder());
-		}).build();
+			clientCodecConfigurer.customCodecs().register(new Jackson2JsonEncoder());})
+				.filter(this.log())
+				.build();
+	}
+	private ExchangeFilterFunction setRequestHeaders() {
+		return (clientRequest, next) -> {
+			ClientRequest modifiedRequest = ClientRequest.from(clientRequest)
+					.header(EducGradTraxApiConstants.CORRELATION_ID, ThreadLocalStateUtil.getCorrelationID())
+					.header(EducGradTraxApiConstants.USER_NAME, ThreadLocalStateUtil.getCurrentUser())
+					.header(EducGradTraxApiConstants.REQUEST_SOURCE, EducGradTraxApiConstants.API_NAME)
+					.build();
+			return next.exchange(modifiedRequest);
+		};
+	}
+
+	private ExchangeFilterFunction log() {
+		return (clientRequest, next) -> next
+				.exchange(clientRequest)
+				.doOnNext((clientResponse -> logHelper.logClientHttpReqResponseDetails(
+						clientRequest.method(),
+						clientRequest.url().toString(),
+						clientResponse.statusCode().value(),
+						clientRequest.headers().get(EducGradTraxApiConstants.CORRELATION_ID),
+						clientRequest.headers().get(EducGradTraxApiConstants.REQUEST_SOURCE),
+						constants.isSplunkLogHelperEnabled())
+				));
 	}
 
 	/**
diff --git a/api/src/main/java/ca/bc/gov/educ/api/trax/config/RequestInterceptor.java b/api/src/main/java/ca/bc/gov/educ/api/trax/config/RequestInterceptor.java
index 2885458d..dd59eda4 100644
--- a/api/src/main/java/ca/bc/gov/educ/api/trax/config/RequestInterceptor.java
+++ b/api/src/main/java/ca/bc/gov/educ/api/trax/config/RequestInterceptor.java
@@ -14,6 +14,7 @@
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import java.time.Instant;
+import java.util.UUID;
 
 @Component
 public class RequestInterceptor implements AsyncHandlerInterceptor {
@@ -34,18 +35,28 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
 		validation.clear();
 		// correlationID
 		val correlationID = request.getHeader(EducGradTraxApiConstants.CORRELATION_ID);
-		if (correlationID != null) {
-			ThreadLocalStateUtil.setCorrelationID(correlationID);
+		ThreadLocalStateUtil.setCorrelationID(correlationID != null ? correlationID : UUID.randomUUID().toString());
+
+		//Request Source
+		val requestSource = request.getHeader(EducGradTraxApiConstants.REQUEST_SOURCE);
+		if(requestSource != null) {
+			ThreadLocalStateUtil.setRequestSource(requestSource);
 		}
 
-		// username
-		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
-		if (auth instanceof JwtAuthenticationToken) {
-			JwtAuthenticationToken authenticationToken = (JwtAuthenticationToken) auth;
-			Jwt jwt = (Jwt) authenticationToken.getCredentials();
-			String username = JwtUtil.getName(jwt);
-			if (username != null) {
-				ThreadLocalStateUtil.setCurrentUser(username);
+		// Header userName
+		val userName = request.getHeader(EducGradTraxApiConstants.USER_NAME);
+		if (userName != null) {
+			ThreadLocalStateUtil.setCurrentUser(userName);
+		}
+		else {
+			Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+			if (auth instanceof JwtAuthenticationToken) {
+				JwtAuthenticationToken authenticationToken = (JwtAuthenticationToken) auth;
+				Jwt jwt = (Jwt) authenticationToken.getCredentials();
+				String username = JwtUtil.getName(jwt);
+				if (username != null) {
+					ThreadLocalStateUtil.setCurrentUser(username);
+				}
 			}
 		}
 		return true;
@@ -62,10 +73,6 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
 	@Override
 	public void afterCompletion(@NonNull final HttpServletRequest request, final HttpServletResponse response, @NonNull final Object handler, final Exception ex) {
 		LogHelper.logServerHttpReqResponseDetails(request, response, constants.isSplunkLogHelperEnabled());
-		val correlationID = request.getHeader(EducGradTraxApiConstants.CORRELATION_ID);
-		if (correlationID != null) {
-			response.setHeader(EducGradTraxApiConstants.CORRELATION_ID, request.getHeader(EducGradTraxApiConstants.CORRELATION_ID));
-		}
 		// clear
 		ThreadLocalStateUtil.clear();
 	}
diff --git a/api/src/main/java/ca/bc/gov/educ/api/trax/service/RESTService.java b/api/src/main/java/ca/bc/gov/educ/api/trax/service/RESTService.java
index b7d8c23f..7d4718d1 100644
--- a/api/src/main/java/ca/bc/gov/educ/api/trax/service/RESTService.java
+++ b/api/src/main/java/ca/bc/gov/educ/api/trax/service/RESTService.java
@@ -2,8 +2,6 @@
 
 
 import ca.bc.gov.educ.api.trax.exception.ServiceException;
-import ca.bc.gov.educ.api.trax.util.EducGradTraxApiConstants;
-import ca.bc.gov.educ.api.trax.util.ThreadLocalStateUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.HttpStatusCode;
@@ -49,7 +47,7 @@ public <T> T get(String url, Class<T> clazz, String accessToken) {
             obj = webClient
                     .get()
                     .uri(url)
-                    .headers(h -> { h.setBearerAuth(accessToken); h.set(EducGradTraxApiConstants.CORRELATION_ID, ThreadLocalStateUtil.getCorrelationID()); })
+                    .headers(h -> h.setBearerAuth(accessToken))
                     .retrieve()
                     // if 5xx errors, throw Service error
                     .onStatus(HttpStatusCode::is5xxServerError,
@@ -78,7 +76,6 @@ public <T> T get(String url, Class<T> clazz, WebClient webClient) {
             obj = webClient
                     .get()
                     .uri(url)
-                    .headers(h -> { h.set(EducGradTraxApiConstants.CORRELATION_ID, ThreadLocalStateUtil.getCorrelationID()); })
                     .retrieve()
                     // if 5xx errors, throw Service error
                     .onStatus(HttpStatusCode::is5xxServerError,
@@ -116,7 +113,7 @@ public <T> T post(String url, Object body, Class<T> clazz, String accessToken) {
         try {
             obj = webClient.post()
                     .uri(url)
-                    .headers(h -> { h.setBearerAuth(accessToken); h.set(EducGradTraxApiConstants.CORRELATION_ID, ThreadLocalStateUtil.getCorrelationID()); })
+                    .headers(h -> h.setBearerAuth(accessToken))
                     .body(BodyInserters.fromValue(body))
                     .retrieve()
                     .onStatus(HttpStatusCode::is5xxServerError,
@@ -141,7 +138,6 @@ public <T> T post(String url, Object body, Class<T> clazz, WebClient webClient)
         try {
             obj = webClient.post()
                     .uri(url)
-                    .headers(h -> { h.set(EducGradTraxApiConstants.CORRELATION_ID, ThreadLocalStateUtil.getCorrelationID()); })
                     .body(BodyInserters.fromValue(body))
                     .retrieve()
                     .onStatus(HttpStatusCode::is5xxServerError,
diff --git a/api/src/main/java/ca/bc/gov/educ/api/trax/util/EducGradTraxApiConstants.java b/api/src/main/java/ca/bc/gov/educ/api/trax/util/EducGradTraxApiConstants.java
index 679bdc49..91ef66ba 100644
--- a/api/src/main/java/ca/bc/gov/educ/api/trax/util/EducGradTraxApiConstants.java
+++ b/api/src/main/java/ca/bc/gov/educ/api/trax/util/EducGradTraxApiConstants.java
@@ -21,6 +21,8 @@ public class EducGradTraxApiConstants {
 
     public static final String INSTITUTE_STREAM_NAME="INSTITUTE_EVENTS";
     public static final String CORRELATION_ID = "correlationID";
+    public static final String USER_NAME = "User-Name";
+    public static final String REQUEST_SOURCE = "Request-Source";
 
     //API end-point Mapping constants
     public static final String API_VERSION_V1 = "/api/v1";
diff --git a/api/src/main/java/ca/bc/gov/educ/api/trax/util/LogHelper.java b/api/src/main/java/ca/bc/gov/educ/api/trax/util/LogHelper.java
index e383e9ec..9cd89eb9 100644
--- a/api/src/main/java/ca/bc/gov/educ/api/trax/util/LogHelper.java
+++ b/api/src/main/java/ca/bc/gov/educ/api/trax/util/LogHelper.java
@@ -4,12 +4,14 @@
 import lombok.extern.slf4j.Slf4j;
 import lombok.val;
 import org.slf4j.MDC;
+import org.springframework.http.HttpMethod;
 import org.springframework.lang.NonNull;
 
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import java.time.Instant;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 @Slf4j
@@ -34,6 +36,10 @@ public static void logServerHttpReqResponseDetails(@NonNull final HttpServletReq
       if (correlationID != null) {
         httpMap.put("correlation_id", correlationID);
       }
+      val requestSource = request.getHeader(EducGradTraxApiConstants.REQUEST_SOURCE);
+      if (requestSource != null) {
+        httpMap.put("request_source", requestSource);
+      }
       httpMap.put("server_http_request_url", String.valueOf(request.getRequestURL()));
       httpMap.put("server_http_request_processing_time_ms", totalTime);
       httpMap.put("server_http_request_payload", String.valueOf(request.getAttribute("payload")));
@@ -46,6 +52,27 @@ public static void logServerHttpReqResponseDetails(@NonNull final HttpServletReq
     }
   }
 
+  public static void logClientHttpReqResponseDetails(@NonNull final HttpMethod method, final String url, final int responseCode, final List<String> correlationID, final List<String> requestSource, final boolean logging) {
+    if (!logging) return;
+    try {
+      final Map<String, Object> httpMap = new HashMap<>();
+      httpMap.put("client_http_response_code", responseCode);
+      httpMap.put("client_http_request_method", method.toString());
+      httpMap.put("client_http_request_url", url);
+      if (correlationID != null) {
+        httpMap.put("correlation_id", String.join(",", correlationID));
+      }
+      if (requestSource != null) {
+        httpMap.put("request_source", String.join(",", requestSource));
+      }
+      MDC.putCloseable("httpEvent", mapper.writeValueAsString(httpMap));
+      log.info("");
+      MDC.clear();
+    } catch (final Exception exception) {
+      log.error(EXCEPTION, exception);
+    }
+  }
+
   /**
    * NATS messaging
    * the event is a json string.
diff --git a/api/src/main/java/ca/bc/gov/educ/api/trax/util/ThreadLocalStateUtil.java b/api/src/main/java/ca/bc/gov/educ/api/trax/util/ThreadLocalStateUtil.java
index 98bb2b1b..d256b23c 100644
--- a/api/src/main/java/ca/bc/gov/educ/api/trax/util/ThreadLocalStateUtil.java
+++ b/api/src/main/java/ca/bc/gov/educ/api/trax/util/ThreadLocalStateUtil.java
@@ -1,8 +1,9 @@
 package ca.bc.gov.educ.api.trax.util;
 
 public class ThreadLocalStateUtil {
-    private static ThreadLocal<String> transaction = new ThreadLocal<>();
-    private static ThreadLocal<String> user = new ThreadLocal<>();
+    private static InheritableThreadLocal<String> transaction = new InheritableThreadLocal<>();
+    private static InheritableThreadLocal<String> user = new InheritableThreadLocal<>();
+    private static final InheritableThreadLocal<String> requestSource = new InheritableThreadLocal<String>();
 
     /**
      * Set the current correlationID for this thread
@@ -40,8 +41,26 @@ public static String getCurrentUser() {
         return user.get();
     }
 
+    /**
+     * Set the requestSource for this thread
+     *
+     * @param reqSource
+     */
+    public static void setRequestSource(String reqSource){
+        requestSource.set(reqSource);
+    }
+    /**
+     * Get the requestSource for this thread
+     *
+     * @return the requestSource, or null if it is unknown.
+     */
+    public static String getRequestSource() {
+        return requestSource.get();
+    }
+
     public static void clear() {
         transaction.remove();
         user.remove();
+        requestSource.remove();
     }
 }


Reviews:
user: login: cditcher
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T22:20:53Z
user: login: cditcher
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T15:42:35Z

Review comments:
diff_hunk: @@ -55,13 +64,15 @@ public WebClient getTraxClientWebClient(OAuth2AuthorizedClientManager authorized
 		ServletOAuth2AuthorizedClientExchangeFilterFunction filter = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
 		filter.setDefaultClientRegistrationId("traxclient");
 		return WebClient.builder()
+				.filter(setRequestHeaders())
 				.exchangeStrategies(ExchangeStrategies
 						.builder()
 						.codecs(codecs -> codecs
 								.defaultCodecs()
 								.maxInMemorySize(50 * 1024 * 1024))
 						.build())
 				.apply(filter.oauth2Configuration())
+				.filter(this.log())
 				.build();
 	}
 
path: api/src/main/java/ca/bc/gov/educ/api/trax/config/GradTraxConfig.java
user: login: cditcher
body: log() and setRequestHeaders() need to be on _all_ WebClient implementations. You missed instituteWebClient.
created_at: 2025-03-12T22:15:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,12 +116,40 @@ public WebClient webClient() {
 		Integer CODEC_50_MB_SIZE = 50 * 1024 * 1024;
 		HttpClient client = HttpClient.create();
 		client.warmup().block();
-		return WebClient.builder().codecs(clientCodecConfigurer -> {
+		return WebClient.builder()
+				.filter(setRequestHeaders())
+				.codecs(clientCodecConfigurer -> {
 			var codec = new Jackson2JsonDecoder();
 			codec.setMaxInMemorySize(CODEC_50_MB_SIZE);
 			clientCodecConfigurer.customCodecs().register(codec);
-			clientCodecConfigurer.customCodecs().register(new Jackson2JsonEncoder());
-		}).build();
+			clientCodecConfigurer.customCodecs().register(new Jackson2JsonEncoder());})
+				.filter(this.log())
+				.build();
+	}
+	private ExchangeFilterFunction setRequestHeaders() {
+		return (clientRequest, next) -> {
+			ClientRequest modifiedRequest = ClientRequest.from(clientRequest)
+					.header(EducGradTraxApiConstants.CORRELATION_ID, ThreadLocalStateUtil.getCorrelationID())
+					.header(EducGradTraxApiConstants.USER_NAME, ThreadLocalStateUtil.getCurrentUser())
+					.header(EducGradTraxApiConstants.REQUEST_SOURCE, EducGradTraxApiConstants.API_NAME)
+					.build();
+			return next.exchange(modifiedRequest);
+		};
+	}
+
+	private ExchangeFilterFunction log() {
+		return (clientRequest, next) -> next
+				.exchange(clientRequest)
+				.doOnNext((clientResponse -> logHelper.logClientHttpReqResponseDetails(
+						clientRequest.method(),
+						clientRequest.url().toString(),
+						clientResponse.statusCode().value(),
+						//GRAD2-1929 Refactoring/Linting replaced rawStatusCode() with statusCode() as it was deprecated.
path: api/src/main/java/ca/bc/gov/educ/api/trax/config/GradTraxConfig.java
user: login: cditcher
body: Remove commented out code
created_at: 2025-03-12T22:16:35Z
author_association: CONTRIBUTOR
path: api/src/main/java/ca/bc/gov/educ/api/trax/service/RESTService.java
user: login: cditcher
body: Remove unused imports
created_at: 2025-03-12T22:18:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,6 +3,7 @@
 public class ThreadLocalStateUtil {
     private static ThreadLocal<String> transaction = new ThreadLocal<>();
path: api/src/main/java/ca/bc/gov/educ/api/trax/util/ThreadLocalStateUtil.java
user: login: cditcher
body: All variables should be InheritableThreadLocal, not just requestSource.
created_at: 2025-03-12T22:20:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-12T22:45:42Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=bcgov_EDUC-GRAD-TRAX-API&pullRequest=433) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=bcgov_EDUC-GRAD-TRAX-API&pullRequest=433&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=bcgov_EDUC-GRAD-TRAX-API&pullRequest=433&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=bcgov_EDUC-GRAD-TRAX-API&pullRequest=433&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=bcgov_EDUC-GRAD-TRAX-API&pullRequest=433&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=bcgov_EDUC-GRAD-TRAX-API&pullRequest=433&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=bcgov_EDUC-GRAD-TRAX-API&pullRequest=433)


author_association: NONE