Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `WorkflowBlockedError` and `WorkflowApprovalRequiredError` exception classes
- Builder-based option classes: `CheckGateOptions`, `StepCompletedOptions`, `CheckToolGateOptions`, `ToolCompletedOptions`
- MCP interceptor types: `MCPInterceptorOptions`, `MCPToolRequest`, `MCPToolHandler`, `MCPToolInterceptor`
- `getCircuitBreakerStatus()` / `getCircuitBreakerStatusAsync()` — query active circuit breaker circuits and emergency stop state
- `getCircuitBreakerHistory(limit)` / `getCircuitBreakerHistoryAsync(limit)` — retrieve circuit breaker trip/reset audit trail
- `getCircuitBreakerConfig(tenantId)` / `getCircuitBreakerConfigAsync(tenantId)` — get effective circuit breaker config (global or tenant-specific)
- `updateCircuitBreakerConfig(config)` / `updateCircuitBreakerConfigAsync(config)` — update per-tenant circuit breaker thresholds

---

Expand Down
172 changes: 172 additions & 0 deletions src/main/java/com/getaxonflow/sdk/AxonFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,178 @@ public CompletableFuture<AuditToolCallResponse> auditToolCallAsync(AuditToolCall
return CompletableFuture.supplyAsync(() -> auditToolCall(request), asyncExecutor);
}

// ========================================================================
// Circuit Breaker Observability
// ========================================================================

/**
* Gets the current circuit breaker status, including all active (tripped) circuits.
*
* <p>Example usage:
* <pre>{@code
* CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus();
* System.out.println("Active circuits: " + status.getCount());
* System.out.println("Emergency stop: " + status.isEmergencyStopActive());
* }</pre>
*
* @return the circuit breaker status
* @throws AxonFlowException if the request fails
*/
public CircuitBreakerStatusResponse getCircuitBreakerStatus() {
return retryExecutor.execute(() -> {
Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/circuit-breaker/status", null);
try (Response response = httpClient.newCall(httpRequest).execute()) {
JsonNode node = parseResponseNode(response);
if (node.has("data") && node.get("data").isObject()) {
return objectMapper.treeToValue(node.get("data"), CircuitBreakerStatusResponse.class);
}
return objectMapper.treeToValue(node, CircuitBreakerStatusResponse.class);
}
}, "getCircuitBreakerStatus");
}

/**
* Asynchronously gets the current circuit breaker status.
*
* @return a future containing the circuit breaker status
*/
public CompletableFuture<CircuitBreakerStatusResponse> getCircuitBreakerStatusAsync() {
return CompletableFuture.supplyAsync(this::getCircuitBreakerStatus, asyncExecutor);
}

/**
* Gets the circuit breaker history, including past trips and resets.
*
* <p>Example usage:
* <pre>{@code
* CircuitBreakerHistoryResponse history = axonflow.getCircuitBreakerHistory(50);
* for (CircuitBreakerHistoryEntry entry : history.getHistory()) {
* System.out.println(entry.getScope() + "/" + entry.getScopeId() + " - " + entry.getState());
* }
* }</pre>
*
* @param limit the maximum number of history entries to return
* @return the circuit breaker history
* @throws IllegalArgumentException if limit is less than 1
* @throws AxonFlowException if the request fails
*/
public CircuitBreakerHistoryResponse getCircuitBreakerHistory(int limit) {
if (limit < 1) {
throw new IllegalArgumentException("limit must be at least 1");
}

return retryExecutor.execute(() -> {
String path = "/api/v1/circuit-breaker/history?limit=" + limit;
Request httpRequest = buildOrchestratorRequest("GET", path, null);
try (Response response = httpClient.newCall(httpRequest).execute()) {
JsonNode node = parseResponseNode(response);
if (node.has("data") && node.get("data").isObject()) {
return objectMapper.treeToValue(node.get("data"), CircuitBreakerHistoryResponse.class);
}
return objectMapper.treeToValue(node, CircuitBreakerHistoryResponse.class);
}
}, "getCircuitBreakerHistory");
}

/**
* Asynchronously gets the circuit breaker history.
*
* @param limit the maximum number of history entries to return
* @return a future containing the circuit breaker history
*/
public CompletableFuture<CircuitBreakerHistoryResponse> getCircuitBreakerHistoryAsync(int limit) {
return CompletableFuture.supplyAsync(() -> getCircuitBreakerHistory(limit), asyncExecutor);
}

