Java
Complete integration guide for ImmutableLog in Java. Choose between automatic middleware for Spring Boot, Quarkus, and Micronaut, direct HTTP client for workers and jobs, or @AuditEvent via Spring AOP to instrument service methods.
Framework filters
The fastest way to integrate is via the framework's native HTTP filter. Every request is automatically captured — latency, status, client IP, and body hash — without modifying any controller. Click the framework to see the complete code.
OncePerRequestFilter with ContentCachingWrapper, HttpClient.sendAsync() fire-and-forget, and @AuditEvent via Spring AOP.
@Component
public class ImmutableLogFilter
extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws ... {
// wrap → chain → emit (async)
}
}View full documentation →
JAX-RS ContainerRequestFilter + ContainerResponseFilter with @Provider. Compatible with RESTEasy Reactive and GraalVM native compilation.
@Provider
@Priority(Priorities.USER - 100)
public class ImmutableLogFilter
implements ContainerRequestFilter,
ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext req) { ... }
@Override
public void filter(ContainerRequestContext req,
ContainerResponseContext res) { ... }
}View full documentation →
HttpServerFilter with reactive Publishers.map(). Zero runtime reflection — compatible with GraalVM Native Image by default.
@Filter("/**")
@Singleton
public class ImmutableLogFilter
implements HttpServerFilter {
@Override
public Publisher<MutableHttpResponse<?>> doFilter(
HttpRequest<?> req, ServerFilterChain chain) {
return Publishers.map(chain.proceed(req),
res -> { emit(req, res); return res; });
}
}View full documentation →
Direct HTTP client
Use java.net.http.HttpClient (Java 11+) to send events directly to ImmutableLog without depending on a web framework. Ideal for workers, Kafka consumers, scheduled jobs, and any JVM code that needs to record events in the ledger.
import java.net.URI;
import java.net.http.*;
import java.util.*;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ImmutableLogClient {
private static final String API_URL = "https://api.immutablelog.com/v1/events";
private final String apiKey;
private final String serviceName;
private final String env;
private final HttpClient http = HttpClient.newHttpClient();
private final ObjectMapper mapper = new ObjectMapper();
public ImmutableLogClient(String apiKey, String serviceName, String env) {
this.apiKey = apiKey;
this.serviceName = serviceName;
this.env = env;
}
// Normaliza o immutable_trail (trim, max 256, sem ':').
// O servidor responde 400 invalid_immutable_trail se a regra for violada.
private static String sanitizeTrail(String v) {
if (v == null) return null;
v = v.trim();
if (v.isEmpty()) return null;
v = v.replace(":", "-");
if (v.length() > 256) v = v.substring(0, 256);
return v;
}
public CompletableFuture<Void> sendEvent(
String eventName,
String type, // "success" | "info" | "error"
Map<String, Object> payload
) {
return sendEvent(eventName, type, payload, null);
}
public CompletableFuture<Void> sendEvent(
String eventName,
String type, // "success" | "info" | "error"
Map<String, Object> payload,
String immutableTrail // optional: groups related events into a trail
) {
try {
String requestId = UUID.randomUUID().toString();
String trail = sanitizeTrail(immutableTrail);
// Todos os valores de meta precisam ser strings.
Map<String, Object> meta = new HashMap<>();
meta.put("type", type);
meta.put("event_name", eventName);
meta.put("service", serviceName);
meta.put("request_id", requestId);
meta.put("env", env);
if (trail != null) meta.put("immutable_trail", trail);
Map<String, Object> body = Map.of(
"payload", mapper.writeValueAsString(payload),
"meta", meta
);
String json = mapper.writeValueAsString(body);
var builder = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
// Idempotency-Key e OBRIGATORIO (sem ele a API responde 400).
.header("Idempotency-Key", eventName + "-" + requestId)
.header("Request-Id", requestId)
.header("X-Client-TZ", "America/Sao_Paulo")
.header("X-Client-Offset-Minutes", "-180");
if (trail != null) builder.header("X-Imtbl-Trail", trail);
var request = builder
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
// Fire and forget — never block the caller
return http.sendAsync(request, HttpResponse.BodyHandlers.discarding())
.thenApply(_ -> null);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
}
// Usage:
var client = new ImmutableLogClient(
System.getenv("IMTBL_API_KEY"), "my-service", "production"
);
client.sendEvent("user.created", "success", Map.of(
"user_id", "usr_123",
"email_hash", sha256("user@example.com"),
"plan", "pro"
));
// Com trilha (agrupa eventos do mesmo fluxo) / With trail:
client.sendEvent("order.paid", "success",
Map.of("order_id", "ord_7782"), "order-7782");sendAsync() returns a CompletableFuture without blocking the thread. The event is sent asynchronously — code execution continues immediately after the call.
Retry with exponential backoff
For high-availability production, add automatic retry with exponential backoff. ImmutableLog returns 202 on success and 429 when the monthly limit is reached — never retry on 429.
public CompletableFuture<Void> sendEventWithRetry(
String eventName,
String type,
Map<String, Object> payload,
int maxAttempts
) {
return sendWithRetry(eventName, type, payload, maxAttempts, 0, 500);
}
private CompletableFuture<Void> sendWithRetry(
String eventName, String type, Map<String, Object> payload,
int maxAttempts, int attempt, long delayMs
) {
return sendEventRaw(eventName, type, payload)
.handle((res, ex) -> {
// 202 = success, 429 = rate limited (never retry)
if (ex == null) return CompletableFuture.<Void>completedFuture(null);
if (attempt + 1 >= maxAttempts) return CompletableFuture.<Void>failedFuture(ex);
return CompletableFuture
.runAsync(() -> {}, CompletableFuture.delayedExecutor(
delayMs, TimeUnit.MILLISECONDS
))
.thenCompose(_ -> sendWithRetry(
eventName, type, payload,
maxAttempts, attempt + 1, delayMs * 2 // exponential backoff
));
})
.thenCompose(f -> f);
}Batch sending
In high-frequency workers, accumulate events and send in parallel using CompletableFuture.allOf(). Each event is sent individually — allOf() parallelizes without failing the entire batch.
public CompletableFuture<Void> sendBatch(List<EventDto> events) {
// Send all events in parallel — fire and forget each one
var futures = events.stream()
.map(e -> sendEvent(e.eventName(), e.type(), e.payload())
.exceptionally(ex -> null) // never fail the batch
)
.toList();
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
}
// Usage:
client.sendBatch(List.of(
new EventDto("order.created", "success", Map.of("order_id", "ord_1")),
new EventDto("payment.captured", "success", Map.of("amount", 9900)),
new EventDto("email.sent", "info", Map.of("template", "welcome"))
));AOP @AuditEvent
With Spring AOP, instrument any service method without modifying business logic. The event is emitted in the finally block — recorded even when the method throws an exception.
// 1. Add dependency: spring-boot-starter-aop
// pom.xml: <artifactId>spring-boot-starter-aop</artifactId>
// 2. Define the annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuditEvent {
String value() default "";
String type() default "success";
}
// 3. Implement the aspect
@Aspect
@Component
public class AuditEventAspect {
private final ImmutableLogClient client;
public AuditEventAspect(ImmutableLogClient client) {
this.client = client;
}
@Around("@annotation(auditEvent)")
public Object audit(ProceedingJoinPoint pjp, AuditEvent auditEvent)
throws Throwable {
long startMs = System.currentTimeMillis();
String name = auditEvent.value().isBlank()
? pjp.getSignature().getDeclaringTypeName() + "." + pjp.getSignature().getName()
: auditEvent.value();
try {
Object result = pjp.proceed();
long latency = System.currentTimeMillis() - startMs;
client.sendEvent(name, auditEvent.type(), Map.of("latency_ms", latency));
return result;
} catch (Throwable ex) {
long latency = System.currentTimeMillis() - startMs;
client.sendEvent(name, "error", Map.of(
"latency_ms", latency,
"error_type", ex.getClass().getName(),
"error_message", ex.getMessage()
));
throw ex;
}
}
}
// 4. Use on any Spring bean:
@Service
public class OrderService {
@AuditEvent("order.processed")
public Order process(OrderRequest req) {
// ... business logic
}
@AuditEvent(value = "payment.captured", type = "success")
public void capturePayment(String orderId, long amountCents) {
// ...
}
}@Around captures exceptions
@Around is used instead of @AfterReturning/@AfterThrowing to ensure the event is emitted in all cases — success, failure, or exception.
Auto name via reflection
If value() is empty, the aspect uses the method name via pjp.getSignature(). Always prefer an explicit semantic name via @AuditEvent("domain.action").
Spring Bean configuration
For Spring Boot projects, register ImmutableLogClient as a @Bean for injection via @Autowired or constructor in any component.
// Register ImmutableLogClient as a Spring bean for injection
@Configuration
public class ImmutableLogConfig {
@Bean
public ImmutableLogClient immutableLogClient(
@Value("${immutablelog.api-key}") String apiKey,
@Value("${immutablelog.service-name:my-service}") String service,
@Value("${immutablelog.env:production}") String env
) {
return new ImmutableLogClient(apiKey, service, env);
}
}
// application.properties:
// immutablelog.api-key=${IMTBL_API_KEY}
// immutablelog.service-name=my-service
// immutablelog.env=productionThis documentation reflects the current API behavior. For questions or advanced integrations, contact the support team.
