Conversation
Add support for SLF4J 2.0 structured logging key-value pairs in EventJsonLayout. This allows developers to add structured data to individual log entries using the addKeyValue() API. Closes dropwizard#9734
|
I left it in draft because I forgot to ask my employer for permission, but I'm currently in the process of getting it. |
|
Put it again on review. I got the approval, but I didn't do it at the moment and forgot for some time 🙈 |
There was a problem hiding this comment.
Pull request overview
This pull request adds support for SLF4J 2.0 structured logging key-value pairs in the JSON logging layout. The implementation follows the existing MDC pattern, allowing developers to add structured data to log entries using the addKeyValue() API. By default, key-value pairs are logged in nested format under the keyValuePairs field, but can be filtered and flattened using new configuration parameters.
Changes:
- Added
KEY_VALUE_PAIRSto theEventAttributeenum to allow enabling/disabling this feature - Implemented key-value pair handling in
EventJsonLayoutwith filtering and flattening support - Added configuration properties
includesKeyValuePairsKeysandflattenKeyValuePairstoEventJsonLayoutBaseFactory - Enhanced
MapBuilderto handle Boolean values and generic Objects with type-aware serialization - Added comprehensive test coverage for all new functionality
- Documented the structured logging feature and configuration options
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| dropwizard-json-logging/src/main/java/io/dropwizard/logging/json/EventAttribute.java | Added KEY_VALUE_PAIRS enum value to support the new attribute |
| dropwizard-json-logging/src/main/java/io/dropwizard/logging/json/layout/EventJsonLayout.java | Core implementation: added logic to process, filter, and flatten key-value pairs similar to MDC handling |
| dropwizard-json-logging/src/main/java/io/dropwizard/logging/json/EventJsonLayoutBaseFactory.java | Added configuration properties for key-value pairs (includesKeyValuePairsKeys, flattenKeyValuePairs) and included KEY_VALUE_PAIRS in default attributes |
| dropwizard-json-logging/src/main/java/io/dropwizard/logging/json/layout/MapBuilder.java | Added addBoolean and generic add methods to support proper type serialization of key-value pair values |
| dropwizard-json-logging/src/test/java/io/dropwizard/logging/json/layout/EventJsonLayoutTest.java | Comprehensive test coverage for nested/flattened formats, filtering, null handling, and type conversion |
| dropwizard-json-logging/src/test/java/io/dropwizard/logging/json/LayoutIntegrationTests.java | Integration tests verifying configuration deserialization and end-to-end logging with key-value pairs |
| dropwizard-json-logging/src/test/resources/yaml/json-log.yml | Test configuration demonstrating key-value pairs configuration options |
| docs/source/manual/core.rst | Added "Structured Logging with JSON" section with usage examples and output format examples |
| docs/source/manual/configuration.rst | Updated JSON layout configuration documentation with new key-value pairs settings |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| @Test | ||
| void testLogsFlattenedKeyValuePairsFilteringNullValues() { | ||
| when(event.getKeyValuePairs()).thenReturn(List.of(new KeyValuePair("test", "value"),new KeyValuePair("testNull", null))); |
There was a problem hiding this comment.
Missing space after comma between the two KeyValuePair arguments. Should be: new KeyValuePair("test", "value"), new KeyValuePair("testNull", null)
| void testFilterKeyValueKeys() { | ||
| final Set<String> includesKeyValuePairsKeys = Set.of("test"); | ||
| when(event.getKeyValuePairs()).thenReturn(List.of(new KeyValuePair("test", "value"), new KeyValuePair("test2", "value2"))); | ||
| eventJsonLayout= new EventJsonLayout(jsonFormatter, timestampFormatter, throwableProxyConverter, DEFAULT_EVENT_ATTRIBUTES, |
There was a problem hiding this comment.
Missing space before the assignment operator. Should be: eventJsonLayout = instead of eventJsonLayout=
| eventJsonLayout= new EventJsonLayout(jsonFormatter, timestampFormatter, throwableProxyConverter, DEFAULT_EVENT_ATTRIBUTES, | |
| eventJsonLayout = new EventJsonLayout(jsonFormatter, timestampFormatter, throwableProxyConverter, DEFAULT_EVENT_ATTRIBUTES, |
| @Test | ||
| void testFilterAndFlattenKeyValueKeys() { | ||
| final Set<String> includesKeyValuePairsKeys = Set.of("test"); | ||
| when(event.getKeyValuePairs()).thenReturn(java.util.List.of(new KeyValuePair("test", "value"), new KeyValuePair("test2", "value2"))); |
There was a problem hiding this comment.
Use List.of consistently instead of the fully qualified java.util.List.of. All other test methods in this file use the shorter form List.of.
| when(event.getKeyValuePairs()).thenReturn(java.util.List.of(new KeyValuePair("test", "value"), new KeyValuePair("test2", "value2"))); | |
| when(event.getKeyValuePairs()).thenReturn(List.of(new KeyValuePair("test", "value"), new KeyValuePair("test2", "value2"))); |
| } | ||
|
|
||
| final boolean includeKeyValuePairs = isIncluded(EventAttribute.KEY_VALUE_PAIRS); | ||
| if(includeKeyValuePairs && event.getKeyValuePairs() != null) { |
There was a problem hiding this comment.
Missing space after if keyword. Should be: if (includeKeyValuePairs instead of if(includeKeyValuePairs
| if(includeKeyValuePairs && event.getKeyValuePairs() != null) { | |
| if (includeKeyValuePairs && event.getKeyValuePairs() != null) { |
| import io.dropwizard.logging.common.DefaultLoggingFactory; | ||
| import io.dropwizard.request.logging.LogbackAccessRequestLogFactory; | ||
| import io.dropwizard.validation.BaseValidator; | ||
| import jdk.jfr.Event; |
There was a problem hiding this comment.
The import jdk.jfr.Event is unused and should be removed.
| import jdk.jfr.Event; |
|
|
||
| @Test | ||
| void testLogsKeyValuePairsFilteringNullValues() { | ||
| when(event.getKeyValuePairs()).thenReturn(List.of(new KeyValuePair("test", "value"),new KeyValuePair("testNull", null))); |
There was a problem hiding this comment.
Missing space after comma between the two KeyValuePair arguments. Should be: new KeyValuePair("test", "value"), new KeyValuePair("testNull", null)
| * Adds the value to the provided map under the provided field name if it should be included. | ||
| * If the value is a String, Number, or Boolean it's added as it is, otherwise it's converted to string using `toString()`. | ||
| */ | ||
| public MapBuilder add(String fieldName, boolean include, @Nullable Object value) { |
There was a problem hiding this comment.
Method MapBuilder.add(..) could be confused with overloaded method add, since dispatch depends on static types.
Method MapBuilder.add(..) could be confused with overloaded method add, since dispatch depends on static types.
Method MapBuilder.add(..) could be confused with overloaded method add, since dispatch depends on static types.
| * Adds the value to the provided map under the provided field name if it should be included. | ||
| * If the value is a String, Number, or Boolean it's added as it is, otherwise it's converted to string using `toString()`. | ||
| */ | ||
| public MapBuilder add(String fieldName, boolean include, @Nullable Object value) { |
Check notice
Code scanning / CodeQL
Confusing overloading of methods Note
Add support for SLF4J 2.0 structured logging key-value pairs in EventJsonLayout. This allows developers to add structured data to individual log entries using the addKeyValue() API.
Problem:
Solves issue #9734
Support for SLF4J 2.0 structured logging in JSON Layout.
Solution:
Implement keyValuePairs handling into
EventJsonLayoutin the same way MDC values are handled.By default all pairs are logged in nested format but they can be filtered and flattened using configuration parameters.
KEY_VALUE_PAIRS has been added to EventAttribute enum to be able to disable it.
Result:
Given
by default is rendered into:
{ "timestamp": "2024-01-02T15:19:21.000+0000", "level": "INFO", "message": "User login completed", "keyValuePairs": { "userId": "12345", "action": "login", "duration": 150 } }