diff --git a/data-prepper-expression/src/main/antlr/DataPrepperExpression.g4 b/data-prepper-expression/src/main/antlr/DataPrepperExpression.g4 index 8a771a4d4b..7d66d9a6bf 100644 --- a/data-prepper-expression/src/main/antlr/DataPrepperExpression.g4 +++ b/data-prepper-expression/src/main/antlr/DataPrepperExpression.g4 @@ -160,7 +160,7 @@ Function fragment FunctionArgs - : (FunctionArg SPACE* COMMA SPACE*)* SPACE* FunctionArg + : ((FunctionArg SPACE* COMMA SPACE*)* SPACE* FunctionArg)? ; fragment diff --git a/data-prepper-expression/src/main/java/org/opensearch/dataprepper/expression/GetEventTypeExpressionFunction.java b/data-prepper-expression/src/main/java/org/opensearch/dataprepper/expression/GetEventTypeExpressionFunction.java new file mode 100644 index 0000000000..86dc6d11d6 --- /dev/null +++ b/data-prepper-expression/src/main/java/org/opensearch/dataprepper/expression/GetEventTypeExpressionFunction.java @@ -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 args, final Event event, final Function convertLiteralType) { + if (!args.isEmpty()) { + throw new RuntimeException("getEventType() does not take any arguments"); + } + return event.getMetadata().getEventType(); + } +} \ No newline at end of file diff --git a/data-prepper-expression/src/main/java/org/opensearch/dataprepper/expression/ParseTreeCoercionService.java b/data-prepper-expression/src/main/java/org/opensearch/dataprepper/expression/ParseTreeCoercionService.java index 692a8c561a..68fc751bfa 100644 --- a/data-prepper-expression/src/main/java/org/opensearch/dataprepper/expression/ParseTreeCoercionService.java +++ b/data-prepper-expression/src/main/java/org/opensearch/dataprepper/expression/ParseTreeCoercionService.java @@ -24,15 +24,17 @@ class ParseTreeCoercionService { private Function convertLiteralType; @Inject - public ParseTreeCoercionService(final Map, Function> literalTypeConversions, ExpressionFunctionProvider expressionFunctionProvider) { + public ParseTreeCoercionService( + final Map, Function> 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; } @@ -44,23 +46,31 @@ 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("(? 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("(? T coerce(final Object obj, Class 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); } } diff --git a/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_ConditionalIT.java b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_ConditionalIT.java index f98546b4dd..23df924e6f 100644 --- a/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_ConditionalIT.java +++ b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_ConditionalIT.java @@ -248,8 +248,10 @@ private static Stream 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 invalidExpressionArguments() { @@ -344,7 +346,10 @@ private static Stream 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) + ); } diff --git a/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_MultiTypeIT.java b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_MultiTypeIT.java index 06b3d7aa2f..7682c71573 100644 --- a/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_MultiTypeIT.java +++ b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_MultiTypeIT.java @@ -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) { @@ -165,20 +158,15 @@ private static Stream validMapExpressionArguments() { ); } - private static Stream 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 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]}")) ); } diff --git a/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GetEventTypeExpressionFunctionTest.java b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GetEventTypeExpressionFunctionTest.java new file mode 100644 index 0000000000..a1db66f9ac --- /dev/null +++ b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GetEventTypeExpressionFunctionTest.java @@ -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)); + } +} \ No newline at end of file diff --git a/docs/expression_syntax.md b/docs/expression_syntax.md index ad0392d408..5485d40181 100644 --- a/docs/expression_syntax.md +++ b/docs/expression_syntax.md @@ -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.