Skip to content

Commit fe3d78a

Browse files
feat: add audit log read methods (searchAuditLogs, getAuditLogsByTenant)
* feat(audit): add audit log read methods Add searchAuditLogs() and getAuditLogsByTenant() methods for reading audit logs from the AxonFlow orchestrator. New types: - AuditSearchRequest: Builder-pattern request with filters - AuditQueryOptions: Simple pagination options for tenant queries - AuditLogEntry: Full audit log entry with all fields - AuditSearchResponse: Response with entries, pagination, hasMore() New methods: - searchAuditLogs(request): POST /api/v1/audit/search - searchAuditLogs(): Search with default limit (100) - searchAuditLogsAsync(request): Async variant - getAuditLogsByTenant(tenantId, options): GET /api/v1/audit/tenant/{id} - getAuditLogsByTenant(tenantId): Query with default options - getAuditLogsByTenantAsync(tenantId, options): Async variant Features: - Handles both array and wrapped response formats - Limit capped at 1000 to prevent excessive queries - URL encoding for tenant IDs with special characters - Comprehensive test coverage (26 tests) Part of Issue #878 - Add audit log read capabilities to SDK * chore: add v1.13.0 changelog entry for audit read methods
1 parent 7ef4f78 commit fe3d78a

7 files changed

Lines changed: 1432 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to the AxonFlow Java SDK will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.13.0] - 2026-01-05
9+
10+
### Added
11+
12+
- **Audit Log Reading**: Added `searchAuditLogs()` for searching audit logs with filters (user email, client ID, time range, request type)
13+
- **Tenant Audit Logs**: Added `getAuditLogsByTenant()` for retrieving audit logs scoped to a specific tenant
14+
- **Audit Types**: Added `AuditLogEntry`, `AuditSearchRequest`, `AuditQueryOptions`, and `AuditSearchResponse` types
15+
16+
---
17+
818
## [1.12.0] - 2026-01-04
919

1020
### Added

src/main/java/com/getaxonflow/sdk/AxonFlow.java

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,141 @@ public CompletableFuture<AuditResult> auditLLMCallAsync(AuditOptions options) {
333333
return CompletableFuture.supplyAsync(() -> auditLLMCall(options), asyncExecutor);
334334
}
335335

