Browse Source

Add `script` information to the cluster info endpoint (#96613)

Add a new target (`script`) to the `/_info` API. It consolidates all the script information from the cluster nodes and returns a summary at the cluster level (compared with `_nodes/stats/script` it lacks the `<node>` dimension).
Pablo Alcantar Morales 2 years ago
parent
commit
5a623ee3bf

+ 6 - 0
docs/changelog/96613.yaml

@@ -0,0 +1,6 @@
+pr: 96613
+summary: Add `script` information to the cluster info endpoint
+area: Stats
+type: enhancement
+issues:
+ - 95394

+ 65 - 0
docs/reference/cluster/cluster-info.asciidoc

@@ -44,6 +44,9 @@ Ingest information.
 
 `thread_pool`::
 Statistics about each thread pool, including current size, queue size and rejected tasks.
+
+`script`::
+Contains script statistics of the cluster.
 --
 
 [role="child_attributes"]
@@ -282,6 +285,65 @@ Number of tasks completed by the thread pool executor.
 =======
 ======
 
+[[cluster-info-api-response-body-script]]
+`script`::
+(object)
+Contains script statistics of the cluster.
++
+.Properties of `script`
+[%collapsible%open]
+======
+`compilations`::
+(integer)
+Total number of inline script compilations performed by the cluster.
+
+`compilations_history`::
+(object)
+Contains the recent history of script compilations.
+
+.Properties of `compilations_history`
+[%collapsible%open]
+=======
+`5m`::
+(long)
+The number of script compilations in the last five minutes.
+`15m`::
+(long)
+The number of script compilations in the last fifteen minutes.
+`24h`::
+(long)
+The number of script compilations in the last twenty-four hours.
+=======
+
+`cache_evictions`::
+(integer)
+Total number of times the script cache has evicted old data.
+
+
+`cache_evictions_history`::
+(object)
+Contains the recent history of script cache evictions.
+
+.Properties of `cache_evictions`
+[%collapsible%open]
+=======
+`5m`::
+(long)
+The number of script cache evictions in the last five minutes.
+`15m`::
+(long)
+The number of script cache evictions in the last fifteen minutes.
+`24h`::
+(long)
+The number of script cache evictions in the last twenty-four hours.
+=======
+
+`compilation_limit_triggered`::
+(integer)
+Total number of times the <<script-compilation-circuit-breaker,script
+compilation>> circuit breaker has limited inline script compilations.
+======
+
 [[cluster-info-api-example]]
 ==== {api-examples-title}
 
@@ -299,6 +361,9 @@ GET /_info/ingest
 # returns the thread_pool info of the cluster
 GET /_info/thread_pool
 
+# returns the script info of the cluster
+GET /_info/script
+
 # returns the http and ingest info of the cluster
 GET /_info/http,ingest
 ----

+ 2 - 1
rest-api-spec/src/main/resources/rest-api-spec/api/cluster.info.json

@@ -23,7 +23,8 @@
                               "_all",
                               "http",
                               "ingest",
-                              "thread_pool"
+                              "thread_pool",
+                              "script"
                           ],
                           "description":"Limit the information returned to the specified target."
                       }

+ 1 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.info/10_info_all.yml

@@ -15,6 +15,7 @@ setup:
   - is_true: http
   - is_true: ingest
   - is_true: thread_pool
+  - is_true: script
 
 ---
 "Cluster Info fails when mixing _all with other targets":

+ 16 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.info/40_info_script.yml

@@ -0,0 +1,16 @@
+---
+"Cluster HTTP Info":
+  - skip:
+      version: " - 8.8.99"
+      reason: "/_info/script only available from v8.9"
+
+  - do:
+      cluster.info:
+        target: [ script ]
+
+  - is_true: cluster_name
+  - is_true: script
+
+  - gte: { script.compilations: 0 }
+  - gte: { script.cache_evictions: 0 }
+  - gte: { script.compilation_limit_triggered: 0 }

+ 12 - 4
server/src/main/java/org/elasticsearch/rest/action/info/RestClusterInfoAction.java

@@ -27,6 +27,7 @@ import org.elasticsearch.rest.Scope;
 import org.elasticsearch.rest.ServerlessScope;
 import org.elasticsearch.rest.action.RestCancellableNodeClient;
 import org.elasticsearch.rest.action.RestResponseListener;
+import org.elasticsearch.script.ScriptStats;
 import org.elasticsearch.threadpool.ThreadPoolStats;
 
 import java.io.IOException;
@@ -39,9 +40,10 @@ import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
-import static org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest.Metric.HTTP;
-import static org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest.Metric.INGEST;
-import static org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest.Metric.THREAD_POOL;
+import static org.elasticsearch.action.admin.cluster.node.stats.NodesStatsRequest.Metric.HTTP;
+import static org.elasticsearch.action.admin.cluster.node.stats.NodesStatsRequest.Metric.INGEST;
+import static org.elasticsearch.action.admin.cluster.node.stats.NodesStatsRequest.Metric.SCRIPT;
+import static org.elasticsearch.action.admin.cluster.node.stats.NodesStatsRequest.Metric.THREAD_POOL;
 import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS;
 
 @ServerlessScope(Scope.PUBLIC)
