Skip to content

Commit b525309

Browse files
Konstantin Pavlovilayaperumalg
authored andcommitted
Introduce PII marker for logging sensitive data
Introduce a utility class `LoggingMarkers` providing an SLF4J marker for tagging log entries with Personally Identifiable Information (PII). Update `BeanOutputConverter` to use the `PII_MARKER` in error logs for invalid JSON conversions. Enhance tests to verify PII marker usage in logging. - Set Java version dynamically and configure Kotlin compiler - Updated Maven configurations to dynamically reference the Java version using `${java.version}`. Added Kotlin compiler settings, including `jvmTarget` alignment with Java version and enabling `javaParameters`. This ensures consistency and better compatibility across builds. - Fix log assertion in BeanOutputConverterTest to use Java 17 - Updated the test to assert log size explicitly before accessing the first log entry. This ensures the test is more robust and avoids potential issues with accessing logs unexpectedly. - Use placeholders in logger.error to prevent string concatenation. - Replaced string concatenation with a placeholder in the logger.error call to improve performance and maintain consistency with logging best practices. This helps avoid unnecessary overhead when logging is disabled. - Update logging markers and improve data classification - Replaced `PII_MARKER` with `SENSITIVE_DATA_MARKER`. Introduced `RESTRICTED_DATA_MARKER`, `REGULATED_DATA_MARKER` and `PUBLIC_DATA_MARKER` - Updated associated logging logic and tests to reflect these changes. - Fix punctuation in Javadoc comments for LoggingMarkers. - Added missing periods to improve consistency and clarity in the Javadoc comments. This change ensures proper formatting and adheres to standard writing conventions. Signed-off-by: Konstantin Pavlov <{ID}+{username}@users.noreply.github.com>
1 parent 74178b9 commit b525309

File tree

5 files changed

+138
-9
lines changed

5 files changed

+138
-9
lines changed

pom.xml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,9 @@
168168
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
169169
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
170170
<java.version>17</java.version>
171-
<maven.compiler.source>17</maven.compiler.source>
172-
<maven.compiler.target>17</maven.compiler.target>
171+
<maven.compiler.source>${java.version}</maven.compiler.source>
172+
<maven.compiler.target>${java.version}</maven.compiler.target>
173+
<kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget>
173174

174175
<!-- production dependencies -->
175176
<spring-boot.version>3.3.6</spring-boot.version>
@@ -344,6 +345,10 @@
344345
<groupId>org.jetbrains.kotlin</groupId>
345346
<artifactId>kotlin-maven-plugin</artifactId>
346347
<version>${kotlin.version}</version>
348+
<configuration>
349+
<jvmTarget>${java.version}</jvmTarget>
350+
<javaParameters>true</javaParameters>
351+
</configuration>
347352
<executions>
348353
<execution>
349354
<id>compile</id>

spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
import org.springframework.core.ParameterizedTypeReference;
4242
import org.springframework.lang.NonNull;
4343

44+
import static org.springframework.ai.util.LoggingMarkers.SENSITIVE_DATA_MARKER;
45+
4446
/**
4547
* An implementation of {@link StructuredOutputConverter} that transforms the LLM output
4648
* to a specific object type using JSON schema. This converter works by generating a JSON
@@ -143,7 +145,7 @@ private void generateSchema() {
143145
this.jsonSchema = objectWriter.writeValueAsString(jsonNode);
144146
}
145147
catch (JsonProcessingException e) {
146-
logger.error("Could not pretty print json schema for jsonNode: " + jsonNode);
148+
logger.error("Could not pretty print json schema for jsonNode: {}", jsonNode);
147149
throw new RuntimeException("Could not pretty print json schema for " + this.type, e);
148150
}
149151
}
@@ -180,7 +182,8 @@ public T convert(@NonNull String text) {
180182
return (T) this.objectMapper.readValue(text, this.objectMapper.constructType(this.type));
181183
}
182184
catch (JsonProcessingException e) {
183-
logger.error("Could not parse the given text to the desired target type:" + text + " into " + this.type);
185+
logger.error(SENSITIVE_DATA_MARKER,
186+
"Could not parse the given text to the desired target type: \"{}\" into {}", text, this.type);
184187
throw new RuntimeException(e);
185188
}
186189
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.util;
18+
19+
import org.slf4j.Marker;
20+
import org.slf4j.MarkerFactory;
21+
22+
/**
23+
* Utility class that provides predefined SLF4J {@link Marker} instances used in logging
24+
* operations within the application. <br>
25+
* This class is not intended to be instantiated, but is open for extension.
26+
*
27+
* @author Konstantin Pavlov
28+
*/
29+
public class LoggingMarkers {
30+
31+
/**
32+
* Marker used to identify log statements associated with <strong>sensitive
33+
* data</strong>, such as:
34+
* <ul>
35+
* <li>Internal business information</li>
36+
* <li>Employee data</li>
37+
* <li>Customer non-regulated data</li>
38+
* <li>Business processes and logic</li>
39+
* <li>etc.</li>
40+
* </ul>
41+
* Typically, logging this information should be avoided.
42+
*/
43+
public static final Marker SENSITIVE_DATA_MARKER = MarkerFactory.getMarker("SENSITIVE");
44+
45+
/**
46+
* Marker used to identify log statements associated with <strong>restricted
47+
* data</strong>, such as:
48+
* <ul>
49+
* <li>Authentication credentials</li>
50+
* <li>Keys and secrets</li>
51+
* <li>Core intellectual property</li>
52+
* <li>Critical security configs</li>
53+
* <li>Trade secrets</li>
54+
* <li>etc.</li>
55+
* </ul>
56+
* Logging of such information is usually prohibited in any circumstances.
57+
*/
58+
public static final Marker RESTRICTED_DATA_MARKER = MarkerFactory.getMarker("RESTRICTED");
59+
60+
/**
61+
* Marker used to identify log statements associated with <strong>regulated
62+
* data</strong>, such as:
63+
* <ul>
64+
* <li>PCI (credit card data)</li>
65+
* <li>PHI (health information)</li>
66+
* <li>PII (personally identifiable info)</li>
67+
* <li>Financial records</li>
68+
* <li>Compliance-controlled data</li>
69+
* <li>etc.</li>
70+
* </ul>
71+
* Logging of such information should be avoided.
72+
*/
73+
public static final Marker REGULATED_DATA_MARKER = MarkerFactory.getMarker("REGULATED");
74+
75+
/**
76+
* Marker used to identify log statements associated with <strong>public
77+
* data</strong>, such as:
78+
* <ul>
79+
* <li>Public documentation</li>
80+
* <li>Marketing materials</li>
81+
* <li>etc.</li>
82+
* </ul>
83+
* There are no restriction for logging such information.
84+
*/
85+
public static final Marker PUBLIC_DATA_MARKER = MarkerFactory.getMarker("PUBLIC");
86+
87+
}

spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,58 @@
1616

1717
package org.springframework.ai.converter;
1818

19-
import java.time.LocalDate;
20-
import java.util.ArrayList;
21-
import java.util.List;
22-
19+
import ch.qos.logback.classic.Logger;
20+
import ch.qos.logback.classic.spi.ILoggingEvent;
21+
import ch.qos.logback.core.read.ListAppender;
2322
import com.fasterxml.jackson.annotation.JsonProperty;
2423
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
2524
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
25+
import com.fasterxml.jackson.core.JsonParseException;
2626
import com.fasterxml.jackson.databind.DeserializationFeature;
2727
import com.fasterxml.jackson.databind.JsonNode;
2828
import com.fasterxml.jackson.databind.ObjectMapper;
29+
import org.junit.jupiter.api.BeforeEach;
2930
import org.junit.jupiter.api.Nested;
3031
import org.junit.jupiter.api.Test;
3132
import org.junit.jupiter.api.extension.ExtendWith;
3233
import org.mockito.Mock;
3334
import org.mockito.junit.jupiter.MockitoExtension;
34-
35+
import org.slf4j.LoggerFactory;
3536
import org.springframework.core.ParameterizedTypeReference;
3637

38+
import java.time.LocalDate;
39+
import java.util.ArrayList;
40+
import java.util.List;
41+
3742
import static org.assertj.core.api.Assertions.assertThat;
43+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
44+
import static org.springframework.ai.util.LoggingMarkers.SENSITIVE_DATA_MARKER;
3845

3946
/**
4047
* @author Sebastian Ullrich
4148
* @author Kirk Lund
4249
* @author Christian Tzolov
4350
* @author Soby Chacko
51+
* @author Konstantin Pavlov
4452
*/
4553
@ExtendWith(MockitoExtension.class)
4654
class BeanOutputConverterTest {
4755

56+
private ListAppender<ILoggingEvent> logAppender;
57+
4858
@Mock
4959
private ObjectMapper objectMapperMock;
5060

61+
@BeforeEach
62+
void beforeEach() {
63+
64+
var logger = (Logger) LoggerFactory.getLogger(BeanOutputConverter.class);
65+
66+
logAppender = new ListAppender<>();
67+
logAppender.start();
68+
logger.addAppender(logAppender);
69+
}
70+
5171
@Test
5272
void shouldHavePreConfiguredDefaultObjectMapper() {
5373
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<TestClass>() {
@@ -135,6 +155,19 @@ void convertClassType() {
135155
assertThat(testClass.getSomeString()).isEqualTo("some value");
136156
}
137157

158+
@Test
159+
void failToConvertInvalidJson() {
160+
var converter = new BeanOutputConverter<>(TestClass.class);
161+
assertThatThrownBy(() -> converter.convert("{invalid json")).hasCauseInstanceOf(JsonParseException.class);
162+
assertThat(logAppender.list).hasSize(1);
163+
final var loggingEvent = logAppender.list.get(0);
164+
assertThat(loggingEvent.getFormattedMessage())
165+
.isEqualTo("Could not parse the given text to the desired target type: \"{invalid json\" into "
166+
+ TestClass.class);
167+
168+
assertThat(loggingEvent.getMarkerList()).contains(SENSITIVE_DATA_MARKER);
169+
}
170+
138171
@Test
139172
void convertClassWithDateType() {
140173
var converter = new BeanOutputConverter<>(TestClassWithDateProperty.class);

spring-ai-docs/src/main/antora/modules/ROOT/pages/contribution-guidelines.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ you'll need to develop a low-level client API class. This often involves utilizi
3030
Ensure your client conforms to the link:https://docs.spring.io/spring-ai/reference/api/generic-model.html[Generic Model API].
3131
Use existing request and response classes if your model's inputs and outputs are supported.
3232
If not, create new classes for the Generic Model API and establish a new Java package.
33+
When 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.
3334

3435
. *Implement Auto-Configuration and a Spring Boot Starter*: This step involves creating the
3536
necessary auto-configuration and Spring Boot Starter to easily instantiate the new model with

0 commit comments

Comments
 (0)