Skip to content

Commit 35f138b

Browse files
vdsirotkinvdsirotkin
vdsirotkin
andauthored
Add JsonPath body filters (zalando#1022)
* add jsonpath body filters * Update README.md Co-authored-by: Vitaly Sirotkin <[email protected]>
1 parent 5b884bf commit 35f138b

File tree

6 files changed

+283
-1
lines changed

6 files changed

+283
-1
lines changed

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,23 @@ Logbook logbook = Logbook.builder()
235235

236236
You can configure as many filters as you want - they will run consecutively.
237237

238+
##### JsonPath body filtering (experimental)
239+
240+
You can now use [json path](https://github.com/json-path/JsonPath) filtering in HTTP logging. Here are some examples:
241+
242+
```java
243+
Logbook logbook = Logbook.builder()
244+
.bodyFilter(JsonPathBodyFilters.jsonPath("$.password").delete()) // 1
245+
.bodyFilter(JsonPathBodyFilters.jsonPath("$.password").replace("XXX")) // 2
246+
.bodyFilter(JsonPathBodyFilters.jsonPath("$.cardNumber").replace("(\\d{6})\\d+(\\d{4})"), "$1********$2") // 3
247+
.build();
248+
```
249+
250+
Explanation:
251+
1. Just deletes all JsonPath matches.
252+
2. Replaces all JsonPath matches with given value. Strings, numbers and booleans are supported.
253+
3. Replaces all JsonPath matches using `java.util.regex.Matcher#replaceAll`. This can be usually used when you need some sort of masking. In this example Logbook will replace string `"5131456812321545"` with `"513145******1545"`.
254+
238255
#### Correlation
239256

240257
Logbook uses a *correlation id* to correlate requests and responses. This allows match-related requests and responses that would usually be located in different places in the log file.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.zalando.logbook.json;
2+
3+
import org.openjdk.jmh.annotations.*;
4+
import org.openjdk.jmh.runner.Runner;
5+
import org.openjdk.jmh.runner.RunnerException;
6+
import org.openjdk.jmh.runner.options.Options;
7+
import org.openjdk.jmh.runner.options.OptionsBuilder;
8+
9+
import java.util.concurrent.TimeUnit;
10+
11+
import static org.zalando.logbook.json.JsonPathBodyFilters.jsonPath;
12+
13+
@Fork(value = 1, warmups = 1)
14+
@Warmup(iterations = 2, time = 3, timeUnit = TimeUnit.SECONDS)
15+
@BenchmarkMode(Mode.Throughput)
16+
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS)
17+
public class JsonPathBodyFilterBenchmark {
18+
19+
public static final String BODY = "{\"test\": \"5213486633218931\", \"test123\": \"5213486633218931\"}";
20+
public static final String CONTENT_TYPE = "application/json";
21+
22+
@Benchmark
23+
public void replaceStringDynamicallyBenchmark() {
24+
jsonPath("$.test").replace("(\\d{6})\\d+(\\d{4})", "$1********$2")
25+
.filter(CONTENT_TYPE, BODY);
26+
}
27+
28+
@Benchmark
29+
public void replaceStringJsonPathBenchmark() {
30+
jsonPath("$.test").replace("***")
31+
.filter(CONTENT_TYPE, BODY);
32+
}
33+
34+
@Benchmark
35+
public void replaceStringPrimitiveBenchmark() {
36+
JsonBodyFilters.replaceJsonStringProperty(s -> s.equals("test"), "***").filter(CONTENT_TYPE, BODY);
37+
}
38+
39+
public static void main(final String[] args) throws RunnerException {
40+
final Options options = new OptionsBuilder().include(JsonPathBodyFilterBenchmark.class.getSimpleName())
41+
.forks(1).build();
42+
new Runner(options).run();
43+
}
44+
45+
46+
}

logbook-json/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
<groupId>com.fasterxml.jackson.core</groupId>
3232
<artifactId>jackson-annotations</artifactId>
3333
</dependency>
34+
<dependency>
35+
<groupId>com.jayway.jsonpath</groupId>
36+
<artifactId>json-path</artifactId>
37+
</dependency>
3438
<!-- testing -->
3539
<dependency>
3640
<groupId>org.zalando</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package org.zalando.logbook.json;
2+
3+
import com.jayway.jsonpath.Configuration;
4+
import com.jayway.jsonpath.DocumentContext;
5+
import com.jayway.jsonpath.JsonPath;
6+
import com.jayway.jsonpath.Option;
7+
import lombok.AccessLevel;
8+
import lombok.NoArgsConstructor;
9+
import lombok.RequiredArgsConstructor;
10+
import org.apiguardian.api.API;
11+
import org.zalando.logbook.BodyFilter;
12+
13+
import javax.annotation.Nullable;
14+
import java.util.List;
15+
import java.util.Optional;
16+
import java.util.function.Function;
17+
import java.util.regex.Matcher;
18+
import java.util.regex.Pattern;
19+
20+
@API(status = API.Status.EXPERIMENTAL)
21+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
22+
public final class JsonPathBodyFilters {
23+
24+
private static final Configuration conf = Configuration.builder().options(Option.AS_PATH_LIST).build();
25+
26+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
27+
public static final class JsonPathBodyFilterBuilder {
28+
private final JsonPath jsonPath;
29+
30+
public BodyFilter delete() {
31+
return (JsonBodyFilter) documentContext -> documentContext.delete(jsonPath).jsonString();
32+
}
33+
34+
public BodyFilter replace(String replacement) {
35+
return replaceInternal(replacement);
36+
}
37+
38+
public BodyFilter replace(Number replacement) {
39+
return replaceInternal(replacement);
40+
}
41+
42+
public BodyFilter replace(Boolean replacement) {
43+
return replaceInternal(replacement);
44+
}
45+
46+
public BodyFilter replace(String matchRegex, String replacementRegex) {
47+
Pattern pattern = Pattern.compile(matchRegex);
48+
return (JsonBodyFilter) documentContext -> {
49+
final List<String> paths = documentContext.read(jsonPath);
50+
final Object document = documentContext.json();
51+
paths.forEach(path -> {
52+
final Object node = JsonPath.compile(path).read(document);
53+
replaceWithRegex(node, pattern, replacementRegex)
54+
.ifPresent(s -> documentContext.set(path, s));
55+
});
56+
return documentContext.jsonString();
57+
};
58+
}
59+
60+
private BodyFilter replaceInternal(Object replacement) {
61+
return (JsonBodyFilter) documentContext -> documentContext.set(jsonPath, replacement).jsonString();
62+
}
63+
64+
private Optional<String> replaceWithRegex(Object node, Pattern pattern, String replacementRegex) {
65+
String stringRepresentation = node.toString();
66+
Matcher matcher = pattern.matcher(stringRepresentation);
67+
if (matcher.find()) {
68+
return Optional.of(matcher.replaceAll(replacementRegex));
69+
}
70+
return Optional.empty();
71+
}
72+
73+
}
74+
75+
private interface JsonBodyFilter extends BodyFilter, Function<DocumentContext, String> {
76+
@Override
77+
default String filter(@Nullable String contentType, String body) {
78+
if (JsonMediaType.JSON.test(contentType)) {
79+
return apply(JsonPath.using(conf).parse(body));
80+
}
81+
return body;
82+
}
83+
}
84+
85+
public static JsonPathBodyFilterBuilder jsonPath(String jsonPath) {
86+
return new JsonPathBodyFilterBuilder(JsonPath.compile(jsonPath));
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.zalando.logbook.json;
2+
3+
import com.jayway.jsonpath.Configuration;
4+
import com.jayway.jsonpath.Option;
5+
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
6+
import com.jayway.jsonpath.spi.json.JsonProvider;
7+
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
8+
import com.jayway.jsonpath.spi.mapper.MappingProvider;
9+
import org.junit.jupiter.api.BeforeAll;
10+
import org.junit.jupiter.api.Test;
11+
import org.zalando.logbook.BodyFilter;
12+
13+
import java.util.HashSet;
14+
import java.util.Set;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.zalando.logbook.json.JsonPathBodyFilters.jsonPath;
18+
19+
class JsonPathBodyFiltersTest {
20+
21+
public static final String CONTENT_TYPE = "application/json";
22+
23+
@Test
24+
public void deleteObjectTest() {
25+
BodyFilter filter = jsonPath("$.test.test123").delete();
26+
String result = filter.filter(CONTENT_TYPE, "{\"test\": {\"test123\":\"testvalue\"}, \"test2\":\"value\"}");
27+
assertThat(result).isEqualTo("{\"test\":{},\"test2\":\"value\"}");
28+
}
29+
30+
@Test
31+
public void replaceArrayWithStringTest() {
32+
BodyFilter filter = jsonPath("$.test").replace("XXX");
33+
String result = filter.filter(CONTENT_TYPE, "{\"test\": [\"test123\", \"test321\", 1.0], \"test123\": \"testing\"}");
34+
assertThat(result).isEqualTo("{\"test\":\"XXX\",\"test123\":\"testing\"}");
35+
}
36+
37+
@Test
38+
public void replaceNumberWithStringTest() {
39+
BodyFilter filter = jsonPath("$.test").replace("XXX");
40+
String result = filter.filter(CONTENT_TYPE, "{\"test\": 1, \"test123\": \"testing\"}");
41+
assertThat(result).isEqualTo("{\"test\":\"XXX\",\"test123\":\"testing\"}");
42+
}
43+
44+
@Test
45+
public void replaceArrayWithNumberTest() {
46+
BodyFilter filter = jsonPath("$.test").replace(10.0);
47+
String result = filter.filter(CONTENT_TYPE, "{\"test\": [\"test123\", \"test321\", 1.0], \"test123\": \"testing\"}");
48+
assertThat(result).isEqualTo("{\"test\":10.0,\"test123\":\"testing\"}");
49+
}
50+
51+
@Test
52+
public void replaceNumberWithNumberTest() {
53+
BodyFilter filter = jsonPath("$.test").replace(10.0);
54+
String result = filter.filter(CONTENT_TYPE, "{\"test\": 1, \"test123\": \"testing\"}");
55+
assertThat(result).isEqualTo("{\"test\":10.0,\"test123\":\"testing\"}");
56+
}
57+
58+
@Test
59+
public void replaceArrayWithBooleanTest() {
60+
BodyFilter filter = jsonPath("$.test").replace(true);
61+
String result = filter.filter(CONTENT_TYPE, "{\"test\": [\"test123\", \"test321\", 1.0], \"test123\": \"testing\"}");
62+
assertThat(result).isEqualTo("{\"test\":true,\"test123\":\"testing\"}");
63+
}
64+
65+
@Test
66+
public void replaceNumberWithBooleanTest() {
67+
BodyFilter filter = jsonPath("$.test").replace(true);
68+
String result = filter.filter(CONTENT_TYPE, "{\"test\": 1, \"test123\": \"testing\"}");
69+
assertThat(result).isEqualTo("{\"test\":true,\"test123\":\"testing\"}");
70+
}
71+
72+
@Test
73+
public void replaceStringDynamicallyTest() {
74+
BodyFilter filter = jsonPath("$.test").replace("(\\d{6})\\d+(\\d{4})", "$1******$2");
75+
String result = filter.filter(CONTENT_TYPE, "{\"test\": \"5213486633218931\", \"test123\": \"5213486633218931\"}");
76+
assertThat(result).isEqualTo("{\"test\":\"521348******8931\",\"test123\":\"5213486633218931\"}");
77+
}
78+
79+
@Test
80+
public void replaceArrayDynamicallyTest() {
81+
BodyFilter filter = jsonPath("$.test").replace("(\\d{6})\\d+(\\d{4})", "$1******$2");
82+
String result = filter.filter(CONTENT_TYPE, "{\"test\": [\"5213486633218931\", \"123\", {}, true], \"test123\": \"5213486633218931\"}");
83+
assertThat(result).isEqualTo("{\"test\":\"[\\\"521348******8931\\\",\\\"123\\\",{},true]\",\"test123\":\"5213486633218931\"}");
84+
}
85+
86+
@Test
87+
public void replaceArrayInRightWayDynamicallyTest() {
88+
BodyFilter filter = jsonPath("$.test.*").replace("(\\d{6})\\d+(\\d{4})", "$1******$2");
89+
String result = filter.filter(CONTENT_TYPE, "{\"test\": [\"5213486633218931\", \"123\", {}, true], \"test123\": \"5213486633218931\"}");
90+
assertThat(result).isEqualTo("{\"test\":[\"521348******8931\",\"123\",{},true],\"test123\":\"5213486633218931\"}");
91+
}
92+
93+
@Test
94+
public void replaceObjectDynamicallyTest() {
95+
BodyFilter filter = jsonPath("$.test").replace("(\\d{6})\\d+(\\d{4})", "$1******$2");
96+
String result = filter.filter(CONTENT_TYPE, "{\"test\": {\"321test\": \"5213486633218931\"}, \"test123\": \"5213486633218931\"}");
97+
assertThat(result).isEqualTo("{\"test\":\"{321test=521348******8931}\",\"test123\":\"5213486633218931\"}");
98+
}
99+
100+
@Test
101+
public void unsuccessfullReplaceStringDynamicallyTest() {
102+
BodyFilter filter = jsonPath("$.test").replace("\\s+", "$1********$2");
103+
String result = filter.filter(CONTENT_TYPE, "{\"test\":5213, \"test123\": \"5213486633218931\"}");
104+
assertThat(result).isEqualTo("{\"test\":5213,\"test123\":\"5213486633218931\"}");
105+
}
106+
107+
@Test
108+
public void unsuccessfullReplaceNumberDynamicallyTest() {
109+
BodyFilter filter = jsonPath("$.test").replace("\\s+", "$1********$2");
110+
String result = filter.filter(CONTENT_TYPE, "{\"test\": \"5213486633218931\", \"test123\": \"5213486633218931\"}");
111+
assertThat(result).isEqualTo("{\"test\":\"5213486633218931\",\"test123\":\"5213486633218931\"}");
112+
}
113+
114+
@Test
115+
public void contentTypeTest() {
116+
BodyFilter filter = jsonPath("$.test").replace("XXX");
117+
String result = filter.filter("application/xml", "{\"test\": \"value\"}");
118+
assertThat(result).isEqualTo("{\"test\": \"value\"}");
119+
}
120+
}

pom.xml

+8-1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
<maven.compiler.target>${java.version}</maven.compiler.target>
7676
<slf4j.version>1.7.30</slf4j.version>
7777
<jackson.version>2.12.3</jackson.version>
78+
<json-path.version>2.5.0</json-path.version>
7879
<reactor-netty.version>1.0.6</reactor-netty.version>
7980

8081
<spring4.version>4.3.30.RELEASE</spring4.version>
@@ -258,6 +259,12 @@
258259
<version>4.0.1</version>
259260
<scope>provided</scope>
260261
</dependency>
262+
<!-- JsonPath -->
263+
<dependency>
264+
<groupId>com.jayway.jsonpath</groupId>
265+
<artifactId>json-path</artifactId>
266+
<version>${json-path.version}</version>
267+
</dependency>
261268
<!-- JUnit (has to come before spring...) -->
262269
<dependency>
263270
<groupId>org.junit</groupId>
@@ -306,7 +313,7 @@
306313
<dependency>
307314
<groupId>com.jayway.jsonpath</groupId>
308315
<artifactId>json-path-assert</artifactId>
309-
<version>2.5.0</version>
316+
<version>${json-path.version}</version>
310317
<scope>test</scope>
311318
<exclusions>
312319
<exclusion>

0 commit comments

Comments
 (0)