Skip to content

Commit 419ce10

Browse files
authored
Add grok and dissect methods to runtime fields (#68088)
This adds a `grok` and a `dissect` method to runtime fields which returns a `Matcher` style object you can use to get the matched patterns. A fairly simple script to extract the "verb" from an apache log line with `grok` would look like this: ``` String verb = grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.verb; if (verb != null) { emit(verb); } ``` And `dissect` would look like: ``` String verb = dissect('%{clientip} %{ident} %{auth} [%{@timestamp}] "%{verb} %{request} HTTP/%{httpversion}" %{status} %{size}').extract(doc["message"].value)?.verb; if (verb != null) { emit(verb); } ``` We'll work later to get it down to a clean looking one liner, but for now, this'll do. The `grok` and `dissect` methods are special in that they only run at script compile time. You can't pass non-constants to them. They'll produce compile errors if you send in a bad pattern. This is nice because they can be expensive to "compile" and there are many other optimizations we can make when the patterns are available up front. Closes #67825
1 parent f5c2982 commit 419ce10

File tree

42 files changed

+889
-94
lines changed

Some content is hidden

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

42 files changed

+889
-94
lines changed

libs/dissect/src/main/java/org/elasticsearch/dissect/DissectKey.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public final class DissectKey {
102102
}
103103

104104
if (name == null || (name.isEmpty() && !skip)) {
105-
throw new DissectException.KeyParse(key, "The key name could be determined");
105+
throw new DissectException.KeyParse(key, "The key name could not be determined");
106106
}
107107
}
108108

libs/dissect/src/main/java/org/elasticsearch/dissect/DissectParser.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
import java.util.stream.Collectors;
3535

3636
/**
37-
* <p>Splits (dissects) a string into its parts based on a pattern.</p><p>A dissect pattern is composed of a set of keys and delimiters.
37+
* Splits (dissects) a string into its parts based on a pattern.
38+
* <p>A dissect pattern is composed of a set of keys and delimiters.
3839
* For example the dissect pattern: <pre>%{a} %{b},%{c}</pre> has 3 keys (a,b,c) and two delimiters (space and comma). This pattern will
3940
* match a string of the form: <pre>foo bar,baz</pre> and will result a key/value pairing of <pre>a=foo, b=bar, and c=baz.</pre>
4041
* <p>Matches are all or nothing. For example, the same pattern will NOT match <pre>foo bar baz</pre> since all of the delimiters did not
@@ -280,7 +281,19 @@ public Map<String, String> parse(String inputString) {
280281
}
281282
Map<String, String> results = dissectMatch.getResults();
282283

283-
if (dissectMatch.isValid(results) == false) {
284+
return dissectMatch.isValid(results) ? results : null;
285+
}
286+
287+
/**
288+
* <p>Entry point to dissect a string into it's parts.</p>
289+
*
290+
* @param inputString The string to dissect
291+
* @return the key/value Map of the results
292+
* @throws DissectException if unable to dissect a pair into it's parts.
293+
*/
294+
public Map<String, String> forceParse(String inputString) {
295+
Map<String, String> results = parse(inputString);
296+
if (results == null) {
284297
throw new DissectException.FindMatch(pattern, inputString);
285298
}
286299
return results;

libs/dissect/src/test/java/org/elasticsearch/dissect/DissectParserTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,11 +349,12 @@ public void testJsonSpecification() throws Exception {
349349
}
350350
}
351351

