Pular para o conteúdo principal
ImmutableLog logo
Voltar
QuarkusJAX-RS

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+):

xml
<!-- 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.

java
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:

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.com

Como 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.

java
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.

java
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.

java
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:

java
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.