Skip to content

Commit 1392997

Browse files
authored
Allow using precise floats in logs (#2005)
* Allow using precise floats in logs * extract usePreciseFloats check into a separate method * add a comment about performance penalty * add strategy to handle different ways of handling floats ...because no one likes boolean flags anymore ¯\_(ツ)_/¯ * leve only one level of wrappers (remove creators) * update README
1 parent efe904b commit 1392997

14 files changed

+169
-15
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,20 @@ a JSON response body will **not** be escaped and represented as a string:
534534
}
535535
```
536536

537+
538+
> [!NOTE]
539+
> Logbook is using [BodyFilters](#Filtering) to inline json payload or to find fields for obfuscation.
540+
> Filters for JSON bodies are using Jackson, which comes with a defect of dropping off precision from floating point
541+
> numbers (see [FasterXML/jackson-core/issues/984](https://github.com/FasterXML/jackson-core/issues/984)).
542+
>
543+
> This can be changed by passing different `JsonGeneratorWrapper` implementations to the filter respective filters.
544+
> Available wrappers:
545+
> * `DefaultJsonGeneratorWrapper` - default implementation, which doesn't alter Jackson's `JsonGenerator` behavior
546+
> * `NumberAsStringJsonGeneratorWrapper` - writes floating point numbers as strings, and preserves their precision.
547+
> * `PreciseFloatJsonGeneratorWrapper` - writes floating point with precision, may lead to a performance penalty as
548+
> BigDecimal is usually used as the representation accessed from JsonParser.
549+
550+
537551
##### Common Log Format
538552

539553
The Common Log Format ([CLF](https://httpd.apache.org/docs/trunk/logs.html#common)) is a standardized text file format used by web servers when generating server log files. The format is supported via

logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public final class CompactingJsonBodyFilter implements BodyFilter {
2121

2222
private final JsonCompactor compactor;
2323

24+
public CompactingJsonBodyFilter(final JsonGeneratorWrapper jsonGeneratorWrapper) {
25+
this(new ParsingJsonCompactor(jsonGeneratorWrapper));
26+
}
27+
2428
public CompactingJsonBodyFilter() {
2529
this(new ParsingJsonCompactor());
2630
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.zalando.logbook.json;
2+
3+
4+
final class DefaultJsonGeneratorWrapper implements JsonGeneratorWrapper {
5+
6+
}

logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,20 @@ public class JacksonJsonFieldBodyFilter implements BodyFilter {
2929
private final String replacement;
3030
private final Set<String> fields;
3131
private final JsonFactory factory;
32+
private final JsonGeneratorWrapper jsonGeneratorWrapper;
3233

33-
public JacksonJsonFieldBodyFilter(final Collection<String> fieldNames, final String replacement, final JsonFactory factory) {
34+
public JacksonJsonFieldBodyFilter(final Collection<String> fieldNames,
35+
final String replacement,
36+
final JsonFactory factory,
37+
final JsonGeneratorWrapper jsonGeneratorWrapper) {
3438
this.fields = new HashSet<>(fieldNames); // thread safe for reading
3539
this.replacement = replacement;
3640
this.factory = factory;
41+
this.jsonGeneratorWrapper = jsonGeneratorWrapper;
42+
}
43+
44+
public JacksonJsonFieldBodyFilter(final Collection<String> fieldNames, final String replacement, final JsonFactory factory) {
45+
this(fieldNames, replacement, factory, new DefaultJsonGeneratorWrapper());
3746
}
3847

3948
public JacksonJsonFieldBodyFilter(final Collection<String> fieldNames, final String replacement) {
@@ -53,8 +62,7 @@ public String filter(final String body) {
5362

5463
JsonToken nextToken;
5564
while ((nextToken = parser.nextToken()) != null) {
56-
57-
generator.copyCurrentEvent(parser);
65+
jsonGeneratorWrapper.copyCurrentEvent(generator, parser);
5866
if (nextToken == JsonToken.FIELD_NAME && fields.contains(parser.currentName())) {
5967
nextToken = parser.nextToken();
6068
generator.writeString(replacement);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.zalando.logbook.json;
2+
3+
import com.fasterxml.jackson.core.JsonGenerator;
4+
import com.fasterxml.jackson.core.JsonParser;
5+
6+
import java.io.IOException;
7+
8+
public interface JsonGeneratorWrapper {
9+
10+
default void copyCurrentEvent(final JsonGenerator delegate, final JsonParser parser) throws IOException {
11+
delegate.copyCurrentEvent(parser);
12+
}
13+
14+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.zalando.logbook.json;
2+
3+
import com.fasterxml.jackson.core.JsonGenerator;
4+
import com.fasterxml.jackson.core.JsonParser;
5+
import com.fasterxml.jackson.core.JsonToken;
6+
7+
import java.io.IOException;
8+
9+
final class NumberAsStringJsonGeneratorWrapper implements JsonGeneratorWrapper {
10+
11+
public void copyCurrentEvent(JsonGenerator delegate, JsonParser parser) throws IOException {
12+
if (parser.getCurrentToken() == JsonToken.VALUE_NUMBER_FLOAT) {
13+
delegate.writeString(parser.getValueAsString());
14+
} else {
15+
delegate.copyCurrentEvent(parser);
16+
}
17+
}
18+
}

logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,23 @@ final class ParsingJsonCompactor implements JsonCompactor {
1111

1212
private final JsonFactory factory;
1313

14+
private final JsonGeneratorWrapper jsonGeneratorWrapper;
15+
16+
public ParsingJsonCompactor(final JsonFactory factory, final JsonGeneratorWrapper jsonGeneratorWrapper) {
17+
this.factory = factory;
18+
this.jsonGeneratorWrapper = jsonGeneratorWrapper;
19+
}
20+
21+
public ParsingJsonCompactor(final JsonGeneratorWrapper jsonGeneratorWrapper) {
22+
this(new JsonFactory(), jsonGeneratorWrapper);
23+
}
24+
1425
public ParsingJsonCompactor() {
1526
this(new JsonFactory());
1627
}
1728

1829
public ParsingJsonCompactor(final JsonFactory factory) {
19-
this.factory = factory;
30+
this(factory, new DefaultJsonGeneratorWrapper());
2031
}
2132

2233
@Override
@@ -26,8 +37,9 @@ public String compact(final String json) throws IOException {
2637
final JsonParser parser = factory.createParser(json);
2738
final JsonGenerator generator = factory.createGenerator(output)) {
2839

40+
2941
while (parser.nextToken() != null) {
30-
generator.copyCurrentEvent(parser);
42+
jsonGeneratorWrapper.copyCurrentEvent(generator, parser);
3143
}
3244

3345
generator.flush();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.zalando.logbook.json;
2+
3+
import com.fasterxml.jackson.core.JsonGenerator;
4+
import com.fasterxml.jackson.core.JsonParser;
5+
6+
import java.io.IOException;
7+
8+
final class PreciseFloatJsonGeneratorWrapper implements JsonGeneratorWrapper {
9+
10+
@Override
11+
public void copyCurrentEvent(JsonGenerator delegate, JsonParser parser) throws IOException {
12+
delegate.copyCurrentEventExact(parser);
13+
}
14+
}

logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,16 @@
2020
public final class PrettyPrintingJsonBodyFilter implements BodyFilter {
2121

2222
private final JsonFactory factory;
23+
private final JsonGeneratorWrapper jsonGeneratorWrapper;
2324

24-
public PrettyPrintingJsonBodyFilter(final JsonFactory factory) {
25+
public PrettyPrintingJsonBodyFilter(final JsonFactory factory,
26+
final JsonGeneratorWrapper jsonGeneratorWrapper) {
2527
this.factory = factory;
28+
this.jsonGeneratorWrapper = jsonGeneratorWrapper;
29+
}
30+
31+
public PrettyPrintingJsonBodyFilter(final JsonFactory factory) {
32+
this(factory, new DefaultJsonGeneratorWrapper());
2633
}
2734

2835
public PrettyPrintingJsonBodyFilter() {
@@ -52,7 +59,7 @@ public String filter(@Nullable final String contentType, final String body) {
5259
generator.useDefaultPrettyPrinter();
5360

5461
while (parser.nextToken() != null) {
55-
generator.copyCurrentEvent(parser);
62+
jsonGeneratorWrapper.copyCurrentEvent(generator, parser);
5663
}
5764

5865
generator.flush();

logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ class CompactingJsonBodyFilterTest {
1212
/*language=JSON*/
1313
private final String pretty = "{\n" +
1414
" \"root\": {\n" +
15-
" \"child\": \"text\"\n" +
15+
" \"child\": \"text\",\n" +
16+
" \"float_child\" : 0.40000000000000002" +
1617
" }\n" +
1718
"}";
1819

1920
/*language=JSON*/
20-
private final String compacted = "{\"root\":{\"child\":\"text\"}}";
21+
private final String compacted = "{\"root\":{\"child\":\"text\",\"float_child\":0.4}}";
2122

2223
@Test
2324
void shouldIgnoreEmptyBody() {
@@ -50,6 +51,22 @@ void shouldTransformValidJsonRequestWithCompatibleContentType() {
5051
assertThat(filtered).isEqualTo(compacted);
5152
}
5253

54+
@Test
55+
void shouldPreserveBigFloatOnCopy() {
56+
final String filtered = new CompactingJsonBodyFilter(new PreciseFloatJsonGeneratorWrapper())
57+
.filter("application/custom+json", pretty);
58+
final String compactedWithPreciseFloat = "{\"root\":{\"child\":\"text\",\"float_child\":0.40000000000000002}}";
59+
assertThat(filtered).isEqualTo(compactedWithPreciseFloat);
60+
}
61+
62+
@Test
63+
void shouldLogFloatAsString() {
64+
final String filtered = new CompactingJsonBodyFilter(new NumberAsStringJsonGeneratorWrapper())
65+
.filter("application/custom+json", pretty);
66+
final String compactedWithFloatAsString = "{\"root\":{\"child\":\"text\",\"float_child\":\"0.40000000000000002\"}}";
67+
assertThat(filtered).isEqualTo(compactedWithFloatAsString);
68+
}
69+
5370
@Test
5471
void shouldSkipInvalidJsonLookingLikeAValidOne() {
5572
final String invalidJson = "{invalid}";

logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package org.zalando.logbook.json;
22

3+
import com.fasterxml.jackson.core.JsonFactory;
34
import org.junit.jupiter.api.Test;
45

56
import java.io.IOException;
67
import java.nio.file.Files;
78
import java.nio.file.Paths;
89
import java.util.Arrays;
910
import java.util.Collection;
11+
import java.util.Collections;
1012
import java.util.HashSet;
1113
import java.util.Set;
1214

@@ -75,6 +77,23 @@ public void doesNotFilterNonJson() throws Exception {
7577
assertThat(filtered).contains("Ford");
7678
}
7779

80+
@Test
81+
public void shouldPreserveBigFloatOnCopy() throws Exception {
82+
final String string = getResource("/student.json").trim();
83+
final JacksonJsonFieldBodyFilter filter = new JacksonJsonFieldBodyFilter(Collections.emptyList(), "XXX", new JsonFactory(), new PreciseFloatJsonGeneratorWrapper());
84+
final String filtered = filter.filter("application/json", string);
85+
assertThat(filtered).contains("\"debt\":123450.40000000000000002");
86+
}
87+
88+
@Test
89+
public void shouldLogFloatAsStringOnCopy() throws Exception {
90+
final String string = getResource("/student.json").trim();
91+
final JacksonJsonFieldBodyFilter filter = new JacksonJsonFieldBodyFilter(Collections.singleton("balance"), "XXX", new JsonFactory(), new NumberAsStringJsonGeneratorWrapper());
92+
final String filtered = filter.filter("application/json", string);
93+
assertThat(filtered).contains("\"balance\":\"XXX\"");
94+
assertThat(filtered).contains("\"debt\":\"123450.40000000000000002\"");
95+
}
96+
7897
private String getResource(final String path) throws IOException {
7998
final byte[] bytes = Files.readAllBytes(Paths.get("src/test/resources/" + path));
8099
return new String(bytes, UTF_8);

logbook-json/src/test/java/org/zalando/logbook/json/JsonHttpLogFormatterTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,13 @@ void shouldNotEmbedReplacedJsonRequestBody(final HttpLogFormatter unit) throws I
204204
void shouldEmbedCustomJsonRequestBody(final HttpLogFormatter unit) throws IOException {
205205
final HttpRequest request = MockHttpRequest.create()
206206
.withContentType("application/custom+json")
207-
.withBodyAsString("{\"name\":\"Bob\"}");
207+
.withBodyAsString("{\"name\":\"Bob\", \"float_value\": 0.40000000000000002 }");
208208

209209
final String json = unit.format(new SimplePrecorrelation("", systemUTC()), request);
210210

211211
with(json)
212-
.assertEquals("$.body.name", "Bob");
212+
.assertEquals("$.body.name", "Bob")
213+
.assertEquals("$.body.float_value", 0.40000000000000002);
213214
}
214215

215216
@ParameterizedTest

logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.zalando.logbook.json;
22

3+
import com.fasterxml.jackson.core.JsonFactory;
34
import com.fasterxml.jackson.databind.ObjectMapper;
45
import org.junit.jupiter.api.Test;
56
import org.zalando.logbook.BodyFilter;
@@ -16,13 +17,23 @@ class PrettyPrintingJsonBodyFilterTest {
1617
private final String pretty = Stream.of(
1718
"{",
1819
" \"root\" : {",
19-
" \"child\" : \"text\"",
20+
" \"child\" : \"text\",",
21+
" \"float_child\" : 0.4",
22+
" }",
23+
"}"
24+
).collect(Collectors.joining(System.lineSeparator()));
25+
26+
private final String compactedWithPreciseFloat = Stream.of(
27+
"{",
28+
" \"root\" : {",
29+
" \"child\" : \"text\",",
30+
" \"float_child\" : 0.40000000000000002",
2031
" }",
2132
"}"
2233
).collect(Collectors.joining(System.lineSeparator()));
2334

2435
/*language=JSON*/
25-
private final String compacted = "{\"root\":{\"child\":\"text\"}}";
36+
private final String compacted = "{\"root\":{\"child\":\"text\", \"float_child\": 0.40000000000000002 }}";
2637

2738
@Test
2839
void shouldIgnoreEmptyBody() {
@@ -68,4 +79,11 @@ void shouldConstructFromObjectMapper() {
6879
assertThat(filtered).isEqualTo(pretty);
6980
}
7081

82+
@Test
83+
void shouldPreserveBigFloatOnCopy() {
84+
final String filtered = new PrettyPrintingJsonBodyFilter(new JsonFactory(), new PreciseFloatJsonGeneratorWrapper())
85+
.filter("application/json", compacted);
86+
assertThat(filtered).isEqualTo(compactedWithPreciseFloat);
87+
}
88+
7189
}

logbook-json/src/test/resources/student.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,7 @@
2020
"Science": 1.9,
2121
"PE": 4.0
2222
},
23-
"nickname": null
24-
}
23+
"nickname": null,
24+
"debt": 123450.40000000000000002,
25+
"balance": 0.40000000000000002
26+
}

0 commit comments

Comments
 (0)