352-
private DissectException assertFail(String pattern, String input){
353-
return expectThrows(DissectException.class, () -> new DissectParser(pattern, null).parse(input));
352+
private DissectException assertFail(String pattern, String input) {
353+
return expectThrows(DissectException.class, () -> new DissectParser(pattern, null).forceParse(input));
354354
}
355355

356356
private void assertMiss(String pattern, String input) {
357+
assertNull(new DissectParser(pattern, null).parse(input));
357358
DissectException e = assertFail(pattern, input);
358359
assertThat(e.getMessage(), CoreMatchers.containsString("Unable to find match for dissect pattern"));
359360
assertThat(e.getMessage(), CoreMatchers.containsString(pattern));

modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/DissectProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public IngestDocument execute(IngestDocument ingestDocument) {
5454
} else if (input == null) {
5555
throw new IllegalArgumentException("field [" + field + "] is null, cannot process it.");
5656
}
57-
dissectParser.parse(input).forEach(ingestDocument::setFieldValue);
57+
dissectParser.forceParse(input).forEach(ingestDocument::setFieldValue);
5858
return ingestDocument;
5959
}
6060

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.painless.spi.annotation;
21+
22+
/**
23+
* Methods annotated with this must be run at compile time so their arguments
24+
* must all be constant and they produce a constant.
25+
*/
26+
public class CompileTimeOnlyAnnotation {
27+
public static final String NAME = "compile_time_only";
28+
29+
public static final CompileTimeOnlyAnnotation INSTANCE = new CompileTimeOnlyAnnotation();
30+
31+
private CompileTimeOnlyAnnotation() {}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.painless.spi.annotation;
21+
22+
import java.util.Map;
23+
24+
/**
25+
* Methods annotated with {@link CompileTimeOnlyAnnotation} must be run at
26+
* compile time so their arguments must all be constant and they produce a
27+
* constant.
28+
*/
29+
public class CompileTimeOnlyAnnotationParser implements WhitelistAnnotationParser {
30+
31+
public static final CompileTimeOnlyAnnotationParser INSTANCE = new CompileTimeOnlyAnnotationParser();
32+
33+
private CompileTimeOnlyAnnotationParser() {}
34+
35+
@Override
36+
public Object parse(Map<String, String> arguments) {
37+
if (arguments.isEmpty() == false) {
38+
throw new IllegalArgumentException(
39+
"unexpected parameters for [@" + CompileTimeOnlyAnnotation.NAME + "] annotation, found " + arguments
40+
);
41+
}
42+
43+
return CompileTimeOnlyAnnotation.INSTANCE;
44+
}
45+
}

modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/WhitelistAnnotationParser.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ public interface WhitelistAnnotationParser {
3636
new AbstractMap.SimpleEntry<>(NoImportAnnotation.NAME, NoImportAnnotationParser.INSTANCE),
3737
new AbstractMap.SimpleEntry<>(DeprecatedAnnotation.NAME, DeprecatedAnnotationParser.INSTANCE),
3838
new AbstractMap.SimpleEntry<>(NonDeterministicAnnotation.NAME, NonDeterministicAnnotationParser.INSTANCE),
39-
new AbstractMap.SimpleEntry<>(InjectConstantAnnotation.NAME, InjectConstantAnnotationParser.INSTANCE)
39+
new AbstractMap.SimpleEntry<>(InjectConstantAnnotation.NAME, InjectConstantAnnotationParser.INSTANCE),
40+
new AbstractMap.SimpleEntry<>(CompileTimeOnlyAnnotation.NAME, CompileTimeOnlyAnnotationParser.INSTANCE)
4041
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
4142
);
4243

modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.elasticsearch.painless.node.SClass;
2727
import org.elasticsearch.painless.phase.DefaultConstantFoldingOptimizationPhase;
2828
import org.elasticsearch.painless.phase.DefaultIRTreeToASMBytesPhase;
29+
import org.elasticsearch.painless.phase.DefaultStaticConstantExtractionPhase;
2930
import org.elasticsearch.painless.phase.DefaultStringConcatenationOptimizationPhase;
3031
import org.elasticsearch.painless.phase.DocFieldsPhase;
3132
import org.elasticsearch.painless.phase.PainlessSemanticAnalysisPhase;
@@ -227,6 +228,7 @@ ScriptScope compile(Loader loader, String name, String source, CompilerSettings
227228
ClassNode classNode = (ClassNode)scriptScope.getDecoration(root, IRNodeDecoration.class).getIRNode();
228229
new DefaultStringConcatenationOptimizationPhase().visitClass(classNode, null);
229230
new DefaultConstantFoldingOptimizationPhase().visitClass(classNode, null);
231+
new DefaultStaticConstantExtractionPhase().visitClass(classNode, scriptScope);
230232
new DefaultIRTreeToASMBytesPhase().visitScript(classNode);
231233
byte[] bytes = classNode.getBytes();
232234

@@ -263,6 +265,7 @@ byte[] compile(String name, String source, CompilerSettings settings, Printer de
263265
ClassNode classNode = (ClassNode)scriptScope.getDecoration(root, IRNodeDecoration.class).getIRNode();
264266
new DefaultStringConcatenationOptimizationPhase().visitClass(classNode, null);
265267
new DefaultConstantFoldingOptimizationPhase().visitClass(classNode, null);
268+
new DefaultStaticConstantExtractionPhase().visitClass(classNode, scriptScope);
266269
classNode.setDebugStream(debugStream);
267270
new DefaultIRTreeToASMBytesPhase().visitScript(classNode);
268271

modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessInstanceBinding.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import java.lang.reflect.Method;
2323
import java.util.List;
24+
import java.util.Map;
2425
import java.util.Objects;
2526

2627
public class PainlessInstanceBinding {
@@ -30,13 +31,21 @@ public class PainlessInstanceBinding {
3031

3132
public final Class<?> returnType;
3233
public final List<Class<?>> typeParameters;
34+
public final Map<Class<?>, Object> annotations;
3335

34-
PainlessInstanceBinding(Object targetInstance, Method javaMethod, Class<?> returnType, List<Class<?>> typeParameters) {
36+
PainlessInstanceBinding(
37+
Object targetInstance,
38+
Method javaMethod,
39+
Class<?> returnType,
40+
List<Class<?>> typeParameters,
41+
Map<Class<?>, Object> annotations
42+
) {
3543
this.targetInstance = targetInstance;
3644
this.javaMethod = javaMethod;
3745

3846
this.returnType = returnType;
3947
this.typeParameters = typeParameters;
48+
this.annotations = annotations;
4049
}
4150

4251
@Override
@@ -54,7 +63,8 @@ public boolean equals(Object object) {
5463
return targetInstance == that.targetInstance &&
5564
Objects.equals(javaMethod, that.javaMethod) &&
5665
Objects.equals(returnType, that.returnType) &&
57-
Objects.equals(typeParameters, that.typeParameters);
66+
Objects.equals(typeParameters, that.typeParameters) &&
67+
Objects.equals(annotations, that.annotations);
5868
}
5969

6070
@Override

modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.elasticsearch.painless.spi.WhitelistField;
3131
import org.elasticsearch.painless.spi.WhitelistInstanceBinding;
3232
import org.elasticsearch.painless.spi.WhitelistMethod;
33+
import org.elasticsearch.painless.spi.annotation.CompileTimeOnlyAnnotation;
3334
import org.elasticsearch.painless.spi.annotation.InjectConstantAnnotation;
3435
import org.elasticsearch.painless.spi.annotation.NoImportAnnotation;
3536
import org.objectweb.asm.ClassWriter;
@@ -174,7 +175,8 @@ public static PainlessLookup buildFromWhitelists(List<Whitelist> whitelists) {
174175
origin = whitelistInstanceBinding.origin;
175176
painlessLookupBuilder.addPainlessInstanceBinding(
176177
whitelistInstanceBinding.targetInstance, whitelistInstanceBinding.methodName,
177-
whitelistInstanceBinding.returnCanonicalTypeName, whitelistInstanceBinding.canonicalTypeNameParameters);
178+
whitelistInstanceBinding.returnCanonicalTypeName, whitelistInstanceBinding.canonicalTypeNameParameters,
179+
whitelistInstanceBinding.painlessAnnotations);
178180
}
179181
}
180182
} catch (Exception exception) {
@@ -393,6 +395,10 @@ public void addPainlessConstructor(Class<?> targetClass, List<Class<?>> typePara
393395
"[[" + targetCanonicalClassName + "], " + typesToCanonicalTypeNames(typeParameters) + "]", iae);
394396
}
395397

398+
if (annotations.containsKey(CompileTimeOnlyAnnotation.class)) {
399+
throw new IllegalArgumentException("constructors can't have @" + CompileTimeOnlyAnnotation.NAME);
400+
}
401+
396402
MethodType methodType = methodHandle.type();
397403

398404
String painlessConstructorKey = buildPainlessConstructorKey(typeParametersSize);
@@ -574,6 +580,10 @@ public void addPainlessMethod(Class<?> targetClass, Class<?> augmentedClass,
574580
}
575581
}
576582

583+
if (annotations.containsKey(CompileTimeOnlyAnnotation.class)) {
584+
throw new IllegalArgumentException("regular methods can't have @" + CompileTimeOnlyAnnotation.NAME);
585+
}
586+
577587
MethodType methodType = methodHandle.type();
578588
boolean isStatic = augmentedClass == null && Modifier.isStatic(javaMethod.getModifiers());
579589
String painlessMethodKey = buildPainlessMethodKey(methodName, typeParametersSize);
@@ -989,6 +999,10 @@ public void addPainlessClassBinding(Class<?> targetClass, String methodName, Cla
989999
"invalid method name [" + methodName + "] for class binding [" + targetCanonicalClassName + "].");
9901000
}
9911001

1002+
if (annotations.containsKey(CompileTimeOnlyAnnotation.class)) {
1003+
throw new IllegalArgumentException("class bindings can't have @" + CompileTimeOnlyAnnotation.NAME);
1004+
}
1005+
9921006
Method[] javaMethods = targetClass.getMethods();
9931007
Method javaMethod = null;
9941008

@@ -1079,7 +1093,8 @@ public void addPainlessClassBinding(Class<?> targetClass, String methodName, Cla
10791093
}
10801094

10811095
public void addPainlessInstanceBinding(Object targetInstance,
1082-
String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {
1096+
String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters,
1097+
Map<Class<?>, Object> painlessAnnotations) {
10831098

10841099
Objects.requireNonNull(targetInstance);
10851100
Objects.requireNonNull(methodName);
@@ -1108,10 +1123,16 @@ public void addPainlessInstanceBinding(Object targetInstance,
11081123
"[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]");
11091124
}
11101125

1111-
addPainlessInstanceBinding(targetInstance, methodName, returnType, typeParameters);
1126+
addPainlessInstanceBinding(targetInstance, methodName, returnType, typeParameters, painlessAnnotations);
11121127
}
11131128

1114-
public void addPainlessInstanceBinding(Object targetInstance, String methodName, Class<?> returnType, List<Class<?>> typeParameters) {
1129+
public void addPainlessInstanceBinding(
1130+
Object targetInstance,
1131+
String methodName,
1132+
Class<?> returnType,
1133+
List<Class<?>> typeParameters,
1134+
Map<Class<?>, Object> painlessAnnotations
1135+
) {
11151136
Objects.requireNonNull(targetInstance);
11161137
Objects.requireNonNull(methodName);
11171138
Objects.requireNonNull(returnType);
@@ -1189,7 +1210,7 @@ public void addPainlessInstanceBinding(Object targetInstance, String methodName,
11891210

11901211
PainlessInstanceBinding existingPainlessInstanceBinding = painlessMethodKeysToPainlessInstanceBindings.get(painlessMethodKey);
11911212
PainlessInstanceBinding newPainlessInstanceBinding =
1192-
new PainlessInstanceBinding(targetInstance, javaMethod, returnType, typeParameters);
1213+
new PainlessInstanceBinding(targetInstance, javaMethod, returnType, typeParameters, painlessAnnotations);
11931214

11941215
if (existingPainlessInstanceBinding == null) {
11951216
newPainlessInstanceBinding = painlessInstanceBindingCache.computeIfAbsent(newPainlessInstanceBinding, key -> key);
@@ -1200,11 +1221,13 @@ public void addPainlessInstanceBinding(Object targetInstance, String methodName,
12001221
"[[" + targetCanonicalClassName + "], " +
12011222
"[" + methodName + "], " +
12021223
"[" + typeToCanonicalTypeName(returnType) + "], " +
1203-
typesToCanonicalTypeNames(typeParameters) + "] and " +
1224+
typesToCanonicalTypeNames(typeParameters) + "], " +
1225+
painlessAnnotations + " and " +
12041226
"[[" + targetCanonicalClassName + "], " +
12051227
"[" + methodName + "], " +
12061228
"[" + typeToCanonicalTypeName(existingPainlessInstanceBinding.returnType) + "], " +
1207-
typesToCanonicalTypeNames(existingPainlessInstanceBinding.typeParameters) + "]");
1229+
typesToCanonicalTypeNames(existingPainlessInstanceBinding.typeParameters) + "], " +
1230+
existingPainlessInstanceBinding.annotations);
12081231
}
12091232
}
12101233

0 commit comments

Comments
 (0)