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;
}
public CompletableFuture<Void> sendEvent(
String eventName,
String type, // "success" | "info" | "error"
Map<String, Object> payload
) {
try {
String requestId = UUID.randomUUID().toString();
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(payload),
"meta", meta
);
String json = mapper.writeValueAsString(body);
var request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.header("Idempotency-Key", requestId)
.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"
));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.
