Skip to content

Commit

Permalink
Backport #474 to 2.x (#490)
Browse files Browse the repository at this point in the history
* Log pattern tool improvement (#474)

* Improve log pattern tool with cutting edge log pattern parser

Signed-off-by: Songkan Tang <[email protected]>

* Minor comment change

Signed-off-by: Songkan Tang <[email protected]>

* Address LogPatternTool class comments

Signed-off-by: Songkan Tang <[email protected]>

* Address BrainLogParser class comments

Signed-off-by: Songkan Tang <[email protected]>

* Address BrainLogParser class comment part02

Signed-off-by: Songkan Tang <[email protected]>

* Minor change of input validation logic

Signed-off-by: Songkan Tang <[email protected]>

* Address forbiddenAPI check issue

Signed-off-by: Songkan Tang <[email protected]>

* Address minor comments in BrainLogParser

Signed-off-by: Songkan Tang <[email protected]>

* Tune preprocessing regex and parameters and change related test result

Signed-off-by: Songkan Tang <[email protected]>

* Expose variable count threshold parameter configuration for LogPatternTool

Signed-off-by: Songkan Tang <[email protected]>

* Minorly tune preprocessing regex

Signed-off-by: Songkan Tang <[email protected]>

---------

Signed-off-by: Songkan Tang <[email protected]>

* Fix compilation issue due to Java version conflicts

Signed-off-by: Songkan Tang <[email protected]>

* Fix compilation issue part2

Signed-off-by: Songkan Tang <[email protected]>

---------

Signed-off-by: Songkan Tang <[email protected]>
  • Loading branch information
songkant-aws authored Jan 24, 2025
1 parent 95db069 commit 735f9f4
Show file tree
Hide file tree
Showing 10 changed files with 949 additions and 216 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,7 @@ public <T> void run(Map<String, String> parameters, ActionListener<T> listener)
.build();
ActionRequest request = new MLPredictionTaskRequest(
modelId,
MLInput.builder().algorithm(FunctionName.REMOTE).inputDataset(inputDataSet).build(),
null
MLInput.builder().algorithm(FunctionName.REMOTE).inputDataset(inputDataSet).build()
);

client.execute(MLPredictionTaskAction.INSTANCE, request, ActionListener.wrap(mlTaskResponse -> {
Expand Down
315 changes: 199 additions & 116 deletions src/main/java/org/opensearch/agent/tools/LogPatternTool.java

Large diffs are not rendered by default.

8 changes: 1 addition & 7 deletions src/main/java/org/opensearch/agent/tools/PPLTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import org.opensearch.client.Client;
import org.opensearch.cluster.metadata.MappingMetadata;
import org.opensearch.core.action.ActionListener;
import org.opensearch.core.action.ActionResponse;
import org.opensearch.index.query.MatchAllQueryBuilder;
import org.opensearch.ml.common.FunctionName;
import org.opensearch.ml.common.dataset.remote.RemoteInferenceInputDataSet;
Expand All @@ -52,7 +51,6 @@
import org.opensearch.search.builder.SearchSourceBuilder;
import org.opensearch.sql.plugin.transport.PPLQueryAction;
import org.opensearch.sql.plugin.transport.TransportPPLQueryRequest;
import org.opensearch.sql.plugin.transport.TransportPPLQueryResponse;
import org.opensearch.sql.ppl.domain.PPLQueryRequest;

import com.google.gson.Gson;
Expand Down Expand Up @@ -228,7 +226,7 @@ public <T> void run(Map<String, String> parameters, ActionListener<T> listener)
.execute(
PPLQueryAction.INSTANCE,
transportPPLQueryRequest,
getPPLTransportActionListener(ActionListener.wrap(transportPPLQueryResponse -> {
ToolHelper.getPPLTransportActionListener(ActionListener.wrap(transportPPLQueryResponse -> {
String results = transportPPLQueryResponse.getResult();
Map<String, String> returnResults = ImmutableMap.of("ppl", ppl, "executionResult", results);
listener
Expand Down Expand Up @@ -439,10 +437,6 @@ private static void extractSamples(Map<String, Object> sampleSource, Map<String,
}
}

private <T extends ActionResponse> ActionListener<T> getPPLTransportActionListener(ActionListener<TransportPPLQueryResponse> listener) {
return ActionListener.wrap(r -> { listener.onResponse(TransportPPLQueryResponse.fromActionResponse(r)); }, listener::onFailure);
}

@SuppressWarnings("unchecked")
private void extractFromChatParameters(Map<String, String> parameters) {
if (parameters.containsKey("input")) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/opensearch/agent/tools/RAGTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public <T> void run(Map<String, String> parameters, ActionListener<T> listener)

RemoteInferenceInputDataSet inputDataSet = RemoteInferenceInputDataSet.builder().parameters(tmpParameters).build();
MLInput mlInput = MLInput.builder().algorithm(FunctionName.REMOTE).inputDataset(inputDataSet).build();
ActionRequest request = new MLPredictionTaskRequest(this.inferenceModelId, mlInput, null);
ActionRequest request = new MLPredictionTaskRequest(this.inferenceModelId, mlInput);

client.execute(MLPredictionTaskAction.INSTANCE, request, ActionListener.wrap(resp -> {
ModelTensorOutput modelTensorOutput = (ModelTensorOutput) resp.getOutput();
Expand Down
369 changes: 369 additions & 0 deletions src/main/java/org/opensearch/agent/tools/utils/BrainLogParser.java

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions src/main/java/org/opensearch/agent/tools/utils/ToolHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
import java.util.HashMap;
import java.util.Map;

import org.opensearch.core.action.ActionListener;
import org.opensearch.core.action.ActionResponse;
import org.opensearch.ml.common.utils.StringUtils;
import org.opensearch.sql.plugin.transport.TransportPPLQueryResponse;

import lombok.extern.log4j.Log4j2;

Expand Down Expand Up @@ -75,4 +78,15 @@ public static void extractFieldNamesTypes(
}
}
}

/**
* Wrapper to get PPL transport action listener
* @param listener input action listener
* @return wrapped action listener
*/
public static <T extends ActionResponse> ActionListener<T> getPPLTransportActionListener(
ActionListener<TransportPPLQueryResponse> listener
) {
return ActionListener.wrap(r -> { listener.onResponse(TransportPPLQueryResponse.fromActionResponse(r)); }, listener::onFailure);
}
}
144 changes: 101 additions & 43 deletions src/test/java/org/opensearch/agent/tools/LogPatternToolTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,27 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.opensearch.agent.tools.AbstractRetrieverTool.DOC_SIZE_FIELD;
import static org.opensearch.agent.tools.LogPatternTool.PATTERN;
import static org.opensearch.agent.tools.AbstractRetrieverTool.INDEX_FIELD;
import static org.opensearch.agent.tools.AbstractRetrieverTool.INPUT_FIELD;
import static org.opensearch.agent.tools.LogPatternTool.PPL_FIELD;
import static org.opensearch.agent.tools.LogPatternTool.SAMPLE_LOG_SIZE;
import static org.opensearch.agent.tools.LogPatternTool.TOP_N_PATTERN;
import static org.opensearch.integTest.BaseAgentToolsIT.gson;
import static org.opensearch.ml.common.utils.StringUtils.gson;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.hamcrest.MatcherAssert;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
Expand All @@ -43,6 +46,8 @@
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.search.SearchHit;
import org.opensearch.search.SearchHits;
import org.opensearch.sql.plugin.transport.PPLQueryAction;
import org.opensearch.sql.plugin.transport.TransportPPLQueryResponse;

import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
Expand All @@ -52,19 +57,25 @@
public class LogPatternToolTests {

public static String responseBodyResourceFile = "org/opensearch/agent/tools/expected_flow_agent_of_log_pattern_tool_response_body.json";
public static final String TEST_QUERY_TEXT = "123fsd23134sdfouh";
public static final String TEST_QUERY_TEXT =
"{\"size\":2,\"query\":{\"bool\":{\"filter\":[{\"range\":{\"timestamp\":{\"from\":\"1734404246000||-1d\",\"to\":\"1734404246000\",\"include_lower\":true,\"include_upper\":true,\"format\":\"epoch_millis\",\"boost\":1}}},{\"range\":{\"bytes\":{\"from\":0,\"to\":null,\"include_lower\":true,\"include_upper\":true,\"boost\":1}}}],\"adjust_pure_negative\":true,\"boost\":1}}}";
private Map<String, Object> params = new HashMap<>();
private final Client client = mock(Client.class);
@Mock
private SearchResponse searchResponse;
@Mock
private SearchHits searchHits;
@Mock
private TransportPPLQueryResponse pplQueryResponse;

@SneakyThrows
@Before
public void setup() {
MockitoAnnotations.openMocks(this);
LogPatternTool.Factory.getInstance().init(client, null);
}

private void mockDSLInvocation() throws IOException {
List<String> fields = List.of("field1", "field2", "field3");
SearchHit[] hits = new SearchHit[] {
createHit(0, null, fields, List.of("123", "123.abc-AB * De /", 12345)),
Expand Down Expand Up @@ -94,14 +105,24 @@ private BytesReference createSource(List<String> fieldNames, List<Object> fieldC
return (BytesReference.bytes(builder));
}

private void mockPPLInvocation() throws IOException {
String pplRawResponse =
"{\"schema\":[{\"name\":\"field1\",\"type\":\"string\"},{\"name\":\"field2\",\"type\":\"string\"},{\"name\":\"field3\",\"type\":\"long\"}],\"datarows\":[[\"123\",\"123.abc-AB * De /\",12345],[\"123\",\"45.abc-AB * De /\",12345],[\"123\",\"12.abc_AB * De /\",12345],[\"123\",\"45.ab_AB * De /\",12345],[\"123\",\".abAB * De /\",12345]],\"total\":5,\"size\":5}";
doAnswer(invocation -> {
ActionListener<TransportPPLQueryResponse> listener = (ActionListener<TransportPPLQueryResponse>) invocation.getArguments()[2];
listener.onResponse(pplQueryResponse);
return null;
}).when(client).execute(eq(PPLQueryAction.INSTANCE), any(), any());
when(pplQueryResponse.getResult()).thenReturn(pplRawResponse);
}

@Test
@SneakyThrows
public void testCreateTool() {
LogPatternTool tool = LogPatternTool.Factory.getInstance().create(params);
assertEquals(LogPatternTool.LOG_PATTERN_DEFAULT_DOC_SIZE, (int) tool.docSize);
assertEquals(LogPatternTool.DEFAULT_TOP_N_PATTERN, tool.getTopNPattern());
assertEquals(LogPatternTool.DEFAULT_SAMPLE_LOG_SIZE, tool.getSampleLogSize());
assertNull(tool.getPattern());
assertEquals("LogPatternTool", tool.getType());
assertEquals("LogPatternTool", tool.getName());
assertEquals(LogPatternTool.DEFAULT_DESCRIPTION, LogPatternTool.Factory.getInstance().getDefaultDescription());
Expand Down Expand Up @@ -160,36 +181,31 @@ public void testCreateToolWithNonPositiveSize() {
@Test
public void testGetQueryBody() {
LogPatternTool tool = LogPatternTool.Factory.getInstance().create(params);
assertEquals(TEST_QUERY_TEXT, tool.getQueryBody(TEST_QUERY_TEXT));
assertEquals(new JSONObject(TEST_QUERY_TEXT).toString(), new JSONObject(tool.getQueryBody(TEST_QUERY_TEXT)).toString());
}

@Test
public void testValidate() {
LogPatternTool tool = LogPatternTool.Factory.getInstance().create(params);
assertTrue(tool.validate(Map.of("index", "test1", "input", "input_value")));
assertTrue(tool.validate(Map.of(INDEX_FIELD, "test1", INPUT_FIELD, "input_value")));
assertTrue(tool.validate(Map.of(INDEX_FIELD, "test1", PPL_FIELD, "ppl_value")));

// validate failure if no index
assertFalse(tool.validate(Map.of("input", "input_value")));
// validate failure if no input or ppl
assertFalse(tool.validate(Map.of(INDEX_FIELD, "test1")));

// validate failure if no
assertFalse(tool.validate(Map.of("index", "test1")));
// validate failure if no index
assertFalse(tool.validate(Map.of(INPUT_FIELD, "input_value")));
}

@Test
public void testFindLongestField() {
assertEquals("field2", LogPatternTool.findLongestField(Map.of("field1", "123", "field2", "1234", "filed3", 1234)));
}

@Test
public void testExtractPattern() {
assertEquals("././", LogPatternTool.extractPattern("123.abc/.AB/", null));
assertEquals("123.c/.AB/", LogPatternTool.extractPattern("123.abc/.AB/", Pattern.compile("ab")));
assertEquals(".abc/.AB/", LogPatternTool.extractPattern("123.abc/.AB/", Pattern.compile("[0-9]")));
}

@SneakyThrows
@Test
public void testExecutionDefault() {
mockDSLInvocation();
LogPatternTool tool = LogPatternTool.Factory.getInstance().create(params);
JsonElement expected = gson
.fromJson(
Expand All @@ -210,12 +226,10 @@ public void testExecutionDefault() {
@SneakyThrows
@Test
public void testExecutionWithSpecifiedPatternField() {
mockDSLInvocation();
LogPatternTool tool = LogPatternTool.Factory.getInstance().create(params);
JsonElement expected = gson
.fromJson(
"[{\"total count\":5,\"sample logs\":[{\"field1\":\"123\",\"field3\":12345,\"field2\":\"123.abc-AB * De /\"},{\"field1\":\"123\",\"field3\":12345,\"field2\":\"45.abc-AB * De /\"}],\"pattern\":\"\"}]",
JsonElement.class
);
.fromJson("[{\"pattern\":\"123\",\"total count\":5,\"sample logs\":[\"123\",\"123\"]}]", JsonElement.class);
tool
.run(
ImmutableMap.of("index", "index_name", "input", "{}", "pattern_field", "field1", "sample_log_size", "2"),
Expand All @@ -227,26 +241,6 @@ public void testExecutionWithSpecifiedPatternField() {
);
}

@SneakyThrows
@Test
public void testExecutionWithSpecifiedPattern() {
LogPatternTool tool = LogPatternTool.Factory.getInstance().create(Map.of(PATTERN, "[a-zA-Z]"));
JsonElement expected = gson
.fromJson(
"[{\"pattern\":\"45.- * /\",\"sample logs\":[{\"field1\":\"123\",\"field3\":12345,\"field2\":\"45.abc-AB * De /\"}],\"total count\":1},{\"pattern\":\". * /\",\"sample logs\":[{\"field1\":\"123\",\"field3\":12345,\"field2\":\".abAB * De /\"}],\"total count\":1},{\"pattern\":\"123.- * /\",\"sample logs\":[{\"field1\":\"123\",\"field3\":12345,\"field2\":\"123.abc-AB * De /\"}],\"total count\":1}]",
JsonElement.class
);
tool
.run(
ImmutableMap.of("index", "index_name", "input", "{}"),
ActionListener
.<String>wrap(
response -> assertEquals(expected, gson.fromJson(response, JsonElement.class)),
e -> fail("Tool runs failed: " + e.getMessage())
)
);
}

@SneakyThrows
@Test
public void testExecutionWithBlankInput() {
Expand All @@ -257,7 +251,8 @@ public void testExecutionWithBlankInput() {
ActionListener
.<String>wrap(
response -> fail(),
e -> MatcherAssert.assertThat(e.getMessage(), containsString("[input] is null or empty, can not process it."))
e -> MatcherAssert
.assertThat(e.getMessage(), containsString("Both DSL and PPL input is null or empty, can not process it."))
)
);
}
Expand Down Expand Up @@ -375,4 +370,67 @@ public void testExecutionFailedInSearch() {
ActionListener.<String>wrap(response -> fail(), e -> assertEquals("Failed in Search", e.getMessage()))
);
}

@SneakyThrows
@Test
public void testExecutionWithPPLInput() {
mockPPLInvocation();
LogPatternTool tool = LogPatternTool.Factory.getInstance().create(params);
JsonElement expected = gson
.fromJson(
Files.readString(Path.of(this.getClass().getClassLoader().getResource(responseBodyResourceFile).toURI())),
JsonElement.class
);
tool
.run(
ImmutableMap.of(INDEX_FIELD, "index_name", PPL_FIELD, "source"),
ActionListener
.<String>wrap(
response -> assertEquals(expected, gson.fromJson(response, JsonElement.class)),
e -> fail("Tool runs failed: " + e.getMessage())
)
);
}

@SneakyThrows
@Test
public void testExecutionWithPPLInputWhenNoDataIsReturned() {
String emptyDataPPLResponse =
"{\"schema\":[{\"name\":\"field1\",\"type\":\"string\"},{\"name\":\"field2\",\"type\":\"string\"},{\"name\":\"field3\",\"type\":\"long\"}],\"datarows\":[],\"total\":0,\"size\":0}";
doAnswer(invocation -> {
ActionListener<TransportPPLQueryResponse> listener = (ActionListener<TransportPPLQueryResponse>) invocation.getArguments()[2];
listener.onResponse(pplQueryResponse);
return null;
}).when(client).execute(eq(PPLQueryAction.INSTANCE), any(), any());
when(pplQueryResponse.getResult()).thenReturn(emptyDataPPLResponse);
LogPatternTool tool = LogPatternTool.Factory.getInstance().create(params);

tool
.run(
ImmutableMap.of(INDEX_FIELD, "index_name", PPL_FIELD, "source"),
ActionListener
.<String>wrap(
response -> assertEquals("Can not get any data row from ppl response.", response),
e -> fail("Tool runs failed: " + e.getMessage())
)
);
}

@SneakyThrows
@Test
public void testExecutionWithPPLFailed() {
String pplFailureMessage = "Failed in execute ppl";
doAnswer(invocation -> {
ActionListener<SearchResponse> listener = (ActionListener<SearchResponse>) invocation.getArguments()[1];
listener.onFailure(new Exception(pplFailureMessage));
return null;
}).when(client).search(any(), any());

LogPatternTool tool = LogPatternTool.Factory.getInstance().create(params);
tool
.run(
ImmutableMap.of(INDEX_FIELD, "index_name", PPL_FIELD, "source"),
ActionListener.<String>wrap(response -> fail(), e -> assertEquals(pplFailureMessage, e.getMessage()))
);
}
}
Loading

0 comments on commit 735f9f4

Please sign in to comment.