@@ -61,7 +63,13 @@ public class RestClusterInfoAction extends BaseRestHandler {
         nodesStatsResponse -> nodesStatsResponse.getNodes()
             .stream()
             .map(NodeStats::getThreadPool)
-            .reduce(ThreadPoolStats.IDENTITY, ThreadPoolStats::merge)
+            .reduce(ThreadPoolStats.IDENTITY, ThreadPoolStats::merge),
+        //
+        SCRIPT.metricName(),
+        nodesStatsResponse -> nodesStatsResponse.getNodes()
+            .stream()
+            .map(NodeStats::getScriptStats)
+            .reduce(ScriptStats.IDENTITY, ScriptStats::merge)
     );
     static final Set<String> AVAILABLE_TARGETS = RESPONSE_MAPPER.keySet();
 

+ 12 - 0
server/src/main/java/org/elasticsearch/script/ScriptContextStats.java

@@ -80,6 +80,18 @@ public record ScriptContextStats(
         );
     }
 
+    public static ScriptContextStats merge(ScriptContextStats first, ScriptContextStats second) {
+        assert first.context.equals(second.context) : "To merge 2 ScriptContextStats both of them must have the same context.";
+        return new ScriptContextStats(
+            first.context,
+            first.compilations + second.compilations,
+            TimeSeries.merge(first.compilationsHistory, second.compilationsHistory),
+            first.cacheEvictions + second.cacheEvictions,
+            TimeSeries.merge(first.cacheEvictionsHistory, second.cacheEvictionsHistory),
+            first.compilationLimitTriggered + second.compilationLimitTriggered
+        );
+    }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         out.writeString(context);

+ 26 - 0
server/src/main/java/org/elasticsearch/script/ScriptStats.java

