Why doOnError logs different when subscribing with .subscribe in webflux - spring-webflux

i need help to understand why the error message is not logged when subscribing to the stream with .subscribe(). However, the error is logged when subscribing with .block(). I understand that it might be to the block() call subscribing on the main thread. I was wondering which changes should i do in order to see the error logged when subscribing with .subscribe(). Also not sure whether it is the best way to deal listen for sqs messages on webflux.
Thanks,
#SqsListener(value = "${sqs.queues.email-notifications}", deletionPolicy = NEVER)
public void listenNotifications(MessageDto message, Acknowledgment acknowledgment) {
Mono.just(message)
.log("NotificationsSQSConsumer.listenNotifications")
.map(this::toSendEmailCommand)
.flatMap(sendEmailUseCase::handle)
.then(sendAck(acknowledgment))
.log("NotificationsSQSConsumer.sendAck")
//.block(); //.subscribe() nop**
}
private Mono<?> sendAck(Acknowledgment acknowledgment) {
return Mono.fromCallable(() -> acknowledgment.acknowledge().get())
.doOnError(throwable -> log.error("ERROR!:" + throwable.getMessage()))
.subscribeOn(Schedulers.boundedElastic());
}
class SendEmailUseCase {
public Mono<Void> handle(SendEMailCommand sendEMailCommand) {
return Mono.just(sendEMailCommand.getTemplateId())
.flatMap(templateLoader::getTemplate) //do a Mono.fromCallable on a bounded elastic
.map(s -> Tuples.of(new StringReader(s), sendEMailCommand.getTemplateId()))
.map(this::newMustache)
.map(c -> Tuples.of(c, sendEMailCommand.getTemplateData()))
.map(this::build)
.map(mailText -> Tuples.of(sendEMailCommand.getReceiver(), sendEMailCommand.getSubject(), mailText))
.map(this::newEmailMessage)
.flatMap(emailSender::sendMail); //do a Mono.fromCallable on bounded elastic
}
}
//Json
{
"dd.span_id":"0",
"dd.trace_id":"0",
"timestamp":"2021-03-04T15:53:00.588 03:00",
"event_type":"logging",
"level":"ERROR",
"thread_name":"boundedElastic-1",
"logger_name":"c.n.c.m.i.m.NotificationsSQSConsumer",
"message":"ERROR!:com.amazonaws.services.sqs.model.AmazonSQSException: Value AQEB/60/Nzg2oC9tdLlZT4BiztR8UAgd503EZ/bHQ7bE6WuKRfF59Y5l/2+gmqMZtSJs8sug6qUNuUt7pmXVM8G2S3TVt9yVc05t
}

I realized that i was missing the error callback on .subscribe()
#Async
#SqsListener(value = "${sqs.queues.email-notifications}", deletionPolicy = NEVER)
public void handle(MessageDto message, Acknowledgment acknowledgment) {
Mono.just(message)
.log("NotificationsSQSConsumer.listenNotifications")
.map(this::toSendEmailCommand)
.flatMap(sendEmailUseCase::handle)
.then(sendAck(acknowledgment))
.log("NotificationsSQSConsumer.sendAck")
.subscribe(o -> {
}, t -> log.error("ERROR!:" + t.getMessage()));
}
private Mono<?> sendAck(Acknowledgment acknowledgment) {
return Mono.fromCallable(() -> acknowledgment.acknowledge().get())
.subscribeOn(Schedulers.boundedElastic());
}

Related

RxAndroidBle waiting for response from peripheral when write on it (not long write) in Kotlin

