DEV Community

Luis Fabrício De Llamas
Luis Fabrício De Llamas

Posted on

Quarkus Reactive Routes na prática: triagem de incidentes com fluxo reativo de verdade

Quarkus Reactive Routes na prática: triagem de incidentes com fluxo reativo de verdade

Se você já trabalhou com observabilidade, suporte ou operação, sabe que incidente costuma começar com pressão, sinais espalhados e pouca clareza sobre o que precisa entrar primeiro na fila. Nesse tipo de cenário, não basta expor um endpoint. O desafio é organizar o fluxo de forma simples, legível e compatível com uma aplicação que precisa reagir rápido.

Neste artigo eu vou mostrar um exemplo de triagem de incidentes com Quarkus Reactive Routes em um contexto mais próximo de uma operação real. A proposta é montar uma API pequena, com fila priorizada, resumo operacional e stream SSE para acompanhamento ao vivo.

Vale registrar um detalhe histórico. A base dessa linha de funcionalidade entrou no Quarkus no trabalho do Martin Kouba, no commit "Add Vertx web extension" do repositório quarkusio/quarkus. Isso ajuda a entender por que o recurso tem uma integração tão direta com o ecossistema do Vert.x.

O que vamos construir

Uma API de triagem operacional com quatro rotas principais:

  • POST /incidents para registrar um incidente
  • GET /incidents/priority-board para listar a fila priorizada
  • GET /incidents/summary para consolidar o estado operacional
  • GET /incidents/live para acompanhar snapshots em SSE

Estrutura do projeto:

src/main/java/br/com/luisf/fabricio/demos/reactive/routes/incident/
├── api/
│ ├── CreateIncidentRequest.java
│ ├── CreatedIncidentEnvelope.java
│ ├── IncidentLiveSignal.java
│ ├── IncidentResponse.java
│ ├── IncidentRoutes.java
│ ├── IncidentSummaryResponse.java
│ └── PriorityBoardResponse.java
├── domain/
│ ├── Incident.java
│ ├── IncidentStatus.java
│ └── Severity.java
├── health/
│ └── IncidentDispatchHealthCheck.java
└── service/
├── IncidentDispatchService.java
└── IncidentMapper.java

Criando a base do projeto

A stack ficou enxuta para destacar o Reactive Routes sem espalhar a atenção em infraestrutura paralela:

  • quarkus-reactive-routes
  • quarkus-rest-jackson
  • quarkus-smallrye-openapi
  • quarkus-swagger-ui
  • quarkus-smallrye-health
  • quarkus-hibernate-validator

O domínio e os contratos

O domínio foi mantido simples. Incident.java representa o incidente, Severity.java define a prioridade operacional e IncidentStatus.java mantém o estado mínimo do fluxo.

Severity.java ficou assim:

public enum Severity {
    LOW(1),
    MEDIUM(2),
    HIGH(3),
    CRITICAL(4);

    private final int priority;

    Severity(int priority) {
        this.priority = priority;
    }

    public int priority() {
        return priority;
    }
}
Enter fullscreen mode Exit fullscreen mode

No pacote api/, cada classe cumpre um papel claro:

  • CreateIncidentRequest.java define o payload de entrada
  • IncidentResponse.java representa o incidente devolvido ao cliente
  • CreatedIncidentEnvelope.java adiciona a mensagem de confirmação
  • PriorityBoardResponse.java devolve a fila ordenada
  • IncidentSummaryResponse.java consolida o resumo operacional
  • IncidentLiveSignal.java modela cada snapshot emitido pelo SSE

Essa separação ajuda bastante porque o contrato da API fica explícito logo na leitura inicial do projeto.

O coração da lógica: IncidentDispatchService.java

Esse é o arquivo principal do projeto. É nele que entram armazenamento em memória, ordenação da fila, criação de incidentes, geração do resumo e emissão dos snapshots ao vivo.

A regra de priorização ficou assim:

private static final Comparator<Incident> INCIDENT_PRIORITY = Comparator
        .comparingInt((Incident incident) -> incident.severity().priority())
        .reversed()
        .thenComparing(Incident::openedAt);
Enter fullscreen mode Exit fullscreen mode

Severidade mais alta sobe na fila e, entre incidentes do mesmo nível, o mais antigo vem primeiro.

A criação do incidente também ficou toda visível no serviço:

public Uni<IncidentResponse> create(CreateIncidentRequest request) {
    OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
    Incident incident = new Incident(
            "INC-" + sequence.incrementAndGet(),
            request.severity(),
            request.affectedService().trim(),
            request.summary().trim(),
            request.owner().trim(),
            IncidentStatus.OPEN,
            now,
            now);
    incidents.add(incident);
    return Uni.createFrom().item(mapper.toResponse(incident));
}
Enter fullscreen mode Exit fullscreen mode

