Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ Function

fragment
FunctionArgs
: (FunctionArg SPACE* COMMA SPACE*)* SPACE* FunctionArg
: ((FunctionArg SPACE* COMMA SPACE*)* SPACE* FunctionArg)?
;

fragment
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.dataprepper.expression;

import org.opensearch.dataprepper.model.event.Event;
import javax.inject.Named;
import java.util.function.Function;
import java.util.List;

@Named
public class GetEventTypeExpressionFunction implements ExpressionFunction {

@Override
public String getFunctionName() {
return "getEventType";
}

@Override
public Object evaluate(final List<Object> args, final Event event, final Function<Object, Object> convertLiteralType) {
if (!args.isEmpty()) {
throw new RuntimeException("getEventType() does not take any arguments");
}
return event.getMetadata().getEventType();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ class ParseTreeCoercionService {
private Function<Object, Object> convertLiteralType;

@Inject
public ParseTreeCoercionService(final Map<Class<? extends Serializable>, Function<Object, Object>> literalTypeConversions, ExpressionFunctionProvider expressionFunctionProvider) {
public ParseTreeCoercionService(
final Map<Class<? extends Serializable>, Function<Object, Object>> literalTypeConversions,
final ExpressionFunctionProvider expressionFunctionProvider) {
this.literalTypeConversions = literalTypeConversions;
convertLiteralType = (value) -> {
if (literalTypeConversions.containsKey(value.getClass())) {
return literalTypeConversions.get(value.getClass()).apply(value);
} else {
throw new ExpressionCoercionException("Unsupported type for value " + value);
}
};
if (literalTypeConversions.containsKey(value.getClass())) {
return literalTypeConversions.get(value.getClass()).apply(value);
} else {
throw new ExpressionCoercionException("Unsupported type for value " + value);
}
};
this.expressionFunctionProvider = expressionFunctionProvider;
}

Expand All @@ -44,31 +46,40 @@ public Object coercePrimaryTerminalNode(final TerminalNode node, final Event eve
final int funcNameIndex = nodeStringValue.indexOf("(");
final String functionName = nodeStringValue.substring(0, funcNameIndex);
final int argsEndIndex = nodeStringValue.indexOf(")", funcNameIndex);
final String argsStr = nodeStringValue.substring(funcNameIndex+1, argsEndIndex);
// Split at commas if there's no backslash before the commas, because commas can be part of a function parameter
final String[] args = argsStr.split("(?<!\\\\),");
List<Object> argList = new ArrayList<>();
for (final String arg: args) {
String trimmedArg = arg.trim();
if (trimmedArg.charAt(0) == '/') {
argList.add(trimmedArg);
} else if (trimmedArg.charAt(0) == '"') {
if (trimmedArg.length() < 2 || trimmedArg.charAt(trimmedArg.length()-1) != '"') {
throw new RuntimeException("Invalid string argument: check if any argument is missing a closing double quote or contains comma that's not escaped with `\\`.");

// Check if the function has at least one argument
if(argsEndIndex > funcNameIndex + 1) {
final String argsStr = nodeStringValue.substring(funcNameIndex + 1, argsEndIndex);
// Split at commas if there's no backslash before the commas, because commas can
// be part of a function parameter
final String[] args = argsStr.split("(?<!\\\\),");

for (final String arg : args) {
String trimmedArg = arg.trim();
if (trimmedArg.charAt(0) == '/') {
argList.add(trimmedArg);
} else if (trimmedArg.charAt(0) == '"') {
if (trimmedArg.length() < 2 || trimmedArg.charAt(trimmedArg.length() - 1) != '"') {
throw new RuntimeException(
"Invalid string argument: check if any argument is missing a closing double quote or contains comma that's not escaped with `\\`.");
}
argList.add(trimmedArg);
} else {
throw new RuntimeException("Unsupported type passed as function argument");
}
argList.add(trimmedArg);
} else {
throw new RuntimeException("Unsupported type passed as function argument");
}
}

return expressionFunctionProvider.provideFunction(functionName, argList, event, convertLiteralType);
case DataPrepperExpressionParser.EscapedJsonPointer:
final String jsonPointerWithoutQuotes = nodeStringValue.substring(1, nodeStringValue.length() - 1);
return resolveJsonPointerValue(jsonPointerWithoutQuotes, event);
case DataPrepperExpressionParser.JsonPointer:
return resolveJsonPointerValue(nodeStringValue, event);
case DataPrepperExpressionParser.String:
final String nodeStringValueWithQuotesStripped = nodeStringValue.substring(1, nodeStringValue.length() - 1);
final String nodeStringValueWithQuotesStripped = nodeStringValue.substring(1,
nodeStringValue.length() - 1);
return nodeStringValueWithQuotesStripped;
case DataPrepperExpressionParser.Integer:
Long longValue = Long.valueOf(nodeStringValue);
Expand Down Expand Up @@ -98,14 +109,15 @@ public <T> T coerce(final Object obj, Class<T> clazz) throws ExpressionCoercionE
if (obj.getClass().isAssignableFrom(clazz)) {
return (T) obj;
}
throw new ExpressionCoercionException("Unable to cast " + obj.getClass().getName() + " into " + clazz.getName());
throw new ExpressionCoercionException(
"Unable to cast " + obj.getClass().getName() + " into " + clazz.getName());
}

private Object resolveJsonPointerValue(final String jsonPointer, final Event event) {
final Object value = event.get(jsonPointer, Object.class);
if (value == null) {
return null;
}
}
return convertLiteralType.apply(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,10 @@ private static Stream<Arguments> validExpressionArguments() {
arguments("/name =~ \".*dataprepper-[0-9]+\"", event("{\"name\": \"dataprepper-abc\"}"), false),
arguments("/name =~ \".*dataprepper-[0-9]+\"", event("{\"other\": \"dataprepper-abc\"}"), false),
arguments("startsWith(\""+strValue+ UUID.randomUUID() + "\",/status)", event("{\"status\":\""+strValue+"\"}"), true),
arguments("startsWith(\""+ UUID.randomUUID() +strValue+ "\",/status)", event("{\"status\":\""+strValue+"\"}"), false)
);
arguments("startsWith(\""+ UUID.randomUUID() +strValue+ "\",/status)", event("{\"status\":\""+strValue+"\"}"), false),
arguments("getEventType() == \"event\"", longEvent, true),
arguments("getEventType() == \"LOG\"", longEvent, false)
);
}

private static Stream<Arguments> invalidExpressionArguments() {
Expand Down Expand Up @@ -344,7 +346,10 @@ private static Stream<Arguments> invalidExpressionSyntaxArguments() {
arguments("getMetadata(10)", tagEvent),
arguments("getMetadata("+ testMetadataKey+ ")", tagEvent),
arguments("getMetadata(\""+ testMetadataKey+")", tagEvent),
arguments("cidrContains(/sourceIp,123)", event("{\"sourceIp\": \"192.0.2.3\"}"))
arguments("cidrContains(/sourceIp,123)", event("{\"sourceIp\": \"192.0.2.3\"}")),
arguments("getEventType() == \"test_event", tagEvent),
arguments("getEventType() == test_event\"", tagEvent)

);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,6 @@ void testMapExpressionEvaluatorWithMultipleThreads(final String expression, fina
}
}

@ParameterizedTest
@MethodSource("exceptionExpressionSyntaxArguments")
void testExpressionSyntaxEvaluatorCausesException(final String expression, final Event event) {
final GenericExpressionEvaluator evaluator = applicationContext.getBean(GenericExpressionEvaluator.class);
assertThrows(ExpressionParsingException.class, () -> evaluator.evaluate(expression, event));
}

@ParameterizedTest
@MethodSource("exceptionExpressionArguments")
void testExpressionEvaluatorCausesException(final String expression, final Event event) {
Expand Down Expand Up @@ -165,20 +158,15 @@ private static Stream<Arguments> validMapExpressionArguments() {
);
}

private static Stream<Arguments> exceptionExpressionSyntaxArguments() {
return Stream.of(
Arguments.of("join()", event("{\"list\":[\"string\", 1, true]}")),
Arguments.of("contains()", event("{\"list\":[\"string\", 1, true]}")),
Arguments.of("startsWith()", event("{\"list\":[\"string\", 1, true]}"))
);
}

private static Stream<Arguments> exceptionExpressionArguments() {
return Stream.of(
// Can't mix Numbers and Strings when using operators
Arguments.of("/status + /message", event("{\"status\": 200, \"message\":\"msg\"}")),
// Wrong number of arguments
Arguments.of("join(/list, \" \", \"third_arg\")", event("{\"list\":[\"string\", 1, true]}"))
Arguments.of("join(/list, \" \", \"third_arg\")", event("{\"list\":[\"string\", 1, true]}")),
Arguments.of("join()", event("{\"list\":[\"string\", 1, true]}")),
Arguments.of("contains()", event("{\"list\":[\"string\", 1, true]}")),
Arguments.of("startsWith()", event("{\"list\":[\"string\", 1, true]}"))
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.opensearch.dataprepper.expression;

import org.opensearch.dataprepper.model.event.Event;
import org.opensearch.dataprepper.model.event.JacksonEvent;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.List;
import java.util.Map;
import java.util.function.Function;

class GetEventTypeExpressionFunctionTest {

private GetEventTypeExpressionFunction createObjectUnderTest() {
return new GetEventTypeExpressionFunction();
}

private Event createTestEvent(final String eventType) {
return JacksonEvent.builder()
.withEventType(eventType)
.withData(Map.of())
.build();
}

@ParameterizedTest
@ValueSource(strings = {"LOG", "TRACE", "METRIC"})
void testGetEventTypeReturnsCorrectType(String eventType) {
GetEventTypeExpressionFunction function = createObjectUnderTest();
Event testEvent = createTestEvent(eventType);

Object result = function.evaluate(List.of(), testEvent, Function.identity());

assertThat(result, equalTo(eventType));
}

@Test
void testGetEventTypeThrowsExceptionWhenArgumentsProvided() {
GetEventTypeExpressionFunction function = createObjectUnderTest();
Event testEvent = createTestEvent("LOG");

assertThrows(RuntimeException.class, () -> function.evaluate(List.of("arg1"), testEvent, Function.identity()));
}

@Test
void testGetEventTypeFunctionRegistered() {
Event testEvent = createTestEvent("event");
final GenericExpressionEvaluator evaluator = mock(GenericExpressionEvaluator.class);
when(evaluator.evaluateConditional("getEventType() == \"event\"", testEvent)).thenReturn(true);
assertDoesNotThrow(() -> evaluator.evaluateConditional("getEventType() == \"event\"", testEvent));
}
Comment on lines +53 to +58

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this test relevant here ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dinujoh This function is to check if the function name registration for getEventType is working as expected within expressions. I have a similar one in generic to check actual execution over mock (as done here):

arguments("getEventType() == \"event\"", longEvent, true),
arguments("getEventType() == \"LOG\"", longEvent, false)

I feel it is good to have both, but not necessary. Please let me know if want it removed, can do it right away.

}
7 changes: 7 additions & 0 deletions docs/expression_syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ Currently, the following functions are supported
- takes one String literal as argument. This is the key to lookup in the event's metadata. If the key contains "/", then recursive lookup into the metadata attributes is done.
- returns the value corresponding to the argument (key) passed. Value can be of any type.
For example, if metadata contains {"key1": "value2", "key2": 10}, then `getMetadata("key1")` returns "value2", and `getMetadata("key2")` return 10.
* `getEventType()`
- Takes no arguments.
- Returns the event type of the given event as a String.
- Throws an error if any arguments are provided.
- Event types is useful from routing requests based on type examples: `LOG`, `TRACE` and `METRIC`.
For example, if the event has an event type `"LOG"`, `getEventType()` will return `"LOG"`.
Example usage in an expression: `getEventType() == "LOG"`.
* `contains()`
- takes two String arguments. Both should be either string literals or Json Pointers with String values.
- returns true if the second argument is a substring of the first argument. Otherwise, return false.
Expand Down
Loading