I am trying to write to a peripheral in Android Kotlin using RxAndroidBle. The application writes to the peripheral and then the peripheral responds if this write request is successful, i.e.
According to the evaluation made of the information sent to the peripheral, the peripheral sends a response to the app if it is the expected information, if not the expected information, then the peripheral responds with a different response; In summary, it is a scenario very similar to an HTTP request via POST, information is sent and the server responds with a status if the information meets the requirements. I already managed to connect perfectly and read information from the peripheral in the following way:
override fun connectDeviceToGetInfoHardwareByBle(mac: String): Observable<Resource<HardwareInfoResponse>> {
val device: RxBleDevice = bleClient.getBleDevice(mac)
return Observable.defer {
device.bluetoothDevice.createBond()// it is a blocking function
device.establishConnection(false) // return Observable<RxBleConnection>
}
.delay(5, TimeUnit.SECONDS)
.flatMapSingle { connection ->
connection.requestMtu(515)
.flatMap {
Single.just(connection)
}
}
.flatMapSingle {
it.readCharacteristic(UUID.fromString(GET_HARDWARE_INFORMATION_CHARACTERISTIC))
.map { byteArray ->
evaluateHardwareInfoResponse(byteArray = byteArray)
}
}
.map {
Resource.success(data = it)
}
.take(1)
.onErrorReturn {
Timber.i("Rointe Ble* Error getting ble information. {$it}")
Resource.error(data = null, message = it.message.toString())
}
.doOnError {
Timber.i("Rointe Ble*","Error getting ble information."+it)
}
.subscribeOn(ioScheduler)
.observeOn(uiScheduler)
}
As you can see, the MTU is needed by the peripheral, and it answers what I need. After that response, I close that BLE connection and the app does another independent job on the network (HTTP). Then it is required to connect again but this time it is necessary to write JSON information to the peripheral and the device analyzes that JSON and gives some answers that I need as a return; How do I implement a write waiting for a response from the peripheral? Is it necessary to do a long-write for a JSON since I'm assigning MTU on the connection? I'm developing this in Kotlin under the Repository pattern.
The JSON sent is this:
{
"data": {
"id_hardware": "[ID_HARDWARE]",
"product_brand": <value>,
"product_type": <value>,
"product_model": <value>,
"nominal_power": <value>,
"industrialization_process_date": <value>,
"platform_api_path": "[Host_API_REST]",
"platform_streaming_path": "[Host_STREAMING]",
"updates_main_path": "[Host_UPDATES]",
"updates_alternative_path": "[Host_ALTERNATIVE_UPDATES]",
"check_updates_time": <value>,
"check_updates_day": <value>,
"auth_main_path": "[Host_AUTHORIZATION]",
"auth_alternative_path": "[Host_BACKUP_AUTHORIZATION]",
"analytics_path": "[Host_ANALYTICS]",
"idToken": "[ID_TOKEN]",
"refreshToken": "[REFRESH_TOKEN]",
"expiresIn": "3600",
"apiKey": "[API_KEY]",
"factory_wifi_ssid": "[FACTORY_WIFI_SSID]",
"factory_wifi_security_type": "[FACTORY_WIFI_TYPE]",
"factory_wifi_passphrase": "[FACTORY_WIFI_PASS]",
"factory_wifi_dhcp": 1,
"factory_wifi_device_ip": "[IPv4]",
"factory_wifi_subnet_mask": "[SubNetMask_IPv4]",
"factory_wifi_gateway": "[IPv4]"
},
"factory_version": 1,
"crc": ""
}
The peripheral analyzes that JSON and gives me some answers according to the JSON sent.
Now, the way I try to do the write expecting a response is this:
private fun setupNotifications(connection: RxBleConnection): Observable<Observable<ByteArray>> =
connection.setupNotification(UUID.fromString(SET_FACTORY_SETTINGS_CHARACTERISTIC))
private fun performWrite(connection: RxBleConnection, notifications: Observable<ByteArray>, data: ByteArray): Observable<ByteArray> {
return connection.writeCharacteristic(UUID.fromString(SET_FACTORY_SETTINGS_CHARACTERISTIC), data).toObservable()
}
override fun connectDeviceToWriteFactorySettingsByBle(mac: String, data: ByteArray): Observable<Resource<HardwareInfoResponse>> {
val device: RxBleDevice = bleClient.getBleDevice(mac)
return Observable.defer {
//device.bluetoothDevice.createBond()// it is a blocking function
device.establishConnection(false) // return Observable<RxBleConnection>
}
.delay(5, TimeUnit.SECONDS)
.flatMapSingle { connection ->
connection.requestMtu(515)
.flatMap {
Single.just(connection)
}
}
.flatMap(
{ connection -> setupNotifications(connection).delay(5, TimeUnit.SECONDS) },
{ connection, deviceCallbacks -> performWrite(connection, deviceCallbacks, data) }
)
.flatMap {
it
}
//.take(1) // after the successful write we are no longer interested in the connection so it will be released
.map {
Timber.i("Rointe Ble: Result write: ok ->{${it.toHex()}}")
Resource.success(data = evaluateHardwareInfoResponse(it))
}
//.take(1)
.onErrorReturn {
Timber.i("Rointe Ble: Result write: failed ->{${it.message.toString()}}")
Resource.error(data = HardwareInfoResponse.NULL_HARDWARE_INFO_RESPONSE, message = "Error write on device.")
}
.doOnError {
Timber.i("Rointe Ble*","Error getting ble information."+it)
}
//.subscribeOn(ioScheduler)
.observeOn(uiScheduler)
}
As can be seen, the MTU is negotiated to the maximum and a single packet is sent (json file shown).
When I run my code it connects but shows this error:
com.polidea.rxandroidble2.exceptions.BleCannotSetCharacteristicNotificationException:
Cannot find client characteristic config descriptor (code 2) with
characteristic UUID 4f4a4554-4520-4341-4c4f-520001000002
Any help on Kotlin?
Thanks a lot!!
When I run my code it connects but shows this error:
com.polidea.rxandroidble2.exceptions.BleCannotSetCharacteristicNotificationException:
Cannot find client characteristic config descriptor (code 2) with
characteristic UUID 4f4a4554-4520-4341-4c4f-520001000002
You can fix this in two ways:
Change your peripheral code to include a Client Characteristic Config Descriptor on the characteristic that you want to use notifications on – this is the preferred way as it would make the peripheral conform to Bluetooth Specification
Use COMPAT mode when setting up notification which does not set CCCD value at all
How to clean UUID's characteristics cache? what happens is the library remember in cache maybe the last UUID registered. How I clean this cache?
It is possible to clear the cache by using BluetoothGatt#refresh and subsequently getting the new set of services which will allow bypassing the library UUID helper — you need to use functions that accept BluetoothGattCharacteristic instead of UUID.
Code that refreshes BluetoothGatt:
RxBleCustomOperation<Void> bluetoothGattRefreshCustomOp = (bluetoothGatt, rxBleGattCallback, scheduler) -> {
try {
Method bluetoothGattRefreshFunction = bluetoothGatt.getClass().getMethod("refresh");
boolean success = (Boolean) bluetoothGattRefreshFunction.invoke(bluetoothGatt);
if (!success) return Observable.error(new RuntimeException("BluetoothGatt.refresh() returned false"));
return Observable.<Void>empty().delay(500, TimeUnit.MILLISECONDS);
} catch (NoSuchMethodException e) {
return Observable.error(e);
} catch (IllegalAccessException e) {
return Observable.error(e);
} catch (InvocationTargetException e) {
return Observable.error(e);
}
};
Code that discovers services bypassing the library caches:
RxBleCustomOperation<List<BluetoothGattService>> discoverServicesCustomOp = (bluetoothGatt, rxBleGattCallback, scheduler) -> {
boolean success = bluetoothGatt.discoverServices();
if (!success) return Observable.error(new RuntimeException("BluetoothGatt.discoverServices() returned false"));
return rxBleGattCallback.getOnServicesDiscovered()
.take(1) // so this RxBleCustomOperation will complete after the first result from BluetoothGattCallback.onServicesDiscovered()
.map(RxBleDeviceServices::getBluetoothGattServices);
};

