Skip to content

Commit

Permalink
Update select_jsonpath to accept strings of JSON in addition to JsonN…
Browse files Browse the repository at this point in the history
…ode objects (#17683) (#17896)

* Update select_jsonpath to accept strings of JSON in addition to JsonNode objects

* Add unit test

* Add changelog entry

* Update changelog with correct issue/pr



---------

Co-authored-by: Zack King <91903901+kingzacko1@users.noreply.github.com>
  • Loading branch information
ryan-carroll-graylog and kingzacko1 authored Jan 11, 2024
1 parent 3a4a9ff commit 56362c7
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 3 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/issue-17647.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type = "c"
message = "Updated the select_jsonpath pipeline function to accept JSON strings as the `json` parameter in addition to parsed JsonNode objects."

issues = ["17647"]
pulls = ["17683"]
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.graylog.plugins.pipelineprocessor.functions.json;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
Expand Down Expand Up @@ -44,18 +45,20 @@ public class SelectJsonPath extends AbstractFunction<Map<String, Object>> {

public static final String NAME = "select_jsonpath";

private final ObjectMapper objectMapper;
private final Configuration configuration;
private final ParameterDescriptor<JsonNode, JsonNode> jsonParam;
private final ParameterDescriptor<Object, Object> jsonParam;
private final ParameterDescriptor<Map<String, String>, Map<String, JsonPath>> pathsParam;

@Inject
public SelectJsonPath(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
configuration = Configuration.builder()
.options(Option.SUPPRESS_EXCEPTIONS)
.jsonProvider(new JacksonJsonNodeJsonProvider(objectMapper))
.build();

jsonParam = ParameterDescriptor.type("json", JsonNode.class).description("A parsed JSON tree").build();
jsonParam = ParameterDescriptor.type("json", Object.class).description("A parsed JSON tree or String representation of a JSON tree").build();
// sigh generics and type erasure
//noinspection unchecked
pathsParam = ParameterDescriptor.type("paths",
Expand All @@ -70,7 +73,21 @@ public SelectJsonPath(ObjectMapper objectMapper) {

@Override
public Map<String, Object> evaluate(FunctionArgs args, EvaluationContext context) {
final JsonNode json = jsonParam.required(args, context);
final Object jsonObj = jsonParam.required(args, context);
JsonNode json = null;
if (jsonObj instanceof JsonNode jsonNode) {
json = jsonNode;
} else if (jsonObj instanceof String jsonString) {
try {
json = objectMapper.readTree(jsonString);
} catch (JsonProcessingException e) {
log.warn(context.pipelineErrorMessage("Unable to parse JSON"), e);
}
} else {
throw new IllegalArgumentException(context.pipelineErrorMessage(
"`json` parameter must be a parsed JSON tree or String representation of a JSON tree"));
}

final Map<String, JsonPath> paths = pathsParam.required(args, context);
if (json == null || paths == null) {
return Collections.emptyMap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,55 @@ public void jsonpath() {
assertThat(message.hasField("this_should_exist")).isTrue();
}

@Test
public void jsonpathFromMessageField() {
final String json = "{\n" +
" \"store\": {\n" +
" \"book\": [\n" +
" {\n" +
" \"category\": \"reference\",\n" +
" \"author\": \"Nigel Rees\",\n" +
" \"title\": \"Sayings of the Century\",\n" +
" \"price\": 8.95\n" +
" },\n" +
" {\n" +
" \"category\": \"fiction\",\n" +
" \"author\": \"Evelyn Waugh\",\n" +
" \"title\": \"Sword of Honour\",\n" +
" \"price\": 12.99\n" +
" },\n" +
" {\n" +
" \"category\": \"fiction\",\n" +
" \"author\": \"Herman Melville\",\n" +
" \"title\": \"Moby Dick\",\n" +
" \"isbn\": \"0-553-21311-3\",\n" +
" \"price\": 8.99\n" +
" },\n" +
" {\n" +
" \"category\": \"fiction\",\n" +
" \"author\": \"J. R. R. Tolkien\",\n" +
" \"title\": \"The Lord of the Rings\",\n" +
" \"isbn\": \"0-395-19395-8\",\n" +
" \"price\": 22.99\n" +
" }\n" +
" ],\n" +
" \"bicycle\": {\n" +
" \"color\": \"red\",\n" +
" \"price\": 19.95\n" +
" }\n" +
" },\n" +
" \"expensive\": 10\n" +
"}";

final Rule rule = parser.parseRule(ruleForTest(), false);
final Message message = evaluateRule(rule, new Message(json, "test", Tools.nowUTC()));

assertThat(message.hasField("author_first")).isTrue();
assertThat(message.getField("author_first")).isEqualTo("Nigel Rees");
assertThat(message.hasField("author_last")).isTrue();
assertThat(message.hasField("this_should_exist")).isTrue();
}

@Test
public void json() {
final String flatJson = "{\"str\":\"foobar\",\"int\":42,\"float\":2.5,\"bool\":true,\"array\":[1,2,3]}";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
rule "jsonpathFromMessageField"
when
is_json(parse_json("{}")) == true &&
is_json("foobar") == false &&
is_json(1234) == false &&
is_json(12.34) == false &&
is_json(true) == false
then
let new_fields = select_jsonpath($message.message,
{ author_first: "$['store']['book'][0]['author']",
author_last: "$['store']['book'][-1:]['author']"
});
set_fields(new_fields);

// Don't fail on empty input
let invalid_json = parse_json("#FOOBAR#");
let invalid_json_fields = select_jsonpath(invalid_json, { some_field: "$.message" });
set_fields(invalid_json_fields);

// Don't fail on missing field
let missing_fields = select_jsonpath($message.message, { some_field: "$.i_dont_exist", this_should_exist: "$['store']['book'][-1:]['author']" });
set_fields(missing_fields);
end

0 comments on commit 56362c7

Please sign in to comment.