Skip to content
5 changes: 5 additions & 0 deletions docs/changelog/136890.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 136890
summary: "Cat API: added endpoint for Circuit Breakers"
area: Infra/REST API
type: enhancement
issues: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"cat.circuit_breaker": {
"documentation": {
"url": "https://www.elastic.co/docs/api/doc/elasticsearch#TODO",
"description": "Get circuit breakers statistics"
},
"stability": "stable",
"visibility": "public",
"headers": {
"accept": [
"text/plain",
"application/json"
]
},
"url": {
"paths": [
{
"path": "/_cat/circuit_breaker",
"methods": [
"GET"
]
},
{
"path": "/_cat/circuit_breaker/{circuit_breaker_patterns}",
"methods": [
"GET"
],
"parts": {
"circuit_breaker_patterns": {
"type": "list",
"description": "A comma-separated list of regular-expressions to filter the circuit breakers in the output"
}
}
}
]
},
"params": {
"format": {
"type": "string",
"default": "text",
"description": "a short version of the Accept header, e.g. json, yaml"
},
"time": {
"type": "enum",
"description": "The unit in which to display time values",
"options": [
"d",
"h",
"m",
"s",
"ms",
"micros",
"nanos"
]
},
"local": {
"type": "boolean",
"default": false,
"description": "Return local information, do not retrieve the state from master node (default: false)"
},
"master_timeout": {
"type": "time",
"default": "30s",
"description": "Explicit operation timeout for connection to master node"
},
"h": {
"type": "list",
"description": "Comma-separated list of column names to display"
},
"help": {
"type": "boolean",
"description": "Return help information",
"default": false
},
"s": {
"type": "list",
"description": "Comma-separated list of column names or column aliases to sort by"
},
"v": {
"type": "boolean",
"description": "Verbose mode. Display column headers",
"default": false
},
"bytes": {
"type": "enum",
"description": "The unit in which to display byte values",
"options": [
"b",
"kb",
"mb",
"gb",
"tb",
"pb"
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
"Test cat circuit_breaker help":
- requires:
capabilities:
- method: GET
path: /_cat/circuit_breaker
capabilities: [ cat_circuit_breaker ]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
capabilities: [ cat_circuit_breaker ]

As mentioned above, defining and checking for a capability shouldn't be needed here. Existence should do

test_runner_features: capabilities
reason: Capability required to run test
- do:
cat.circuit_breaker:
help: true

- match:
$body: |
/^ node_id .+ \n
node_name .+ \n
breaker .+ \n
limit .+ \n
limit_bytes .+ \n
estimated .+ \n
estimated_bytes .+ \n
tripped .+ \n
overhead .+ \n $/

---
"Test cat circuit_breaker output":
- requires:
capabilities:
- method: GET
path: /_cat/circuit_breaker
capabilities: [ cat_circuit_breaker ]
test_runner_features: capabilities
reason: Capability required to run test
- do:
cat.circuit_breaker: {}

- match:
$body: |
/ #node_id breaker \s+ limit \s+ estimated \s+ tripped \n
^ (\S+ \s+ \S+ \s+ \S+ \s+ \S+ \s+ \d+ \n)+ $/

- do:
cat.circuit_breaker:
v: true

- match:
$body: |
/^ node_id \s+ breaker \s+ limit \s+ estimated \s+ tripped \n
(\S+ \s+ \S+ \s+ \S+ \s+ \S+ \s+ \d+ \n)+ $/

- do:
cat.circuit_breaker:
circuit_breaker_patterns: request,fielddata
h: node_id,breaker,limit,estimated,tripped
v: true

- match:
$body: |
/^ node_id \s+ breaker \s+ limit \s+ estimated \s+ tripped \n
(\S+ \s+ (request|fielddata) \s+ \S+ \s+ \S+ \s+ \d+ \n){2,} $/

- do:
cat.circuit_breaker:
circuit_breaker_patterns: request
h: node_id,breaker,limit,limit_bytes,estimated,estimated_bytes,tripped,overhead
v: true

- match:
$body: |
/^ node_id \s+ breaker \s+ limit \s+ limit_bytes \s+ estimated \s+ estimated_bytes \s+ tripped \s+ overhead \n
(\S+ \s+ request \s+ \S+ \s+ \d+ \s+ \S+ \s+ \d+ \s+ \d+ \s+ \d+\.\d+ \n)+ $/

- do:
cat.circuit_breaker:
circuit_breaker_patterns: "*"
h: node_id,breaker,tripped

- match:
$body: |
/ #node_id breaker \s+ tripped \n
^ (\S+ \s+ \S+ \s+ \d+ \n)+ $/
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@
import org.elasticsearch.rest.action.cat.RestAliasAction;
import org.elasticsearch.rest.action.cat.RestAllocationAction;
import org.elasticsearch.rest.action.cat.RestCatAction;
import org.elasticsearch.rest.action.cat.RestCatCircuitBreakerAction;
import org.elasticsearch.rest.action.cat.RestCatComponentTemplateAction;
import org.elasticsearch.rest.action.cat.RestCatRecoveryAction;
import org.elasticsearch.rest.action.cat.RestFielddataAction;
Expand Down Expand Up @@ -1028,6 +1029,7 @@ public void initRestHandlers(Supplier<DiscoveryNodes> nodesInCluster, Predicate<
registerHandler.accept(new org.elasticsearch.rest.action.cat.RestPendingClusterTasksAction());
registerHandler.accept(new RestAliasAction());
registerHandler.accept(new RestThreadPoolAction());
registerHandler.accept(new RestCatCircuitBreakerAction());
registerHandler.accept(new RestPluginsAction());
registerHandler.accept(new RestFielddataAction());
registerHandler.accept(new RestNodeAttrsAction());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.rest.action.cat;

import org.elasticsearch.action.FailedNodeException;
import org.elasticsearch.action.admin.cluster.node.stats.NodeStats;
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsRequest;
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsRequestParameters;
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse;
import org.elasticsearch.client.internal.node.NodeClient;
import org.elasticsearch.common.Table;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.indices.breaker.CircuitBreakerStats;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.Scope;
import org.elasticsearch.rest.ServerlessScope;
import org.elasticsearch.rest.action.RestResponseListener;

import java.util.List;
import java.util.Set;

import static org.elasticsearch.common.util.set.Sets.addToCopy;
import static org.elasticsearch.rest.RestRequest.Method.GET;

@ServerlessScope(Scope.INTERNAL)
public class RestCatCircuitBreakerAction extends AbstractCatAction {

private static final Set<String> RESPONSE_PARAMS = addToCopy(AbstractCatAction.RESPONSE_PARAMS, "circuit_breaker_patterns");

@Override
public String getName() {
return "cat_circuitbreaker_action";
}

@Override
public List<Route> routes() {
return List.of(new Route(GET, "/_cat/circuit_breaker"), new Route(GET, "/_cat/circuit_breaker/{circuit_breaker_patterns}"));
}

@Override
protected void documentation(StringBuilder sb) {
sb.append("/_cat/circuit_breaker\n");
sb.append("/_cat/circuit_breaker/{circuit_breaker_patterns}\n");
}

@Override
protected Set<String> responseParams() {
return RESPONSE_PARAMS;
}

@Override
public Set<String> supportedCapabilities() {
return Sets.union(Set.of("cat_circuit_breaker"), super.supportedCapabilities());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be necessary, the pure existence of the REST endpoint should be enough

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know how I can verify it?
I saw other tests added this capability filter, like cat_circuit_breaker in my case, but where does that get defined as a capability, if not in that method? ( I trust you, I am just curious :D )

}

@Override
protected RestChannelConsumer doCatRequest(RestRequest request, NodeClient client) {
NodesStatsRequest nodesStatsRequest = new NodesStatsRequest(); // Empty nodes array sends request to all nodes.
nodesStatsRequest.clear().addMetric(NodesStatsRequestParameters.Metric.BREAKER);

return channel -> client.admin().cluster().nodesStats(nodesStatsRequest, new RestResponseListener<>(channel) {
@Override
public RestResponse buildResponse(final NodesStatsResponse nodesStatsResponse) throws Exception {
RestResponse response = RestTable.buildResponse(buildTable(request, nodesStatsResponse), channel);
if (nodesStatsResponse.failures().isEmpty() == false) {
response.addHeader("Warning", "Partial success, missing info from " + nodesStatsResponse.failures().size() + " nodes.");
}
return response;
}
});
}

@Override
protected Table getTableWithHeader(RestRequest request) {
Table table = new Table();
table.startHeaders();
table.addCell("node_id", "default:true;alias:id;desc:persistent node id");
table.addCell("node_name", "default:false;alias:nn;desc:node name");
table.addCell("breaker", "default:true;alias:br;desc:breaker name");
table.addCell("limit", "default:true;alias:l;desc:limit size");
table.addCell("limit_bytes", "default:false;alias:lb;desc:limit size in bytes");
table.addCell("estimated", "default:true;alias:e;desc:estimated size");
table.addCell("estimated_bytes", "default:false;alias:eb;desc:estimated size in bytes");
table.addCell("tripped", "default:true;alias:t;desc:tripped count");
table.addCell("overhead", "default:false;alias:o;desc:overhead");
table.endHeaders();
return table;
}

private Table buildTable(RestRequest request, NodesStatsResponse nodesStatsResponse) {
final Table table = getTableWithHeader(request);
final String[] circuitBreakers = request.paramAsStringArray("circuit_breaker_patterns", new String[] { "*" });

for (final NodeStats nodeStats : nodesStatsResponse.getNodes()) {
if (nodeStats.getBreaker() == null) {
continue;
}
for (final CircuitBreakerStats circuitBreakerStats : nodeStats.getBreaker().getAllStats()) {
if (Regex.simpleMatch(circuitBreakers, circuitBreakerStats.getName()) == false) {
continue;
}
table.startRow();
table.addCell(nodeStats.getNode().getId());
table.addCell(nodeStats.getNode().getName());
table.addCell(circuitBreakerStats.getName());
table.addCell(ByteSizeValue.ofBytes(circuitBreakerStats.getLimit()));
table.addCell(circuitBreakerStats.getLimit());
table.addCell(ByteSizeValue.ofBytes(circuitBreakerStats.getEstimated()));
table.addCell(circuitBreakerStats.getEstimated());
table.addCell(circuitBreakerStats.getTrippedCount());
table.addCell(circuitBreakerStats.getOverhead());
table.endRow();
}
}

for (final FailedNodeException errors : nodesStatsResponse.failures()) {
table.startRow();
table.addCell(errors.nodeId());
table.addCell("N/A");
table.addCell(errors.getMessage());
table.addCell("N/A");
table.addCell("N/A");
table.addCell("N/A");
table.addCell("N/A");
table.addCell("N/A");
table.addCell("N/A");
table.endRow();
}

return table;
}
}
Loading