Skip to content

Commit a1ca71f

Browse files
deepanshgoyal33mayankrai09deepanshgoyal
authored
Http sink: Empty String if the value does not exist in json body Template (#89)
* fix: add jsonpath Option while parsing template * fix: * imports are failing the builds * fix: code cleanup and test cases * fix: code cleanup * add: test cases to comprehensively test the new functionality * fix: code cleaning * add: documentation * update: firehose version --------- Co-authored-by: Mayank Rai <mayank.rai@gojek.com> Co-authored-by: deepanshgoyal <deepansh.goyal@gojek.com>
1 parent 5800bd3 commit a1ca71f

File tree

7 files changed

+259
-24
lines changed

7 files changed

+259
-24
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ lombok {
3333
}
3434

3535
group 'com.gotocompany'
36-
version '0.12.16'
36+
version '0.12.17'
3737

3838
def projName = "firehose"
3939

docs/docs/sinks/http-sink.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,26 @@ Currently supported typecasting target: DOUBLE, INTEGER, LONG, STRING
159159

160160
- Example value: `[{"jsonPath": "$.root.someIntegerField", "type": "INTEGER"}, {"jsonPath": "$..[*].doubleField", "type": "DOUBLE"}]`
161161
- Type: `optional`
162-
- Default Value: `[]`
162+
- Default Value: `[]`
163+
164+
### `SINK_HTTP_JSON_BODY_TEMPLATE_PARSE_OPTION`
165+
166+
Defines the parsing options for the JSON body template. This configuration controls how JsonPath expressions behave when parsing the protobuf message to extract values for the template.
167+
**Available Options:**
168+
169+
- **`DEFAULT_PATH_LEAF_TO_NULL`**: Returns `null` for missing leaf properties instead of throwing exceptions. Only affects missing final properties in a path (e.g., `$.user.name` on `{"user": {}}` returns `null`). Recommended for graceful handling of optional fields.
170+
171+
- **`ALWAYS_RETURN_LIST`**: Forces all JsonPath queries to return results as a List, even for single values. For example, `$.name` returns `["John"]` instead of `"John"`. Useful when you need consistent List return types.
172+
173+
- **`AS_PATH_LIST`**: Returns the JsonPath expressions pointing to matching nodes instead of their actual values. For example, returns `["$['users'][0]['name']"]` instead of `"John"`. Useful when you need to know data location rather than the data itself.
174+
175+
- **`SUPPRESS_EXCEPTIONS`**: Suppresses ALL exceptions during JsonPath evaluation and returns `null` or empty collections for any errors (syntax errors, missing paths, type errors). Provides complete fault tolerance but less specific error handling.
176+
177+
- **`REQUIRE_PROPERTIES`**: Throws exceptions if any property referenced in the path doesn't exist. Opposite of `DEFAULT_PATH_LEAF_TO_NULL` - enforces strict validation that all path components exist.
178+
179+
**Key Differences:**
180+
- `SUPPRESS_EXCEPTIONS` vs `DEFAULT_PATH_LEAF_TO_NULL`: The former suppresses all types of exceptions broadly, while the latter only handles missing leaf properties specifically, allowing other exceptions (like syntax errors) to still be thrown for better debugging.
181+
182+
- Example value: `DEFAULT_PATH_LEAF_TO_NULL`
183+
- Type: `optional`
184+
- Default Value: `""`

src/main/java/com/gotocompany/firehose/config/HttpSinkConfig.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package com.gotocompany.firehose.config;
22

3+
import com.gotocompany.firehose.config.converter.RangeToHashMapConverter;
4+
import com.gotocompany.firehose.config.converter.HttpSinkRequestMethodConverter;
5+
import com.gotocompany.firehose.config.converter.HttpSinkParameterSourceTypeConverter;
6+
import com.gotocompany.firehose.config.converter.HttpSinkParameterDataFormatConverter;
7+
import com.gotocompany.firehose.config.converter.HttpJsonBodyTemplateParseOptionConverter;
8+
import com.gotocompany.firehose.config.converter.HttpSinkParameterPlacementTypeConverter;
39
import com.gotocompany.firehose.config.converter.HttpSinkSerializerJsonTypecastConfigConverter;
410
import com.gotocompany.firehose.config.enums.HttpSinkDataFormatType;
511
import com.gotocompany.firehose.config.enums.HttpSinkParameterPlacementType;
612
import com.gotocompany.firehose.config.enums.HttpSinkParameterSourceType;
713
import com.gotocompany.firehose.config.enums.HttpSinkRequestMethodType;
8-
import com.gotocompany.firehose.config.converter.HttpSinkRequestMethodConverter;
9-
import com.gotocompany.firehose.config.converter.HttpSinkParameterDataFormatConverter;
10-
import com.gotocompany.firehose.config.converter.HttpSinkParameterPlacementTypeConverter;
11-
import com.gotocompany.firehose.config.converter.HttpSinkParameterSourceTypeConverter;
12-
import com.gotocompany.firehose.config.converter.RangeToHashMapConverter;
14+
import com.jayway.jsonpath.Option;
1315

1416
import java.util.Map;
1517
import java.util.function.Function;
@@ -80,6 +82,11 @@ public interface HttpSinkConfig extends AppConfig {
8082
@DefaultValue("")
8183
String getSinkHttpJsonBodyTemplate();
8284

85+
@Key("SINK_HTTP_JSON_BODY_TEMPLATE_PARSE_OPTION")
86+
@DefaultValue("")
87+
@ConverterClass(HttpJsonBodyTemplateParseOptionConverter.class)
88+
Option getSinkHttpJsonBodyTemplateParseOption();
89+
8390
@Key("SINK_HTTP_PARAMETER_PLACEMENT")
8491
@DefaultValue("header")
8592
@ConverterClass(HttpSinkParameterPlacementTypeConverter.class)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.gotocompany.firehose.config.converter;
2+
3+
import com.jayway.jsonpath.Option;
4+
import org.aeonbits.owner.Converter;
5+
6+
import java.lang.reflect.Method;
7+
import java.util.Arrays;
8+
import java.util.stream.Collectors;
9+
10+
public class HttpJsonBodyTemplateParseOptionConverter implements Converter<Option> {
11+
@Override
12+
public Option convert(Method method, String input) {
13+
if (isNullOrBlank(input)) {
14+
return null;
15+
}
16+
String normalizedInput = input.trim().toUpperCase();
17+
try {
18+
return Option.valueOf(normalizedInput);
19+
} catch (IllegalArgumentException e) {
20+
throw new IllegalArgumentException(
21+
String.format("Invalid JSONPath option: '%s'. Valid options are: %s",
22+
input, getValidOptionsString()), e);
23+
}
24+
}
25+
26+
private boolean isNullOrBlank(String input) {
27+
return input == null || input.trim().isEmpty();
28+
}
29+
30+
private String getValidOptionsString() {
31+
return Arrays.stream(Option.values())
32+
.map(Enum::name)
33+
.collect(Collectors.joining(", "));
34+
}
35+
}

src/main/java/com/gotocompany/firehose/serializer/MessageToTemplatizedJson.java

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import com.google.protobuf.DynamicMessage;
1010
import com.google.protobuf.InvalidProtocolBufferException;
1111
import com.google.protobuf.util.JsonFormat;
12+
import com.jayway.jsonpath.Configuration;
1213
import com.jayway.jsonpath.JsonPath;
14+
import com.jayway.jsonpath.Option;
1315
import com.jayway.jsonpath.PathNotFoundException;
1416
import com.gotocompany.stencil.Parser;
1517
import org.json.simple.parser.JSONParser;
@@ -32,24 +34,26 @@ public class MessageToTemplatizedJson implements MessageSerializer {
3234
private Parser protoParser;
3335
private HashSet<String> pathsToReplace;
3436
private JSONParser jsonParser;
37+
private Configuration jsonPathConfig;
3538
private FirehoseInstrumentation firehoseInstrumentation;
3639

37-
public static MessageToTemplatizedJson create(FirehoseInstrumentation firehoseInstrumentation, String httpSinkJsonBodyTemplate, Parser protoParser) {
38-
MessageToTemplatizedJson messageToTemplatizedJson = new MessageToTemplatizedJson(firehoseInstrumentation, httpSinkJsonBodyTemplate, protoParser);
40+
public static MessageToTemplatizedJson create(FirehoseInstrumentation firehoseInstrumentation, String httpSinkJsonBodyTemplate, Parser protoParser, Option option) {
41+
MessageToTemplatizedJson messageToTemplatizedJson = new MessageToTemplatizedJson(firehoseInstrumentation, httpSinkJsonBodyTemplate, protoParser, option);
3942
if (messageToTemplatizedJson.isInvalidJson()) {
40-
throw new ConfigurationException("Given HTTPSink JSON body template :"
41-
+ httpSinkJsonBodyTemplate
42-
+ ", must be a valid JSON.");
43+
throw new ConfigurationException("Given HTTPSink JSON body template: " + httpSinkJsonBodyTemplate + " must be a valid JSON.");
4344
}
4445
messageToTemplatizedJson.setPathsFromTemplate();
4546
return messageToTemplatizedJson;
4647
}
4748

48-
public MessageToTemplatizedJson(FirehoseInstrumentation firehoseInstrumentation, String httpSinkJsonBodyTemplate, Parser protoParser) {
49+
public MessageToTemplatizedJson(FirehoseInstrumentation firehoseInstrumentation, String httpSinkJsonBodyTemplate, Parser protoParser, Option option) {
4950
this.httpSinkJsonBodyTemplate = httpSinkJsonBodyTemplate;
5051
this.protoParser = protoParser;
5152
this.jsonParser = new JSONParser();
5253
this.gson = new Gson();
54+
this.jsonPathConfig = option == null
55+
? Configuration.defaultConfiguration()
56+
: Configuration.defaultConfiguration().addOptions(option);
5357
this.firehoseInstrumentation = firehoseInstrumentation;
5458
}
5559

@@ -81,15 +85,23 @@ public String serialize(Message message) throws DeserializerException {
8185
DynamicMessage msg = protoParser.parse(message.getLogMessage());
8286
jsonMessage = JsonFormat.printer().includingDefaultValueFields().preservingProtoFieldNames().print(msg);
8387
String finalMessage = httpSinkJsonBodyTemplate;
88+
8489
for (String path : pathsToReplace) {
8590
if (path.equals(ALL_FIELDS_FROM_TEMPLATE)) {
8691
jsonString = jsonMessage;
8792
} else {
88-
Object element = JsonPath.read(jsonMessage, path.replaceAll("\"", ""));
89-
jsonString = gson.toJson(element);
93+
Object element = JsonPath.using(jsonPathConfig).parse(jsonMessage).read(path.replaceAll("\"", ""));
94+
if (element == null && (jsonPathConfig.getOptions().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)
95+
|| jsonPathConfig.getOptions().contains(Option.SUPPRESS_EXCEPTIONS))) {
96+
firehoseInstrumentation.logWarn("Missing value for path: {}", path);
97+
jsonString = "";
98+
} else {
99+
jsonString = gson.toJson(element);
100+
}
90101
}
91102
finalMessage = finalMessage.replace(path, jsonString);
92103
}
104+
93105
return finalMessage;
94106
} catch (InvalidProtocolBufferException | PathNotFoundException e) {
95107
throw new DeserializerException(e.getMessage());

src/main/java/com/gotocompany/firehose/sink/http/factory/SerializerFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public MessageSerializer build() {
3939
} else {
4040
firehoseInstrumentation.logDebug("Serializer type: EsbMessageToTemplatizedJson");
4141
return getTypecastedJsonSerializer(
42-
MessageToTemplatizedJson.create(new FirehoseInstrumentation(statsDReporter, MessageToTemplatizedJson.class), httpSinkConfig.getSinkHttpJsonBodyTemplate(), protoParser));
42+
MessageToTemplatizedJson.create(new FirehoseInstrumentation(statsDReporter, MessageToTemplatizedJson.class), httpSinkConfig.getSinkHttpJsonBodyTemplate(), protoParser, httpSinkConfig.getSinkHttpJsonBodyTemplateParseOption()));
4343
}
4444
}
4545

0 commit comments

Comments
 (0)