فهرست منبع

ES|QL: add metrics for functions (#114620) (#115191)

Luigi Dell'Aquila 1 سال پیش
والد
کامیت
b0b9968490

+ 5 - 0
docs/changelog/114620.yaml

@@ -0,0 +1,5 @@
+pr: 114620
+summary: "ES|QL: add metrics for functions"
+area: ES|QL
+type: enhancement
+issues: []

+ 2 - 1
docs/reference/rest-api/usage.asciidoc

@@ -38,9 +38,10 @@ include::{es-ref-dir}/rest-api/common-parms.asciidoc[tag=master-timeout]
 ------------------------------------------------------------
 GET /_xpack/usage
 ------------------------------------------------------------
-// TEST[s/usage/usage?filter_path=-watcher.execution.actions.index*\,-watcher.execution.actions.logging*,-watcher.execution.actions.email*/]
+// TEST[s/usage/usage?filter_path=-watcher.execution.actions.index*\,-watcher.execution.actions.logging*,-watcher.execution.actions.email*,-esql.functions*/]
 // This response filter removes watcher logging results if they are included
 // to avoid errors in the CI builds.
+// Same for ES|QL functions, that is a long list and quickly evolving.
 
 [source,console-result]
 ------------------------------------------------------------

+ 2 - 1
x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java

@@ -46,6 +46,7 @@ import org.elasticsearch.xpack.esql.core.type.DataType;
 import org.elasticsearch.xpack.esql.core.type.EsField;
 import org.elasticsearch.xpack.esql.core.util.DateUtils;
 import org.elasticsearch.xpack.esql.core.util.StringUtils;
+import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.RLike;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.WildcardLike;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals;
@@ -260,7 +261,7 @@ public final class EsqlTestUtils {
 
     public static final Configuration TEST_CFG = configuration(new QueryPragmas(Settings.EMPTY));
 
-    public static final Verifier TEST_VERIFIER = new Verifier(new Metrics());
+    public static final Verifier TEST_VERIFIER = new Verifier(new Metrics(new EsqlFunctionRegistry()));
 
     private EsqlTestUtils() {}
 

+ 6 - 1
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

@@ -390,7 +390,12 @@ public class EsqlCapabilities {
         /**
          * Fix for https://github.com/elastic/elasticsearch/issues/114714
          */
-        FIX_STATS_BY_FOLDABLE_EXPRESSION;
+        FIX_STATS_BY_FOLDABLE_EXPRESSION,
+
+        /**
+         * Adding stats for functions (stack telemetry)
+         */
+        FUNCTION_STATS;
 
         private final boolean snapshotOnly;
         private final FeatureFlag featureFlag;

+ 4 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java

@@ -58,6 +58,7 @@ import org.elasticsearch.xpack.esql.stats.Metrics;
 import java.util.ArrayList;
 import java.util.BitSet;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
@@ -480,6 +481,9 @@ public class Verifier {
         for (int i = b.nextSetBit(0); i >= 0; i = b.nextSetBit(i + 1)) {
             metrics.inc(FeatureMetric.values()[i]);
         }
+        Set<Class<?>> functions = new HashSet<>();
+        plan.forEachExpressionDown(Function.class, p -> functions.add(p.getClass()));
+        functions.forEach(f -> metrics.incFunctionMetric(f));
     }
 
     /**

+ 1 - 1
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/execution/PlanExecutor.java

@@ -48,7 +48,7 @@ public class PlanExecutor {
         this.preAnalyzer = new PreAnalyzer();
         this.functionRegistry = new EsqlFunctionRegistry();
         this.mapper = new Mapper(functionRegistry);
-        this.metrics = new Metrics();
+        this.metrics = new Metrics(functionRegistry);
         this.verifier = new Verifier(metrics);
         this.planningMetricsManager = new PlanningMetricsManager(meterRegistry);
     }

+ 43 - 1
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/Metrics.java

@@ -10,8 +10,11 @@ package org.elasticsearch.xpack.esql.stats;
 import org.elasticsearch.common.metrics.CounterMetric;
 import org.elasticsearch.common.util.Maps;
 import org.elasticsearch.xpack.core.watcher.common.stats.Counters;
+import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
+import org.elasticsearch.xpack.esql.expression.function.FunctionDefinition;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Locale;
 import java.util.Map;
@@ -36,10 +39,17 @@ public class Metrics {
     private final Map<QueryMetric, Map<OperationType, CounterMetric>> opsByTypeMetrics;
     // map that holds one counter per esql query "feature" (eval, sort, limit, where....)
     private final Map<FeatureMetric, CounterMetric> featuresMetrics;
+    private final Map<String, CounterMetric> functionMetrics;
     protected static String QPREFIX = "queries.";
     protected static String FPREFIX = "features.";
+    protected static String FUNC_PREFIX = "functions.";
 
-    public Metrics() {
+    private final EsqlFunctionRegistry functionRegistry;
+    private final Map<Class<?>, String> classToFunctionName;
+
+    public Metrics(EsqlFunctionRegistry functionRegistry) {
+        this.functionRegistry = functionRegistry.snapshotRegistry();
+        this.classToFunctionName = initClassToFunctionType();
         Map<QueryMetric, Map<OperationType, CounterMetric>> qMap = new LinkedHashMap<>();
         for (QueryMetric metric : QueryMetric.values()) {
             Map<OperationType, CounterMetric> metricsMap = Maps.newLinkedHashMapWithExpectedSize(OperationType.values().length);
@@ -56,6 +66,26 @@ public class Metrics {
             fMap.put(featureMetric, new CounterMetric());
         }
         featuresMetrics = Collections.unmodifiableMap(fMap);
+
+        functionMetrics = initFunctionMetrics();
+    }
+
+    private Map<String, CounterMetric> initFunctionMetrics() {
+        Map<String, CounterMetric> result = new LinkedHashMap<>();
+        for (var entry : classToFunctionName.entrySet()) {
+            result.put(entry.getValue(), new CounterMetric());
+        }
+        return Collections.unmodifiableMap(result);
+    }
+
+    private Map<Class<?>, String> initClassToFunctionType() {
+        Map<Class<?>, String> tmp = new HashMap<>();
+        for (FunctionDefinition func : functionRegistry.listFunctions()) {
+            if (tmp.containsKey(func.clazz()) == false) {
+                tmp.put(func.clazz(), func.name());
+            }
+        }
+        return Collections.unmodifiableMap(tmp);
     }
 
     /**
@@ -81,6 +111,13 @@ public class Metrics {
         this.featuresMetrics.get(metric).inc();
     }
 
+    public void incFunctionMetric(Class<?> functionType) {
+        String functionName = classToFunctionName.get(functionType);
+        if (functionName != null) {
+            functionMetrics.get(functionName).inc();
+        }
+    }
+
     public Counters stats() {
         Counters counters = new Counters();
 
@@ -102,6 +139,11 @@ public class Metrics {
             counters.inc(FPREFIX + entry.getKey().toString(), entry.getValue().count());
         }
 
+        // function metrics
+        for (Entry<String, CounterMetric> entry : functionMetrics.entrySet()) {
+            counters.inc(FUNC_PREFIX + entry.getKey(), entry.getValue().count());
+        }
+
         return counters;
     }
 }

+ 1 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java

@@ -143,7 +143,7 @@ public class LocalPhysicalPlanOptimizerTests extends MapperServiceTestCase {
 
         return new Analyzer(
             new AnalyzerContext(config, new EsqlFunctionRegistry(), getIndexResult, enrichResolution),
-            new Verifier(new Metrics())
+            new Verifier(new Metrics(new EsqlFunctionRegistry()))
         );
     }
 

+ 1 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/QueryTranslatorTests.java

@@ -46,7 +46,7 @@ public class QueryTranslatorTests extends ESTestCase {
 
         return new Analyzer(
             new AnalyzerContext(EsqlTestUtils.TEST_CFG, new EsqlFunctionRegistry(), getIndexResult, new EnrichResolution()),
-            new Verifier(new Metrics())
+            new Verifier(new Metrics(new EsqlFunctionRegistry()))
         );
     }
 

+ 93 - 2
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/stats/VerifierMetricsTests.java

@@ -10,9 +10,14 @@ package org.elasticsearch.xpack.esql.stats;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.xpack.core.watcher.common.stats.Counters;
 import org.elasticsearch.xpack.esql.analysis.Verifier;
+import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
+import org.elasticsearch.xpack.esql.expression.function.FunctionDefinition;
 import org.elasticsearch.xpack.esql.parser.EsqlParser;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning;
 import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.analyzer;
@@ -32,6 +37,7 @@ import static org.elasticsearch.xpack.esql.stats.FeatureMetric.SORT;
 import static org.elasticsearch.xpack.esql.stats.FeatureMetric.STATS;
 import static org.elasticsearch.xpack.esql.stats.FeatureMetric.WHERE;
 import static org.elasticsearch.xpack.esql.stats.Metrics.FPREFIX;
+import static org.elasticsearch.xpack.esql.stats.Metrics.FUNC_PREFIX;
 
 public class VerifierMetricsTests extends ESTestCase {
 
@@ -54,6 +60,8 @@ public class VerifierMetricsTests extends ESTestCase {
         assertEquals(0, drop(c));
         assertEquals(0, keep(c));
         assertEquals(0, rename(c));
+
+        assertEquals(1, function("concat", c));
     }
 
     public void testEvalQuery() {
@@ -73,6 +81,8 @@ public class VerifierMetricsTests extends ESTestCase {
         assertEquals(0, drop(c));
         assertEquals(0, keep(c));
         assertEquals(0, rename(c));
+
+        assertEquals(1, function("length", c));
     }
 
     public void testGrokQuery() {
@@ -92,6 +102,8 @@ public class VerifierMetricsTests extends ESTestCase {
         assertEquals(0, drop(c));
         assertEquals(0, keep(c));
         assertEquals(0, rename(c));
+
+        assertEquals(1, function("concat", c));
     }
 
     public void testLimitQuery() {
@@ -149,6 +161,8 @@ public class VerifierMetricsTests extends ESTestCase {
         assertEquals(0, drop(c));
         assertEquals(0, keep(c));
         assertEquals(0, rename(c));
+
+        assertEquals(1, function("max", c));
     }
 
     public void testWhereQuery() {
@@ -190,7 +204,7 @@ public class VerifierMetricsTests extends ESTestCase {
     }
 
     public void testTwoQueriesExecuted() {
-        Metrics metrics = new Metrics();
+        Metrics metrics = new Metrics(new EsqlFunctionRegistry());
         Verifier verifier = new Verifier(metrics);
         esqlWithVerifier("""
                from employees
@@ -226,6 +240,64 @@ public class VerifierMetricsTests extends ESTestCase {
         assertEquals(0, drop(c));
         assertEquals(0, keep(c));
         assertEquals(0, rename(c));
+
+        assertEquals(1, function("length", c));
+        assertEquals(1, function("concat", c));
+        assertEquals(1, function("max", c));
+        assertEquals(1, function("min", c));
+
+        assertEquals(0, function("sin", c));
+        assertEquals(0, function("cos", c));
+    }
+
+    public void testMultipleFunctions() {
+        Metrics metrics = new Metrics(new EsqlFunctionRegistry());
+        Verifier verifier = new Verifier(metrics);
+        esqlWithVerifier("""
+               from employees
+               | where languages > 2
+               | limit 5
+               | eval name_len = length(first_name), surname_len = length(last_name)
+               | sort length(first_name)
+               | limit 3
+            """, verifier);
+
+        Counters c = metrics.stats();
+        assertEquals(1, function("length", c));
+        assertEquals(0, function("concat", c));
+
+        esqlWithVerifier("""
+              from employees
+              | where languages > 2
+              | sort first_name desc nulls first
+              | dissect concat(first_name, " ", last_name) "%{a} %{b}"
+              | grok concat(first_name, " ", last_name) "%{WORD:a} %{WORD:b}"
+              | eval name_len = length(first_name), surname_len = length(last_name)
+              | stats x = max(languages)
+              | sort x
+              | stats y = min(x) by x
+            """, verifier);
+        c = metrics.stats();
+
+        assertEquals(2, function("length", c));
+        assertEquals(1, function("concat", c));
+        assertEquals(1, function("max", c));
+        assertEquals(1, function("min", c));
+
+        EsqlFunctionRegistry fr = new EsqlFunctionRegistry().snapshotRegistry();
+        Map<Class<?>, String> functions = new HashMap<>();
+        for (FunctionDefinition func : fr.listFunctions()) {
+            if (functions.containsKey(func.clazz()) == false) {
+                functions.put(func.clazz(), func.name());
+            }
+        }
+        for (String value : functions.values()) {
+            if (Set.of("length", "concat", "max", "min").contains(value) == false) {
+                assertEquals(0, function(value, c));
+            }
+        }
+        Map<?, ?> map = (Map<?, ?>) c.toNestedMap().get("functions");
+        assertEquals(functions.size(), map.size());
     }
 
     public void testEnrich() {
@@ -251,6 +323,8 @@ public class VerifierMetricsTests extends ESTestCase {
         assertEquals(0, drop(c));
         assertEquals(1L, keep(c));
         assertEquals(0, rename(c));
+
+        assertEquals(1, function("to_string", c));
     }
 
     public void testMvExpand() {
@@ -298,6 +372,8 @@ public class VerifierMetricsTests extends ESTestCase {
         assertEquals(0, drop(c));
         assertEquals(0, keep(c));
         assertEquals(0, rename(c));
+
+        assertEquals(1, function("count", c));
     }
 
     public void testRow() {
@@ -336,6 +412,8 @@ public class VerifierMetricsTests extends ESTestCase {
         assertEquals(1L, drop(c));
         assertEquals(0, keep(c));
         assertEquals(1L, rename(c));
+
+        assertEquals(1, function("count", c));
     }
 
     public void testKeep() {
@@ -422,6 +500,19 @@ public class VerifierMetricsTests extends ESTestCase {
         return c.get(FPREFIX + RENAME);
     }
 
+    private long function(String function, Counters c) {
+        return c.get(FUNC_PREFIX + function);
+    }
+
+    private void assertNullFunction(String function, Counters c) {
+        try {
+            c.get(FUNC_PREFIX + function);
+            fail();
+        } catch (NullPointerException npe) {
+
+        }
+    }
+
     private Counters esql(String esql) {
         return esql(esql, null);
     }
@@ -434,7 +525,7 @@ public class VerifierMetricsTests extends ESTestCase {
         Verifier verifier = v;
         Metrics metrics = null;
         if (v == null) {
-            metrics = new Metrics();
+            metrics = new Metrics(new EsqlFunctionRegistry());
             verifier = new Verifier(metrics);
         }
         analyzer(verifier).analyze(parser.createStatement(esql));

+ 13 - 2
x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml

@@ -5,7 +5,7 @@ setup:
         - method: POST
           path: /_query
           parameters: [ method, path, parameters, capabilities ]
-          capabilities: [ no_meta ]
+          capabilities: [ function_stats ]
       reason: "META command removed which changes the count of the data returned"
       test_runner_features: [capabilities]
 
@@ -51,11 +51,16 @@ setup:
   - set: {esql.queries.kibana.failed: kibana_failed_counter}
   - set: {esql.queries._all.total: all_total_counter}
   - set: {esql.queries._all.failed: all_failed_counter}
+  - set: {esql.functions.max: functions_max}
+  - set: {esql.functions.min: functions_min}
+  - set: {esql.functions.cos: functions_cos}
+  - set: {esql.functions.to_long: functions_to_long}
+  - set: {esql.functions.coalesce: functions_coalesce}
 
   - do:
       esql.query:
         body:
-          query: 'from test | where data > 2 | sort count desc | limit 5 | stats m = max(data)'
+          query: 'from test | where data > 2 and to_long(data) > 2 | sort count desc | limit 5 | stats m = max(data)'
 
   - do: {xpack.usage: {}}
   - match: { esql.available: true }
@@ -73,3 +78,9 @@ setup:
   - match: {esql.queries.kibana.failed: $kibana_failed_counter}
   - gt: {esql.queries._all.total: $all_total_counter}
   - match: {esql.queries._all.failed: $all_failed_counter}
+  - gt: {esql.functions.max: $functions_max}
+  - match: {esql.functions.min: $functions_min}
+  - match: {esql.functions.cos: $functions_cos}
+  - gt: {esql.functions.to_long: $functions_to_long}
+  - match: {esql.functions.coalesce: $functions_coalesce}
+  - length: {esql.functions: 117}