E o stream SSE sai do mesmo serviço:

public Multi<IncidentLiveSignal> liveSignals() {
    return Multi.createFrom().ticks().every(Duration.ofSeconds(1))
            .onItem().transform(index -> new IncidentLiveSignal(
                    Math.toIntExact(index) + 1,
                    "incident-dispatch",
                    OffsetDateTime.now(ZoneOffset.UTC),
                    summarySnapshot(),
                    priorityBoardSnapshot().queue().stream().findFirst().orElse(null)))
            .select().first(5);
}
Enter fullscreen mode Exit fullscreen mode

Mapeamento e borda HTTP

IncidentMapper.java faz a conversão entre domínio e resposta HTTP. Pode parecer detalhe, mas essa separação evita misturar regra de negócio com montagem de payload.

IncidentRoutes.java concentra as quatro rotas da aplicação. A criação do incidente ficou assim:

@Route(path = "", methods = Route.HttpMethod.POST, consumes = "application/json")
public Uni<CreatedIncidentEnvelope> create(HttpServerResponse response, @Body @Valid CreateIncidentRequest request) {
    response.setStatusCode(201);
    return service.create(request)
            .onItem().transform(incident -> new CreatedIncidentEnvelope(
                    "Incident registered and queued for reactive triage.", incident));
}
Enter fullscreen mode Exit fullscreen mode

A fila priorizada e o resumo retornam Uni:

@Route(path = "priority-board", methods = Route.HttpMethod.GET)
public Uni<PriorityBoardResponse> priorityBoard() {
    return service.priorityBoard();
}

@Route(path = "summary", methods = Route.HttpMethod.GET)
public Uni<IncidentSummaryResponse> summary() {
    return service.summary();
}
Enter fullscreen mode Exit fullscreen mode

E o acompanhamento ao vivo usa SSE:

@Route(path = "live", methods = Route.HttpMethod.GET, produces = "text/event-stream")
public Multi<String> live() {
    return service.liveSignals().onItem().transform(this::toSsePayload);
}
Enter fullscreen mode Exit fullscreen mode

Health check e configuração

IncidentDispatchHealthCheck.java responde a uma pergunta simples: o serviço está de pé?

O application.properties também ficou enxuto:

quarkus.http.port=8080
quarkus.swagger-ui.always-include=true
quarkus.smallrye-openapi.info-title=Reactive Incident Dispatch API
quarkus.smallrye-openapi.info-version=1.0.0
quarkus.smallrye-openapi.info-description=API reativa para despacho e triagem de incidentes operacionais.
quarkus.http.enable-compression=true
Enter fullscreen mode Exit fullscreen mode

Como rodar

git clone https://github.com/dellamas/quarkus-reactive-routes-incident-dispatch-api
cd quarkus-reactive-routes-incident-dispatch-api
mvn quarkus:dev
Enter fullscreen mode Exit fullscreen mode

Criando um incidente:

curl -X POST http://localhost:8080/incidents \
  -H "Content-Type: application/json" \
  -d '{
    "severity": "HIGH",
    "affectedService": "telemetry-gateway",
    "summary": "Atraso na entrega de métricas de disponibilidade",
    "owner": "observability-core"
  }'
Enter fullscreen mode Exit fullscreen mode

Consultando a fila:

curl http://localhost:8080/incidents/priority-board
Enter fullscreen mode Exit fullscreen mode

Consultando o resumo:

curl http://localhost:8080/incidents/summary
Enter fullscreen mode Exit fullscreen mode

Abrindo o stream:

curl http://localhost:8080/incidents/live
Enter fullscreen mode Exit fullscreen mode

O que este projeto mostra na prática

O Quarkus Reactive Routes funciona muito bem quando você quer expor rotas curtas, fluxo reativo explícito e um contrato HTTP fácil de acompanhar. Neste exemplo, isso aparece na combinação entre criação de incidente, fila priorizada, resumo consolidado e stream SSE.

Mostrar as classes do fluxo também ajuda bastante. Você entende onde está o contrato, onde está a regra, onde está o mapeamento e onde está a borda HTTP.

O código completo está no GitHub:

https://github.com/dellamas/quarkus-reactive-routes-incident-dispatch-api

Se quiser acompanhar mais conteúdo meu, me segue no LinkedIn:

https://www.linkedin.com/in/luisfabriciodellamas/

E no Insta:

https://instagram.com/luisdellamas

O código completo está no GitHub:
https://github.com/dellamas/quarkus-reactive-routes-incident-dispatch-api

Se quiser acompanhar mais conteúdo meu sobre Java, Quarkus e comunidade, me segue no LinkedIn:
https://www.linkedin.com/in/luisfabriciodellamas/

E no Instagram:
https://instagram.com/luisdellamas

Top comments (0)