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;
}
}
No pacote api/, cada classe cumpre um papel claro:
-
CreateIncidentRequest.javadefine o payload de entrada -
IncidentResponse.javarepresenta o incidente devolvido ao cliente -
CreatedIncidentEnvelope.javaadiciona a mensagem de confirmação -
PriorityBoardResponse.javadevolve a fila ordenada -
IncidentSummaryResponse.javaconsolida o resumo operacional -
IncidentLiveSignal.javamodela 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);
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));
}
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);
}
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));
}
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();
}
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);
}
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
Como rodar
git clone https://github.com/dellamas/quarkus-reactive-routes-incident-dispatch-api
cd quarkus-reactive-routes-incident-dispatch-api
mvn quarkus:dev
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"
}'
Consultando a fila:
curl http://localhost:8080/incidents/priority-board
Consultando o resumo:
curl http://localhost:8080/incidents/summary
Abrindo o stream:
curl http://localhost:8080/incidents/live
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:
Top comments (0)