Implement trace-id with Spring Webflux - spring-boot

I'd like to generate unique traceId per request and pass it through all services. In Spring MVC it was fairly easy by using MDC context and putting traceId in header, but in reactive stack it isn't working at all because of ThreadLocal.
In general I'd like to log each request and response on every service I have with single traceId which can identify specific action in whole system.
I tried to create custom filter based on article: https://azizulhaq-ananto.medium.com/how-to-handle-logs-and-tracing-in-spring-webflux-and-microservices-a0b45adc4610 but it's seems to not working.
My current solution only log responses and traceId are losing after making request, so there is no on response.
Let's try imagine that there are two services: service1 and service2. Below I tried to sketch how it should work.
How should it work
client -> service1 - service1 should generate traceId and log request
service1 -> service2 - service2 should fetch traceId from request, then log request
service1 <- service2 - after some calculation service2 should log response and return response to service1
client <- service1 - at the end service1 should log response (still with the same traceId) and return response to client
How it actually works
client -> service1 - nothing in logs
service1 -> service2 - nothings in logs
service1 <- service2 - service2 is logging correctly and return response to service1
client <- service1 - service1 is logging response (but without traceId)
Here is my approach
#Component
public class TraceIdFilter implements WebFilter {
private static final Logger log = LoggerFactory.getLogger(TraceIdFilter.class);
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Map<String, String> headers = exchange.getRequest().getHeaders().toSingleValueMap();
return Mono.fromCallable(() -> {
final long startTime = System.currentTimeMillis();
return new ServerWebExchangeDecorator(exchange) {
#Override
public ServerHttpRequest getRequest() {
return new RequestLoggingInterceptor(super.getRequest(), false);
}
#Override
public ServerHttpResponse getResponse() {
return new ResponseLoggingInterceptor(super.getResponse(), startTime, false);
}
};
}).contextWrite(context -> {
var traceId = "";
if (headers.containsKey("X-B3-TRACEID")) {
traceId = headers.get("X-B3-TRACEID");
MDC.put("X-B3-TraceId", traceId);
} else if (!exchange.getRequest().getURI().getPath().contains("/actuator")) {
traceId = UUID.randomUUID().toString();
MDC.put("X-B3-TraceId", traceId);
}
Context contextTmp = context.put("X-B3-TraceId", traceId);
exchange.getAttributes().put("X-B3-TraceId", traceId);
return contextTmp;
}).flatMap(chain::filter);
}
}
Github: https://github.com/Faelivrinx/kotlin-spring-boot
There is any existing solution do that?

In Spring Webflux you don't have anymore a ThreadLocal but you have a unique context for each chain request. You can attach a traceId to this context as following:
#Component
public class TraceIdFilter implements WebFilter {
private static final Logger log = LoggerFactory.getLogger(TraceIdFilter.class);
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.subscriberContext(
ctx -> {
.....
var traceId = UUID.randomUUID().toString();
return ctx.put("X-B3-TraceId", traceId);
.....
}
);
}
}
Now the chain in your service will have in the context this attribute. You can retrive it from your service using static method Mono.subscriberContext(). For example you can get traceId in this way
Mono.subscriberContext()
.flaMap(ctx -> {
.....
var traceId = ctx.getOrDefault("traceId", null);
.....
)

Sleuth 3.0 provides automatic instrumentation for WebFlux, which means, if you do nothing, you will always get the current span of the thread which subscribes to your Mono or Flux. If you wish to overwrite this, for instance because you are batching a lot of requests and want a unique trace for every transaction, all you have to do is to manipulate the Context of your operator chain.
private Mono<Data> performRequest() {
var span = tracer.spanBuilder().setNoParent().start(); // generate a completely new trace
// note: you can also just generate a new span if you want
return Mono.defer(() -> callRealService())
.contextWrite(Context.of(TraceContext.class, span.context());
}
When following this approach, make sure to import org.springframework.cloud.sleuth.Tracer and not the one by brave, because they use different types and Reactor will drop your Mono with an ugly error message if they don't align correctly (unfortunately, since the Context ist just a plain old Map<Object, Object>, you won't get a compiler error).

Related

Spring Boot WebFlux with RouterFunction log request and response body

I'm trying to log request and response body of every call received by my microservice which is using reactive WebFlux with routing function defined as bellow:
#Configuration
public class FluxRouter {
#Autowired
LoggingHandlerFilterFunction loggingHandlerFilterFunction;
#Bean
public RouterFunction<ServerResponse> routes(FluxHandler fluxHandler) {
return RouterFunctions
.route(POST("/post-flux").and(accept(MediaType.APPLICATION_JSON)), fluxHandler::testFlux)
.filter(loggingHandlerFilterFunction);
}
}
#Component
public class FluxHandler {
private static final Logger logger = LoggerFactory.getLogger(FluxHandler.class);
#LogExecutionTime(ActionType.APP_LOGIC)
public Mono<ServerResponse> testFlux(ServerRequest request) {
logger.info("FluxHandler.testFlux");
return request
.bodyToMono(TestBody.class)
.doOnNext(body -> logger.info("FluxHandler-2: "+ body.getName()))
.flatMap(testBody -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue("Hello, ")));
}
}
and a router filter defined like this:
#Component
public class LoggingHandlerFilterFunction implements HandlerFilterFunction<ServerResponse, ServerResponse> {
private static final Logger logger = LoggerFactory.getLogger(LoggingHandlerFilterFunction.class);
#Override
public Mono<ServerResponse> filter(ServerRequest request, HandlerFunction<ServerResponse> handlerFunction) {
logger.info("Inside LoggingHandlerFilterFunction");
return request.bodyToMono(Object.class)
.doOnNext(o -> logger.info("Request body: "+ o.toString()))
.flatMap(testBody -> handlerFunction.handle(request))
.doOnNext(serverResponse -> logger.info("Response body: "+ serverResponse.toString()));
}
}
Console logs here are:
Inside LoggingHandlerFilterFunction
LoggingHandlerFilterFunction-1: {name=Name, surname=Surname}
FluxHandler.testFlux
As you can see I'm able to log the request but after doing it the body inside the FluxHandler seems empty because this is not triggered .doOnNext(body -> logger.info("FluxHandler-2: "+ body.getName()) and also no response body is produced. Can someone show me how to fix this or is there any other way to log request/response body when using routes in webflux?