/**
* Gets the circuit breaker configuration for a specific tenant.
*
* <p>Example usage:
* <pre>{@code
* CircuitBreakerConfig config = axonflow.getCircuitBreakerConfig("tenant_123");
* System.out.println("Error threshold: " + config.getErrorThreshold());
* System.out.println("Auto recovery: " + config.isEnableAutoRecovery());
* }</pre>
*
* @param tenantId the tenant ID to get configuration for
* @return the circuit breaker configuration
* @throws NullPointerException if tenantId is null
* @throws IllegalArgumentException if tenantId is empty
* @throws AxonFlowException if the request fails
*/
public CircuitBreakerConfig getCircuitBreakerConfig(String tenantId) {
Objects.requireNonNull(tenantId, "tenantId cannot be null");
if (tenantId.isEmpty()) {
throw new IllegalArgumentException("tenantId cannot be empty");
}

return retryExecutor.execute(() -> {
String path = "/api/v1/circuit-breaker/config?tenant_id=" + java.net.URLEncoder.encode(tenantId, java.nio.charset.StandardCharsets.UTF_8);
Request httpRequest = buildOrchestratorRequest("GET", path, null);
try (Response response = httpClient.newCall(httpRequest).execute()) {
JsonNode node = parseResponseNode(response);
if (node.has("data") && node.get("data").isObject()) {
return objectMapper.treeToValue(node.get("data"), CircuitBreakerConfig.class);
}
return objectMapper.treeToValue(node, CircuitBreakerConfig.class);
}
}, "getCircuitBreakerConfig");
}

/**
* Asynchronously gets the circuit breaker configuration for a specific tenant.
*
* @param tenantId the tenant ID to get configuration for
* @return a future containing the circuit breaker configuration
*/
public CompletableFuture<CircuitBreakerConfig> getCircuitBreakerConfigAsync(String tenantId) {
return CompletableFuture.supplyAsync(() -> getCircuitBreakerConfig(tenantId), asyncExecutor);
}

/**
* Updates the circuit breaker configuration for a tenant.
*
* <p>Example usage:
* <pre>{@code
* CircuitBreakerConfig updated = axonflow.updateCircuitBreakerConfig(
* CircuitBreakerConfigUpdate.builder()
* .tenantId("tenant_123")
* .errorThreshold(10)
* .violationThreshold(5)
* .enableAutoRecovery(true)
* .build());
* }</pre>
*
* @param config the configuration update
* @return confirmation with tenant_id and message
* @throws NullPointerException if config is null
* @throws AxonFlowException if the request fails
*/
public CircuitBreakerConfigUpdateResponse updateCircuitBreakerConfig(CircuitBreakerConfigUpdate config) {
Objects.requireNonNull(config, "config cannot be null");

return retryExecutor.execute(() -> {
Request httpRequest = buildOrchestratorRequest("PUT", "/api/v1/circuit-breaker/config", config);
try (Response response = httpClient.newCall(httpRequest).execute()) {
JsonNode node = parseResponseNode(response);
if (node.has("data") && node.get("data").isObject()) {
return objectMapper.treeToValue(node.get("data"), CircuitBreakerConfigUpdateResponse.class);
}
return objectMapper.treeToValue(node, CircuitBreakerConfigUpdateResponse.class);
}
}, "updateCircuitBreakerConfig");
}

/**
* Asynchronously updates the circuit breaker configuration for a tenant.
*
* @param config the configuration update
* @return a future containing the update confirmation
*/
public CompletableFuture<CircuitBreakerConfigUpdateResponse> updateCircuitBreakerConfigAsync(CircuitBreakerConfigUpdate config) {
return CompletableFuture.supplyAsync(() -> updateCircuitBreakerConfig(config), asyncExecutor);
}

// ========================================================================
// Proxy Mode - Query Execution
// ========================================================================
Expand Down
86 changes: 86 additions & 0 deletions src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2025 AxonFlow
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.getaxonflow.sdk.types;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Map;

