Skip to content

Commit dccdf5b

Browse files
feat: add circuit breaker observability methods (#1176) (#115)
* feat: add circuit breaker observability methods (#1176) New SDK methods: getCircuitBreakerStatus, getCircuitBreakerHistory, getCircuitBreakerConfig, updateCircuitBreakerConfig. * chore: add v4.2.0 changelog entry * fix: correct update response type + URL-encode tenantId in getConfig Backend returns {tenant_id, message} not full CircuitBreakerConfig. Added CircuitBreakerConfigUpdateResponse type. URL-encode tenantId in getCircuitBreakerConfig query parameter.
1 parent 5ed905d commit dccdf5b

9 files changed

Lines changed: 1115 additions & 0 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- `WorkflowBlockedError` and `WorkflowApprovalRequiredError` exception classes
2020
- Builder-based option classes: `CheckGateOptions`, `StepCompletedOptions`, `CheckToolGateOptions`, `ToolCompletedOptions`
2121
- MCP interceptor types: `MCPInterceptorOptions`, `MCPToolRequest`, `MCPToolHandler`, `MCPToolInterceptor`
22+
- `getCircuitBreakerStatus()` / `getCircuitBreakerStatusAsync()` — query active circuit breaker circuits and emergency stop state
23+
- `getCircuitBreakerHistory(limit)` / `getCircuitBreakerHistoryAsync(limit)` — retrieve circuit breaker trip/reset audit trail
24+
- `getCircuitBreakerConfig(tenantId)` / `getCircuitBreakerConfigAsync(tenantId)` — get effective circuit breaker config (global or tenant-specific)
25+
- `updateCircuitBreakerConfig(config)` / `updateCircuitBreakerConfigAsync(config)` — update per-tenant circuit breaker thresholds
2226

2327
---
2428

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

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,178 @@ public CompletableFuture<AuditToolCallResponse> auditToolCallAsync(AuditToolCall
649649
return CompletableFuture.supplyAsync(() -> auditToolCall(request), asyncExecutor);
650650
}
651651

652+
// ========================================================================
653+
// Circuit Breaker Observability
654+
// ========================================================================
655+
656+
/**
657+
* Gets the current circuit breaker status, including all active (tripped) circuits.
658+
*
659+
* <p>Example usage:
660+
* <pre>{@code
661+
* CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus();
662+
* System.out.println("Active circuits: " + status.getCount());
663+
* System.out.println("Emergency stop: " + status.isEmergencyStopActive());
664+
* }</pre>
665+
*
666+
* @return the circuit breaker status
667+
* @throws AxonFlowException if the request fails
668+
*/
669+
public CircuitBreakerStatusResponse getCircuitBreakerStatus() {
670+
return retryExecutor.execute(() -> {
671+
Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/circuit-breaker/status", null);
672+
try (Response response = httpClient.newCall(httpRequest).execute()) {
673+
JsonNode node = parseResponseNode(response);
674+
if (node.has("data") && node.get("data").isObject()) {
675+
return objectMapper.treeToValue(node.get("data"), CircuitBreakerStatusResponse.class);
676+
}
677+
return objectMapper.treeToValue(node, CircuitBreakerStatusResponse.class);
678+
}
679+
}, "getCircuitBreakerStatus");
680+
}
681+
682+
/**
683+
* Asynchronously gets the current circuit breaker status.
684+
*
685+
* @return a future containing the circuit breaker status
686+
*/
687+
public CompletableFuture<CircuitBreakerStatusResponse> getCircuitBreakerStatusAsync() {
688+
return CompletableFuture.supplyAsync(this::getCircuitBreakerStatus, asyncExecutor);
689+
}
690+
691+
/**
692+
* Gets the circuit breaker history, including past trips and resets.
693+
*
694+
* <p>Example usage:
695+
* <pre>{@code
696+
* CircuitBreakerHistoryResponse history = axonflow.getCircuitBreakerHistory(50);
697+
* for (CircuitBreakerHistoryEntry entry : history.getHistory()) {
698+
* System.out.println(entry.getScope() + "/" + entry.getScopeId() + " - " + entry.getState());
699+
* }
700+
* }</pre>
701+
*
702+
* @param limit the maximum number of history entries to return
703+
* @return the circuit breaker history
704+
* @throws IllegalArgumentException if limit is less than 1
705+
* @throws AxonFlowException if the request fails
706+
*/
707+
public CircuitBreakerHistoryResponse getCircuitBreakerHistory(int limit) {
708+
if (limit < 1) {
709+
throw new IllegalArgumentException("limit must be at least 1");
710+
}
711+
712+
return retryExecutor.execute(() -> {
713+
String path = "/api/v1/circuit-breaker/history?limit=" + limit;
714+
Request httpRequest = buildOrchestratorRequest("GET", path, null);
715+
try (Response response = httpClient.newCall(httpRequest).execute()) {
716+
JsonNode node = parseResponseNode(response);
717+
if (node.has("data") && node.get("data").isObject()) {
718+
return objectMapper.treeToValue(node.get("data"), CircuitBreakerHistoryResponse.class);
719+
}
720+
return objectMapper.treeToValue(node, CircuitBreakerHistoryResponse.class);
721+
}
722+
}, "getCircuitBreakerHistory");
723+
}
724+
725+
/**
726+
* Asynchronously gets the circuit breaker history.
727+
*
728+
* @param limit the maximum number of history entries to return
729+
* @return a future containing the circuit breaker history
730+
*/
731+
public CompletableFuture<CircuitBreakerHistoryResponse> getCircuitBreakerHistoryAsync(int limit) {
732+
return CompletableFuture.supplyAsync(() -> getCircuitBreakerHistory(limit), asyncExecutor);
733+
}
734+
735+
/**
736+
* Gets the circuit breaker configuration for a specific tenant.
737+
*
738+
* <p>Example usage:
739+
* <pre>{@code
740+
* CircuitBreakerConfig config = axonflow.getCircuitBreakerConfig("tenant_123");
741+
* System.out.println("Error threshold: " + config.getErrorThreshold());
742+
* System.out.println("Auto recovery: " + config.isEnableAutoRecovery());
743+
* }</pre>
744+
*
745+
* @param tenantId the tenant ID to get configuration for
746+
* @return the circuit breaker configuration
747+
* @throws NullPointerException if tenantId is null
748+
* @throws IllegalArgumentException if tenantId is empty
749+
* @throws AxonFlowException if the request fails
750+
*/
751+
public CircuitBreakerConfig getCircuitBreakerConfig(String tenantId) {
752+
Objects.requireNonNull(tenantId, "tenantId cannot be null");
753+
if (tenantId.isEmpty()) {
754+
throw new IllegalArgumentException("tenantId cannot be empty");
755+
}
756+
757+
return retryExecutor.execute(() -> {
758+
String path = "/api/v1/circuit-breaker/config?tenant_id=" + java.net.URLEncoder.encode(tenantId, java.nio.charset.StandardCharsets.UTF_8);
759+
Request httpRequest = buildOrchestratorRequest("GET", path, null);
760+
try (Response response = httpClient.newCall(httpRequest).execute()) {
761+
JsonNode node = parseResponseNode(response);
762+
if (node.has("data") && node.get("data").isObject()) {
763+
return objectMapper.treeToValue(node.get("data"), CircuitBreakerConfig.class);
764+
}
765+
return objectMapper.treeToValue(node, CircuitBreakerConfig.class);
766+
}
767+
}, "getCircuitBreakerConfig");
768+
}
769+
770+
/**
771+
* Asynchronously gets the circuit breaker configuration for a specific tenant.
772+
*
773+
* @param tenantId the tenant ID to get configuration for
774+
* @return a future containing the circuit breaker configuration
775+
*/
776+
public CompletableFuture<CircuitBreakerConfig> getCircuitBreakerConfigAsync(String tenantId) {
777+
return CompletableFuture.supplyAsync(() -> getCircuitBreakerConfig(tenantId), asyncExecutor);
778+
}
779+
780+
/**
781+
* Updates the circuit breaker configuration for a tenant.
782+
*
783+
* <p>Example usage:
784+
* <pre>{@code
785+
* CircuitBreakerConfig updated = axonflow.updateCircuitBreakerConfig(
786+
* CircuitBreakerConfigUpdate.builder()
787+
* .tenantId("tenant_123")
788+
* .errorThreshold(10)
789+
* .violationThreshold(5)
790+
* .enableAutoRecovery(true)
791+
* .build());
792+
* }</pre>
793+
*
794+
* @param config the configuration update
795+
* @return confirmation with tenant_id and message
796+
* @throws NullPointerException if config is null
797+
* @throws AxonFlowException if the request fails
798+
*/
799+
public CircuitBreakerConfigUpdateResponse updateCircuitBreakerConfig(CircuitBreakerConfigUpdate config) {
800+
Objects.requireNonNull(config, "config cannot be null");
801+
802+
return retryExecutor.execute(() -> {
803+
Request httpRequest = buildOrchestratorRequest("PUT", "/api/v1/circuit-breaker/config", config);
804+
try (Response response = httpClient.newCall(httpRequest).execute()) {
805+
JsonNode node = parseResponseNode(response);
806+
if (node.has("data") && node.get("data").isObject()) {
807+
return objectMapper.treeToValue(node.get("data"), CircuitBreakerConfigUpdateResponse.class);
808+
}
809+
return objectMapper.treeToValue(node, CircuitBreakerConfigUpdateResponse.class);
810+
}
811+
}, "updateCircuitBreakerConfig");
812+
}
813+
814+
/**
815+
* Asynchronously updates the circuit breaker configuration for a tenant.
816+
*
817+
* @param config the configuration update
818+
* @return a future containing the update confirmation
819+
*/
820+
public CompletableFuture<CircuitBreakerConfigUpdateResponse> updateCircuitBreakerConfigAsync(CircuitBreakerConfigUpdate config) {
821+
return CompletableFuture.supplyAsync(() -> updateCircuitBreakerConfig(config), asyncExecutor);
822+
}
823+
652824
// ========================================================================
653825
// Proxy Mode - Query Execution
654826
// ========================================================================
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2025 AxonFlow
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.getaxonflow.sdk.types;
17+
18+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
21+
import java.util.Map;
22+
23+
/**
24+
* Circuit breaker configuration for a tenant.
25+
*/
26+
@JsonIgnoreProperties(ignoreUnknown = true)
27+
public final class CircuitBreakerConfig {
28+
29+
@JsonProperty("source")
30+
private final String source;
31+
32+
@JsonProperty("error_threshold")
33+
private final int errorThreshold;
34+
35+
@JsonProperty("violation_threshold")
36+
private final int violationThreshold;
37+
38+
@JsonProperty("window_seconds")
39+
private final int windowSeconds;
40+
41+
@JsonProperty("default_timeout_seconds")
42+
private final int defaultTimeoutSeconds;
43+
44+
@JsonProperty("max_timeout_seconds")
45+
private final int maxTimeoutSeconds;
46+
47+
@JsonProperty("enable_auto_recovery")
48+
private final boolean enableAutoRecovery;
49+
50+
@JsonProperty("tenant_id")
51+
private final String tenantId;
52+
53+
@JsonProperty("overrides")
54+
private final Map<String, Object> overrides;
55+
56+
public CircuitBreakerConfig(
57+
@JsonProperty("source") String source,
58+
@JsonProperty("error_threshold") int errorThreshold,
59+
@JsonProperty("violation_threshold") int violationThreshold,
60+
@JsonProperty("window_seconds") int windowSeconds,
61+
@JsonProperty("default_timeout_seconds") int defaultTimeoutSeconds,
62+
@JsonProperty("max_timeout_seconds") int maxTimeoutSeconds,
63+
@JsonProperty("enable_auto_recovery") boolean enableAutoRecovery,
64+
@JsonProperty("tenant_id") String tenantId,
65+
@JsonProperty("overrides") Map<String, Object> overrides) {
66+
this.source = source;
67+
this.errorThreshold = errorThreshold;
68+
this.violationThreshold = violationThreshold;
69+
this.windowSeconds = windowSeconds;
70+
this.defaultTimeoutSeconds = defaultTimeoutSeconds;
71+
this.maxTimeoutSeconds = maxTimeoutSeconds;
72+
this.enableAutoRecovery = enableAutoRecovery;
73+
this.tenantId = tenantId;
74+
this.overrides = overrides != null ? Map.copyOf(overrides) : null;
75+
}
76+
77+
public String getSource() { return source; }
78+
public int getErrorThreshold() { return errorThreshold; }
79+
public int getViolationThreshold() { return violationThreshold; }
80+
public int getWindowSeconds() { return windowSeconds; }
81+
public int getDefaultTimeoutSeconds() { return defaultTimeoutSeconds; }
82+
public int getMaxTimeoutSeconds() { return maxTimeoutSeconds; }
83+
public boolean isEnableAutoRecovery() { return enableAutoRecovery; }
84+
public String getTenantId() { return tenantId; }
85+
public Map<String, Object> getOverrides() { return overrides; }
86+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2025 AxonFlow
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.getaxonflow.sdk.types;
17+
18+
import com.fasterxml.jackson.annotation.JsonInclude;
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
21+
import java.util.Objects;
22+
23+
/**
24+
* Request to update circuit breaker configuration for a tenant.
25+
*
26+
* <p>Use the {@link Builder} to construct instances:
27+
* <pre>{@code
28+
* CircuitBreakerConfigUpdate update = CircuitBreakerConfigUpdate.builder()
29+
* .tenantId("tenant_123")
30+
* .errorThreshold(10)
31+
* .enableAutoRecovery(true)
32+
* .build();
33+
* }</pre>
34+
*/
35+
@JsonInclude(JsonInclude.Include.NON_NULL)
36+
public final class CircuitBreakerConfigUpdate {
37+
38+
@JsonProperty("tenant_id")
39+
private final String tenantId;
40+
41+
@JsonProperty("error_threshold")
42+
private final Integer errorThreshold;
43+
44+
@JsonProperty("violation_threshold")
45+
private final Integer violationThreshold;
46+
47+
@JsonProperty("window_seconds")
48+
private final Integer windowSeconds;
49+
50+
@JsonProperty("default_timeout_seconds")
51+
private final Integer defaultTimeoutSeconds;
52+
53+
@JsonProperty("max_timeout_seconds")
54+
private final Integer maxTimeoutSeconds;
55+
56+
@JsonProperty("enable_auto_recovery")
57+
private final Boolean enableAutoRecovery;
58+
59+
private CircuitBreakerConfigUpdate(Builder builder) {
60+
this.tenantId = Objects.requireNonNull(builder.tenantId, "tenantId cannot be null");
61+
if (this.tenantId.isEmpty()) {
62+
throw new IllegalArgumentException("tenantId cannot be empty");
63+
}
64+
this.errorThreshold = builder.errorThreshold;
65+
this.violationThreshold = builder.violationThreshold;
66+
this.windowSeconds = builder.windowSeconds;
67+
this.defaultTimeoutSeconds = builder.defaultTimeoutSeconds;
68+
this.maxTimeoutSeconds = builder.maxTimeoutSeconds;
69+
this.enableAutoRecovery = builder.enableAutoRecovery;
70+
}
71+
72+
/**
73+
* Creates a new builder for CircuitBreakerConfigUpdate.
74+
*
75+
* @return a new builder
76+
*/
77+
public static Builder builder() {
78+
return new Builder();
79+
}
80+
81+
public String getTenantId() { return tenantId; }
82+
public Integer getErrorThreshold() { return errorThreshold; }
83+
public Integer getViolationThreshold() { return violationThreshold; }
84+
public Integer getWindowSeconds() { return windowSeconds; }
85+
public Integer getDefaultTimeoutSeconds() { return defaultTimeoutSeconds; }
86+
public Integer getMaxTimeoutSeconds() { return maxTimeoutSeconds; }
87+
public Boolean getEnableAutoRecovery() { return enableAutoRecovery; }
88+
89+
/**
90+
* Builder for {@link CircuitBreakerConfigUpdate}.
91+
*/
92+
public static final class Builder {
93+
private String tenantId;
94+
private Integer errorThreshold;
95+
private Integer violationThreshold;
96+
private Integer windowSeconds;
97+
private Integer defaultTimeoutSeconds;
98+
private Integer maxTimeoutSeconds;
99+
private Boolean enableAutoRecovery;
100+
101+
public Builder tenantId(String tenantId) { this.tenantId = tenantId; return this; }
102+
public Builder errorThreshold(int errorThreshold) { this.errorThreshold = errorThreshold; return this; }
103+
public Builder violationThreshold(int violationThreshold) { this.violationThreshold = violationThreshold; return this; }
104+
public Builder windowSeconds(int windowSeconds) { this.windowSeconds = windowSeconds; return this; }
105+
public Builder defaultTimeoutSeconds(int defaultTimeoutSeconds) { this.defaultTimeoutSeconds = defaultTimeoutSeconds; return this; }
106+
public Builder maxTimeoutSeconds(int maxTimeoutSeconds) { this.maxTimeoutSeconds = maxTimeoutSeconds; return this; }
107+
public Builder enableAutoRecovery(boolean enableAutoRecovery) { this.enableAutoRecovery = enableAutoRecovery; return this; }
108+
109+
/**
110+
* Builds the CircuitBreakerConfigUpdate.
111+
*
112+
* @return the config update
113+
* @throws NullPointerException if tenantId is null
114+
* @throws IllegalArgumentException if tenantId is empty
115+
*/
116+
public CircuitBreakerConfigUpdate build() {
117+
return new CircuitBreakerConfigUpdate(this);
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)