├── .gitignore ├── README.md ├── pom.xml └── src └── main ├── java └── uk │ └── co │ └── taidev │ └── experiments │ └── correlationidasync │ ├── Application.java │ ├── controllers │ └── NewsController.java │ ├── correlationfutures │ ├── CorrelationCallable.java │ └── ListenableFutureAdapter.java │ ├── reporting │ └── RequestCorrelation.java │ ├── services │ ├── ExternalNewsService.java │ ├── ExternalNewsServiceRest.java │ ├── NewsService.java │ └── NewsServiceSimple.java │ └── web │ ├── client │ ├── CorrelatingRestClient.java │ └── RestClient.java │ └── filters │ └── CorrelationHeaderFilter.java └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | target/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | correlation-id-async 2 | ==================== 3 | 4 | This is a simple Spring boot-based application that demonstrates how request correlationIds could be generated and managed in order to correlate and track an initial request's handling (and journey) through a distributed SOA/microservices application. 5 | 6 | This version extends on the work in my other correclation-id-sync repo (which works correctly only with synchronous request handling) by providing a series of wrapper Classes to transparently propogate the correlation Id across differing Threads. Note that the main NewsController.externalNews method returns a ```DeferredResult``` instead of a ```String``` directly. 7 | 8 | 9 | Notes 10 | ----- 11 | The adapter code between the DeferredResult and Future is currently dependent on Google Guava, but at some point I may re-write this to utilise the new Spring 4.0 ListenableFuture or Java 8 CompletableFuture. 12 | 13 | This code also utilses Java 8 syntax, namely a method reference in the NewsController, but this could easily be converted for Java 7 compliance. 14 | 15 | This code could do with some tidying-up (e.g. extraction of ```private ListeningExecutorService service``` in NewsController) ;-) 16 | 17 | 18 | Disclaimer 19 | ---------- 20 | This is just an experiment and toy-example! I'm not suggesting this code is production-ready! :-) 21 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | uk.co.taidev.experiments 7 | correlation-id-async 8 | 0.0.1-SNAPSHOT 9 | 10 | correlation-id-async 11 | A simple example of implementing request correlation ids within a asynchronous context 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-parent 16 | 1.0.1.RELEASE 17 | 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-web 23 | 24 | 25 | com.google.guava 26 | guava 27 | 17.0 28 | 29 | 30 | 31 | 32 | uk.co.taidev.experiments.correlationidasync.Application 33 | 1.8 34 | 35 | 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-maven-plugin 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/Application.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.boot.context.embedded.FilterRegistrationBean; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | import uk.co.taidev.experiments.correlationidasync.web.filters.CorrelationHeaderFilter; 10 | 11 | import java.util.Arrays; 12 | 13 | @Configuration 14 | @EnableAutoConfiguration 15 | @ComponentScan(basePackages = "uk.co.taidev.experiments.correlationidasync") 16 | public class Application { 17 | 18 | public static void main(String[] args) { 19 | SpringApplication.run(Application.class, args); 20 | } 21 | 22 | 23 | @Bean 24 | public FilterRegistrationBean correlationHeaderFilter() { 25 | FilterRegistrationBean filterRegBean = new FilterRegistrationBean(); 26 | filterRegBean.setFilter(new CorrelationHeaderFilter()); 27 | filterRegBean.setUrlPatterns(Arrays.asList("/*")); 28 | 29 | return filterRegBean; 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/controllers/NewsController.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync.controllers; 2 | 3 | import com.google.common.util.concurrent.ListeningExecutorService; 4 | import com.google.common.util.concurrent.MoreExecutors; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | import org.springframework.web.context.request.async.DeferredResult; 9 | import uk.co.taidev.experiments.correlationidasync.correlationfutures.CorrelationCallable; 10 | import uk.co.taidev.experiments.correlationidasync.correlationfutures.ListenableFutureAdapter; 11 | import uk.co.taidev.experiments.correlationidasync.services.ExternalNewsService; 12 | import uk.co.taidev.experiments.correlationidasync.services.NewsService; 13 | 14 | import java.util.concurrent.Executors; 15 | 16 | /** 17 | * NewsController 18 | */ 19 | @RestController 20 | public class NewsController { 21 | 22 | @Autowired 23 | private ExternalNewsService externalNewsService; 24 | 25 | @Autowired 26 | private NewsService newsService; 27 | 28 | 29 | //TODO: This should be extracted/encapsulated away from the Controller for production apps 30 | private ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); 31 | 32 | 33 | @RequestMapping("externalNews") 34 | public DeferredResult externalNews() { 35 | return new ListenableFutureAdapter<>(service.submit(new CorrelationCallable<>(externalNewsService::getNews))); 36 | } 37 | 38 | 39 | @RequestMapping("news") 40 | public DeferredResult news() { 41 | return new ListenableFutureAdapter<>(service.submit(new CorrelationCallable<>(newsService::getNews))); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/correlationfutures/CorrelationCallable.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync.correlationfutures; 2 | 3 | import uk.co.taidev.experiments.correlationidasync.reporting.RequestCorrelation; 4 | 5 | import java.util.concurrent.Callable; 6 | 7 | /** 8 | * CorrelationCallable 9 | */ 10 | public class CorrelationCallable implements Callable { 11 | 12 | private String correlationId; 13 | private Callable callable; 14 | 15 | public CorrelationCallable(Callable targetCallable) { 16 | correlationId = RequestCorrelation.getId(); 17 | callable = targetCallable; 18 | } 19 | 20 | @Override 21 | public V call() throws Exception { 22 | RequestCorrelation.setId(correlationId); 23 | return callable.call(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/correlationfutures/ListenableFutureAdapter.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync.correlationfutures; 2 | 3 | import com.google.common.util.concurrent.FutureCallback; 4 | import com.google.common.util.concurrent.Futures; 5 | import com.google.common.util.concurrent.ListenableFuture; 6 | import org.springframework.web.context.request.async.DeferredResult; 7 | 8 | /** 9 | * ListenableFutureAdapter 10 | */ 11 | public class ListenableFutureAdapter extends DeferredResult { 12 | 13 | public ListenableFutureAdapter(final ListenableFuture target) { 14 | Futures.addCallback(target, new FutureCallback() { 15 | @Override 16 | public void onSuccess(T result) { 17 | setResult(result.toString()); 18 | } 19 | 20 | @Override 21 | public void onFailure(Throwable t) { 22 | setErrorResult(t); 23 | } 24 | }); 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/reporting/RequestCorrelation.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync.reporting; 2 | 3 | /** 4 | * Utility class which stores ThreadLocal (Request) correlation Id. 5 | */ 6 | public class RequestCorrelation { 7 | 8 | public static final String CORRELATION_ID_HEADER = "correlationId"; 9 | 10 | 11 | private static final ThreadLocal id = new ThreadLocal(); 12 | 13 | public static String getId() { 14 | return id.get(); 15 | } 16 | 17 | public static void setId(String correlationId) { 18 | id.set(correlationId); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/services/ExternalNewsService.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync.services; 2 | 3 | /** 4 | * ExternalNewsService 5 | */ 6 | public interface ExternalNewsService { 7 | 8 | String getNews(); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/services/ExternalNewsServiceRest.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync.services; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import uk.co.taidev.experiments.correlationidasync.web.client.RestClient; 6 | 7 | /** 8 | * ExternalNewsServiceRest 9 | */ 10 | @Service 11 | public class ExternalNewsServiceRest implements ExternalNewsService { 12 | 13 | private static final String NEWS_LOCATION = "http://localhost:8080/news"; 14 | 15 | 16 | @Autowired 17 | private RestClient restClient; 18 | 19 | 20 | @Override 21 | public String getNews() { 22 | //TODO: error-handling and fault-tolerance would be required here in production code 23 | return restClient.getForString(NEWS_LOCATION); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/services/NewsService.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync.services; 2 | 3 | /** 4 | * NewsService 5 | */ 6 | public interface NewsService { 7 | 8 | String getNews(); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/services/NewsServiceSimple.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync.services; 2 | 3 | import org.springframework.stereotype.Service; 4 | import uk.co.taidev.experiments.correlationidasync.reporting.RequestCorrelation; 5 | 6 | /** 7 | * NewsServiceSimple 8 | */ 9 | @Service 10 | public class NewsServiceSimple implements NewsService { 11 | 12 | @Override 13 | public String getNews() { 14 | //you probably wouldn't pollute service code with correlation Ids (instead only include them 15 | //when making external calls), but it is included here as an example of how to retrieve ids 16 | return String.format("No news is good news (with correlation Id '%s')", RequestCorrelation.getId()); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/web/client/CorrelatingRestClient.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync.web.client; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.http.HttpEntity; 6 | import org.springframework.http.HttpHeaders; 7 | import org.springframework.http.HttpMethod; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.client.RestTemplate; 11 | import uk.co.taidev.experiments.correlationidasync.reporting.RequestCorrelation; 12 | 13 | /** 14 | * CorrelatingRestClient 15 | */ 16 | @Component 17 | public class CorrelatingRestClient implements RestClient { 18 | 19 | private static final Logger LOGGER = LoggerFactory.getLogger(CorrelatingRestClient.class); 20 | 21 | private RestTemplate restTemplate = new RestTemplate(); 22 | 23 | 24 | @Override 25 | public String getForString(String uri) { 26 | String correlationId = RequestCorrelation.getId(); 27 | HttpHeaders httpHeaders = new HttpHeaders(); 28 | httpHeaders.set(RequestCorrelation.CORRELATION_ID_HEADER, correlationId); 29 | 30 | LOGGER.info("start REST request to {} with correlationId {}", uri, correlationId); 31 | 32 | //TODO: error-handling and fault-tolerance in production 33 | ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, 34 | new HttpEntity(httpHeaders), String.class); 35 | 36 | LOGGER.info("completed REST request to {} with correlationId {}", uri, correlationId); 37 | 38 | return response.getBody(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/web/client/RestClient.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync.web.client; 2 | 3 | /** 4 | * RestClient 5 | */ 6 | public interface RestClient { 7 | 8 | String getForString(String uri); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/uk/co/taidev/experiments/correlationidasync/web/filters/CorrelationHeaderFilter.java: -------------------------------------------------------------------------------- 1 | package uk.co.taidev.experiments.correlationidasync.web.filters; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import uk.co.taidev.experiments.correlationidasync.reporting.RequestCorrelation; 6 | 7 | import javax.servlet.*; 8 | import javax.servlet.http.HttpServletRequest; 9 | import java.io.IOException; 10 | import java.util.UUID; 11 | 12 | /** 13 | * CorrelationHeaderFilter 14 | */ 15 | public class CorrelationHeaderFilter implements Filter { 16 | 17 | private static final Logger LOGGER = LoggerFactory.getLogger(CorrelationHeaderFilter.class); 18 | 19 | 20 | public void init(FilterConfig filterConfig) throws ServletException { 21 | } 22 | 23 | 24 | @Override 25 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) 26 | throws IOException, ServletException { 27 | 28 | final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; 29 | String currentCorrId = httpServletRequest.getHeader(RequestCorrelation.CORRELATION_ID_HEADER); 30 | 31 | if (!currentRequestIsAsyncDispatcher(httpServletRequest)) { 32 | if (currentCorrId == null) { 33 | currentCorrId = UUID.randomUUID().toString(); 34 | LOGGER.info("No correlationId found in Header. Generated : " + currentCorrId); 35 | } else { 36 | LOGGER.info("Found correlationId in Header : " + currentCorrId); 37 | } 38 | 39 | RequestCorrelation.setId(currentCorrId); 40 | } 41 | 42 | filterChain.doFilter(httpServletRequest, servletResponse); 43 | } 44 | 45 | 46 | @Override 47 | public void destroy() { 48 | } 49 | 50 | 51 | private boolean currentRequestIsAsyncDispatcher(HttpServletRequest httpServletRequest) { 52 | return httpServletRequest.getDispatcherType().equals(DispatcherType.ASYNC); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielbryantuk/correlation-id-async/138724929372868b5152fa2edab2fb31ee715f00/src/main/resources/application.properties --------------------------------------------------------------------------------