Learnitweb

Implementing Cross Cutting Concerns – Tracing and Logging Using Gateway

Cross-cutting concerns such as logging and tracing are crucial in microservices architectures to ensure observability and maintainability. Implementing these concerns at the gateway level allows uniform handling of requests and responses before they reach backend services.

Implementation

We’ll create a package com.learnitweb.gatewayserver.filters and add following classes:

FilterUtility.java

package com.learnitweb.gatewayserver.filters;

import java.util.List;

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

@Component
public class FilterUtility {

	public static final String CORRELATION_ID = "eshop-correlation-id";

	public String getCorrelationId(HttpHeaders requestHeaders) {
		if (requestHeaders.get(CORRELATION_ID) != null) {
			List<String> requestHeaderList = requestHeaders.get(CORRELATION_ID);
			return requestHeaderList.stream().findFirst().get();
		} else {
			return null;
		}
	}

	public ServerWebExchange setRequestHeader(ServerWebExchange exchange, String name, String value) {
		return exchange.mutate().request(exchange.getRequest().mutate().header(name, value).build()).build();
	}

	public ServerWebExchange setCorrelationId(ServerWebExchange exchange, String correlationId) {
		return this.setRequestHeader(exchange, CORRELATION_ID, correlationId);
	}

}

Key functions:

  • getCorrelationId(HttpHeaders requestHeaders) : Retrieves the eshop-correlation-id from request headers.
  • setRequestHeader(ServerWebExchange exchange, String name, String value): Modifies the request to add/update a specific header.
  • setCorrelationId(ServerWebExchange exchange, String correlationId): Calls setRequestHeader() to ensure the correlation ID is set in the request.

RequestTraceFilter.java

package com.learnitweb.gatewayserver.filters;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Mono;

@Order(1)
@Component
public class RequestTraceFilter implements GlobalFilter {

	private static final Logger logger = LoggerFactory.getLogger(RequestTraceFilter.class);

	@Autowired
	FilterUtility filterUtility;

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		HttpHeaders requestHeaders = exchange.getRequest().getHeaders();
		if (isCorrelationIdPresent(requestHeaders)) {
			logger.debug("eshop-correlation-id found in RequestTraceFilter : {}",
					filterUtility.getCorrelationId(requestHeaders));
		} else {
			String correlationID = generateCorrelationId();
			exchange = filterUtility.setCorrelationId(exchange, correlationID);
			logger.debug("eshop-correlation-id generated in RequestTraceFilter : {}", correlationID);
		}
		return chain.filter(exchange);
	}

	private boolean isCorrelationIdPresent(HttpHeaders requestHeaders) {
		if (filterUtility.getCorrelationId(requestHeaders) != null) {
			return true;
		} else {
			return false;
		}
	}

	private String generateCorrelationId() {
		return java.util.UUID.randomUUID().toString();
	}

}
  • Checks if a correlation ID (eshop-correlation-id) exists in request headers.
    • If present, logs the existing ID.
    • If absent, generates a new UUID, adds it to the request, and logs it.
  • Uses FilterUtility to retrieve and set the correlation ID.
  • Passes the request to the next filter in the chain.

ResponseTraceFilter.java

package com.learnitweb.gatewayserver.filters;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;

import reactor.core.publisher.Mono;

@Configuration
public class ResponseTraceFilter {

	private static final Logger logger = LoggerFactory.getLogger(ResponseTraceFilter.class);

	@Autowired
	FilterUtility filterUtility;

	@Bean
	public GlobalFilter postGlobalFilter() {
		return (exchange, chain) -> {
			return chain.filter(exchange).then(Mono.fromRunnable(() -> {
				HttpHeaders requestHeaders = exchange.getRequest().getHeaders();
				String correlationId = filterUtility.getCorrelationId(requestHeaders);
				logger.debug("Updated the correlation id to the outbound headers: {}", correlationId);
				exchange.getResponse().getHeaders().add(filterUtility.CORRELATION_ID, correlationId);
			}));
		};
	}
}

The ResponseTraceFilter is a post-processing filter in Spring Cloud Gateway that adds a correlation ID to the outbound response headers.

  • Intercepts responses after processing the request.
  • Retrieves the correlation ID from the request headers using FilterUtility.
  • Logs the correlation ID for debugging.
  • Adds the correlation ID to the response headers to maintain traceability.

application.yml

Add the following to the application.yml:

logging:
  level:
    com:
      learnitweb:
        gatewayserver: DEBUG

Now, read the eshop-correlation-id header in productservice and log using logger.

@RestController
@RequestMapping("/api")
public class InventoryController {

	@Autowired
	private InventoryFeignClient inventoryFeignClient;

	private static final Logger logger = LoggerFactory.getLogger(InventoryController.class);

	@GetMapping("/inventory/{name}")
	public ResponseEntity<?> getProductInventory(@RequestHeader("eshop-correlation-id") String correlationId,
			@PathVariable String name) {
		logger.debug("eshop-correlation-id found: {} ", correlationId);
		ResponseEntity<InventoryDto> result = inventoryFeignClient.getProductInventory(name, correlationId);
		return result;

	}

}

Make RequestHeader changes to the feign client.

@FeignClient(name = "InventoryService")
public interface InventoryFeignClient {

	@GetMapping("/api/inventory/{name}")
	public ResponseEntity<InventoryDto> getProductInventory(@PathVariable("name") String name,
			@RequestHeader("eshop-correlation-id") String correlationId);

}

Make changes to the inventoryservice for RequestHeader.

@RestController
@RequestMapping("/api")
public class InventoryController {

	private static final Logger logger = LoggerFactory.getLogger(InventoryController.class);

	@GetMapping("/inventory/{name}")
	public ResponseEntity<?> getProductInventory(@PathVariable String name,
			@RequestHeader("eshop-correlation-id") String correlationId) {

		logger.debug("eshop-correlation-id found: {} ", correlationId);

		if (name.equals("car")) {
			InventoryDto inventory = new InventoryDto();
			inventory.setProductName("some car");
			inventory.setQuantityRemaining(100);

			return new ResponseEntity<InventoryDto>(inventory, HttpStatus.OK);
		}
		return new ResponseEntity(null, HttpStatus.OK);

	}

}

In gateway server we see something like the following:

eshop-correlation-id generated in RequestTraceFilter : 33614ca0-3989-42fd-b7f8-aeaddcf3caaf
Updated the correlation id to the outbound headers: 33614ca0-3989-42fd-b7f8-aeaddcf3caaf