336+
// ========================================================================
337+
// Audit Log Read Methods
338+
// ========================================================================
339+
340+
/**
341+
* Searches audit logs with flexible filtering options.
342+
*
343+
* <p>Example usage:
344+
* <pre>{@code
345+
* AuditSearchResponse response = axonflow.searchAuditLogs(
346+
* AuditSearchRequest.builder()
347+
* .userEmail("analyst@company.com")
348+
* .startTime(Instant.now().minus(Duration.ofDays(7)))
349+
* .requestType("llm_chat")
350+
* .limit(100)
351+
* .build());
352+
*
353+
* for (AuditLogEntry entry : response.getEntries()) {
354+
* System.out.println(entry.getId() + ": " + entry.getQuerySummary());
355+
* }
356+
* }</pre>
357+
*
358+
* @param request the search request with optional filters
359+
* @return the search response containing matching audit log entries
360+
* @throws AxonFlowException if the search fails
361+
*/
362+
public AuditSearchResponse searchAuditLogs(AuditSearchRequest request) {
363+
return retryExecutor.execute(() -> {
364+
AuditSearchRequest req = request != null ? request : AuditSearchRequest.builder().build();
365+
Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/audit/search", req);
366+
try (Response response = httpClient.newCall(httpRequest).execute()) {
367+
JsonNode node = parseResponseNode(response);
368+
369+
// Handle both array and wrapped response formats
370+
if (node.isArray()) {
371+
List<AuditLogEntry> entries = objectMapper.convertValue(
372+
node, new TypeReference<List<AuditLogEntry>>() {});
373+
return AuditSearchResponse.fromArray(entries,
374+
req.getLimit() != null ? req.getLimit() : 100,
375+
req.getOffset() != null ? req.getOffset() : 0);
376+
}
377+
378+
return objectMapper.treeToValue(node, AuditSearchResponse.class);
379+
}
380+
}, "searchAuditLogs");
381+
}
382+
383+
/**
384+
* Searches audit logs with default options (last 100 entries).
385+
*
386+
* @return the search response
387+
*/
388+
public AuditSearchResponse searchAuditLogs() {
389+
return searchAuditLogs(null);
390+
}
391+
392+
/**
393+
* Asynchronously searches audit logs.
394+
*
395+
* @param request the search request
396+
* @return a future containing the search response
397+
*/
398+
public CompletableFuture<AuditSearchResponse> searchAuditLogsAsync(AuditSearchRequest request) {
399+
return CompletableFuture.supplyAsync(() -> searchAuditLogs(request), asyncExecutor);
400+
}
401+
402+
/**
403+
* Gets audit logs for a specific tenant.
404+
*
405+
* <p>Example usage:
406+
* <pre>{@code
407+
* AuditSearchResponse response = axonflow.getAuditLogsByTenant("tenant-abc",
408+
* AuditQueryOptions.builder()
409+
* .limit(100)
410+
* .offset(50)
411+
* .build());
412+
*
413+
* System.out.println("Total entries: " + response.getTotal());
414+
* System.out.println("Has more: " + response.hasMore());
415+
* }</pre>
416+
*
417+
* @param tenantId the tenant ID to query
418+
* @param options optional pagination options
419+
* @return the search response containing audit log entries for the tenant
420+
* @throws IllegalArgumentException if tenantId is null or empty
421+
* @throws AxonFlowException if the query fails
422+
*/
423+
public AuditSearchResponse getAuditLogsByTenant(String tenantId, AuditQueryOptions options) {
424+
if (tenantId == null || tenantId.isEmpty()) {
425+
throw new IllegalArgumentException("tenantId is required");
426+
}
427+
428+
return retryExecutor.execute(() -> {
429+
AuditQueryOptions opts = options != null ? options : AuditQueryOptions.defaults();
430+
String encodedTenantId = java.net.URLEncoder.encode(tenantId, "UTF-8");
431+
String path = "/api/v1/audit/tenant/" + encodedTenantId +
432+
"?limit=" + opts.getLimit() + "&offset=" + opts.getOffset();
433+
434+
Request httpRequest = buildOrchestratorRequest("GET", path, null);
435+
try (Response response = httpClient.newCall(httpRequest).execute()) {
436+
JsonNode node = parseResponseNode(response);
437+
438+
// Handle both array and wrapped response formats
439+
if (node.isArray()) {
440+
List<AuditLogEntry> entries = objectMapper.convertValue(
441+
node, new TypeReference<List<AuditLogEntry>>() {});
442+
return AuditSearchResponse.fromArray(entries, opts.getLimit(), opts.getOffset());
443+
}
444+
445+
return objectMapper.treeToValue(node, AuditSearchResponse.class);
446+
}
447+
}, "getAuditLogsByTenant");
448+
}
449+
450+
/**
451+
* Gets audit logs for a specific tenant with default options.
452+
*
453+
* @param tenantId the tenant ID to query
454+
* @return the search response
455+
*/
456+
public AuditSearchResponse getAuditLogsByTenant(String tenantId) {
457+
return getAuditLogsByTenant(tenantId, null);
458+
}
459+
460+
/**
461+
* Asynchronously gets audit logs for a specific tenant.
462+
*
463+
* @param tenantId the tenant ID to query
464+
* @param options optional pagination options
465+
* @return a future containing the search response
466+
*/
467+
public CompletableFuture<AuditSearchResponse> getAuditLogsByTenantAsync(String tenantId, AuditQueryOptions options) {
468+
return CompletableFuture.supplyAsync(() -> getAuditLogsByTenant(tenantId, options), asyncExecutor);
469+
}
470+
336471
// ========================================================================
337472
// Proxy Mode - Query Execution
338473
// ========================================================================

0 commit comments

Comments
 (0)