Como criar traços de pilha assíncronos?

ATUALIZAR: A versão mais recente do Intellij IDEAimplementa exatamente o que estou procurando. A questão é como implementar isso fora do IDE (para que eu possa despejar traços de pilha assíncrona em arquivos de log), idealmente sem o uso de um agente de instrumentação.

Desde que converti meu aplicativo de um modelo síncrono para assíncrono, estou tendo problemas para depurar falhas.

Quando uso APIs síncronas, sempre encontro minhas classes em rastreamentos de pilha de exceção, para saber por onde começar a procurar se algo der errado. Com APIs assíncronas, estou obtendo rastreamentos de pilha que não fazem referência a minhas classes nem indicam qual solicitação acionou a falha.

Vou dar um exemplo concreto, mas estou interessado em uma solução geral para esse tipo de problema.

Exemplo concreto

Eu faço uma solicitação HTTP usandoJersey:

new Client().target("http://test.com/").request().rx().get(JsonNode.class);

Onderx() indica que a solicitação deve ocorrer de forma assíncrona, retornando umCompletionStage<JsonNode> em vez de umJsonNode diretamente. Se esta chamada falhar, eu recebo este stacktrace:

javax.ws.rs.ForbiddenException: HTTP 403 Authentication Failed
    at org.glassfish.jersey.client.JerseyInvocation.convertToException(JerseyInvocation.java:1083)
    at org.glassfish.jersey.client.JerseyInvocation.translate(JerseyInvocation.java:883)
    at org.glassfish.jersey.client.JerseyInvocation.lambda$invoke$1(JerseyInvocation.java:767)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:316)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:298)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:229)
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:414)
    at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:765)
    at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:456)
    at org.glassfish.jersey.client.JerseyCompletionStageRxInvoker.lambda$method$1(JerseyCompletionStageRxInvoker.java:70)
    at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)

Aviso prévio:

O stacktrace não faz referência ao código do usuário.A mensagem de exceção não contém informações contextuais sobre a solicitação HTTP que acionou o erro (método HTTP, URI, etc).

Como resultado, não tenho como rastrear a exceção de volta à sua origem.

Porque isso está acontecendo

Se você cavar debaixo do capô, descobrirá queJersey está invocando:

CompletableFuture.supplyAsync(() -> getSyncInvoker().method(name, entity, responseType))

pararx() invocações. Como o fornecedor é construído por Jersey, não há referência ao código do usuário.

O que eu tentei

Tentei arquivar um relatório de erro contra o Jetty por um exemplo assíncrono não relacionado e foi posteriormente recusado por motivos de segurança.

Em vez disso, adicionei informações contextuais da seguinte maneira:

makeHttpRequest().exceptionally(e ->
{
    throw new RuntimeException(e);
});

Ou seja, estou adicionando manualmenteexceptionally() depois de cada solicitação HTTP no meu código. Quaisquer exceções lançadas por Jersey são agrupadas em uma exceção secundária que faz referência ao meu código. O stacktrace resultante é assim:

java.lang.RuntimeException: javax.ws.rs.ForbiddenException: HTTP 403 Authentication Failed
    at my.user.code.Testcase.lambda$null$1(Testcase.java:25)
    at java.util.concurrent.CompletableFuture.uniExceptionally(CompletableFuture.java:870)
    ... 6 common frames omitted
Caused by: javax.ws.rs.ForbiddenException: HTTP 403 Authentication Failed
    at org.glassfish.jersey.client.JerseyInvocation.convertToException(JerseyInvocation.java:1083)
    at org.glassfish.jersey.client.JerseyInvocation.translate(JerseyInvocation.java:883)
    at org.glassfish.jersey.client.JerseyInvocation.lambda$invoke$1(JerseyInvocation.java:767)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:316)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:298)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:229)
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:414)
    at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:765)
    at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:456)
    at org.glassfish.jersey.client.JerseyCompletionStageRxInvoker.lambda$method$1(JerseyCompletionStageRxInvoker.java:70)
    at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
    ... 3 common frames omitted

Não gosto dessa abordagem porque é propensa a erros e diminui a legibilidade do código. Se eu omitir isso erroneamente por alguma solicitação HTTP, terminarei com um rastreamento de pilha vago e gastarei muito tempo rastreando-o.

Além disso, se eu quiser ocultar esse truque atrás de uma classe de utilitário, preciso instanciar uma exceção fora de uma classeCompletionStage; caso contrário, a classe de utilitário será exibida no rastreamento de pilha, em vez do site de chamada real. Instanciar uma exceção fora de umCompletionStage é extremamente caro porque esse código é executado mesmo que nenhuma exceção seja lançada pela chamada assíncrona.

Minha pergunta

Existe uma abordagem robusta e de fácil manutenção para adicionar informações contextuais às chamadas assíncronas?

Como alternativa, existe uma abordagem eficiente para rastrear os rastreamentos de pilha de volta à sua origem sem essas informações contextuais?

questionAnswers(1)

yourAnswerToTheQuestion