Spring reactive web client REST request with oauth token in case of 401 response

I wanted to play around with Spring reactive web client and an actually simple example: Ask for a REST resource and in case of a 401 response get new OAuth access token.
The first part seemed to be easy:
return webClientBuilder
.baseUrl(targetInstance.getBaseUrl())
.build()
.get().uri(targetInstance.getItemEndpointUrl())
.retrieve()
.bodyToMono(ItemResponse.class)
....
But here the confusion already started. I tried something like
.onStatus(HttpStatus::is4xxClientError, (response) -> {
if(response.rawStatusCode() == 401) {
oAuthClient.initToken()
My token should then be saved within an instance JPA entity. But I have a lack of conceptual understanding here I guess. When the OAuth client receives the OAuth response I need to extract it first to persist it (as embedded object) within my instance entity. And therefore I need to block it, right?
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
OAuthResponse oauthResponse = response.bodyToMono(OAuthResponse.class).block();
}
Based on the response result of the OAuth client I need some kind of Mono to tell the actual REST client then if it should start a retry? And which way should be the preferred on: .retrieve() or .exchangeToMono()? So I'm a bit lost here if I'm on the right path or if something like that should better be done with the classic RestTemplate? But I've also read that the RestTemplate is no deprecated...
Thanks for sharing some thoughts with me.
Ok, in the meantime I've found a non-blocking way. Maybe not the best, but it works out well for me.
The client:
class ApiClient {
public Mono<MyResponse> getResponse(Tenant tenant) {
return webClientBuilder
.baseUrl(tenant.getUrl())
.clientConnector(getClientConnector())
.build()
.get().uri("/api/my-content-entpoint")
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(MyResponse.class);
} else if(response.statusCode().equals(HttpStatus.FORBIDDEN)) {
return Mono.error(new MyOAuthExcpetion());
} else {
return Mono.empty();
}
});
}
}
the service:
#Service
public class MyService {
private final ApiClient apiClient;
private final RetryStrategy retryStrategy;
private final TenantService tenantService;
public Mono<MyResponse> getResponse(String tenantId){
return tenantService.getTenant(tenantId)
.flatMap(tenant-> apiClient.getResponse(instance))
.retryWhen(Retry.from(signals -> signals
.flatMap(retrySignal -> retryStrategy.reconnect(retrySignal, tenantId))));
}
}
and the retry strategy
#Component
public class RetryStrategy {
private final TenantService tenantService;
public Publisher<? extends Long> reconnect(RetrySignal retrySignal, String tenantId) {
long count = retrySignal.totalRetriesInARow();
Throwable failure = retrySignal.failure();
if(count > 0) {
return Mono.error(new UnsupportedOperationException("Retry failed", failure));
}
Mono<Tenant> updatedTenant = null;
if(failure instanceof MyOAuthExcpetion) {
updatedTenant = tenantService.getTenant(tenantId)
.flatMap(tenant -> tenantService.refreshOAuth(tenant));
}
if(updatedTenant == null) {
return Mono.error(new UnsupportedOperationException("Retry failed", failure));
}
return updatedTenant.then(Mono.delay(Duration.ofSeconds(1)));
}
}
Happy for any feedback or improvements.
In my application I went with prechecking the token before requests are being made:
client.get()
.uri("...")
.header("Authorization", "Bearer " + authenticator.getToken(client,token))
.retrieve()
...
And in Authenticator Service I verify the validity of the token as follow:
String getToken(WebClient client, String token) {
if (token == null || isTokenExpired(token)) {
return this.fetchToken(client); // fetches a new token
}
return token;
}
private boolean isTokenExpired(String token) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().before(new Date());
}

