diff --git a/pom.xml b/pom.xml index 84187c76c60..2badae5e717 100644 --- a/pom.xml +++ b/pom.xml @@ -168,8 +168,9 @@ UTF-8 UTF-8 17 - 17 - 17 + ${java.version} + ${java.version} + ${java.version} 3.3.6 @@ -344,6 +345,10 @@ org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} + + ${java.version} + true + compile diff --git a/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java index ea7cf314194..64b64a77a78 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java @@ -41,6 +41,8 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.lang.NonNull; +import static org.springframework.ai.util.LoggingMarkers.SENSITIVE_DATA_MARKER; + /** * An implementation of {@link StructuredOutputConverter} that transforms the LLM output * to a specific object type using JSON schema. This converter works by generating a JSON @@ -143,7 +145,7 @@ private void generateSchema() { this.jsonSchema = objectWriter.writeValueAsString(jsonNode); } catch (JsonProcessingException e) { - logger.error("Could not pretty print json schema for jsonNode: " + jsonNode); + logger.error("Could not pretty print json schema for jsonNode: {}", jsonNode); throw new RuntimeException("Could not pretty print json schema for " + this.type, e); } } @@ -180,7 +182,8 @@ public T convert(@NonNull String text) { return (T) this.objectMapper.readValue(text, this.objectMapper.constructType(this.type)); } catch (JsonProcessingException e) { - logger.error("Could not parse the given text to the desired target type:" + text + " into " + this.type); + logger.error(SENSITIVE_DATA_MARKER, + "Could not parse the given text to the desired target type: \"{}\" into {}", text, this.type); throw new RuntimeException(e); } } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/util/LoggingMarkers.java b/spring-ai-core/src/main/java/org/springframework/ai/util/LoggingMarkers.java new file mode 100644 index 00000000000..a4c31e3b6df --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/util/LoggingMarkers.java @@ -0,0 +1,69 @@ +package org.springframework.ai.util; + +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +/** + * Utility class that provides predefined SLF4J {@link Marker} instances used in logging + * operations within the application.
+ * This class is not intended to be instantiated, but is open for extension. + */ +public class LoggingMarkers { + + /** + * Marker used to identify log statements associated with sensitive + * data, such as: + * + * Typically, logging this information should be avoided. + */ + public static final Marker SENSITIVE_DATA_MARKER = MarkerFactory.getMarker("SENSITIVE"); + + /** + * Marker used to identify log statements associated with restricted + * data, such as: + * + * Logging of such information is usually prohibited in any circumstances. + */ + public static final Marker RESTRICTED_DATA_MARKER = MarkerFactory.getMarker("RESTRICTED"); + + /** + * Marker used to identify log statements associated with regulated + * data, such as: + * + * Logging of such information should be avoided. + */ + public static final Marker REGULATED_DATA_MARKER = MarkerFactory.getMarker("REGULATED"); + + /** + * Marker used to identify log statements associated with public + * data, such as: + * + * There are no restriction for logging such information. + */ + public static final Marker PUBLIC_DATA_MARKER = MarkerFactory.getMarker("PUBLIC"); + +} diff --git a/spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java b/spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java index ec3f5c5f92f..d8b83c0b115 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java @@ -16,25 +16,32 @@ package org.springframework.ai.converter; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; - +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - +import org.slf4j.LoggerFactory; import org.springframework.core.ParameterizedTypeReference; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.ai.util.LoggingMarkers.SENSITIVE_DATA_MARKER; /** * @author Sebastian Ullrich @@ -45,9 +52,21 @@ @ExtendWith(MockitoExtension.class) class BeanOutputConverterTest { + private ListAppender logAppender; + @Mock private ObjectMapper objectMapperMock; + @BeforeEach + void beforeEach() { + + var logger = (Logger) LoggerFactory.getLogger(BeanOutputConverter.class); + + logAppender = new ListAppender<>(); + logAppender.start(); + logger.addAppender(logAppender); + } + @Test void shouldHavePreConfiguredDefaultObjectMapper() { var converter = new BeanOutputConverter<>(new ParameterizedTypeReference() { @@ -135,6 +154,19 @@ void convertClassType() { assertThat(testClass.getSomeString()).isEqualTo("some value"); } + @Test + void failToConvertInvalidJson() { + var converter = new BeanOutputConverter<>(TestClass.class); + assertThatThrownBy(() -> converter.convert("{invalid json")).hasCauseInstanceOf(JsonParseException.class); + assertThat(logAppender.list).hasSize(1); + final var loggingEvent = logAppender.list.get(0); + assertThat(loggingEvent.getFormattedMessage()) + .isEqualTo("Could not parse the given text to the desired target type: \"{invalid json\" into " + + TestClass.class); + + assertThat(loggingEvent.getMarkerList()).contains(SENSITIVE_DATA_MARKER); + } + @Test void convertClassWithDateType() { var converter = new BeanOutputConverter<>(TestClassWithDateProperty.class); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/contribution-guidelines.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/contribution-guidelines.adoc index 28ecfad59c5..8776bdcc70d 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/contribution-guidelines.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/contribution-guidelines.adoc @@ -30,6 +30,7 @@ you'll need to develop a low-level client API class. This often involves utilizi Ensure your client conforms to the link:https://docs.spring.io/spring-ai/reference/api/generic-model.html[Generic Model API]. Use existing request and response classes if your model's inputs and outputs are supported. If not, create new classes for the Generic Model API and establish a new Java package. +Be careful with logging Personally Identifiable Information (PII), mark it with https://github.com/spring-projects/spring-ai/tree/main/spring-ai-core/src/main/java/org/springframework/ai/util/LoggingMarkers.java[`PII_MARKER`] Slf4j marker. . *Implement Auto-Configuration and a Spring Boot Starter*: This step involves creating the necessary auto-configuration and Spring Boot Starter to easily instantiate the new model with