Skip to content

Commit 0d5c5db

Browse files
feat(replay): add Execution Replay API methods (#763)
Add SDK methods for the Execution Replay API: - listExecutions() - List workflow executions with filtering - getExecution() - Get execution details with all steps - getExecutionSteps() - Get all steps for an execution - getExecutionTimeline() - Get execution timeline view - exportExecution() - Export execution for compliance - deleteExecution() - Delete an execution Also adds orchestratorUrl config option for specifying the orchestrator endpoint (defaults to agent URL port 8081).
1 parent d24a22d commit 0d5c5db

4 files changed

Lines changed: 798 additions & 0 deletions

File tree

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

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.getaxonflow.sdk.exceptions.*;
1919
import com.getaxonflow.sdk.types.*;
2020
import com.getaxonflow.sdk.types.codegovernance.*;
21+
import com.getaxonflow.sdk.types.executionreplay.ExecutionReplayTypes.*;
2122
import com.getaxonflow.sdk.types.policies.PolicyTypes.*;
2223
import com.getaxonflow.sdk.util.*;
2324
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -32,6 +33,7 @@
3233

3334
import java.io.Closeable;
3435
import java.io.IOException;
36+
import java.net.URI;
3537
import java.util.ArrayList;
3638
import java.util.Collections;
3739
import java.util.HashMap;
@@ -1781,6 +1783,297 @@ public String exportCodeGovernanceDataCSV(ExportOptions options) throws IOExcept
17811783
}
17821784
}
17831785

