diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/eql/EqlFeatureSetUsage.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/eql/EqlFeatureSetUsage.java index a9c8b6dc711..0484a2fe0bc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/eql/EqlFeatureSetUsage.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/eql/EqlFeatureSetUsage.java @@ -52,7 +52,7 @@ public class EqlFeatureSetUsage extends XPackFeatureSet.Usage { @Override public Version getMinimalSupportedVersion() { - return Version.V_7_7_0; + return Version.V_7_9_0; } } diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/DataLoader.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/DataLoader.java index 6875d40b64f..d64b37bf414 100644 --- a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/DataLoader.java +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/DataLoader.java @@ -39,7 +39,7 @@ public class DataLoader { private static final String TEST_DATA = "/test_data.json"; private static final String MAPPING = "/mapping-default.json"; static final String indexPrefix = "endgame"; - static final String testIndexName = indexPrefix + "-1.4.0"; + public static final String testIndexName = indexPrefix + "-1.4.0"; public static void main(String[] args) throws IOException { try (RestClient client = RestClient.builder(new HttpHost("localhost", 9200)).build()) { @@ -52,15 +52,23 @@ public class DataLoader { } } - @SuppressWarnings("unchecked") - protected static void loadDatasetIntoEs(RestHighLevelClient client, + public static void loadDatasetIntoEs(RestHighLevelClient client, CheckedBiFunction p) throws IOException { + createTestIndex(client); + loadData(client, p); + } + + private static void createTestIndex(RestHighLevelClient client) throws IOException { CreateIndexRequest request = new CreateIndexRequest(testIndexName) .mapping(Streams.readFully(DataLoader.class.getResourceAsStream(MAPPING)), XContentType.JSON); client.indices().create(request, RequestOptions.DEFAULT); + } + @SuppressWarnings("unchecked") + private static void loadData(RestHighLevelClient client, CheckedBiFunction p) + throws IOException { BulkRequest bulk = new BulkRequest(); bulk.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/stats/FeatureMetric.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/stats/FeatureMetric.java new file mode 100644 index 00000000000..05d282e1a12 --- /dev/null +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/stats/FeatureMetric.java @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.test.eql.stats; + +import java.util.Locale; + +public enum FeatureMetric { + SEQUENCE, + JOIN, + EVENT, + SEQUENCE_MAXSPAN, + SEQUENCE_UNTIL, + SEQUENCE_QUERIES_TWO, + SEQUENCE_QUERIES_THREE, + SEQUENCE_QUERIES_FOUR, + SEQUENCE_QUERIES_FIVE_OR_MORE, + JOIN_QUERIES_TWO, + JOIN_QUERIES_THREE, + JOIN_QUERIES_FOUR, + JOIN_QUERIES_FIVE_OR_MORE, + JOIN_UNTIL, + JOIN_KEYS_ONE, + JOIN_KEYS_TWO, + JOIN_KEYS_THREE, + JOIN_KEYS_FOUR, + JOIN_KEYS_FIVE_OR_MORE, + PIPE_HEAD, + PIPE_TAIL; + + @Override + public String toString() { + return this.name().toLowerCase(Locale.ROOT); + } +} diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/stats/RestEqlUsageTestCase.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/stats/RestEqlUsageTestCase.java new file mode 100644 index 00000000000..fe0783fb696 --- /dev/null +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/stats/RestEqlUsageTestCase.java @@ -0,0 +1,378 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.test.eql.stats; + +import org.elasticsearch.Build; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.test.eql.DataLoader; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.junit.Before; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.Collections.unmodifiableSet; + +/** + * Tests a random number of queries that increase various (most of the times, one query will "touch" multiple metrics values) metrics. + */ +public abstract class RestEqlUsageTestCase extends ESRestTestCase { + + private RestHighLevelClient highLevelClient; + private Map baseMetrics = new HashMap(); + private Integer baseAllTotalQueries = 0; + private Integer baseAllFailedQueries = 0; + + @BeforeClass + public static void checkForSnapshot() { + assumeTrue("Only works on snapshot builds for now", Build.CURRENT.isSnapshot()); + } + + /** + * This method gets the metrics' values before the test runs, in case these values + * were changed by other tests running in the same REST test cluster. The test itself + * will count the new metrics' values starting from the base values initialized here. + * These values will increase during the execution of the test with updates in {@link #assertFeatureMetric(int, Map, String)} + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Before + private void getBaseMetrics() throws UnsupportedOperationException, IOException { + Map baseStats = getStats(); + List>> nodesListStats = (List) baseStats.get("stats"); + + for (Map perNodeStats : nodesListStats) { + Map queriesMetrics = (Map) ((Map) perNodeStats.get("stats")).get("queries"); + Map featuresMetrics = getFeaturesMetrics(perNodeStats); + + for (FeatureMetric metric : FeatureMetric.values()) { + String metricName = metric.toString(); + if (baseMetrics.containsKey(metricName)) { + baseMetrics.put(metricName, baseMetrics.get(metricName) + ((Integer) featuresMetrics.get(metricName))); + } else { + baseMetrics.put(metricName, (Integer) featuresMetrics.get(metricName)); + } + } + + // initialize the "base" metric values with whatever values are already recorded on ES + baseAllTotalQueries += ((Map) queriesMetrics.get("_all")).get("total"); + baseAllFailedQueries += ((Map) queriesMetrics.get("_all")).get("failed"); + } + } + + /** + * "Flatten" the response from ES putting all the features metrics in the same Map. + * "features": { + * "joins": { + * "join_queries_three": 0, + * "join_queries_two": 0, + * "join_until": 0, + * "join_queries_five_or_more": 0, + * "join_queries_four": 0 + * }, + * "sequence": 0, + * "keys": { + * "join_keys_two": 0, + * "join_keys_one": 0, + * "join_keys_three": 0, + * "join_keys_five_or_more": 0, + * "join_keys_four": 0 + * }, + * "join": 0, + * "sequences": { + * "sequence_queries_three": 0, + * "sequence_queries_four": 0, + * "sequence_queries_two": 0, + * "sequence_until": 0, + * "sequence_queries_five_or_more": 0, + * "sequence_maxspan": 0 + * }, + * "event": 0, + * "pipes": { + * "pipe_tail": 0, + * "pipe_head": 0 + * } + * } + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Map getFeaturesMetrics(Map perNodeStats) { + Map featuresMetrics = (Map) ((Map) perNodeStats.get("stats")).get("features"); + featuresMetrics.putAll((Map) featuresMetrics.get("keys")); + featuresMetrics.putAll((Map) featuresMetrics.get("sequences")); + featuresMetrics.putAll((Map) featuresMetrics.get("joins")); + featuresMetrics.putAll((Map) featuresMetrics.get("pipes")); + return featuresMetrics; + } + + public void testEqlRestUsage() throws IOException { + DataLoader.loadDatasetIntoEs(highLevelClient(), (t, u) -> createParser(t, u)); + + // + // random event queries + // + int randomEventExecutions = randomIntBetween(1, 15); + int allTotalQueries = baseAllTotalQueries + randomEventExecutions; + + for (int i = 0; i < randomEventExecutions; i++) { + runEql("process where serial_event_id < 4 | head 3"); + } + + Map responseAsMap = getStats(); + Set metricsToCheck = unmodifiableSet(new HashSet<>(Arrays.asList("pipe_head", "event"))); + assertFeaturesMetrics(randomEventExecutions, responseAsMap, metricsToCheck); + assertFeaturesMetricsExcept(responseAsMap, metricsToCheck); + assertAllQueryMetrics(allTotalQueries, responseAsMap); + + // + // random two sequences queries + // + int randomSequenceExecutions = randomIntBetween(1, 15); + allTotalQueries += randomSequenceExecutions; + for (int i = 0; i < randomSequenceExecutions; i++) { + runEql("sequence [process where serial_event_id = 1] [process where serial_event_id = 2]"); + } + responseAsMap = getStats(); + metricsToCheck = unmodifiableSet(new HashSet<>(Arrays.asList("sequence", "sequence_queries_two", "pipe_head"))); + assertFeaturesMetrics(randomSequenceExecutions, responseAsMap, metricsToCheck); + assertFeaturesMetricsExcept(responseAsMap, metricsToCheck); + assertAllQueryMetrics(allTotalQueries, responseAsMap); + + // + // random tail queries + // + int randomTailExecutions = randomIntBetween(1, 15); + allTotalQueries += randomTailExecutions; + for (int i = 0; i < randomTailExecutions; i++) { + runEql("process where serial_event_id < 4 | tail 2"); + } + responseAsMap = getStats(); + metricsToCheck = unmodifiableSet(new HashSet<>(Arrays.asList("pipe_tail", "event"))); + assertFeaturesMetrics(randomTailExecutions, responseAsMap, metricsToCheck); + assertFeaturesMetricsExcept(responseAsMap, metricsToCheck); + assertAllQueryMetrics(allTotalQueries, responseAsMap); + + // + // random sequence with maxspan and four queries + // + int randomMaxspanExecutions = randomIntBetween(1, 15); + allTotalQueries += randomMaxspanExecutions; + for (int i = 0; i < randomMaxspanExecutions; i++) { + runEql("sequence with maxspan=1d" + + " [process where serial_event_id < 4] by exit_code" + + " [process where opcode == 1] by user" + + " [process where opcode == 2] by user" + + " [file where parent_process_name == 'file_delete_event'] by exit_code" + + " until [process where opcode=1] by ppid" + + " | head 4" + + " | tail 2"); + } + responseAsMap = getStats(); + metricsToCheck = unmodifiableSet(new HashSet<>(Arrays.asList("sequence", "sequence_maxspan", "sequence_queries_four", + "pipe_head", "pipe_tail", "join_keys_one", "sequence_until"))); + assertFeaturesMetrics(randomMaxspanExecutions, responseAsMap, metricsToCheck); + assertFeaturesMetricsExcept(responseAsMap, metricsToCheck); + assertAllQueryMetrics(allTotalQueries, responseAsMap); + + // + // random sequence with three queries + // + int randomThreeQueriesSequences = randomIntBetween(1, 15); + allTotalQueries += randomThreeQueriesSequences; + for (int i = 0; i < randomThreeQueriesSequences; i++) { + runEql("sequence with maxspan=1d" + + " [process where serial_event_id < 4] by exit_code" + + " [process where opcode == 1] by user" + + " [process where opcode == 2] by user"); + } + responseAsMap = getStats(); + metricsToCheck = unmodifiableSet(new HashSet<>(Arrays.asList("sequence", "sequence_queries_three", "pipe_head", "join_keys_one", + "sequence_maxspan"))); + assertFeaturesMetrics(randomThreeQueriesSequences, responseAsMap, metricsToCheck); + assertFeaturesMetricsExcept(responseAsMap, metricsToCheck); + assertAllQueryMetrics(allTotalQueries, responseAsMap); + + // + // random sequence with five queries and three join keys + // + int randomFiveQueriesSequences = randomIntBetween(1, 15); + allTotalQueries += randomFiveQueriesSequences; + for (int i = 0; i < randomFiveQueriesSequences; i++) { + runEql("sequence by user, ppid, exit_code with maxspan=1m" + + " [process where serial_event_id < 4]" + + " [process where opcode == 1]" + + " [file where parent_process_name == 'file_delete_event']" + + " [process where serial_event_id < 4]" + + " [process where opcode == 1]" + + "| tail 4"); + } + responseAsMap = getStats(); + metricsToCheck = unmodifiableSet(new HashSet<>(Arrays.asList("sequence", "sequence_queries_five_or_more", "pipe_tail", + "join_keys_three", "sequence_maxspan"))); + assertFeaturesMetrics(randomFiveQueriesSequences, responseAsMap, metricsToCheck); + assertFeaturesMetricsExcept(responseAsMap, metricsToCheck); + assertAllQueryMetrics(allTotalQueries, responseAsMap); + + // + // random sequence with four join keys + // + int randomFourJoinKeysExecutions = randomIntBetween(1, 15); + allTotalQueries += randomFourJoinKeysExecutions; + for (int i = 0; i < randomFourJoinKeysExecutions; i++) { + runEql("sequence by exit_code, user, serial_event_id, pid" + + " [process where serial_event_id < 4]" + + " [process where opcode == 1]"); + } + responseAsMap = getStats(); + metricsToCheck = unmodifiableSet(new HashSet<>(Arrays.asList("sequence", "sequence_queries_two", "pipe_head", "join_keys_four"))); + assertFeaturesMetrics(randomFourJoinKeysExecutions, responseAsMap, metricsToCheck); + assertFeaturesMetricsExcept(responseAsMap, metricsToCheck); + assertAllQueryMetrics(allTotalQueries, responseAsMap); + + // + // random sequence with five join keys + // + int randomFiveJoinKeysExecutions = randomIntBetween(1, 15); + allTotalQueries += randomFiveJoinKeysExecutions; + for (int i = 0; i < randomFiveJoinKeysExecutions; i++) { + runEql("sequence by exit_code, user, serial_event_id, pid, ppid" + + " [process where serial_event_id < 4]" + + " [process where opcode == 1]"); + } + responseAsMap = getStats(); + metricsToCheck = unmodifiableSet(new HashSet<>(Arrays.asList("sequence", "sequence_queries_two", "pipe_head", + "join_keys_five_or_more"))); + assertFeaturesMetrics(randomFiveJoinKeysExecutions, responseAsMap, metricsToCheck); + assertFeaturesMetricsExcept(responseAsMap, metricsToCheck); + assertAllQueryMetrics(allTotalQueries, responseAsMap); + + // + // random failed queries + // + int randomFailedExecutions = randomIntBetween(1, 15); + int allFailedQueries = baseAllFailedQueries + randomFailedExecutions; + allTotalQueries += randomFailedExecutions; + for (int i = 0; i < randomFailedExecutions; i++) { + // not interested in the exception type, but in the fact that the metrics are incremented when an exception is thrown + expectThrows(Exception.class, () -> { + runEql( + randomFrom( + "process where missing_field < 4 | tail 2", + "sequence abc [process where serial_event_id = 1]", + "sequence with maxspan=1x [process where serial_event_id = 1]", + "sequence by exit_code, user [process where serial_event_id < 4] by ppid", + "sequence by" + ) + ); + }); + } + responseAsMap = getStats(); + assertAllFailedQueryMetrics(allFailedQueries, responseAsMap); + assertAllQueryMetrics(allTotalQueries, responseAsMap); + } + + private void assertAllQueryMetrics(int allTotalQueries, Map responseAsMap) throws IOException { + assertAllQueryMetric(allTotalQueries, responseAsMap, "total"); + } + + private void assertAllFailedQueryMetrics(int allFailedQueries, Map responseAsMap) throws IOException { + assertAllQueryMetric(allFailedQueries, responseAsMap, "failed"); + } + + private Map getStats() throws UnsupportedOperationException, IOException { + Request request = new Request("GET", "/_eql/stats"); + Map responseAsMap; + try (InputStream content = client().performRequest(request).getEntity().getContent()) { + responseAsMap = XContentHelper.convertToMap(JsonXContent.jsonXContent, content, false); + } + + return responseAsMap; + } + + private void runEql(String eql) throws IOException { + Request request = new Request("POST", DataLoader.testIndexName + "/_eql/search"); + request.setJsonEntity("{\"query\":\"" + eql +"\"}"); + client().performRequest(request); + } + + private void assertFeaturesMetrics(int expected, Map responseAsMap, Set metricsToCheck) throws IOException { + for(String metricName : metricsToCheck) { + assertFeatureMetric(expected, responseAsMap, metricName); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void assertFeatureMetric(int expected, Map responseAsMap, String feature) throws IOException { + List> nodesListStats = (List>) responseAsMap.get("stats"); + int actualMetricValue = 0; + for (Map perNodeStats : nodesListStats) { + Map featuresMetrics = getFeaturesMetrics(perNodeStats); + actualMetricValue += (int) featuresMetrics.get(feature); + } + assertEquals(expected + baseMetrics.get(feature), actualMetricValue); + + /* + * update the base value for future checks in {@link #assertFeaturesMetricsExcept(Set, Map)} + */ + baseMetrics.put(feature, expected + baseMetrics.get(feature)); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void assertQueryMetric(int expected, Map responseAsMap, String queryType, String metric) throws IOException { + List>> nodesListStats = (List) responseAsMap.get("stats"); + int actualMetricValue = 0; + for (Map perNodeStats : nodesListStats) { + Map queriesMetrics = (Map) ((Map) perNodeStats.get("stats")).get("queries"); + Map perTypeQueriesMetrics = (Map) queriesMetrics.get(queryType); + actualMetricValue += (int) perTypeQueriesMetrics.get(metric); + } + assertEquals(expected, actualMetricValue); + } + + private void assertAllQueryMetric(int expected, Map responseAsMap, String metric) throws IOException { + assertQueryMetric(expected, responseAsMap, "_all", metric); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void assertFeaturesMetricsExcept(Map responseAsMap, Set exceptions) { + List> nodesListStats = (List>) responseAsMap.get("stats"); + for (FeatureMetric metric : FeatureMetric.values()) { + String metricName = metric.toString(); + if (exceptions.contains(metricName) == false) { + Integer actualValue = 0; + for (Map perNodeStats : nodesListStats) { + Map featuresMetrics = getFeaturesMetrics(perNodeStats); + Integer featureMetricValue = (Integer) featuresMetrics.get(metricName); + actualValue += featureMetricValue; + } + + assertEquals(baseMetrics.get(metricName), actualValue); + } + } + } + + private RestHighLevelClient highLevelClient() { + if (highLevelClient == null) { + highLevelClient = new RestHighLevelClient( + client(), + ignore -> { + }, + Collections.emptyList()) { + }; + } + return highLevelClient; + } +} diff --git a/x-pack/plugin/eql/qa/rest/src/test/java/org/elasticsearch/xpack/eql/EqlStatsIT.java b/x-pack/plugin/eql/qa/rest/src/test/java/org/elasticsearch/xpack/eql/EqlStatsIT.java new file mode 100644 index 00000000000..03d75a15749 --- /dev/null +++ b/x-pack/plugin/eql/qa/rest/src/test/java/org/elasticsearch/xpack/eql/EqlStatsIT.java @@ -0,0 +1,7 @@ +package org.elasticsearch.xpack.eql; + +import org.elasticsearch.test.eql.stats.RestEqlUsageTestCase; + +public class EqlStatsIT extends RestEqlUsageTestCase { + +} diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/Verifier.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/Verifier.java index 0d526c4d052..41080f788e6 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/Verifier.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/Verifier.java @@ -6,16 +6,25 @@ package org.elasticsearch.xpack.eql.analysis; +import org.elasticsearch.xpack.eql.plan.logical.Head; +import org.elasticsearch.xpack.eql.plan.logical.Join; +import org.elasticsearch.xpack.eql.plan.logical.Sequence; +import org.elasticsearch.xpack.eql.plan.logical.Tail; +import org.elasticsearch.xpack.eql.stats.FeatureMetric; +import org.elasticsearch.xpack.eql.stats.Metrics; import org.elasticsearch.xpack.ql.capabilities.Unresolvable; import org.elasticsearch.xpack.ql.common.Failure; import org.elasticsearch.xpack.ql.expression.Attribute; import org.elasticsearch.xpack.ql.expression.UnresolvedAttribute; import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; +import org.elasticsearch.xpack.ql.plan.logical.Project; import org.elasticsearch.xpack.ql.tree.Node; import org.elasticsearch.xpack.ql.type.DataTypes; +import org.elasticsearch.xpack.ql.util.Holder; import org.elasticsearch.xpack.ql.util.StringUtils; import java.util.ArrayList; +import java.util.BitSet; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; @@ -23,6 +32,27 @@ import java.util.Map; import java.util.Set; import static java.util.stream.Collectors.toMap; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.EVENT; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_UNTIL; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_KEYS_FIVE_OR_MORE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_KEYS_FOUR; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_KEYS_ONE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_KEYS_THREE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_KEYS_TWO; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_QUERIES_FIVE_OR_MORE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_QUERIES_FOUR; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_QUERIES_THREE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_QUERIES_TWO; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.PIPE_HEAD; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.PIPE_TAIL; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_UNTIL; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_MAXSPAN; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_QUERIES_FIVE_OR_MORE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_QUERIES_FOUR; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_QUERIES_THREE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_QUERIES_TWO; import static org.elasticsearch.xpack.ql.common.Failure.fail; /** @@ -31,6 +61,12 @@ import static org.elasticsearch.xpack.ql.common.Failure.fail; */ public class Verifier { + private final Metrics metrics; + + public Verifier(Metrics metrics) { + this.metrics = metrics; + } + public Map, String> verifyFailures(LogicalPlan plan) { Collection failures = verify(plan); return failures.stream().collect(toMap(Failure::node, Failure::message)); @@ -104,6 +140,87 @@ public class Verifier { failures.addAll(localFailures); }); + // gather metrics + if (failures.isEmpty()) { + BitSet b = new BitSet(FeatureMetric.values().length); + Holder isLikelyAnEventQuery = new Holder<>(false); + + plan.forEachDown(p -> { + if (p instanceof Project) { + isLikelyAnEventQuery.set(true); + } else if (p instanceof Head) { + b.set(PIPE_HEAD.ordinal()); + } else if (p instanceof Tail) { + b.set(PIPE_TAIL.ordinal()); + } else if (p instanceof Join) { + Join j = (Join) p; + + if (p instanceof Sequence) { + b.set(SEQUENCE.ordinal()); + Sequence s = (Sequence) p; + if (s.maxSpan().duration() > 0) { + b.set(SEQUENCE_MAXSPAN.ordinal()); + } + + int queriesCount = s.queries().size(); + switch (queriesCount) { + case 2: b.set(SEQUENCE_QUERIES_TWO.ordinal()); + break; + case 3: b.set(SEQUENCE_QUERIES_THREE.ordinal()); + break; + case 4: b.set(SEQUENCE_QUERIES_FOUR.ordinal()); + break; + default: b.set(SEQUENCE_QUERIES_FIVE_OR_MORE.ordinal()); + break; + } + if (j.until().keys().isEmpty() == false) { + b.set(SEQUENCE_UNTIL.ordinal()); + } + } else { + b.set(FeatureMetric.JOIN.ordinal()); + int queriesCount = j.queries().size(); + switch (queriesCount) { + case 2: b.set(JOIN_QUERIES_TWO.ordinal()); + break; + case 3: b.set(JOIN_QUERIES_THREE.ordinal()); + break; + case 4: b.set(JOIN_QUERIES_FOUR.ordinal()); + break; + default: b.set(JOIN_QUERIES_FIVE_OR_MORE.ordinal()); + break; + } + if (j.until().keys().isEmpty() == false) { + b.set(JOIN_UNTIL.ordinal()); + } + } + + int joinKeysCount = j.queries().get(0).keys().size(); + switch (joinKeysCount) { + case 1: b.set(JOIN_KEYS_ONE.ordinal()); + break; + case 2: b.set(JOIN_KEYS_TWO.ordinal()); + break; + case 3: b.set(JOIN_KEYS_THREE.ordinal()); + break; + case 4: b.set(JOIN_KEYS_FOUR.ordinal()); + break; + default: if (joinKeysCount >= 5) { + b.set(JOIN_KEYS_FIVE_OR_MORE.ordinal()); + } + break; + } + } + }); + + if (isLikelyAnEventQuery.get() && b.get(SEQUENCE.ordinal()) == false && b.get(JOIN.ordinal()) == false) { + b.set(EVENT.ordinal()); + } + + for (int i = b.nextSetBit(0); i >= 0; i = b.nextSetBit(i + 1)) { + metrics.inc(FeatureMetric.values()[i]); + } + } + return failures; } } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/PlanExecutor.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/PlanExecutor.java index e6c30be5355..d899b701f1a 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/PlanExecutor.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/PlanExecutor.java @@ -19,6 +19,7 @@ import org.elasticsearch.xpack.eql.session.EqlConfiguration; import org.elasticsearch.xpack.eql.session.EqlSession; import org.elasticsearch.xpack.eql.session.Results; import org.elasticsearch.xpack.eql.stats.Metrics; +import org.elasticsearch.xpack.eql.stats.QueryMetric; import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.ql.index.IndexResolver; @@ -49,7 +50,7 @@ public class PlanExecutor { this.metrics = new Metrics(); this.preAnalyzer = new PreAnalyzer(); - this.verifier = new Verifier(); + this.verifier = new Verifier(metrics); this.optimizer = new Optimizer(); this.planner = new Planner(); } @@ -59,7 +60,11 @@ public class PlanExecutor { } public void eql(EqlConfiguration cfg, String eql, ParserParams parserParams, ActionListener listener) { - newSession(cfg).eql(eql, parserParams, wrap(listener::onResponse, listener::onFailure)); + metrics.total(QueryMetric.ALL); + newSession(cfg).eql(eql, parserParams, wrap(listener::onResponse, ex -> { + metrics.failed(QueryMetric.ALL); + listener.onFailure(ex); + })); } public Metrics metrics() { diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/FeatureMetric.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/FeatureMetric.java index c9734214314..e035b5b8076 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/FeatureMetric.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/FeatureMetric.java @@ -11,10 +11,50 @@ import java.util.Locale; public enum FeatureMetric { SEQUENCE, JOIN, - PIPE; + EVENT, + SEQUENCE_MAXSPAN, + SEQUENCE_UNTIL, + SEQUENCE_QUERIES_TWO, + SEQUENCE_QUERIES_THREE, + SEQUENCE_QUERIES_FOUR, + SEQUENCE_QUERIES_FIVE_OR_MORE, + JOIN_QUERIES_TWO, + JOIN_QUERIES_THREE, + JOIN_QUERIES_FOUR, + JOIN_QUERIES_FIVE_OR_MORE, + JOIN_UNTIL, + JOIN_KEYS_ONE, + JOIN_KEYS_TWO, + JOIN_KEYS_THREE, + JOIN_KEYS_FOUR, + JOIN_KEYS_FIVE_OR_MORE, + PIPE_HEAD, + PIPE_TAIL; + + private final String prefix; + + FeatureMetric() { + String featureName = this.toString(); + String prefix = "features."; + + if (featureName.startsWith("sequence_")) { + prefix += "sequences."; + } else if (featureName.startsWith("join_k")) { + prefix += "keys."; + } else if (featureName.startsWith("join_")) { + prefix += "joins."; + } else if (featureName.startsWith("pipe_")) { + prefix += "pipes."; + } + this.prefix = prefix; + } @Override public String toString() { return this.name().toLowerCase(Locale.ROOT); } + + public String prefixedName() { + return this.prefix + this.toString(); + } } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/Metrics.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/Metrics.java index 1dd9f2117a8..519bbb6fc1f 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/Metrics.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/Metrics.java @@ -19,6 +19,7 @@ import java.util.Map.Entry; * Class encapsulating the metrics collected for EQL */ public class Metrics { + private enum OperationType { FAILED, TOTAL; @@ -27,20 +28,28 @@ public class Metrics { return this.name().toLowerCase(Locale.ROOT); } } - - // map that holds total/failed counters for each eql "feature" (join, pipe, sequence...) - private final Map> featuresMetrics; - protected static String FPREFIX = "features."; - + + // map that holds total/failed counters for all queries, atm + private final Map> opsByTypeMetrics; + // map that holds counters for each eql "feature" (join, pipe, sequence...) + private final Map featuresMetrics; + protected static String QPREFIX = "queries."; + public Metrics() { - Map> fMap = new LinkedHashMap<>(); - for (FeatureMetric metric : FeatureMetric.values()) { + Map> qMap = new LinkedHashMap<>(); + for (QueryMetric metric : QueryMetric.values()) { Map metricsMap = new LinkedHashMap<>(OperationType.values().length); for (OperationType type : OperationType.values()) { metricsMap.put(type, new CounterMetric()); } - - fMap.put(metric, Collections.unmodifiableMap(metricsMap)); + + qMap.put(metric, Collections.unmodifiableMap(metricsMap)); + } + opsByTypeMetrics = Collections.unmodifiableMap(qMap); + + Map fMap = new LinkedHashMap<>(FeatureMetric.values().length); + for (FeatureMetric featureMetric : FeatureMetric.values()) { + fMap.put(featureMetric, new CounterMetric()); } featuresMetrics = Collections.unmodifiableMap(fMap); } @@ -49,37 +58,49 @@ public class Metrics { * Increments the "total" counter for a metric * This method should be called only once per query. */ - public void total(FeatureMetric metric) { + public void total(QueryMetric metric) { inc(metric, OperationType.TOTAL); } - + /** * Increments the "failed" counter for a metric */ - public void failed(FeatureMetric metric) { + public void failed(QueryMetric metric) { inc(metric, OperationType.FAILED); } - private void inc(FeatureMetric metric, OperationType op) { - this.featuresMetrics.get(metric).get(op).inc(); + private void inc(QueryMetric metric, OperationType op) { + this.opsByTypeMetrics.get(metric).get(op).inc(); + } + + /** + * Increments the counter for a "features" metric + */ + public void inc(FeatureMetric metric) { + this.featuresMetrics.get(metric).inc(); } public Counters stats() { Counters counters = new Counters(); - + // queries metrics - for (Entry> entry : featuresMetrics.entrySet()) { + for (Entry> entry : opsByTypeMetrics.entrySet()) { String metricName = entry.getKey().toString(); - + for (OperationType type : OperationType.values()) { long metricCounter = entry.getValue().get(type).count(); String operationTypeName = type.toString(); - counters.inc(FPREFIX + metricName + "." + operationTypeName, metricCounter); - counters.inc(FPREFIX + "_all." + operationTypeName, metricCounter); + counters.inc(QPREFIX + metricName + "." + operationTypeName, metricCounter); + counters.inc(QPREFIX + "_all." + operationTypeName, metricCounter); } } - + + // features metrics + for (Entry entry : featuresMetrics.entrySet()) { + counters.inc(entry.getKey().prefixedName(), entry.getValue().count()); + } + return counters; } } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/QueryMetric.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/QueryMetric.java new file mode 100644 index 00000000000..6cf487b7d49 --- /dev/null +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/QueryMetric.java @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.eql.stats; + +import java.util.Locale; + +public enum QueryMetric { + ALL; + + @Override + public String toString() { + return this.name().toLowerCase(Locale.ROOT); + } +} diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java index 69872bfdbe9..e84c740b3f0 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java @@ -10,6 +10,7 @@ import org.elasticsearch.xpack.eql.EqlTestUtils; import org.elasticsearch.xpack.eql.expression.function.EqlFunctionRegistry; import org.elasticsearch.xpack.eql.parser.EqlParser; import org.elasticsearch.xpack.eql.parser.ParsingException; +import org.elasticsearch.xpack.eql.stats.Metrics; import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.IndexResolution; import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; @@ -36,7 +37,7 @@ public class VerifierTests extends ESTestCase { private LogicalPlan accept(IndexResolution resolution, String eql) { PreAnalyzer preAnalyzer = new PreAnalyzer(); - Analyzer analyzer = new Analyzer(EqlTestUtils.TEST_CFG_CASE_INSENSITIVE, new EqlFunctionRegistry(), new Verifier()); + Analyzer analyzer = new Analyzer(EqlTestUtils.TEST_CFG_CASE_INSENSITIVE, new EqlFunctionRegistry(), new Verifier(new Metrics())); return analyzer.analyze(preAnalyzer.preAnalyze(parser.createStatement(eql), resolution)); } diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/optimizer/OptimizerTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/optimizer/OptimizerTests.java index e1c25f3e30e..c737ca221a7 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/optimizer/OptimizerTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/optimizer/OptimizerTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.eql.plan.logical.LimitWithOffset; import org.elasticsearch.xpack.eql.plan.logical.Sequence; import org.elasticsearch.xpack.eql.plan.logical.Tail; import org.elasticsearch.xpack.eql.plan.physical.LocalRelation; +import org.elasticsearch.xpack.eql.stats.Metrics; import org.elasticsearch.xpack.ql.expression.Attribute; import org.elasticsearch.xpack.ql.expression.EmptyAttribute; import org.elasticsearch.xpack.ql.expression.FieldAttribute; @@ -66,13 +67,13 @@ public class OptimizerTests extends ESTestCase { return TypesTests.loadMapping(name); } - private IndexResolution loadIndexResolution(String name) { + public static IndexResolution loadIndexResolution(String name) { return IndexResolution.valid(new EsIndex(INDEX_NAME, loadEqlMapping(name))); } private LogicalPlan accept(IndexResolution resolution, String eql) { PreAnalyzer preAnalyzer = new PreAnalyzer(); - Analyzer analyzer = new Analyzer(TEST_CFG_CASE_INSENSITIVE, new EqlFunctionRegistry(), new Verifier()); + Analyzer analyzer = new Analyzer(TEST_CFG_CASE_INSENSITIVE, new EqlFunctionRegistry(), new Verifier(new Metrics())); return optimizer.optimize(analyzer.analyze(preAnalyzer.preAnalyze(parser.createStatement(eql), resolution))); } diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/optimizer/TomlFoldTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/optimizer/TomlFoldTests.java index 15d6ae64402..245c87b4f9b 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/optimizer/TomlFoldTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/optimizer/TomlFoldTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.eql.analysis.Verifier; import org.elasticsearch.xpack.eql.expression.function.EqlFunctionRegistry; import org.elasticsearch.xpack.eql.parser.EqlParser; import org.elasticsearch.xpack.eql.plan.physical.LocalRelation; +import org.elasticsearch.xpack.eql.stats.Metrics; import org.elasticsearch.xpack.ql.expression.Alias; import org.elasticsearch.xpack.ql.expression.Expression; import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; @@ -38,7 +39,7 @@ public class TomlFoldTests extends ESTestCase { private static EqlParser parser = new EqlParser(); private static final EqlFunctionRegistry functionRegistry = new EqlFunctionRegistry(); - private static Verifier verifier = new Verifier(); + private static Verifier verifier = new Verifier(new Metrics()); private static Analyzer caseSensitiveAnalyzer = new Analyzer(TEST_CFG_CASE_SENSITIVE, functionRegistry, verifier); private static Analyzer caseInsensitiveAnalyzer = new Analyzer(TEST_CFG_CASE_INSENSITIVE, functionRegistry, verifier); diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/planner/AbstractQueryFolderTestCase.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/planner/AbstractQueryFolderTestCase.java index 5dd7fe20281..cca0f234498 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/planner/AbstractQueryFolderTestCase.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/planner/AbstractQueryFolderTestCase.java @@ -16,6 +16,7 @@ import org.elasticsearch.xpack.eql.optimizer.Optimizer; import org.elasticsearch.xpack.eql.parser.EqlParser; import org.elasticsearch.xpack.eql.plan.physical.PhysicalPlan; import org.elasticsearch.xpack.eql.session.EqlConfiguration; +import org.elasticsearch.xpack.eql.stats.Metrics; import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.IndexResolution; @@ -25,7 +26,7 @@ public abstract class AbstractQueryFolderTestCase extends ESTestCase { protected EqlParser parser = new EqlParser(); protected PreAnalyzer preAnalyzer = new PreAnalyzer(); protected EqlConfiguration configuration = EqlTestUtils.randomConfiguration(); - protected Analyzer analyzer = new Analyzer(configuration, new EqlFunctionRegistry(), new Verifier()); + protected Analyzer analyzer = new Analyzer(configuration, new EqlFunctionRegistry(), new Verifier(new Metrics())); protected Optimizer optimizer = new Optimizer(); protected Planner planner = new Planner(); diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/stats/VerifierMetricsTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/stats/VerifierMetricsTests.java new file mode 100644 index 00000000000..149f6ece7fc --- /dev/null +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/stats/VerifierMetricsTests.java @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.eql.stats; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.watcher.common.stats.Counters; +import org.elasticsearch.xpack.eql.EqlTestUtils; +import org.elasticsearch.xpack.eql.analysis.Analyzer; +import org.elasticsearch.xpack.eql.analysis.PreAnalyzer; +import org.elasticsearch.xpack.eql.analysis.Verifier; +import org.elasticsearch.xpack.eql.expression.function.EqlFunctionRegistry; +import org.elasticsearch.xpack.eql.optimizer.OptimizerTests; +import org.elasticsearch.xpack.eql.parser.EqlParser; +import org.elasticsearch.xpack.ql.index.IndexResolution; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static java.util.Collections.unmodifiableSet; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.EVENT; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_KEYS_FIVE_OR_MORE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_KEYS_FOUR; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_KEYS_ONE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_KEYS_THREE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_KEYS_TWO; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_QUERIES_TWO; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.JOIN_UNTIL; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.PIPE_HEAD; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.PIPE_TAIL; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_MAXSPAN; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_QUERIES_FIVE_OR_MORE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_QUERIES_FOUR; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_QUERIES_THREE; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_QUERIES_TWO; +import static org.elasticsearch.xpack.eql.stats.FeatureMetric.SEQUENCE_UNTIL; + +public class VerifierMetricsTests extends ESTestCase { + + private EqlParser parser = new EqlParser(); + private PreAnalyzer preAnalyzer = new PreAnalyzer(); + private EqlFunctionRegistry eqlFunctionRegistry = new EqlFunctionRegistry(); + private IndexResolution index = OptimizerTests.loadIndexResolution("mapping-default.json"); + + public void testEventQuery() { + Counters c = eql("process where serial_event_id < 4"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(EVENT, PIPE_HEAD)))); + } + + public void testSequenceQuery() { + Counters c = eql("sequence\r\n" + + " [process where serial_event_id = 1]\r\n" + + " [process where serial_event_id = 2]"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(SEQUENCE, PIPE_HEAD, SEQUENCE_QUERIES_TWO)))); + } + + @AwaitsFix(bugUrl = "waiting for the join implementation") + public void testJoinQuery() { + Counters c = eql("join\r\n" + + " [file where file_name=\"*.exe\"] by ppid\r\n" + + " [file where file_name=\"*.com\"] by pid\r\n" + + "until [process where opcode=1] by ppid\r\n" + + "| head 1"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(JOIN, PIPE_HEAD, JOIN_UNTIL, JOIN_QUERIES_TWO, JOIN_KEYS_ONE)))); + } + + public void testHeadQuery() { + Counters c = eql("process where serial_event_id < 4 | head 2"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(EVENT, PIPE_HEAD)))); + } + + public void testTailQuery() { + Counters c = eql("process where serial_event_id < 4 | tail 2"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(EVENT, PIPE_TAIL)))); + } + + public void testSequenceMaxSpanQuery() { + Counters c = eql("sequence with maxspan=1d\r\n" + + " [process where serial_event_id < 4] by exit_code\r\n" + + " [process where opcode == 1] by user\r\n" + + " [process where opcode == 2] by user\r\n" + + " [file where parent_process_name == \"file_delete_event\"] by exit_code\r\n" + + "until [process where opcode=1] by ppid\r\n" + + "| head 4\r\n" + + "| tail 2"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(SEQUENCE, PIPE_HEAD, PIPE_TAIL, SEQUENCE_MAXSPAN, SEQUENCE_UNTIL, + SEQUENCE_QUERIES_FOUR, JOIN_KEYS_ONE)))); + } + + public void testSequenceWithTwoQueries() { + Counters c = eql("sequence with maxspan=1d\r\n" + + " [process where serial_event_id < 4] by exit_code\r\n" + + " [process where opcode == 1] by user\r\n" + + "until [process where opcode=1] by ppid\r\n" + + "| head 4\r\n" + + "| tail 2"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(SEQUENCE, PIPE_HEAD, PIPE_TAIL, SEQUENCE_MAXSPAN, SEQUENCE_UNTIL, + SEQUENCE_QUERIES_TWO, JOIN_KEYS_ONE)))); + } + + public void testSequenceWithThreeQueries() { + Counters c = eql("sequence with maxspan=1d\r\n" + + " [process where serial_event_id < 4] by exit_code\r\n" + + " [process where opcode == 1] by user\r\n" + + " [file where parent_process_name == \"file_delete_event\"] by exit_code\r\n" + + "| head 4"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(SEQUENCE, PIPE_HEAD, SEQUENCE_MAXSPAN, SEQUENCE_QUERIES_THREE, + JOIN_KEYS_ONE)))); + } + + public void testSequenceWithFiveQueries() { + Counters c = eql("sequence with maxspan=1d\r\n" + + " [process where serial_event_id < 4] by exit_code\r\n" + + " [process where opcode == 1] by user\r\n" + + " [file where parent_process_name == \"file_delete_event\"] by exit_code\r\n" + + " [process where serial_event_id < 4] by exit_code\r\n" + + " [process where opcode == 1] by user\r\n" + + "| head 4"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(SEQUENCE, PIPE_HEAD, SEQUENCE_MAXSPAN, SEQUENCE_QUERIES_FIVE_OR_MORE, + JOIN_KEYS_ONE)))); + } + + public void testSequenceWithSevenQueries() { + Counters c = eql("sequence by exit_code, user\r\n" + + " [process where serial_event_id < 4]\r\n" + + " [process where opcode == 1]\r\n" + + " [file where parent_process_name == \"file_delete_event\"]\r\n" + + " [process where serial_event_id < 4]\r\n" + + " [process where opcode == 1]\r\n" + + " [process where true]\r\n" + + " [process where true]\r\n" + + "| tail 1"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(SEQUENCE, PIPE_TAIL, SEQUENCE_QUERIES_FIVE_OR_MORE, JOIN_KEYS_TWO)))); + } + + public void testSequenceWithThreeKeys() { + Counters c = eql("sequence by exit_code, user, serial_event_id\r\n" + + " [process where serial_event_id < 4]\r\n" + + " [process where opcode == 1]\r\n"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(SEQUENCE, PIPE_HEAD, SEQUENCE_QUERIES_TWO, JOIN_KEYS_THREE)))); + } + + public void testSequenceWithFourKeys() { + Counters c = eql("sequence by exit_code, user, serial_event_id, pid\r\n" + + " [process where serial_event_id < 4]\r\n" + + " [process where opcode == 1]\r\n"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(SEQUENCE, PIPE_HEAD, SEQUENCE_QUERIES_TWO, JOIN_KEYS_FOUR)))); + } + + public void testSequenceWithFiveKeys() { + Counters c = eql("sequence by exit_code, user, serial_event_id, pid, ppid\r\n" + + " [process where serial_event_id < 4]\r\n" + + " [process where opcode == 1]\r\n"); + assertCounters(c, unmodifiableSet(new HashSet<>(Arrays.asList(SEQUENCE, PIPE_HEAD, SEQUENCE_QUERIES_TWO, JOIN_KEYS_FIVE_OR_MORE)))); + } + + private void assertCounters(Counters actual, Set metrics) { + MetricsHolder expected = new MetricsHolder(); + expected.set(metrics); + + for (FeatureMetric metric : FeatureMetric.values()) { + assertEquals(expected.get(metric), actual.get(metric.prefixedName())); + } + } + + private Counters eql(String query) { + return eql(query, null); + } + + private Counters eql(String query, Verifier v) { + Verifier verifier = v; + Metrics metrics = null; + if (v == null) { + metrics = new Metrics(); + verifier = new Verifier(metrics); + } + Analyzer analyzer = new Analyzer(EqlTestUtils.randomConfiguration(), eqlFunctionRegistry, verifier); + analyzer.analyze(preAnalyzer.preAnalyze(parser.createStatement(query), index)); + + return metrics == null ? null : metrics.stats(); + } + + private class MetricsHolder { + long[] metrics; + + MetricsHolder() { + this.metrics = new long[FeatureMetric.values().length]; + for (int i = 0; i < this.metrics.length; i++) { + this.metrics[i] = 0; + } + } + + void set(Set metrics) { + for (FeatureMetric metric : metrics) { + set(metric); + } + } + + void set(FeatureMetric metric) { + this.metrics[metric.ordinal()] = 1L; + } + + long get(FeatureMetric metric) { + return this.metrics[metric.ordinal()]; + } + } +} \ No newline at end of file