RxJava2 Flowable that emits results of multiple network calls without using create?

I have a generic screen that subscribes to an RxJava2 flowable that returns a List. It then displays the content in the list.
I have a use case now though where I need to collect data from multiple endpoints, and emit data once some complete, and then emit data again once the remaining ones complete.
I'm doing this using Flowable.create() but I've seen a lot of posts saying that there's usually a better and safer way to do so than using create? I seem to believe that is the case since I need to subscribe to an observable within the observable which ideally I wouldn't want to do?
Because I subscribe within, I know the emitter can become cancelled within the observable while other network calls are completing so I've added checks to ensure it doesn't throw an error after its disposed which do work (at least in testing...) [I also just remembered I have the code available to dispose of the inner subscription if I kept it like this, when the outer is disposed]
The first 2 calls may be incredibly fast (or instant) which is why i want to emit the first result right away, and then the following 4 network calls which rely on that data may take time to process.
It looks roughly like this right now...
return Flowable.create<List<Object>>({ activeEmitter ->
Single.zip(
single1(),
single2(),
BiFunction { single1Result: Object, single2result: Object ->
if (single1result.something || single2Result.somethingElse) {
activeEmitter.onNext(function(single1result, single2result) //returns list
}
Single.zip(
single3(single1result),
single4(single2result),
single5(single1result),
single6(single2result),
Function4 { single3Result: Object,
single4Result: Object,
single5Result: Object,
single6Result: Object ->
ObjectHolder(single1Result, single2Result, single3Result, single4Result, single5Result, single6Result)
}
)
}
).flatMap { objectHolder ->
objects.flatMap { objectHolder ->
Single.just(parseObjects(objectHolder))
}
}.subscribeBy(
onError = { error ->
if (!activeEmitter.isCancelled) {
activeEmitter.onError(error)
}
},
onSuccess = { results ->
if (!activeEmitter.isCancelled) {
activeEmitter.onNext(results)
activeEmitter.onComplete()
}
}
)
}, BackpressureStrategy.BUFFER)
I can't figure out another way to return a Flowable that emits the results of multiple different network calls without doing it like this?
Is there a different/better way I can't find?
I worked this out given ctranxuan response. Posting so he can tweak/optimize and then I accept his answer
return Single.zip(single1(), single2(),
BiFunction { single1result: Object, single2result: Object ->
Pair(single1result, single2result)
}
).toFlowable()
.flatMap { single1AndSingle2 ->
if (isFirstLoad) {
createItemOrNull(single1AndSingle2.first, single1AndSingle2.second)?.let { result ->
Single.just(listOf(result)).mergeWith(proceedWithFinalNetworkCalls(single1AndSingle2))
}.orElse {
proceedWithFinalNetworkCalls(single1AndSingle2).toFlowable()
}
} else {
proceedWithFinalNetworkCalls(single1AndSingle2).toFlowable()
}
}.doOnComplete {
isFirstLoad = false
}
fun proceedWithFinalNetworkCalls(): Flowable<List> {
return Single.zip(
single3(single1result),
single4(single2result),
single5(single1result),
single6(single2result),
Function4 { single3Result: Object,
single4Result: Object,
single5Result: Object,
single6Result: Object ->
ObjectHolder(single1Result, single2Result, single3Result, single4Result, single5Result, single6Result)
}
)
Sorry, it's in Java but from what I've understood, something like that may be a possible solution?
public static void main(String[] args) {
final Single<String> single1 = single1().cache();
single1.map(List::of)
.mergeWith(single1.zipWith(single2(), Map::entry)
.flatMap(entry -> Single.zip(
single3(entry.getKey()),
single4(entry.getValue()),
single5(entry.getKey()),
single6(entry.getValue()),
(el3, el4, el5, el6) -> objectHolder(entry.getKey(), entry.getValue(), el3, el4, el5, el6))))
.subscribe(System.out::println,
System.err::println);
Flowable.timer(1, MINUTES) // Just to block the main thread for a while
.blockingSubscribe();
}
private static List<String> objectHolder(final String el1,
final String el2,
final String el3,
final String el4,
final String el5,
final String el6) {
return List.of(el1, el2, el3, el4, el5, el6);
}
static Single<String> single1() {
return Single.just("s1");
}
static Single<String> single2() {
return Single.just("s2");
}
static Single<String> single3(String value) {
return single("s3", value);
}
static Single<String> single4(String value) {
return single("s4", value);
}
static Single<String> single5(String value) {
return single("s5", value);
}
static Single<String> single6(String value) {
return single("s6", value);
}
static Single<String> single(String value1, String value2) {
return Single.just(value1).map(l -> l + "_" + value2);
}
This outputs:
[s1]
[s1, s2, s3_s1, s4_s2, s5_s1, s6_s2]

Park XML message in invalid format to AMQP parking lot queue

Given I have IntegrationFlow
IntegrationFlows.from(
Amqp.inboundAdapter(rabbitConnectionFactory, QUEUE)
.messageConverter(new MarshallingMessageConverter(xmlMarshaller))
.defaultRequeueRejected(false)
.concurrentConsumers(2)
.maxConcurrentConsumers(4)
.channelTransacted(true)
.errorHandler(new ConditionalRejectingErrorHandler())
)
.log(INFO, AMQP_LOGGER_CATEGORY)
.publishSubscribeChannel(s -> s
.subscribe(f -> f
.handle(deathCheckHandler))
.subscribe(f -> f.handle(service))
)
.get();
where deathCheckHandler is
#Component
public class DeathCheckHandler {
private static final Logger logger = LoggerFactory.getLogger(lookup().lookupClass());
private static final int RETRY_COUNT = 3;
private final RabbitTemplate rabbitTemplate;
private final Jaxb2Marshaller xmlMarshaller;
public DeathCheckHandler(RabbitTemplate rabbitTemplate, Jaxb2Marshaller xmlMarshaller) {
this.rabbitTemplate = rabbitTemplate;
this.xmlMarshaller = xmlMarshaller;
}
#ServiceActivator
public void check(Message<?> message) {
MessageHeaders headers = message.getHeaders();
Optional<XDeath> rejected = findAnyRejectedXDeathMessageHeader(headers);
if (rejected.isPresent()) {
int rejectedCount = rejected.get().getCount();
logger.debug("Rejected count is {}", rejectedCount);
if (rejectedCount > RETRY_COUNT) {
parkMessage(message);
}
}
}
private void parkMessage(Message<?> message) {
Object payload = message.getPayload();
MessageHeaders headers = message.getHeaders();
String parkingExchange = (String) headers.get("amqp_receivedExchange");
String parkingRoutingKey = ((String) headers.get("amqp_consumerQueue")).replace("queue", "plq");
rabbitTemplate.setMessageConverter(new MarshallingMessageConverter(xmlMarshaller));
logger.warn("Tried more than {} times. Parking rejected message: {} to exchange {} and routing key {}", RETRY_COUNT, payload, parkingExchange, parkingRoutingKey);
rabbitTemplate.convertAndSend(parkingExchange, parkingRoutingKey, payload);
// cause the message to be acknowledged and not routed to DLQ
throw new ImmediateAcknowledgeAmqpException("Give up retrying message: " + payload);
}
}
DeathCheckHandler handles dead-lettering which is set up on AMQP queues.
How can I park an XML message in incorrect format, i.e. when MarshallingMessageConverter throws UnmarshallingFailureException.
I want to park it in a similar way how I do it in DeathCheckHandler#parkMessage
It should be probably possible with ConditionalRejectingErrorHandler, but I don't know how.
Clone the ConditionalRejectingErrorHandler.
Use this method as a template...
#Override
public void handleError(Throwable t) {
log(t);
if (!this.causeChainContainsARADRE(t) && this.exceptionStrategy.isFatal(t)) {
if (this.discardFatalsWithXDeath && t instanceof ListenerExecutionFailedException) {
Message failed = ((ListenerExecutionFailedException) t).getFailedMessage();
if (failed != null) {
List<Map<String, ?>> xDeath = failed.getMessageProperties().getXDeathHeader();
if (xDeath != null && xDeath.size() > 0) {
this.logger.error("x-death header detected on a message with a fatal exception; "
+ "perhaps requeued from a DLQ? - discarding: " + failed);
throw new ImmediateAcknowledgeAmqpException("Fatal and x-death present");
}
}
}
throw new AmqpRejectAndDontRequeueException("Error Handler converted exception to fatal", this.rejectManual,
t);
}
}
By default, fatal exceptions with an x-death header are discarded via a ImmediateAcknowledgeAmqpException.
It's not easy to subclass and override this method because the fields are private so it would be easiest to just copy this class (and publish to the parking lot before throwing the IAAE).
I will make some improvements to this class to make it easier to customize/override.
Pull Request.

Accessing Context inside an ExchangeFilterFunction

For some reason a context inside the doAfterSuccessOrError method is not available (populated) from the upstream. I've tried to access it using Mono.subscriberContext() (see the snipped). I would expect to have it present but for some reason is not. Am I doing something wrong?
public class LoggingRequestExchangeFunction implements ExchangeFilterFunction {
private final Logger log = LoggerFactory.getLogger(getClass());
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
long start = System.currentTimeMillis();
return next.exchange(request).doAfterSuccessOrError((res, ex) -> {
Mono.subscriberContext().map((ctx -> {
log.info("doAfterSuccessOrError Context {}",ctx);
// log req/res ...
return ctx;
})).subscribe();
}).subscriberContext( ctx -> {
log.info("SubscriberContext: {}" , ctx);
return ctx;
});
}
}
Here is a log output
23:16:59.426 INFO [reactor-http-epoll-2] .p.c.LoggingRequestExchangeFunction [] SubscriberContext: Context1{nexmo-tracing-context=TracingContext{{traceId=f04961da-933a-4d1d-85d5-3bea2c47432f, clientIp=N/A}}}
23:16:59.589 INFO [reactor-http-epoll-2] .p.c.LoggingRequestExchangeFunction [] doAfterSuccessOrError Context Context0{}
The reason is that you create a new Mono inside doAfterSuccessOrError which is independent from the original reactor chain since you subscribe to it separately.
If you just want to log something inside, your alternative is to use doOnEach operator which beside the signal type gives you access to the context as well.
Mono.just("hello")
.doOnEach((signal) ->
{
if (signal.isOnError() || signal.isOnComplete())
{
Context ctx = signal.getContext();
log.info("doAfterSuccessOrError Context {}",ctx);
// log req/res ...
}
})
.subscriberContext( ctx -> {
log.info("SubscriberContext: {}" , ctx);
return ctx;
})
.subscribe();