|
140 | 140 | BudgetStatus, |
141 | 141 | CacheConfig, |
142 | 142 | CancelPlanResponse, |
| 143 | + CircuitBreakerConfig, |
| 144 | + CircuitBreakerConfigUpdate, |
| 145 | + CircuitBreakerHistoryEntry, |
| 146 | + CircuitBreakerHistoryResponse, |
| 147 | + CircuitBreakerStatusResponse, |
143 | 148 | ClientRequest, |
144 | 149 | ClientResponse, |
145 | 150 | ConnectorHealthStatus, |
@@ -1850,6 +1855,171 @@ async def audit_tool_call( |
1850 | 1855 | timestamp=response["timestamp"], |
1851 | 1856 | ) |
1852 | 1857 |
|
| 1858 | + # ========================================================================= |
| 1859 | + # Circuit Breaker Observability Methods |
| 1860 | + # ========================================================================= |
| 1861 | + |
| 1862 | + async def get_circuit_breaker_status(self) -> CircuitBreakerStatusResponse: |
| 1863 | + """Get all active circuit breaker circuits. |
| 1864 | +
|
| 1865 | + Returns the current state of all circuit breakers, including which |
| 1866 | + circuits are open (tripped) and whether any emergency stop is active. |
| 1867 | +
|
| 1868 | + Returns: |
| 1869 | + CircuitBreakerStatusResponse with active circuits and counts. |
| 1870 | +
|
| 1871 | + Raises: |
| 1872 | + AxonFlowError: If the request fails. |
| 1873 | +
|
| 1874 | + Example: |
| 1875 | + >>> status = await client.get_circuit_breaker_status() |
| 1876 | + >>> print(f"{status.count} active circuits") |
| 1877 | + >>> if status.emergency_stop_active: |
| 1878 | + ... print("Emergency stop is active!") |
| 1879 | + """ |
| 1880 | + if self._config.debug: |
| 1881 | + self._logger.debug("Getting circuit breaker status") |
| 1882 | + |
| 1883 | + response = await self._request("GET", "/api/v1/circuit-breaker/status") |
| 1884 | + data = response.get("data", response) |
| 1885 | + |
| 1886 | + return CircuitBreakerStatusResponse( |
| 1887 | + active_circuits=data.get("active_circuits") or [], |
| 1888 | + count=data.get("count", 0), |
| 1889 | + emergency_stop_active=data.get("emergency_stop_active", False), |
| 1890 | + ) |
| 1891 | + |
| 1892 | + async def get_circuit_breaker_history( |
| 1893 | + self, |
| 1894 | + limit: int | None = None, |
| 1895 | + ) -> CircuitBreakerHistoryResponse: |
| 1896 | + """Get circuit breaker history for audit trail. |
| 1897 | +
|
| 1898 | + Returns the history of circuit breaker state transitions, including |
| 1899 | + trips, resets, and auto-recovery events. |
| 1900 | +
|
| 1901 | + Args: |
| 1902 | + limit: Maximum number of history entries to return. |
| 1903 | +
|
| 1904 | + Returns: |
| 1905 | + CircuitBreakerHistoryResponse with history entries. |
| 1906 | +
|
| 1907 | + Raises: |
| 1908 | + AxonFlowError: If the request fails. |
| 1909 | +
|
| 1910 | + Example: |
| 1911 | + >>> history = await client.get_circuit_breaker_history(limit=50) |
| 1912 | + >>> for entry in history.history: |
| 1913 | + ... print(f"{entry.scope}/{entry.scope_id}: {entry.state}") |
| 1914 | + """ |
| 1915 | + if self._config.debug: |
| 1916 | + self._logger.debug( |
| 1917 | + "Getting circuit breaker history", |
| 1918 | + limit=limit, |
| 1919 | + ) |
| 1920 | + |
| 1921 | + path = "/api/v1/circuit-breaker/history" |
| 1922 | + if limit is not None: |
| 1923 | + path = f"{path}?limit={limit}" |
| 1924 | + |
| 1925 | + response = await self._request("GET", path) |
| 1926 | + data = response.get("data", response) |
| 1927 | + |
| 1928 | + history = [CircuitBreakerHistoryEntry(**entry) for entry in (data.get("history") or [])] |
| 1929 | + |
| 1930 | + return CircuitBreakerHistoryResponse( |
| 1931 | + history=history, |
| 1932 | + count=data.get("count", 0), |
| 1933 | + ) |
| 1934 | + |
| 1935 | + async def get_circuit_breaker_config( |
| 1936 | + self, |
| 1937 | + tenant_id: str | None = None, |
| 1938 | + ) -> CircuitBreakerConfig: |
| 1939 | + """Get circuit breaker configuration (global or tenant-specific). |
| 1940 | +
|
| 1941 | + Args: |
| 1942 | + tenant_id: If provided, returns tenant-specific config with |
| 1943 | + any overrides applied. Otherwise returns global defaults. |
| 1944 | +
|
| 1945 | + Returns: |
| 1946 | + CircuitBreakerConfig with thresholds and recovery settings. |
| 1947 | +
|
| 1948 | + Raises: |
| 1949 | + AxonFlowError: If the request fails. |
| 1950 | +
|
| 1951 | + Example: |
| 1952 | + >>> config = await client.get_circuit_breaker_config() |
| 1953 | + >>> print(f"Error threshold: {config.error_threshold}") |
| 1954 | + >>> tenant_config = await client.get_circuit_breaker_config( |
| 1955 | + ... tenant_id="tenant-123" |
| 1956 | + ... ) |
| 1957 | + """ |
| 1958 | + if self._config.debug: |
| 1959 | + self._logger.debug( |
| 1960 | + "Getting circuit breaker config", |
| 1961 | + tenant_id=tenant_id, |
| 1962 | + ) |
| 1963 | + |
| 1964 | + path = "/api/v1/circuit-breaker/config" |
| 1965 | + if tenant_id is not None: |
| 1966 | + path = f"{path}?tenant_id={tenant_id}" |
| 1967 | + |
| 1968 | + response = await self._request("GET", path) |
| 1969 | + data = response.get("data", response) |
| 1970 | + |
| 1971 | + return CircuitBreakerConfig(**data) |
| 1972 | + |
| 1973 | + async def update_circuit_breaker_config( |
| 1974 | + self, |
| 1975 | + config: CircuitBreakerConfigUpdate, |
| 1976 | + ) -> dict[str, Any]: |
| 1977 | + """Update per-tenant circuit breaker configuration. |
| 1978 | +
|
| 1979 | + Sets tenant-specific overrides for circuit breaker thresholds and |
| 1980 | + recovery behavior. |
| 1981 | +
|
| 1982 | + Args: |
| 1983 | + config: Configuration update with tenant_id and override values. |
| 1984 | +
|
| 1985 | + Returns: |
| 1986 | + Server response confirming the update. |
| 1987 | +
|
| 1988 | + Raises: |
| 1989 | + ValueError: If tenant_id is empty. |
| 1990 | + AxonFlowError: If the request fails. |
| 1991 | +
|
| 1992 | + Example: |
| 1993 | + >>> from axonflow.types import CircuitBreakerConfigUpdate |
| 1994 | + >>> result = await client.update_circuit_breaker_config( |
| 1995 | + ... CircuitBreakerConfigUpdate( |
| 1996 | + ... tenant_id="tenant-123", |
| 1997 | + ... error_threshold=10, |
| 1998 | + ... violation_threshold=5, |
| 1999 | + ... ) |
| 2000 | + ... ) |
| 2001 | + """ |
| 2002 | + if not config.tenant_id or not config.tenant_id.strip(): |
| 2003 | + msg = "tenant_id is required and cannot be empty" |
| 2004 | + raise ValueError(msg) |
| 2005 | + |
| 2006 | + if self._config.debug: |
| 2007 | + self._logger.debug( |
| 2008 | + "Updating circuit breaker config", |
| 2009 | + tenant_id=config.tenant_id, |
| 2010 | + ) |
| 2011 | + |
| 2012 | + request_body = config.model_dump(by_alias=True, exclude_none=True) |
| 2013 | + |
| 2014 | + response = await self._request( |
| 2015 | + "PUT", |
| 2016 | + "/api/v1/circuit-breaker/config", |
| 2017 | + json_data=request_body, |
| 2018 | + ) |
| 2019 | + |
| 2020 | + result: dict[str, Any] = response.get("data", response) |
| 2021 | + return result |
| 2022 | + |
1853 | 2023 | # ========================================================================= |
1854 | 2024 | # Audit Log Read Methods |
1855 | 2025 | # ========================================================================= |
@@ -6271,6 +6441,33 @@ def audit_tool_call( |
6271 | 6441 | """Record a non-LLM tool call in the audit trail.""" |
6272 | 6442 | return self._run_sync(self._async_client.audit_tool_call(request)) |
6273 | 6443 |
|
| 6444 | + # Circuit Breaker Observability sync wrappers |
| 6445 | + |
| 6446 | + def get_circuit_breaker_status(self) -> CircuitBreakerStatusResponse: |
| 6447 | + """Get all active circuit breaker circuits.""" |
| 6448 | + return self._run_sync(self._async_client.get_circuit_breaker_status()) |
| 6449 | + |
| 6450 | + def get_circuit_breaker_history( |
| 6451 | + self, |
| 6452 | + limit: int | None = None, |
| 6453 | + ) -> CircuitBreakerHistoryResponse: |
| 6454 | + """Get circuit breaker history for audit trail.""" |
| 6455 | + return self._run_sync(self._async_client.get_circuit_breaker_history(limit=limit)) |
| 6456 | + |
| 6457 | + def get_circuit_breaker_config( |
| 6458 | + self, |
| 6459 | + tenant_id: str | None = None, |
| 6460 | + ) -> CircuitBreakerConfig: |
| 6461 | + """Get circuit breaker config (global or tenant-specific).""" |
| 6462 | + return self._run_sync(self._async_client.get_circuit_breaker_config(tenant_id=tenant_id)) |
| 6463 | + |
| 6464 | + def update_circuit_breaker_config( |
| 6465 | + self, |
| 6466 | + config: CircuitBreakerConfigUpdate, |
| 6467 | + ) -> dict[str, Any]: |
| 6468 | + """Update per-tenant circuit breaker config.""" |
| 6469 | + return self._run_sync(self._async_client.update_circuit_breaker_config(config)) |
| 6470 | + |
6274 | 6471 | # Policy CRUD sync wrappers |
6275 | 6472 |
|
6276 | 6473 | def list_static_policies( |
|
0 commit comments