@@ -19,6 +19,8 @@ import org.elasticsearch.common.xcontent.ChunkedToXContentHelper;
 import org.elasticsearch.xcontent.ToXContent;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -51,6 +53,8 @@ public record ScriptStats(
     TimeSeries cacheEvictionsHistory
 ) implements Writeable, ChunkedToXContent {
 
+    public static final ScriptStats IDENTITY = new ScriptStats(0, 0, 0, new TimeSeries(0), new TimeSeries(0));
+
     public ScriptStats(
         long compilations,
         long cacheEvictions,
@@ -68,6 +72,28 @@ public record ScriptStats(
         );
     }
 
+    public static ScriptStats merge(ScriptStats first, ScriptStats second) {
+        var mergedScriptContextStats = List.<ScriptContextStats>of();
+
+        if (first.contextStats.isEmpty() == false || second.contextStats.isEmpty() == false) {
+            var mapToCollectMergedStats = new HashMap<String, ScriptContextStats>();
+
+            first.contextStats.forEach(cs -> mapToCollectMergedStats.merge(cs.context(), cs, ScriptContextStats::merge));
+            second.contextStats.forEach(cs -> mapToCollectMergedStats.merge(cs.context(), cs, ScriptContextStats::merge));
+
+            mergedScriptContextStats = new ArrayList<>(mapToCollectMergedStats.values());
+        }
+
+        return new ScriptStats(
+            mergedScriptContextStats,
+            first.compilations + second.compilations,
+            first.cacheEvictions + second.cacheEvictions,
+            first.compilationLimitTriggered + second.compilationLimitTriggered,
+            TimeSeries.merge(first.compilationsHistory, second.compilationsHistory),
+            TimeSeries.merge(first.cacheEvictionsHistory, second.cacheEvictionsHistory)
+        );
+    }
+
     public static ScriptStats read(List<ScriptContextStats> contextStats) {
         long compilations = 0;
         long cacheEvictions = 0;

+ 9 - 0
server/src/main/java/org/elasticsearch/script/TimeSeries.java

@@ -45,6 +45,15 @@ public class TimeSeries implements Writeable, ToXContentFragment {
         return new TimeSeries(fiveMinutes, fifteenMinutes, twentyFourHours, total);
     }
 
+    public static TimeSeries merge(TimeSeries first, TimeSeries second) {
+        return new TimeSeries(
+            first.fiveMinutes + second.fiveMinutes,
+            first.fifteenMinutes + second.fifteenMinutes,
+            first.twentyFourHours + second.twentyFourHours,
+            first.total + second.total
+        );
+    }
+
     public TimeSeries(StreamInput in) throws IOException {
         fiveMinutes = in.readVLong();
         fifteenMinutes = in.readVLong();

+ 58 - 0
server/src/test/java/org/elasticsearch/script/ScriptContextStatsTests.java

@@ -0,0 +1,58 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script;
+
+import org.elasticsearch.test.ESTestCase;
+
+import static org.elasticsearch.script.TimeSeriesTests.randomTimeseries;
+
+public class ScriptContextStatsTests extends ESTestCase {
+    public void testMerge() {
+        {
+            var first = randomScriptContextStats();
+            var second = randomScriptContextStats();
+
+            var e = expectThrows(AssertionError.class, () -> ScriptContextStats.merge(first, second));
+            assertEquals(e.getMessage(), "To merge 2 ScriptContextStats both of them must have the same context.");
+        }
+        {
+            var context = randomAlphaOfLength(30);
+            var first = randomScriptContextStats(context);
+            var second = randomScriptContextStats(context);
+
+            assertEquals(
+                ScriptContextStats.merge(first, second),
+                new ScriptContextStats(
+                    context,
+                    first.compilations() + second.compilations(),
+                    TimeSeries.merge(first.compilationsHistory(), second.compilationsHistory()),
+                    first.cacheEvictions() + second.cacheEvictions(),
+                    TimeSeries.merge(first.cacheEvictionsHistory(), second.cacheEvictionsHistory()),
+                    first.compilationLimitTriggered() + second.compilationLimitTriggered()
+                )
+            );
+        }
+    }
+
+    public static ScriptContextStats randomScriptContextStats() {
+        return randomScriptContextStats(randomAlphaOfLength(30));
+    }
+
+    public static ScriptContextStats randomScriptContextStats(String contextName) {
+        return new ScriptContextStats(
+            contextName,
+            randomLongBetween(0, 10000),
+            randomTimeseries(),
+            randomLongBetween(0, 10000),
+            randomTimeseries(),
+            randomLongBetween(0, 10000)
+        );
+    }
+
+}

+ 34 - 0
server/src/test/java/org/elasticsearch/script/ScriptStatsTests.java

@@ -22,6 +22,8 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Function;
 
+import static org.elasticsearch.script.ScriptContextStatsTests.randomScriptContextStats;
+import static org.elasticsearch.script.TimeSeriesTests.randomTimeseries;
 import static org.hamcrest.Matchers.equalTo;
 
 public class ScriptStatsTests extends ESTestCase {
@@ -166,6 +168,38 @@ public class ScriptStatsTests extends ESTestCase {
         assertEquals(stats, deserStats);
     }
 
+    public void testMerge() {
+        var first = randomScriptStats();
+        var second = randomScriptStats();
+
+        assertEquals(
+            ScriptStats.merge(first, second),
+            new ScriptStats(
+                List.of(
+                    ScriptContextStats.merge(first.contextStats().get(0), second.contextStats().get(0)),
+                    ScriptContextStats.merge(first.contextStats().get(1), second.contextStats().get(1)),
+                    ScriptContextStats.merge(first.contextStats().get(2), second.contextStats().get(2))
+                ),
+                first.compilations() + second.compilations(),
+                first.cacheEvictions() + second.cacheEvictions(),
+                first.compilationLimitTriggered() + second.compilationLimitTriggered(),
+                TimeSeries.merge(first.compilationsHistory(), second.compilationsHistory()),
+                TimeSeries.merge(first.cacheEvictionsHistory(), second.cacheEvictionsHistory())
+            )
+        );
+    }
+
+    public static ScriptStats randomScriptStats() {
+        return new ScriptStats(
+            List.of(randomScriptContextStats("context-a"), randomScriptContextStats("context-b"), randomScriptContextStats("context-c")),
+            randomLongBetween(0, 10000),
+            randomLongBetween(0, 10000),
+            randomLongBetween(0, 10000),
+            randomTimeseries(),
+            randomTimeseries()
+        );
+    }
+
     public ScriptContextStats serDeser(TransportVersion outVersion, TransportVersion inVersion, ScriptContextStats stats)
         throws IOException {
         try (BytesStreamOutput out = new BytesStreamOutput()) {

+ 37 - 0
server/src/test/java/org/elasticsearch/script/TimeSeriesTests.java

@@ -0,0 +1,37 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script;
+
+import org.elasticsearch.test.ESTestCase;
+
+public class TimeSeriesTests extends ESTestCase {
+    public void testMerge() {
+        var first = randomTimeseries();
+        var second = randomTimeseries();
+
+        assertEquals(
+            TimeSeries.merge(first, second),
+            new TimeSeries(
+                first.fiveMinutes + second.fiveMinutes,
+                first.fifteenMinutes + second.fifteenMinutes,
+                first.twentyFourHours + second.twentyFourHours,
+                first.total + second.total
+            )
+        );
+    }
+
+    static TimeSeries randomTimeseries() {
+        return new TimeSeries(
+            randomLongBetween(0, 10000),
+            randomLongBetween(0, 10000),
+            randomLongBetween(0, 10000),
+            randomLongBetween(0, 10000)
+        );
+    }
+}