Quarkus — ImmutableLog
Integre o ImmutableLog em aplicações Quarkus usando ContainerRequestFilter e ContainerResponseFilter do JAX-RS. O provider é descoberto automaticamente — zero configuração extra.
Instalação
O filtro usa apenas a API padrão JAX-RS presente no Quarkus. Para RESTEasy Reactive (padrão Quarkus 3+):
<!-- pom.xml -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
</dependency>Jackson está disponível via quarkus-resteasy-reactive-jackson. O java.net.http.HttpClient está disponível no Java 11+ — sem dependências extras.
JAX-RS ContainerFilter
O Quarkus descobre automaticamente a classe anotada com @Provider. ContainerRequestFilter roda antes do endpoint; ContainerResponseFilter roda depois — com acesso ao status HTTP.
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.ext.Provider;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.io.IOException;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.*;
@Provider
@Priority(Priorities.USER - 100)
public class ImmutableLogFilter implements ContainerRequestFilter, ContainerResponseFilter {
@ConfigProperty(name = "immutablelog.api-key")
String apiKey;
@ConfigProperty(name = "immutablelog.service-name", defaultValue = "my-service")
String serviceName;
@ConfigProperty(name = "immutablelog.env", defaultValue = "production")
String env;
@ConfigProperty(name = "immutablelog.url", defaultValue = "https://api.immutablelog.com")
String apiUrl;
private static final String START_KEY = "imtbl.startedAt";
private static final String REQ_ID_KEY = "imtbl.requestId";
private static final Set<String> SKIP_PATHS = Set.of("/health", "/q/health", "/ping");
private final ObjectMapper mapper = new ObjectMapper();
private final HttpClient http = HttpClient.newHttpClient();
@Override
public void filter(ContainerRequestContext req) {
String path = req.getUriInfo().getPath();
if (SKIP_PATHS.stream().anyMatch(path::startsWith)) return;
req.setProperty(START_KEY, System.currentTimeMillis());
req.setProperty(REQ_ID_KEY, UUID.randomUUID().toString());
}
@Override
public void filter(ContainerRequestContext req, ContainerResponseContext res) {
Long startedAt = (Long) req.getProperty(START_KEY);
if (startedAt == null) return; // skipped
String requestId = (String) req.getProperty(REQ_ID_KEY);
long latencyMs = System.currentTimeMillis() - startedAt;
int status = res.getStatus();
String method = req.getMethod();
String path = req.getUriInfo().getPath();
String customEvent = (String) req.getProperty("imtbl.eventName");
String eventName = customEvent != null
? customEvent
: method.toLowerCase() + "." + path.replace("/", ".").replaceAll("^\\.+", "");
String type = status >= 500 ? "error"
: status >= 400 ? "error"
: status >= 300 ? "info"
: "success";
emit(requestId, eventName, type, method, path, status, latencyMs, req, null);
}
private void emit(String requestId, String eventName, String type,
String method, String path, int status,
long latencyMs, ContainerRequestContext req, Throwable ex) {
try {
Map<String, Object> payloadMap = new LinkedHashMap<>();
payloadMap.put("method", method);
payloadMap.put("path", path);
payloadMap.put("status", status);
payloadMap.put("latency_ms", latencyMs);
payloadMap.put("client_ip", getClientIp(req));
payloadMap.put("user_agent", req.getHeaderString("User-Agent"));
if (ex != null) {
Map<String, Object> errMap = new LinkedHashMap<>();
errMap.put("type", ex.getClass().getName());
errMap.put("message", ex.getMessage());
errMap.put("retryable", !(ex instanceof IllegalArgumentException));
payloadMap.put("error", errMap);
}
Map<String, Object> meta = Map.of(
"type", type,
"event_name", eventName,
"service", serviceName,
"request_id", requestId,
"env", env
);
Map<String, Object> body = Map.of(
"payload", mapper.writeValueAsString(payloadMap),
"meta", meta
);
String json = mapper.writeValueAsString(body);
var httpReq = HttpRequest.newBuilder()
.uri(URI.create(apiUrl + "/v1/events"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.header("Idempotency-Key", requestId)
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
http.sendAsync(httpReq, HttpResponse.BodyHandlers.discarding());
} catch (Exception ignored) {}
}
private String getClientIp(ContainerRequestContext req) {
String xff = req.getHeaderString("X-Forwarded-For");
if (xff != null && !xff.isBlank()) return xff.split(",")[0].trim();
// UriInfo doesn't expose remote addr in JAX-RS; inject @Context HttpServletRequest if needed
return "unknown";
}
}Configuração
Use MicroProfile Config (padrão no Quarkus) via application.properties:
# src/main/resources/application.properties
immutablelog.api-key=${IMTBL_API_KEY}
immutablelog.service-name=${IMTBL_SERVICE_NAME:my-service}
immutablelog.env=${IMTBL_ENV:production}
immutablelog.url=https://api.immutablelog.comComo funciona
1. ContainerRequestFilter (pré-requisição)
O filtro de request registra startedAt e um requestId como propriedades do contexto. Se o path estiver na lista de exclusão, nenhuma propriedade é setada.
2. ContainerResponseFilter (pós-resposta)
O filtro de response verifica se startedAt existe (se não, o path foi ignorado). Calcula latência, monta o payload e envia de forma assíncrona.
3. @Priority(USER - 100)
A prioridade garante que o filtro ImmutableLog roda antes dos filtros de autenticação/autorização da aplicação, capturando inclusive tentativas não autorizadas.
4. GraalVM Native Image
O filtro é compatível com compilação nativa do Quarkus (quarkus build --native). java.net.http.HttpClient e jackson-databind precisam de reflection hints — o Quarkus gera automaticamente para classes anotadas com @Provider.
ExceptionMapper
Para capturar exceções não tratadas e incluí-las no evento, use um ExceptionMapper que armazena a exceção como propriedade do request context antes de retornar a resposta de erro.
import jakarta.ws.rs.core.*;
import jakarta.ws.rs.ext.*;
/**
* ExceptionMapper enriches error events before the response filter runs.
* Set the exception on the request context so the response filter can include it.
*/
@Provider
public class ImmutableLogExceptionMapper implements ExceptionMapper<Exception> {
@Context
ContainerRequest request;
@Override
public Response toResponse(Exception ex) {
// Store exception for the response filter
request.setProperty("imtbl.exception", ex);
int status = 500;
if (ex instanceof NotFoundException) status = 404;
else if (ex instanceof BadRequestException) status = 400;
return Response.status(status)
.entity(Map.of("error", ex.getMessage()))
.type(MediaType.APPLICATION_JSON)
.build();
}
}Evento customizado
Injete ContainerRequestContext no resource e defina imtbl.eventName como propriedade para sobrescrever o nome gerado automaticamente.
import jakarta.ws.rs.*;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.*;
@Path("/payments")
public class PaymentResource {
@Context
ContainerRequestContext requestContext;
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response create(PaymentDto dto) {
// Override the auto-generated event name
requestContext.setProperty("imtbl.eventName", "payment.created");
// ... process payment ...
return Response.ok(result).build();
}
}RESTEasy Reactive
O filtro funciona igualmente com RESTEasy Reactive (Quarkus 3+). Resources retornam Uni/Multi — os filtros JAX-RS ainda são executados antes e depois de cada request.
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
/**
* With RESTEasy Reactive, filters work the same way but with reactive types.
* Use io.quarkus.vertx.http.runtime.filters.Filters for lower-level Vert.x filtering.
*/
@Path("/orders")
public class OrderResource {
@GET
@Path("/{id}")
public Uni<Response> get(@PathParam("id") Long id) {
return orderService.findById(id)
.map(order -> Response.ok(order).build());
}
}IP do cliente (modo servlet)
Para acessar o endereço remoto real no Quarkus no modo JVM com Undertow, injete HttpServletRequest via @Context:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.container.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.ext.Provider;
@Provider
@Priority(Priorities.USER - 100)
public class ImmutableLogFilter implements ContainerRequestFilter, ContainerResponseFilter {
// Inject HttpServletRequest to access remote address
@Context
HttpServletRequest servletRequest;
private String getClientIp(ContainerRequestContext req) {
String xff = req.getHeaderString("X-Forwarded-For");
if (xff != null && !xff.isBlank()) return xff.split(",")[0].trim();
String xri = req.getHeaderString("X-Real-IP");
if (xri != null && !xri.isBlank()) return xri;
return servletRequest.getRemoteAddr();
}
}Esta documentação reflete o comportamento atual da integração. Para dúvidas ou integrações avançadas, entre em contato com o time de suporte.