1786+
// ========================================================================
1787+
// Execution Replay API
1788+
// ========================================================================
1789+
1790+
/**
1791+
* Gets the orchestrator URL for Execution Replay API.
1792+
* Falls back to agent URL with port 8081 if not configured.
1793+
*/
1794+
private String getOrchestratorUrl() {
1795+
String orchestratorUrl = config.getOrchestratorUrl();
1796+
if (orchestratorUrl != null && !orchestratorUrl.isEmpty()) {
1797+
return orchestratorUrl;
1798+
}
1799+
// Default: assume orchestrator is on same host as agent, port 8081
1800+
try {
1801+
URI uri = URI.create(config.getAgentUrl());
1802+
return uri.getScheme() + "://" + uri.getHost() + ":8081";
1803+
} catch (Exception e) {
1804+
return "http://localhost:8081";
1805+
}
1806+
}
1807+
1808+
/**
1809+
* Builds a request for the orchestrator API.
1810+
*/
1811+
private Request buildOrchestratorRequest(String method, String path, Object body) {
1812+
HttpUrl url = HttpUrl.parse(getOrchestratorUrl() + path);
1813+
if (url == null) {
1814+
throw new ConfigurationException("Invalid URL: " + getOrchestratorUrl() + path);
1815+
}
1816+
1817+
Request.Builder builder = new Request.Builder()
1818+
.url(url)
1819+
.header("User-Agent", config.getUserAgent())
1820+
.header("Accept", "application/json");
1821+
1822+
addAuthHeaders(builder);
1823+
1824+
RequestBody requestBody = null;
1825+
if (body != null) {
1826+
try {
1827+
String json = objectMapper.writeValueAsString(body);
1828+
requestBody = RequestBody.create(json, JSON);
1829+
} catch (JsonProcessingException e) {
1830+
throw new AxonFlowException("Failed to serialize request body", e);
1831+
}
1832+
}
1833+
1834+
switch (method.toUpperCase()) {
1835+
case "GET":
1836+
builder.get();
1837+
break;
1838+
case "POST":
1839+
builder.post(requestBody != null ? requestBody : RequestBody.create("", JSON));
1840+
break;
1841+
case "DELETE":
1842+
builder.delete(requestBody);
1843+
break;
1844+
default:
1845+
throw new IllegalArgumentException("Unsupported method: " + method);
1846+
}
1847+
1848+
return builder.build();
1849+
}
1850+
1851+
/**
1852+
* Lists workflow executions with optional filtering and pagination.
1853+
*
1854+
* @param options filtering and pagination options
1855+
* @return paginated list of execution summaries
1856+
*
1857+
* @example
1858+
* <pre>{@code
1859+
* ListExecutionsResponse response = axonflow.listExecutions(
1860+
* ListExecutionsOptions.builder()
1861+
* .setStatus("completed")
1862+
* .setLimit(10)
1863+
* );
1864+
* for (ExecutionSummary exec : response.getExecutions()) {
1865+
* System.out.println(exec.getRequestId() + ": " + exec.getStatus());
1866+
* }
1867+
* }</pre>
1868+
*/
1869+
public ListExecutionsResponse listExecutions(ListExecutionsOptions options) {
1870+
return retryExecutor.execute(() -> {
1871+
StringBuilder path = new StringBuilder("/api/v1/executions");
1872+
StringBuilder query = new StringBuilder();
1873+
1874+
if (options != null) {
1875+
if (options.getLimit() != null) {
1876+
appendQueryParam(query, "limit", options.getLimit().toString());
1877+
}
1878+
if (options.getOffset() != null) {
1879+
appendQueryParam(query, "offset", options.getOffset().toString());
1880+
}
1881+
if (options.getStatus() != null) {
1882+
appendQueryParam(query, "status", options.getStatus());
1883+
}
1884+
if (options.getWorkflowId() != null) {
1885+
appendQueryParam(query, "workflow_id", options.getWorkflowId());
1886+
}
1887+
if (options.getStartTime() != null) {
1888+
appendQueryParam(query, "start_time", options.getStartTime());
1889+
}
1890+
if (options.getEndTime() != null) {
1891+
appendQueryParam(query, "end_time", options.getEndTime());
1892+
}
1893+
}
1894+
1895+
if (query.length() > 0) {
1896+
path.append("?").append(query);
1897+
}
1898+
1899+
Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null);
1900+
try (Response response = httpClient.newCall(httpRequest).execute()) {
1901+
return parseResponse(response, ListExecutionsResponse.class);
1902+
}
1903+
}, "listExecutions");
1904+
}
1905+
1906+
/**
1907+
* Lists workflow executions with default options.
1908+
*
1909+
* @return list of execution summaries
1910+
*/
1911+
public ListExecutionsResponse listExecutions() {
1912+
return listExecutions(null);
1913+
}
1914+
1915+
/**
1916+
* Gets a complete execution record including summary and all steps.
1917+
*
1918+
* @param executionId the execution ID (request_id)
1919+
* @return full execution details with all step snapshots
1920+
*
1921+
* @example
1922+
* <pre>{@code
1923+
* ExecutionDetail detail = axonflow.getExecution("exec-abc123");
1924+
* System.out.println("Status: " + detail.getSummary().getStatus());
1925+
* for (ExecutionSnapshot step : detail.getSteps()) {
1926+
* System.out.println("Step " + step.getStepIndex() + ": " + step.getStepName());
1927+
* }
1928+
* }</pre>
1929+
*/
1930+
public ExecutionDetail getExecution(String executionId) {
1931+
Objects.requireNonNull(executionId, "executionId cannot be null");
1932+
1933+
return retryExecutor.execute(() -> {
1934+
Request httpRequest = buildOrchestratorRequest("GET",
1935+
"/api/v1/executions/" + executionId, null);
1936+
try (Response response = httpClient.newCall(httpRequest).execute()) {
1937+
return parseResponse(response, ExecutionDetail.class);
1938+
}
1939+
}, "getExecution");
1940+
}
1941+
1942+
/**
1943+
* Gets all step snapshots for an execution.
1944+
*
1945+
* @param executionId the execution ID (request_id)
1946+
* @return list of step snapshots
1947+
*/
1948+
public List<ExecutionSnapshot> getExecutionSteps(String executionId) {
1949+
Objects.requireNonNull(executionId, "executionId cannot be null");
1950+
1951+
return retryExecutor.execute(() -> {
1952+
Request httpRequest = buildOrchestratorRequest("GET",
1953+
"/api/v1/executions/" + executionId + "/steps", null);
1954+
try (Response response = httpClient.newCall(httpRequest).execute()) {
1955+
return parseResponse(response, new TypeReference<List<ExecutionSnapshot>>() {});
1956+
}
1957+
}, "getExecutionSteps");
1958+
}
1959+
1960+
/**
1961+
* Gets a timeline view of execution events for visualization.
1962+
*
1963+
* @param executionId the execution ID (request_id)
1964+
* @return list of timeline entries
1965+
*/
1966+
public List<TimelineEntry> getExecutionTimeline(String executionId) {
1967+
Objects.requireNonNull(executionId, "executionId cannot be null");
1968+
1969+
return retryExecutor.execute(() -> {
1970+
Request httpRequest = buildOrchestratorRequest("GET",
1971+
"/api/v1/executions/" + executionId + "/timeline", null);
1972+
try (Response response = httpClient.newCall(httpRequest).execute()) {
1973+
return parseResponse(response, new TypeReference<List<TimelineEntry>>() {});
1974+
}
1975+
}, "getExecutionTimeline");
1976+
}
1977+
1978+
/**
1979+
* Exports a complete execution record for compliance or archival.
1980+
*
1981+
* @param executionId the execution ID (request_id)
1982+
* @param options export options
1983+
* @return execution data as a map
1984+
*
1985+
* @example
1986+
* <pre>{@code
1987+
* Map<String, Object> export = axonflow.exportExecution("exec-abc123",
1988+
* ExecutionExportOptions.builder()
1989+
* .setIncludeInput(true)
1990+
* .setIncludeOutput(true));
1991+
* // Save to file for audit
1992+
* }</pre>
1993+
*/
1994+
public Map<String, Object> exportExecution(String executionId, ExecutionExportOptions options) {
1995+
Objects.requireNonNull(executionId, "executionId cannot be null");
1996+
1997+
return retryExecutor.execute(() -> {
1998+
StringBuilder path = new StringBuilder("/api/v1/executions/" + executionId + "/export");
1999+
StringBuilder query = new StringBuilder();
2000+
2001+
if (options != null) {
2002+
if (options.getFormat() != null) {
2003+
appendQueryParam(query, "format", options.getFormat());
2004+
}
2005+
if (options.isIncludeInput()) {
2006+
appendQueryParam(query, "include_input", "true");
2007+
}
2008+
if (options.isIncludeOutput()) {
2009+
appendQueryParam(query, "include_output", "true");
2010+
}
2011+
if (options.isIncludePolicies()) {
2012+
appendQueryParam(query, "include_policies", "true");
2013+
}
2014+
}
2015+
2016+
if (query.length() > 0) {
2017+
path.append("?").append(query);
2018+
}
2019+
2020+
Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null);
2021+
try (Response response = httpClient.newCall(httpRequest).execute()) {
2022+
return parseResponse(response, new TypeReference<Map<String, Object>>() {});
2023+
}
2024+
}, "exportExecution");
2025+
}
2026+
2027+
/**
2028+
* Exports a complete execution record with default options.
2029+
*
2030+
* @param executionId the execution ID (request_id)
2031+
* @return execution data as a map
2032+
*/
2033+
public Map<String, Object> exportExecution(String executionId) {
2034+
return exportExecution(executionId, null);
2035+
}
2036+
2037+
/**
2038+
* Deletes an execution and all associated step snapshots.
2039+
*
2040+
* @param executionId the execution ID (request_id)
2041+
*/
2042+
public void deleteExecution(String executionId) {
2043+
Objects.requireNonNull(executionId, "executionId cannot be null");
2044+
2045+
retryExecutor.execute(() -> {
2046+
Request httpRequest = buildOrchestratorRequest("DELETE",
2047+
"/api/v1/executions/" + executionId, null);
2048+
try (Response response = httpClient.newCall(httpRequest).execute()) {
2049+
if (!response.isSuccessful() && response.code() != 204) {
2050+
handleErrorResponse(response);
2051+
}
2052+
return null;
2053+
}
2054+
}, "deleteExecution");
2055+
}
2056+
2057+
/**
2058+
* Asynchronously lists workflow executions.
2059+
*
2060+
* @param options filtering and pagination options
2061+
* @return a future containing the list of executions
2062+
*/
2063+
public CompletableFuture<ListExecutionsResponse> listExecutionsAsync(ListExecutionsOptions options) {
2064+
return CompletableFuture.supplyAsync(() -> listExecutions(options), asyncExecutor);
2065+
}
2066+
2067+
/**
2068+
* Asynchronously gets execution details.
2069+
*
2070+
* @param executionId the execution ID
2071+
* @return a future containing the execution details
2072+
*/
2073+
public CompletableFuture<ExecutionDetail> getExecutionAsync(String executionId) {
2074+
return CompletableFuture.supplyAsync(() -> getExecution(executionId), asyncExecutor);
2075+
}
2076+
17842077
@Override
17852078
public void close() {
17862079
httpClient.dispatcher().executorService().shutdown();

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public final class AxonFlowConfig {
4949
public static final String DEFAULT_AGENT_URL = "http://localhost:8080";
5050

5151
private final String agentUrl;
52+
private final String orchestratorUrl;
5253
private final String clientId;
5354
private final String clientSecret;
5455
private final String licenseKey;
@@ -62,6 +63,7 @@ public final class AxonFlowConfig {
6263

6364
private AxonFlowConfig(Builder builder) {
6465
this.agentUrl = normalizeUrl(builder.agentUrl != null ? builder.agentUrl : DEFAULT_AGENT_URL);
66+
this.orchestratorUrl = normalizeUrl(builder.orchestratorUrl);
6567
this.clientId = builder.clientId;
6668
this.clientSecret = builder.clientSecret;
6769
this.licenseKey = builder.licenseKey;
@@ -179,6 +181,10 @@ public String getAgentUrl() {
179181
return agentUrl;
180182
}
181183

184+
public String getOrchestratorUrl() {
185+
return orchestratorUrl;
186+
}
187+
182188
public String getClientId() {
183189
return clientId;
184190
}
@@ -257,6 +263,7 @@ public String toString() {
257263
*/
258264
public static final class Builder {
259265
private String agentUrl;
266+
private String orchestratorUrl;
260267
private String clientId;
261268
private String clientSecret;
262269
private String licenseKey;
@@ -281,6 +288,19 @@ public Builder agentUrl(String agentUrl) {
281288
return this;
282289
}
283290

291+
/**
292+
* Sets the Orchestrator URL for Execution Replay API.
293+
*
294+
* <p>If not set, defaults to the Agent URL with port 8081.
295+
*
296+
* @param orchestratorUrl the Orchestrator URL
297+
* @return this builder
298+
*/
299+
public Builder orchestratorUrl(String orchestratorUrl) {
300+
this.orchestratorUrl = orchestratorUrl;
301+
return this;
302+
}
303+
284304
/**
285305
* Sets the client ID for authentication.
286306
*

0 commit comments

Comments
 (0)