@@ -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