Micronaut — ImmutableLog
Integre o ImmutableLog em aplicações Micronaut usando HttpServerFilter. Zero overhead de reflection — compatível com compilação nativa GraalVM e cloud-native deployments.
Instalação
O filtro usa apenas APIs do Micronaut core. Adicione as dependências abaixo se não as tiver:
<!-- pom.xml -->
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut.serde</groupId>
<artifactId>micronaut-serde-jackson</artifactId>
</dependency>HttpServerFilter
Crie a classe abaixo. A anotação @Filter("/**") registra o filtro globalmente. Use Publishers.map() para transformar a response de forma reativa sem bloquear a thread.
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micronaut.context.annotation.Value;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.http.*;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.filter.*;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import java.net.URI;
import java.net.http.*;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.*;
@Filter("/**")
@Singleton
public class ImmutableLogFilter implements HttpServerFilter {
@Value("${immutablelog.api-key}")
String apiKey;
@Value("${immutablelog.service-name:my-service}")
String serviceName;
@Value("${immutablelog.env:production}")
String env;
@Value("${immutablelog.url:https://api.immutablelog.com}")
String apiUrl;
private static final Set<String> SKIP_PATHS = Set.of("/health", "/ping");
private final ObjectMapper mapper = new ObjectMapper();
private final HttpClient http = HttpClient.newHttpClient();
@Override
public int getOrder() {
return ServerFilterPhase.FIRST.order();
}
@Override
public Publisher<MutableHttpResponse<?>> doFilter(
HttpRequest<?> request,
ServerFilterChain chain
) {
String path = request.getPath();
if (SKIP_PATHS.stream().anyMatch(path::startsWith)) {
return chain.proceed(request);
}
long startedAt = System.currentTimeMillis();
String requestId = UUID.randomUUID().toString();
return Publishers.map(chain.proceed(request), response -> {
long latencyMs = System.currentTimeMillis() - startedAt;
emit(request, response, requestId, latencyMs, null);
return response;
});
}
public void emitError(HttpRequest<?> request, String requestId,
long latencyMs, Throwable ex) {
emit(request, null, requestId, latencyMs, ex);
}
private void emit(HttpRequest<?> request, MutableHttpResponse<?> response,
String requestId, long latencyMs, Throwable ex) {
try {
String method = request.getMethodName();
String path = request.getPath();
int status = response != null ? response.getStatus().getCode() : 500;
// Custom event name via request attribute
String customEvent = request.getAttribute("imtbl.eventName", String.class).orElse(null);
String eventName = customEvent != null
? customEvent
: method.toLowerCase() + "." + path.replace("/", ".").replaceAll("^\\.+", "");
String type = status >= 500 ? "error"
: status >= 400 ? "error"
: status >= 300 ? "info"
: "success";
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(request));
payloadMap.put("user_agent", request.getHeaders().get("User-Agent"));
request.getBody(byte[].class).ifPresent(body -> {
if (body.length > 0) {
payloadMap.put("request_body_hash", sha256Hex(body));
}
});
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> bodyMap = Map.of(
"payload", mapper.writeValueAsString(payloadMap),
"meta", meta
);
String json = mapper.writeValueAsString(bodyMap);
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(HttpRequest<?> request) {
String xff = request.getHeaders().get("X-Forwarded-For");
if (xff != null && !xff.isBlank()) return xff.split(",")[0].trim();
String xri = request.getHeaders().get("X-Real-IP");
if (xri != null && !xri.isBlank()) return xri;
return request.getRemoteAddress().getAddress().getHostAddress();
}
private String sha256Hex(byte[] data) {
try {
var digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data);
StringBuilder sb = new StringBuilder();
for (byte b : hash) sb.append(String.format("%02x", b));
return sb.toString();
} catch (Exception e) {
return "hash-error";
}
}
}Configuração
Adicione as propriedades no application.yml:
# src/main/resources/application.yml
immutablelog:
api-key: ${IMTBL_API_KEY}
service-name: ${IMTBL_SERVICE_NAME:my-service}
env: ${IMTBL_ENV:production}
url: https://api.immutablelog.comComo funciona
1. ServerFilterPhase.FIRST
getOrder() retorna a fase FIRST para garantir que o filtro rode antes de qualquer outro filtro da aplicação, capturando inclusive requisições que falham em filtros subsequentes.
2. Publishers.map() reativo
Publishers.map() é usado para interceptar a resposta sem bloquear. O Micronaut usa Netty internamente — nunca bloqueie a thread de I/O do event loop.
3. request.getAttribute()
Atributos do request são thread-safe e propagados pelo contexto reativo. Use para passar o nome de evento customizado do controller para o filtro.
4. HttpClient.sendAsync (Java 11+)
O envio ao ImmutableLog é fire-and-forget via HttpClient.sendAsync(). Nunca bloqueia a thread do Netty — a resposta ao cliente já foi enviada antes do POST.
ExceptionHandler
Para capturar exceções não tratadas, implemente ExceptionHandler. Ele é chamado pelo Micronaut antes de retornar a resposta de erro, permitindo enriquecer o evento com detalhes da exceção.
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.*;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.exceptions.*;
import jakarta.inject.Singleton;
/**
* ExceptionHandler to capture errors and include them in the audit event.
* Register alongside the filter to capture unhandled exceptions.
*/
@Produces
@Singleton
@Requires(classes = {Exception.class, ExceptionHandler.class})
public class ImmutableLogExceptionHandler
implements ExceptionHandler<Exception, HttpResponse<?>> {
private final ImmutableLogFilter filter;
public ImmutableLogExceptionHandler(ImmutableLogFilter filter) {
this.filter = filter;
}
@Override
public HttpResponse<?> handle(HttpRequest request, Exception ex) {
String requestId = request.getAttribute("imtbl.requestId", String.class)
.orElse(UUID.randomUUID().toString());
long startedAt = request.getAttribute("imtbl.startedAt", Long.class)
.orElse(System.currentTimeMillis());
long latencyMs = System.currentTimeMillis() - startedAt;
filter.emitError(request, requestId, latencyMs, ex);
return HttpResponse.serverError(Map.of("error", ex.getMessage()));
}
}Evento customizado
Injete HttpRequest no controller e defina o atributo imtbl.eventName para sobrescrever o nome gerado automaticamente pelo filtro.
import io.micronaut.http.HttpRequest;
import io.micronaut.http.annotation.*;
@Controller("/payments")
public class PaymentController {
@Post
public Payment create(HttpRequest<?> request, @Body PaymentDto dto) {
// Override the auto-generated event name
request.setAttribute("imtbl.eventName", "payment.created");
// ... process payment ...
return payment;
}
}Filtro reativo (Reactor)
Alternativa usando Reactor (Mono/Flux) para composição reativa mais expressiva. Útil quando sua stack usa Micronaut + Reactor ao invés de RxJava.
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.filter.*;
import reactor.core.publisher.Mono;
/**
* Alternative: Reactive filter using Reactor for non-blocking chains.
* Use when your handlers are reactive (Mono/Flux from Reactor).
*/
@Filter("/**")
public class ImmutableLogReactiveFilter implements HttpServerFilter {
@Override
public Publisher<MutableHttpResponse<?>> doFilter(
HttpRequest<?> request, ServerFilterChain chain
) {
long startedAt = System.currentTimeMillis();
String requestId = UUID.randomUUID().toString();
return Mono.from(chain.proceed(request))
.doOnSuccess(response -> {
long latencyMs = System.currentTimeMillis() - startedAt;
emitAsync(request, response, requestId, latencyMs, null);
})
.doOnError(ex -> {
long latencyMs = System.currentTimeMillis() - startedAt;
emitAsync(request, null, requestId, latencyMs, ex);
});
}
private void emitAsync(/* ... */) {
// same emit logic as above
}
}Native Image (GraalVM)
O Micronaut processa anotações em tempo de compilação — sem reflection em runtime. O filtro é totalmente compatível com compilação nativa. Se necessário, adicione hints manuais:
// src/main/resources/META-INF/native-image/reflect-config.json
// For GraalVM native image compilation, add reflection hints if needed:
[
{
"name": "com.example.ImmutableLogFilter",
"allPublicConstructors": true,
"allPublicMethods": true
}
]
// With Micronaut's @Singleton + @Filter, AOT processing handles most cases automatically.
// Run: ./mvnw package -Dpackaging=native-imageNa maioria dos casos o Micronaut AOT gera os hints automaticamente para classes com @Singleton e @Filter. Teste com: mvn package -Dpackaging=native-image
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.