/**
* Circuit breaker configuration for a tenant.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class CircuitBreakerConfig {

@JsonProperty("source")
private final String source;

@JsonProperty("error_threshold")
private final int errorThreshold;

@JsonProperty("violation_threshold")
private final int violationThreshold;

@JsonProperty("window_seconds")
private final int windowSeconds;

@JsonProperty("default_timeout_seconds")
private final int defaultTimeoutSeconds;

@JsonProperty("max_timeout_seconds")
private final int maxTimeoutSeconds;

@JsonProperty("enable_auto_recovery")
private final boolean enableAutoRecovery;

@JsonProperty("tenant_id")
private final String tenantId;

@JsonProperty("overrides")
private final Map<String, Object> overrides;

public CircuitBreakerConfig(
@JsonProperty("source") String source,
@JsonProperty("error_threshold") int errorThreshold,
@JsonProperty("violation_threshold") int violationThreshold,
@JsonProperty("window_seconds") int windowSeconds,
@JsonProperty("default_timeout_seconds") int defaultTimeoutSeconds,
@JsonProperty("max_timeout_seconds") int maxTimeoutSeconds,
@JsonProperty("enable_auto_recovery") boolean enableAutoRecovery,
@JsonProperty("tenant_id") String tenantId,
@JsonProperty("overrides") Map<String, Object> overrides) {
this.source = source;
this.errorThreshold = errorThreshold;
this.violationThreshold = violationThreshold;
this.windowSeconds = windowSeconds;
this.defaultTimeoutSeconds = defaultTimeoutSeconds;
this.maxTimeoutSeconds = maxTimeoutSeconds;
this.enableAutoRecovery = enableAutoRecovery;
this.tenantId = tenantId;
this.overrides = overrides != null ? Map.copyOf(overrides) : null;
}

public String getSource() { return source; }
public int getErrorThreshold() { return errorThreshold; }
public int getViolationThreshold() { return violationThreshold; }
public int getWindowSeconds() { return windowSeconds; }
public int getDefaultTimeoutSeconds() { return defaultTimeoutSeconds; }
public int getMaxTimeoutSeconds() { return maxTimeoutSeconds; }
public boolean isEnableAutoRecovery() { return enableAutoRecovery; }
public String getTenantId() { return tenantId; }
public Map<String, Object> getOverrides() { return overrides; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2025 AxonFlow
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.getaxonflow.sdk.types;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Objects;

/**
* Request to update circuit breaker configuration for a tenant.
*
* <p>Use the {@link Builder} to construct instances:
* <pre>{@code
* CircuitBreakerConfigUpdate update = CircuitBreakerConfigUpdate.builder()
* .tenantId("tenant_123")
* .errorThreshold(10)
* .enableAutoRecovery(true)
* .build();
* }</pre>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class CircuitBreakerConfigUpdate {

@JsonProperty("tenant_id")
private final String tenantId;

@JsonProperty("error_threshold")
private final Integer errorThreshold;

@JsonProperty("violation_threshold")
private final Integer violationThreshold;

@JsonProperty("window_seconds")
private final Integer windowSeconds;

@JsonProperty("default_timeout_seconds")
private final Integer defaultTimeoutSeconds;

@JsonProperty("max_timeout_seconds")
private final Integer maxTimeoutSeconds;

@JsonProperty("enable_auto_recovery")
private final Boolean enableAutoRecovery;

private CircuitBreakerConfigUpdate(Builder builder) {
this.tenantId = Objects.requireNonNull(builder.tenantId, "tenantId cannot be null");
if (this.tenantId.isEmpty()) {
throw new IllegalArgumentException("tenantId cannot be empty");
}
this.errorThreshold = builder.errorThreshold;
this.violationThreshold = builder.violationThreshold;
this.windowSeconds = builder.windowSeconds;
this.defaultTimeoutSeconds = builder.defaultTimeoutSeconds;
this.maxTimeoutSeconds = builder.maxTimeoutSeconds;
this.enableAutoRecovery = builder.enableAutoRecovery;
}

/**
* Creates a new builder for CircuitBreakerConfigUpdate.
*
* @return a new builder
*/
public static Builder builder() {
return new Builder();
}

public String getTenantId() { return tenantId; }
public Integer getErrorThreshold() { return errorThreshold; }
public Integer getViolationThreshold() { return violationThreshold; }
public Integer getWindowSeconds() { return windowSeconds; }
public Integer getDefaultTimeoutSeconds() { return defaultTimeoutSeconds; }
public Integer getMaxTimeoutSeconds() { return maxTimeoutSeconds; }
public Boolean getEnableAutoRecovery() { return enableAutoRecovery; }

/**
* Builder for {@link CircuitBreakerConfigUpdate}.
*/
public static final class Builder {
private String tenantId;
private Integer errorThreshold;
private Integer violationThreshold;
private Integer windowSeconds;
private Integer defaultTimeoutSeconds;
private Integer maxTimeoutSeconds;
private Boolean enableAutoRecovery;

public Builder tenantId(String tenantId) { this.tenantId = tenantId; return this; }
public Builder errorThreshold(int errorThreshold) { this.errorThreshold = errorThreshold; return this; }
public Builder violationThreshold(int violationThreshold) { this.violationThreshold = violationThreshold; return this; }
public Builder windowSeconds(int windowSeconds) { this.windowSeconds = windowSeconds; return this; }
public Builder defaultTimeoutSeconds(int defaultTimeoutSeconds) { this.defaultTimeoutSeconds = defaultTimeoutSeconds; return this; }
public Builder maxTimeoutSeconds(int maxTimeoutSeconds) { this.maxTimeoutSeconds = maxTimeoutSeconds; return this; }
public Builder enableAutoRecovery(boolean enableAutoRecovery) { this.enableAutoRecovery = enableAutoRecovery; return this; }

/**
* Builds the CircuitBreakerConfigUpdate.
*
* @return the config update
* @throws NullPointerException if tenantId is null
* @throws IllegalArgumentException if tenantId is empty
*/
public CircuitBreakerConfigUpdate build() {
return new CircuitBreakerConfigUpdate(this);
}
}
}
Loading
Loading