Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

## 2.2.0
- [#453](https://github.com/wultra/java-core/issues/453) - Added `logging-support` module with `StructuredArgumentsConverter` for rendering `kv()` structured arguments in plain-text log patterns.
- [#451](https://github.com/wultra/java-core/issues/451) - Migrated to Spring Boot 4 and Jackson 3.

22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,3 +430,25 @@ Right now, these annotations are available:

- `PublicApi` - Marker for interfaces intended **to be called by extension**.
- `PublicSpi` - Marker for interfaces intended **to be implemented by extensions** and called by core.

## Wultra Logging Support

The `logging-support` module provides Logback utilities for structured logging in dev/test environments.

### StructuredArgumentsConverter

`StructuredArgumentsConverter` is a Logback `ClassicConverter` that renders `StructuredArguments.kv()` key-value pairs in plain-text log output. This is useful during development and testing when JSON output is not configured, making structured fields visible in the console.

**Usage in `logback.xml`:**
```xml
<conversionRule conversionWord="sa"
converterClass="com.wultra.core.logging.logback.StructuredArgumentsConverter"/>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%sa%n</pattern>
```

**Example output:**
```
10:35:22.123 [main] INFO c.w.EnrollmentService - action: createIdentity, state: succeeded {processId=abc-123, identityVerificationId=def-456}
```

When no structured arguments are present, the converter returns an empty string so no trailing whitespace appears in the log line.
6 changes: 6 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
<artifactId>rest-model-base</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>com.wultra.core</groupId>
<artifactId>logging-support</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down
44 changes: 44 additions & 0 deletions logging-support/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.wultra.core</groupId>
<artifactId>wultra-java-core-parent</artifactId>
<version>2.2.0-SNAPSHOT</version>
</parent>

<artifactId>logging-support</artifactId>
<name>wultra-core-logging-support</name>
<description>Logging support utilities for Wultra services</description>

<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>${logstash.version}</version>
</dependency>
Comment thread
vita-kotacka marked this conversation as resolved.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Comment thread
vita-kotacka marked this conversation as resolved.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2026 Wultra s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wultra.core.logging.logback;
Comment thread
vita-kotacka marked this conversation as resolved.

import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import net.logstash.logback.argument.StructuredArgument;

/**
* Logback {@link ClassicConverter} that renders {@link StructuredArgument} instances
* (e.g. from {@code StructuredArguments.kv()}) in plain-text log output.
*
* <p>Usage in {@code logback.xml}:
* <pre>{@code
* <conversionRule conversionWord="sa"
* converterClass="com.wultra.core.logging.logback.StructuredArgumentsConverter"/>
* <!-- Pattern example (no space before %sa — the converter prepends its own space) -->
* <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%sa%n</pattern>
* }</pre>
*
* <p>Output example:
* <pre>
* 10:35:22.123 [main] INFO c.w.EnrollmentService - action: createIdentity, state: succeeded {processId=abc-123, identityVerificationId=def-456}
* </pre>
*
* <p>When no structured arguments are present, an empty string is returned so no trailing
* whitespace appears in the log line.
*
* @author Vit Kotacka, vit.kotacka@wultra.com
*/
Comment thread
vita-kotacka marked this conversation as resolved.
public class StructuredArgumentsConverter extends ClassicConverter {

@Override
public String convert(final ILoggingEvent event) {
final Object[] args = event.getArgumentArray();
if (args == null || args.length == 0) {
return "";
}
final StringBuilder sb = new StringBuilder();
for (final Object arg : args) {
if (arg instanceof StructuredArgument) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(arg);
}
}
return sb.length() == 0 ? "" : " {" + sb + "}";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2026 Wultra s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wultra.core.logging.logback;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.LoggingEvent;
import org.junit.jupiter.api.Test;

import static net.logstash.logback.argument.StructuredArguments.kv;
import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* Tests for {@link StructuredArgumentsConverter}.
*
* @author Vit Kotacka, vit.kotacka@wultra.com
*/
class StructuredArgumentsConverterTest {
Comment thread
vita-kotacka marked this conversation as resolved.

private final StructuredArgumentsConverter converter = new StructuredArgumentsConverter();

@Test
void convert_withStructuredArguments_rendersKeyValuePairs() {
final LoggingEvent event = buildEvent("action: test, state: succeeded",
kv("processId", "abc-123"), kv("userId", "user-42"));

assertEquals(" {processId=abc-123, userId=user-42}", converter.convert(event));
}

@Test
void convert_withNoArguments_returnsEmpty() {
final LoggingEvent event = buildEvent("plain message");
assertEquals("", converter.convert(event));
}

@Test
void convert_withNullArgumentArray_returnsEmpty() {
final LoggingEvent event = buildEvent("plain message", (Object[]) null);
assertEquals("", converter.convert(event));
}

@Test
void convert_withMixedArguments_rendersOnlyStructuredOnes() {
final LoggingEvent event = buildEvent("msg {}", "plainValue", kv("id", "xyz"));
assertEquals(" {id=xyz}", converter.convert(event));
}

private static LoggingEvent buildEvent(final String message, final Object... args) {
final LoggerContext ctx = new LoggerContext();
final Logger logger = ctx.getLogger(StructuredArgumentsConverterTest.class);
return new LoggingEvent(
StructuredArgumentsConverterTest.class.getName(),
logger, Level.INFO, message, null, args);
}

}
2 changes: 2 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<module>http-common</module>
<module>rest-model-base</module>
<module>rest-client-base</module>
<module>logging-support</module>
</modules>

<properties>
Expand All @@ -64,6 +65,7 @@
<!-- Dependencies -->
<spring-boot.version>4.0.6</spring-boot.version>
<shedlock.version>7.7.0</shedlock.version>
<logstash.version>9.0</logstash.version>
</properties>

<dependencyManagement>
Expand Down
Loading