Async RabbitMQ communcation using Spring Integration

I have two spring boot services that communicate using RabbitMQ.
Service1 sends request for session creation to Service2.
Service2 handles request and should return response.
Service1 should handle the response.
Service1 method for requesting session:
public void startSession()
{
ListenableFuture<SessionCreationResponseDTO> sessionCreationResponse = sessionGateway.requestNewSession();
sessionCreationResponse.addCallback(response -> {
//handle success
}, ex -> {
// handle exception
});
}
On Service1 I have defined AsyncOutboundGateway, like:
#Bean
public IntegrationFlow requestSessionFlow(MessageChannel requestNewSessionChannel,
AsyncRabbitTemplate amqpTemplate,
SessionProperties sessionProperties)
{
return flow -> flow.channel(requestNewSessionChannel)
.handle(Amqp.asyncOutboundGateway(amqpTemplate)
.exchangeName(sessionProperties.getRequestSession().getExchangeName())
.routingKey(sessionProperties.getRequestSession().getRoutingKey()));
}
On Service2, I have flow for receiving these messages:
#Bean
public IntegrationFlow requestNewSessionFlow(ConnectionFactory connectionFactory,
SessionProperties sessionProperties,
MessageConverter messageConverter,
RequestNewSessionHandler requestNewSessionHandler)
{
return IntegrationFlows.from(Amqp.inboundGateway(connectionFactory,
sessionProperties.requestSessionProperties().queueName())
.handle(requestNewSessionHandler)
.get();
Service2 handles there requests:
#ServiceActivator(async = "true")
public ListenableFuture<SessionCreationResponseDTO> handleRequestNewSession()
{
SettableListenableFuture<SessionCreationResponseDTO> settableListenableFuture = new SettableListenableFuture<>();
// Goes through asynchronous process of creating session and sets value in listenable future
return settableListenableFuture;
}
Problem is that Service2 immediately returns ListenableFuture to Service1 as message payload, instead of waiting for result of future and sending back result.
If I understood documentation correctly Docs by setting async parameter in #ServiceActivator to true, successful result should be returned and in case of exception, error channel would be used.
Probably I misunderstood documentation, so that I need to unpack ListenableFuture in flow of Service2 before returning it as response, but I am not sure how to achieve that.
I tried something with publishSubscribeChannel but without much luck.
Your problem is here:
.handle(requestNewSessionHandler)
Such a configuration doesn't see your #ServiceActivator(async = "true") and uses it as a regular blocking service-activator.
Let's see if this helps you:
.handle(requestNewSessionHandler, "handleRequestNewSession", e -> e.async(true))
It is better to think about it like: or only annotation configuration. or only programmatic, via Java DSL.

Spring Webflux - Publish all HTTP requests to pubsub

In my app I have one endpoint under /my-endpoint path which supports only post method. It accepts a body that must be compatible with my MyRequest class.
#Validated
data class MyRequest(
#get:JsonProperty("age", required = true)
#field:Size(min = 3, max = 128, message = "age must be between 3 and 128")
val age: String,
#get:JsonProperty("zip_code", required = true)
#field:Pattern(regexp = "\\d{2}-\\d{3}", message = "address.zip_code is invalid. It is expected to match pattern \"\\d{2}-\\d{3}\"")
val zipCode: String
)
And my controller looks like this
#PostMapping("/my-endpoint")
fun myEndpoint(
#Valid #RequestBody request: MyRequest,
): Mono<ResponseEntity<MyResponse>> {
return myService.processRequest(request)
.map { ResponseEntity.ok().body(it) }
}
Each time I receive some request to THIS particular endpoint (I have other endpoints but them should be ignored) - I'd like to publish a message to my pubsub consisting raw request body (as a string) - no matter whether the request body was valid or not.
How to intercept the request to be able to publish the message - still having the endpoint working ?
I think you could implement your own WebFilter. Filter the API path through exchange.getRequest().getPath() using simple if block and get the body through exchange.getRequest().getBody()
#Component
#RequiredArgsConstructor
#Slf4j
public class MyFilter implements WebFilter {
private final MyPublisher myPublisher;
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (pathMatches(exchange.getRequest().getPath()) {
return exchange.getRequest().getBody()
.map(dataBuffer -> {
final String requestBody = dataBuffer.toString(StandardCharsets.UTF_8));
this.myPublisher.publish(requestBody).subscribe();
return exchange;
}).then(chain.filter(exchange));
}
return chain.filter(exchange);
}
}

Request response over HTTP with Spring and activemq

I am building a simple REST api which connects a web server to a back end service, which performs a simple check and sends a response.
So client (over HTTP) -> to Web Server (over ACTIVEMQ/CAMEL)-> to Checking-Service, and back again.
The endpoint for the GET request is "/{id}". I'm trying to make this send a message through queue:ws-out to queue:cs-in and map it all the way back again to the original GET request.
The Checking-Service (cs) code is fine, it simply changes a value in the CheckMessage object to true using jmslistener.
I've searched the web thoroughly for examples, but can't get anything to work. The closest one I found was the following.
This is what I have so far on the Web Server (ws).
RestController
import ...
#RestController
public class RESTController extends Exception{
#Autowired
CamelContext camelContext;
#Autowired
JmsTemplate jmsTemplate;
#GetMapping("/{id}")
public String testCamel(#PathVariable String id) {
//Object used to send out
CheckMessage outMsg = new CheckMessage(id);
//Object used to receive response
CheckMessage inMsg = new CheckMessage(id);
//Sending the message out (working)
jmsTemplate.convertAndSend("ws-out", outMsg);
//Returning the response to the client (need correlation to the out message"
return jmsTemplate.receiveSelectedAndConvert("ws-in", ??);
}
}
Listener on ws
#Service
public class WSListener {
//For receiving the response from Checking-Service
#JmsListener(destination = "ws-in")
public void receiveMessage(CheckMessage response) {
}
}
Thanks!
your receive messages from "ws-in" with 2 consumers jmsTemplate.receiveSelectedAndConvert and WSListener !! message from a queue is consumed by one of the 2.
you send messages to "ws-out" and consume from "ws-in" ?? last queue
is empty and not receive any message, you have to send messages to
it
you need a valid selector to retrieve the message with receiveSelectedAndConvert based on JMSCorrelationID as the example you mntioned or the id received from the rest request but you need to add this id to the message headers like below
this.jmsTemplate.convertAndSend("ws-out", id, new MessageCreator() {
#Override
public Message createMessage(Session session) throws JMSException {
TextMessage tm = session.createTextMessage(new CheckMessage(id));
tm.setJMSCorrelationID(id);
return tm;
}
});
return jmsTemplate.receiveSelectedAndConvert("ws-in", "JMSCorrelationID='" + id+ "'");
forward messages from "ws-out" to "ws-in"
#Service
public class WSListener {
//For receiving the response from Checking-Service
#JmsListener(destination = "ws-out")
public void receiveMessage(CheckMessage response) {
jmsTemplate.convertAndSend("ws-in", response);
}
}

Can I use Spring WebFlux to implement REST services which get data through Kafka request/response topics?

I'm developing REST service which, in turn, will query slow legacy system so response time will be measured in seconds. We also expect massive load so I was thinking about asynchronous/non-blocking approaches to avoid hundreds of "servlet" threads blocked on calls to slow system.
As I see this can be implemented using AsyncContext which is present in new servlet API specs. I even developed small prototype and it seems to be working.
On the other hand it looks like I can achieve the same using Spring WebFlux.
Unfortunately I did not find any example where custom "backend" calls are wrapped with Mono/Flux. Most of the examples just reuse already-prepared reactive connectors, like ReactiveCassandraOperations.java, etc.
My data flow is the following:
JS client --> Spring RestController --> send request to Kafka topic --> read response from Kafka reply topic --> return data to client
Can I wrap Kafka steps into Mono/Flux and how to do this?
How my RestController method should look like?
Here is my simple implementation which achieves the same using Servlet 3.1 API
//took the idea from some Jetty examples
public class AsyncRestServlet extends HttpServlet {
...
#Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String result = (String) req.getAttribute(RESULTS_ATTR);
if (result == null) { //data not ready yet: schedule async processing
final AsyncContext async = req.startAsync();
//generate some unique request ID
String uid = "req-" + String.valueOf(req.hashCode());
//share it to Kafka receive together with AsyncContext
//when Kafka receiver will get the response it will put it in Servlet request attribute and call async.dispatch()
//This doGet() method will be called again and it will send the response to client
receiver.rememberKey(uid, async);
//send request to Kafka
sender.send(uid, param);
//data is not ready yet so we are releasing Servlet thread
return;
}
//return result as html response
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println(result);
out.close();
}
Here's a short example - Not the WebFlux client you probably had in mind, but at least it would enable you to utilize Flux and Mono for asynchronous processing, which I interpreted to be the point of your question. The web objects should work without additional configurations, but of course you will need to configure Kafka as the KafkaTemplate object will not work on its own.
#Bean // Using org.springframework.web.reactive.function.server.RouterFunction<ServerResponse>
public RouterFunction<ServerResponse> sendMessageToTopic(KafkaController kafkaController){
return RouterFunctions.route(RequestPredicates.POST("/endpoint"), kafkaController::sendMessage);
}
#Component
public class ResponseHandler {
public getServerResponse() {
return ServerResponse.ok().body(Mono.just(Status.SUCCESS), String.class);
}
}
#Component
public class KafkaController {
public Mono<ServerResponse> auditInvalidTransaction(ServerRequest request) {
return request.bodyToMono(TopicMsgMap.class)
// your HTTP call may not return immediately without this
.subscribeOn(Schedulers.single()) // for a single worker thread
.flatMap(topicMsgMap -> {
MyKafkaPublisher.sendMessages(topicMsgMap);
}.flatMap(responseHandler::getServerResponse);
}
}
#Data // model class just to easily convert the ServerRequest (from json, for ex.)
// + ~#constructors
public class TopicMsgMap() {
private Map<String, String> topicMsgMap;
}
#Service // Using org.springframework.kafka.core.KafkaTemplate<String, String>
public class MyKafkaPublisher {
#Autowired
private KafkaTemplate<String, String> template;
#Value("${topic1}")
private String topic1;
#Value("${topic2}")
private String topic2;
public void sendMessages(Map<String, String> topicMsgMap){
topicMsgMap.forEach((top, msg) -> {
if (topic.equals("topic1") kafkaTemplate.send(topic1, message);
if (topic.equals("topic2") kafkaTemplate.send(topic2, message);
});
}
}
Guessing this isn't the use-case you had in mind, but hope you find this general structure useful.
There is several approaches including KafkaReplyingRestTemplate for this problem but continuing your approach in servlet api's the solution will be something like this in spring Webflux.
Your Controller method looks like this:
#RequestMapping(path = "/completable-future", method = RequestMethod.POST)
Mono<Response> asyncTransaction(#RequestBody RequestDto requestDto, #RequestHeader Map<String, String> requestHeaders) {
String internalTransactionId = UUID.randomUUID().toString();
kafkaSender.send(Request.builder()
.transactionId(requestHeaders.get("transactionId"))
.internalTransactionId(internalTransactionId)
.sourceIban(requestDto.getSourceIban())
.destIban(requestDto.getDestIban())
.build());
CompletableFuture<Response> completableFuture = new CompletableFuture();
taskHolder.pushTask(completableFuture, internalTransactionId);
return Mono.fromFuture(completableFuture);
}
Your taskHolder component will be something like this:
#Component
public class TaskHolder {
private Map<String, CompletableFuture> taskHolder = new ConcurrentHashMap();
public void pushTask(CompletableFuture<Response> task, String transactionId) {
this.taskHolder.put(transactionId, task);
}
public Optional<CompletableFuture> remove(String transactionId) {
return Optional.ofNullable(this.taskHolder.remove(transactionId));
}
}
And finally your Kafka ResponseListener looks like this:
#Component
public class ResponseListener {
#Autowired
TaskHolder taskHolder;
#KafkaListener(topics = "reactive-response-topic", groupId = "test")
public void listen(Response response) {
taskHolder.remove(response.getInternalTransactionId()).orElse(
new CompletableFuture()).complete(response);
}
}
In this example I used internalTransactionId as CorrelationId but you can use "kafka_correlationId" that is a known kafka header.

Resources