Skip to content

Commit 5cb0905

Browse files
Add support for EQL samples queries (#91312)
1 parent 5befc4f commit 5cb0905

File tree

78 files changed

+4178
-1040
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+4178
-1040
lines changed

docs/changelog/91312.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 91312
2+
summary: EQL samples
3+
area: EQL
4+
type: feature
5+
issues: []

x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/BaseEqlSpecTestCase.java

Lines changed: 75 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.apache.http.client.config.RequestConfig;
1212
import org.elasticsearch.client.Request;
1313
import org.elasticsearch.client.RequestOptions;
14+
import org.elasticsearch.client.Response;
1415
import org.elasticsearch.client.ResponseException;
1516
import org.elasticsearch.client.RestClient;
1617
import org.elasticsearch.common.Strings;
@@ -29,6 +30,8 @@
2930
import java.util.List;
3031
import java.util.Map;
3132
import java.util.StringJoiner;
33+
import java.util.function.Function;
34+
import java.util.stream.Collectors;
3235

3336
public abstract class BaseEqlSpecTestCase extends RemoteClusterAwareEqlRestTestCase {
3437

@@ -37,21 +40,43 @@ public abstract class BaseEqlSpecTestCase extends RemoteClusterAwareEqlRestTestC
3740
private final String index;
3841
private final String query;
3942
private final String name;
40-
private final long[] eventIds;
43+
private final List<long[]> eventIds;
4144
/**
4245
* Join keys can be of multiple types, but toml is very restrictive and doesn't allow mixed types values in the same array of values
4346
* For now, every value will be converted to a String.
4447
*/
4548
private final String[] joinKeys;
4649

50+
/**
51+
* any negative value means undefined (ie. no "size" will be passed to the query)
52+
*/
53+
private final int size;
54+
4755
@Before
4856
public void setup() throws Exception {
4957
RestClient provisioningClient = provisioningClient();
50-
if (provisioningClient.performRequest(new Request("HEAD", "/" + unqualifiedIndexName())).getStatusLine().getStatusCode() == 404) {
58+
boolean dataLoaded = Arrays.stream(index.split(","))
59+
.anyMatch(
60+
indexName -> doWithRequest(
61+
new Request("HEAD", "/" + unqualifiedIndexName(indexName)),
62+
provisioningClient,
63+
response -> response.getStatusLine().getStatusCode() == 200
64+
)
65+
);
66+
67+
if (dataLoaded == false) {
5168
DataLoader.loadDatasetIntoEs(highLevelClient(provisioningClient), this::createParser);
5269
}
5370
}
5471

72+
private boolean doWithRequest(Request request, RestClient client, Function<Response, Boolean> consumer) {
73+
try {
74+
return consumer.apply(client.performRequest(request));
75+
} catch (IOException e) {
76+
throw new RuntimeException(e);
77+
}
78+
}
79+
5580
@AfterClass
5681
public static void wipeTestData() throws IOException {
5782
try {
@@ -77,19 +102,20 @@ protected static List<Object[]> asArray(List<EqlSpec> specs) {
77102
name = "" + (counter);
78103
}
79104

80-
results.add(new Object[] { spec.query(), name, spec.expectedEventIds(), spec.joinKeys() });
105+
results.add(new Object[] { spec.query(), name, spec.expectedEventIds(), spec.joinKeys(), spec.size() });
81106
}
82107

83108
return results;
84109
}
85110

86-
BaseEqlSpecTestCase(String index, String query, String name, long[] eventIds, String[] joinKeys) {
111+
BaseEqlSpecTestCase(String index, String query, String name, List<long[]> eventIds, String[] joinKeys, Integer size) {
87112
this.index = index;
88113

89114
this.query = query;
90115
this.name = name;
91116
this.eventIds = eventIds;
92117
this.joinKeys = joinKeys;
118+
this.size = size == null ? -1 : size;
93119
}
94120

95121
public void test() throws Exception {
@@ -109,7 +135,7 @@ private void assertResponse(ObjectPath response) throws Exception {
109135
}
110136
}
111137

112-
private ObjectPath runQuery(String index, String query) throws Exception {
138+
protected ObjectPath runQuery(String index, String query) throws Exception {
113139
XContentBuilder builder = JsonXContent.contentBuilder();
114140
builder.startObject();
115141
builder.field("query", query);
@@ -119,7 +145,7 @@ private ObjectPath runQuery(String index, String query) throws Exception {
119145
if (tiebreaker != null) {
120146
builder.field("tiebreaker_field", tiebreaker);
121147
}
122-
builder.field("size", requestSize());
148+
builder.field("size", this.size < 0 ? requestSize() : this.size);
123149
builder.field("fetch_size", requestFetchSize());
124150
builder.field("result_position", requestResultPosition());
125151
builder.endObject();
@@ -149,20 +175,42 @@ public String toString() {
149175
}
150176
});
151177

152-
long[] expected = eventIds;
153178
long[] actual = extractIds(events);
154-
assertArrayEquals(
155-
LoggerMessageFormat.format(
156-
null,
157-
"unexpected result for spec[{}] [{}] -> {} vs {}",
158-
name,
159-
query,
160-
Arrays.toString(expected),
161-
Arrays.toString(actual)
162-
),
163-
expected,
164-
actual
165-
);
179+
if (eventIds.size() == 1) {
180+
long[] expected = eventIds.get(0);
181+
assertArrayEquals(
182+
LoggerMessageFormat.format(
183+
null,
184+
"unexpected result for spec[{}] [{}] -> {} vs {}",
185+
name,
186+
query,
187+
Arrays.toString(expected),
188+
Arrays.toString(actual)
189+
),
190+
expected,
191+
actual
192+
);
193+
} else {
194+
boolean succeeded = false;
195+
for (long[] expected : eventIds) {
196+
if (Arrays.equals(expected, actual)) {
197+
succeeded = true;
198+
break;
199+
}
200+
}
201+
if (succeeded == false) {
202+
String msg = LoggerMessageFormat.format(
203+
null,
204+
"unexpected result for spec[{}] [{}]. Found: {} - Expected one of the following: {}",
205+
name,
206+
query,
207+
Arrays.toString(actual),
208+
eventIds.stream().map(Arrays::toString).collect(Collectors.joining(", "))
209+
);
210+
fail(msg);
211+
}
212+
}
213+
166214
}
167215

168216
private String eventsToString(List<Map<String, Object>> events) {
@@ -182,7 +230,7 @@ private long[] extractIds(List<Map<String, Object>> events) {
182230
for (int i = 0; i < len; i++) {
183231
Map<String, Object> event = events.get(i);
184232
Map<String, Object> source = (Map<String, Object>) event.get("_source");
185-
Object field = source.get(tiebreaker());
233+
Object field = source.get(idField());
186234
ids[i] = ((Number) field).longValue();
187235
}
188236
return ids;
@@ -262,6 +310,10 @@ protected String eventCategory() {
262310
return "event.category";
263311
}
264312

313+
protected String idField() {
314+
return tiebreaker();
315+
}
316+
265317
protected abstract String tiebreaker();
266318

267319
protected int requestSize() {
@@ -278,8 +330,8 @@ protected String requestResultPosition() {
278330
}
279331

280332
// strip any qualification from the received index string
281-
private String unqualifiedIndexName() {
282-
int offset = index.indexOf(':');
283-
return offset >= 0 ? index.substring(offset + 1) : index;
333+
private static String unqualifiedIndexName(String indexName) {
334+
int offset = indexName.indexOf(':');
335+
return offset >= 0 ? indexName.substring(offset + 1) : indexName;
284336
}
285337
}

x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/DataLoader.java

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
* - endgame-140 - for existing data
4949
* - endgame-140-nanos - same as endgame-140, but with nano-precision timestamps
5050
* - extra - additional data
51+
* - sample* - data for "sample" functionality
5152
*
5253
* While the loader could be made generic, the queries are bound to each index and generalizing that would make things way too complicated.
5354
*/
@@ -56,6 +57,7 @@ public class DataLoader {
5657
public static final String TEST_INDEX = "endgame-140";
5758
public static final String TEST_EXTRA_INDEX = "extra";
5859
public static final String TEST_NANOS_INDEX = "endgame-140-nanos";
60+
public static final String TEST_SAMPLE = "sample1,sample2,sample3";
5961

6062
private static final Map<String, String[]> replacementPatterns = Collections.unmodifiableMap(getReplacementPatterns());
6163

@@ -99,27 +101,31 @@ public static void loadDatasetIntoEs(
99101
// chosen Windows filetime timestamps (2017+) can coincidentally also be readily used as nano-resolution unix timestamps (1973+).
100102
// There are mixed values with and without nanos precision so that the filtering is properly tested for both cases.
101103
load(client, TEST_NANOS_INDEX, TEST_INDEX, DataLoader::timestampToUnixNanos, p);
104+
load(client, TEST_SAMPLE, null, null, p);
102105
}
103106

104107
private static void load(
105108
RestHighLevelClient client,
106-
String indexName,
109+
String indexNames,
107110
String dataName,
108111
Consumer<Map<String, Object>> datasetTransform,
109112
CheckedBiFunction<XContent, InputStream, XContentParser, IOException> p
110113
) throws IOException {
111-
String name = "/data/" + indexName + ".mapping";
112-
URL mapping = DataLoader.class.getResource(name);
113-
if (mapping == null) {
114-
throw new IllegalArgumentException("Cannot find resource " + name);
115-
}
116-
name = "/data/" + (dataName != null ? dataName : indexName) + ".data";
117-
URL data = DataLoader.class.getResource(name);
118-
if (data == null) {
119-
throw new IllegalArgumentException("Cannot find resource " + name);
114+
String[] splitNames = indexNames.split(",");
115+
for (String indexName : splitNames) {
116+
String name = "/data/" + indexName + ".mapping";
117+
URL mapping = DataLoader.class.getResource(name);
118+
if (mapping == null) {
119+
throw new IllegalArgumentException("Cannot find resource " + name);
120+
}
121+
name = "/data/" + (dataName != null ? dataName : indexName) + ".data";
122+
URL data = DataLoader.class.getResource(name);
123+
if (data == null) {
124+
throw new IllegalArgumentException("Cannot find resource " + name);
125+
}
126+
createTestIndex(client, indexName, readMapping(mapping));
127+
loadData(client, indexName, datasetTransform, data, p);
120128
}
121-
createTestIndex(client, indexName, readMapping(mapping));
122-
loadData(client, indexName, datasetTransform, data, p);
123129
}
124130

125131
private static void createTestIndex(RestHighLevelClient client, String indexName, String mapping) throws IOException {

x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlDateNanosSpecTestCase.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ public static List<Object[]> readTestSpecs() throws Exception {
2121
}
2222

2323
// constructor for "local" rest tests
24-
public EqlDateNanosSpecTestCase(String query, String name, long[] eventIds, String[] joinKeys) {
25-
this(TEST_NANOS_INDEX, query, name, eventIds, joinKeys);
24+
public EqlDateNanosSpecTestCase(String query, String name, List<long[]> eventIds, String[] joinKeys, Integer size) {
25+
this(TEST_NANOS_INDEX, query, name, eventIds, joinKeys, size);
2626
}
2727

2828
// constructor for multi-cluster tests
29-
public EqlDateNanosSpecTestCase(String index, String query, String name, long[] eventIds, String[] joinKeys) {
30-
super(index, query, name, eventIds, joinKeys);
29+
public EqlDateNanosSpecTestCase(String index, String query, String name, List<long[]> eventIds, String[] joinKeys, Integer size) {
30+
super(index, query, name, eventIds, joinKeys, size);
3131
}
3232

3333
@Override

x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlExtraSpecTestCase.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ public static List<Object[]> readTestSpecs() throws Exception {
2121
}
2222

2323
// constructor for "local" rest tests
24-
public EqlExtraSpecTestCase(String query, String name, long[] eventIds, String[] joinKeys) {
25-
this(TEST_EXTRA_INDEX, query, name, eventIds, joinKeys);
24+
public EqlExtraSpecTestCase(String query, String name, List<long[]> eventIds, String[] joinKeys, Integer size) {
25+
this(TEST_EXTRA_INDEX, query, name, eventIds, joinKeys, size);
2626
}
2727

2828
// constructor for multi-cluster tests
29-
public EqlExtraSpecTestCase(String index, String query, String name, long[] eventIds, String[] joinKeys) {
30-
super(index, query, name, eventIds, joinKeys);
29+
public EqlExtraSpecTestCase(String index, String query, String name, List<long[]> eventIds, String[] joinKeys, Integer size) {
30+
super(index, query, name, eventIds, joinKeys, size);
3131
}
3232

3333
@Override

x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlRestTestCase.java

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ public void checkSearchContent() throws Exception {
5050
{ String.format(Locale.ROOT, """
5151
{"query": "%s", "size": -1}
5252
""", validQuery), "size must be greater than or equal to 0" },
53+
{ String.format(Locale.ROOT, """
54+
{"query": "%s", "fetch_size": 1}
55+
""", validQuery), "fetch size must be greater than 1" },
5356
{ String.format(Locale.ROOT, """
5457
{"query": "%s", "filter": null}
5558
""", validQuery), "filter doesn't support values of type: VALUE_NULL" },
@@ -60,23 +63,35 @@ public void checkSearchContent() throws Exception {
6063
public void testBadRequests() throws Exception {
6164
createIndex(defaultValidationIndexName, (String) null);
6265

63-
final String contentType = "application/json";
6466
for (String[] test : testBadRequests) {
65-
final String endpoint = "/" + indexPattern(defaultValidationIndexName) + "/_eql/search";
66-
Request request = new Request("GET", endpoint);
67-
request.setJsonEntity(test[0]);
68-
69-
ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request));
70-
Response response = e.getResponse();
71-
72-
assertThat(response.getHeader("Content-Type"), containsString(contentType));
73-
assertThat(EntityUtils.toString(response.getEntity()), containsString(test[1]));
74-
assertThat(response.getStatusLine().getStatusCode(), is(400));
67+
assertBadRequest(test[0], test[1], 400);
7568
}
7669

70+
bulkIndex("""
71+
{"index": {"_index": "%s", "_id": 1}}
72+
{"event":{"category":"process"},"@timestamp":"2020-01-01T12:34:56Z"}
73+
""".formatted(defaultValidationIndexName));
74+
assertBadRequest("""
75+
{"query": "sample by event.category [any where true] [any where true]",
76+
"fetch_size": 1001}
77+
""", "Fetch size cannot be greater than [1000]", 500);
78+
7779
deleteIndexWithProvisioningClient(defaultValidationIndexName);
7880
}
7981

82+
private void assertBadRequest(String query, String errorMessage, int errorCode) throws IOException {
83+
final String endpoint = "/" + indexPattern(defaultValidationIndexName) + "/_eql/search";
84+
Request request = new Request("GET", endpoint);
85+
request.setJsonEntity(query);
86+
87+
ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request));
88+
Response response = e.getResponse();
89+
90+
assertThat(response.getHeader("Content-Type"), containsString("application/json"));
91+
assertThat(EntityUtils.toString(response.getEntity()), containsString(errorMessage));
92+
assertThat(response.getStatusLine().getStatusCode(), is(errorCode));
93+
}
94+
8095
@SuppressWarnings("unchecked")
8196
public void testIndexWildcardPatterns() throws Exception {
8297
createIndex("test1", """
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.test.eql;
9+
10+
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
11+
12+
import java.util.List;
13+
14+
import static org.elasticsearch.test.eql.DataLoader.TEST_SAMPLE;
15+
16+
public abstract class EqlSampleTestCase extends BaseEqlSpecTestCase {
17+
18+
public EqlSampleTestCase(String query, String name, List<long[]> eventIds, String[] joinKeys, Integer size) {
19+
this(TEST_SAMPLE, query, name, eventIds, joinKeys, size);
20+
}
21+
22+
public EqlSampleTestCase(String index, String query, String name, List<long[]> eventIds, String[] joinKeys, Integer size) {
23+
super(index, query, name, eventIds, joinKeys, size);
24+
}
25+
26+
@ParametersFactory(shuffle = false, argumentFormatting = PARAM_FORMATTING)
27+
public static List<Object[]> readTestSpecs() throws Exception {
28+
return asArray(EqlSpecLoader.load("/test_sample.toml"));
29+
}
30+
31+
@Override
32+
protected String tiebreaker() {
33+
return null;
34+
}
35+
36+
@Override
37+
protected String idField() {
38+
return "id";
39+
}
40+
41+
@Override
42+
protected int requestFetchSize() {
43+
// a more relevant fetch_size value for Samples, from algorithm point of view, so we'll mostly test this value
44+
return frequently() ? 2 : super.requestFetchSize();
45+
}
46+
}

0 commit comments

Comments
 (0)