Skip to content

Commit 6fbd00b

Browse files
Add RequestLog URI filter
Add servlet init parameter `excludePatterns` to configure a comma-separate list of ant-like patterns (wildcards "*" and "**") to exclude uris from request log generation. This can be used to suppress request logs from health or metrics endpoints. Signed-off-by: Karsten Schnitter <k.schnitter@sap.com>
1 parent ff4d592 commit 6fbd00b

File tree

4 files changed

+279
-1
lines changed

4 files changed

+279
-1
lines changed

cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/GenerateRequestLogFilter.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ public class GenerateRequestLogFilter extends AbstractLoggingFilter {
2323

2424
public static final String WRAP_RESPONSE_INIT_PARAM = "wrapResponse";
2525
public static final String WRAP_REQUEST_INIT_PARAM = "wrapRequest";
26+
public static final String EXCLUDE_PATTERNS_INIT_PARAM = "excludePatterns";
2627

2728
private final RequestRecordFactory requestRecordFactory;
2829

2930
private boolean wrapResponse = true;
3031
private boolean wrapRequest = true;
32+
private RequestUriMatcher excludeMatcher = new RequestUriMatcher(null);
3133

3234
public GenerateRequestLogFilter() {
3335
this(new RequestRecordFactory(new LogOptionalFieldsSettings(GenerateRequestLogFilter.class.getName())));
@@ -47,6 +49,7 @@ public void init(FilterConfig filterConfig) throws ServletException {
4749
if ("false".equalsIgnoreCase(value)) {
4850
wrapRequest = false;
4951
}
52+
excludeMatcher = new RequestUriMatcher(filterConfig.getInitParameter(EXCLUDE_PATTERNS_INIT_PARAM));
5053
}
5154

5255
@Override
@@ -75,7 +78,7 @@ protected void doFilterRequest(HttpServletRequest request, HttpServletResponse r
7578
try {
7679
doFilter(chain, request, response);
7780
} finally {
78-
if (!request.isAsyncStarted()) {
81+
if (!request.isAsyncStarted() && !excludeMatcher.matches(request.getRequestURI())) {
7982
logger.logRequest();
8083
}
8184

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.sap.hcp.cf.logging.servlet.filter;
2+
3+
import java.util.Arrays;
4+
import java.util.Collections;
5+
import java.util.List;
6+
7+
/**
8+
* Matches request URIs against a list of Ant-style path patterns.
9+
* <p>
10+
* Supported wildcards:
11+
* <ul>
12+
* <li>{@code *} &ndash; matches any sequence of characters within a single path segment (no slash)</li>
13+
* <li>{@code **} &ndash; matches any sequence of characters across path segment boundaries (including slashes)</li>
14+
* </ul>
15+
*/
16+
public class RequestUriMatcher {
17+
18+
private final List<String> patterns;
19+
20+
/**
21+
* Creates a matcher from a comma-separated list of Ant-style path patterns.
22+
*
23+
* @param commaSeparatedPatterns
24+
* comma-separated pattern string, may be {@code null} or blank
25+
*/
26+
public RequestUriMatcher(String commaSeparatedPatterns) {
27+
if (commaSeparatedPatterns == null || commaSeparatedPatterns.isBlank()) {
28+
this.patterns = Collections.emptyList();
29+
} else {
30+
this.patterns = Arrays.stream(commaSeparatedPatterns.split(",")).map(String::trim).filter(s -> !s.isEmpty())
31+
.toList();
32+
}
33+
}
34+
35+
/**
36+
* Recursively matches {@code pattern} starting at {@code pi} (pattern index) against {@code uri} starting at
37+
* {@code ui} (uri index). No heap allocation occurs during matching.
38+
*/
39+
private static boolean matches(String pattern, int pi, String uri, int ui) {
40+
while (pi < pattern.length()) {
41+
char pc = pattern.charAt(pi);
42+
if (pc == '*') {
43+
boolean doubleWildcard = pi + 1 < pattern.length() && pattern.charAt(pi + 1) == '*';
44+
if (doubleWildcard) {
45+
// ** — skip the ** and try matching the rest from every position in uri
46+
pi += 2;
47+
if (pi == pattern.length()) {
48+
return true; // ** at end matches everything remaining
49+
}
50+
for (int i = ui; i <= uri.length(); i++) {
51+
if (matches(pattern, pi, uri, i)) {
52+
return true;
53+
}
54+
}
55+
return false;
56+
} else {
57+
// * — advance past * and try matching the rest from every position
58+
// within the current segment (no slash crossing)
59+
pi++;
60+
if (pi == pattern.length()) {
61+
return uri.indexOf('/', ui) == -1; // * at end matches rest of segment
62+
}
63+
for (int i = ui; i <= uri.length(); i++) {
64+
if (i > ui && uri.charAt(i - 1) == '/') {
65+
return false; // * cannot cross a slash
66+
}
67+
if (matches(pattern, pi, uri, i)) {
68+
return true;
69+
}
70+
}
71+
return false;
72+
}
73+
} else {
74+
// literal character — must match exactly
75+
if (ui >= uri.length() || uri.charAt(ui) != pc) {
76+
return false;
77+
}
78+
pi++;
79+
ui++;
80+
}
81+
}
82+
return ui == uri.length();
83+
}
84+
85+
/**
86+
* Returns {@code true} if the given URI matches any of the configured patterns.
87+
*/
88+
public boolean matches(String uri) {
89+
if (uri == null) {
90+
return false;
91+
}
92+
for (String pattern: patterns) {
93+
if (matches(pattern, 0, uri, 0)) {
94+
return true;
95+
}
96+
}
97+
return false;
98+
}
99+
}

cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/GenerateRequestLogFilterTest.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,55 @@ public void directlyForwardsRequestResponseWhenLogIsDisabled(ConsoleOutput conso
119119
assertThat(console.getAllEvents()).isEmpty();
120120

121121
((LoggerContext) LoggerFactory.getILoggerFactory()).getLogger(RequestLogger.class).setLevel(Level.INFO);
122+
}
123+
124+
@Test
125+
public void doesNotWriteRequestLogForExcludedUri(ConsoleOutput console) throws Exception {
126+
when(request.getRequestURI()).thenReturn("/health");
127+
GenerateRequestLogFilter filter = new GenerateRequestLogFilter(requestRecordFactory);
128+
filter.init(new ExcludePatternsConfig("/health,/metrics"));
129+
130+
filter.doFilter(request, response, chain);
131+
132+
assertThat(console.getAllEvents()).isEmpty();
133+
}
134+
135+
@Test
136+
public void writesRequestLogForNonExcludedUri(ConsoleOutput console) throws Exception {
137+
when(request.getRequestURI()).thenReturn("/api/orders");
138+
GenerateRequestLogFilter filter = new GenerateRequestLogFilter(requestRecordFactory);
139+
filter.init(new ExcludePatternsConfig("/health,/metrics"));
140+
141+
filter.doFilter(request, response, chain);
142+
143+
assertThat(console.getAllEvents()).isNotEmpty();
144+
}
145+
146+
@Test
147+
public void stillEnrichesContextForExcludedUri(ConsoleOutput console) throws Exception {
148+
when(request.getRequestURI()).thenReturn("/actuator/health");
149+
GenerateRequestLogFilter filter = new GenerateRequestLogFilter(requestRecordFactory);
150+
filter.init(new ExcludePatternsConfig("/actuator/**"));
151+
152+
filter.doFilter(request, response, chain);
153+
154+
verify(request).setAttribute(eq(MDC.class.getName()), anyMap());
155+
assertThat(console.getAllEvents()).isEmpty();
156+
}
157+
158+
@Test
159+
public void doesNotWriteRequestLogForWildcardExcludedUri(ConsoleOutput console) throws Exception {
160+
when(request.getRequestURI()).thenReturn("/actuator/health/liveness");
161+
GenerateRequestLogFilter filter = new GenerateRequestLogFilter(requestRecordFactory);
162+
filter.init(new ExcludePatternsConfig("/actuator/**"));
163+
164+
filter.doFilter(request, response, chain);
122165

166+
assertThat(console.getAllEvents()).isEmpty();
123167
}
124168

125169
private static class NoRequestWrappingConfig implements FilterConfig {
170+
126171
@Override
127172
public String getFilterName() {
128173
return "no-request-wrapping";
@@ -143,4 +188,36 @@ public Enumeration<String> getInitParameterNames() {
143188
return enumeration(List.of("wrapRequest"));
144189
}
145190
}
191+
192+
private static class ExcludePatternsConfig implements FilterConfig {
193+
194+
private final String excludePatterns;
195+
196+
ExcludePatternsConfig(String excludePatterns) {
197+
this.excludePatterns = excludePatterns;
198+
}
199+
200+
@Override
201+
public String getFilterName() {
202+
return "exclude-patterns";
203+
}
204+
205+
@Override
206+
public ServletContext getServletContext() {
207+
return null;
208+
}
209+
210+
@Override
211+
public String getInitParameter(String name) {
212+
if (GenerateRequestLogFilter.EXCLUDE_PATTERNS_INIT_PARAM.equals(name)) {
213+
return excludePatterns;
214+
}
215+
return null;
216+
}
217+
218+
@Override
219+
public Enumeration<String> getInitParameterNames() {
220+
return enumeration(List.of(GenerateRequestLogFilter.EXCLUDE_PATTERNS_INIT_PARAM));
221+
}
222+
}
146223
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.sap.hcp.cf.logging.servlet.filter;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.params.ParameterizedTest;
5+
import org.junit.jupiter.params.provider.CsvSource;
6+
import org.junit.jupiter.params.provider.ValueSource;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
class RequestUriMatcherTest {
11+
12+
@Test
13+
void doesNotMatchWhenNoPatternsConfigured() {
14+
RequestUriMatcher matcher = new RequestUriMatcher(null);
15+
assertThat(matcher.matches("/health")).isFalse();
16+
}
17+
18+
@Test
19+
void doesNotMatchWhenPatternStringIsBlank() {
20+
RequestUriMatcher matcher = new RequestUriMatcher(" ");
21+
assertThat(matcher.matches("/health")).isFalse();
22+
}
23+
24+
@Test
25+
void doesNotMatchNullUri() {
26+
RequestUriMatcher matcher = new RequestUriMatcher("/health");
27+
assertThat(matcher.matches(null)).isFalse();
28+
}
29+
30+
@ParameterizedTest
31+
@ValueSource(strings = { "/health", "/metrics", "/actuator" })
32+
void matchesExactPattern(String uri) {
33+
RequestUriMatcher matcher = new RequestUriMatcher("/health,/metrics,/actuator");
34+
assertThat(matcher.matches(uri)).isTrue();
35+
}
36+
37+
@Test
38+
void doesNotMatchDifferentPath() {
39+
RequestUriMatcher matcher = new RequestUriMatcher("/health");
40+
assertThat(matcher.matches("/api/orders")).isFalse();
41+
}
42+
43+
@Test
44+
void matchesSingleWildcardWithinSegment() {
45+
RequestUriMatcher matcher = new RequestUriMatcher("/api/*/status");
46+
assertThat(matcher.matches("/api/orders/status")).isTrue();
47+
}
48+
49+
@Test
50+
void singleWildcardDoesNotMatchAcrossSegments() {
51+
RequestUriMatcher matcher = new RequestUriMatcher("/api/*/status");
52+
assertThat(matcher.matches("/api/orders/items/status")).isFalse();
53+
}
54+
55+
@ParameterizedTest
56+
@CsvSource({ "/actuator/**, /actuator/health", "/actuator/**, /actuator/health/liveness",
57+
"/actuator/**, /actuator/", "/api/**, /api/v1/orders/123" })
58+
void matchesDoubleWildcardAcrossSegments(String pattern, String uri) {
59+
RequestUriMatcher matcher = new RequestUriMatcher(pattern.trim());
60+
assertThat(matcher.matches(uri.trim())).isTrue();
61+
}
62+
63+
@Test
64+
void doubleWildcardDoesNotMatchSiblingPath() {
65+
RequestUriMatcher matcher = new RequestUriMatcher("/actuator/**");
66+
assertThat(matcher.matches("/api/orders")).isFalse();
67+
}
68+
69+
@Test
70+
void matchesFirstOfMultiplePatterns() {
71+
RequestUriMatcher matcher = new RequestUriMatcher("/health, /actuator/**, /metrics");
72+
assertThat(matcher.matches("/health")).isTrue();
73+
}
74+
75+
@Test
76+
void matchesMiddleOfMultiplePatterns() {
77+
RequestUriMatcher matcher = new RequestUriMatcher("/health, /actuator/**, /metrics");
78+
assertThat(matcher.matches("/actuator/health")).isTrue();
79+
}
80+
81+
@Test
82+
void matchesLastOfMultiplePatterns() {
83+
RequestUriMatcher matcher = new RequestUriMatcher("/health, /actuator/**, /metrics");
84+
assertThat(matcher.matches("/metrics")).isTrue();
85+
}
86+
87+
@Test
88+
void doesNotMatchWhenNoneOfMultiplePatternsApply() {
89+
RequestUriMatcher matcher = new RequestUriMatcher("/health, /actuator/**, /metrics");
90+
assertThat(matcher.matches("/api/orders")).isFalse();
91+
}
92+
93+
@Test
94+
void ignoresWhitespaceAroundPatterns() {
95+
RequestUriMatcher matcher = new RequestUriMatcher(" /health , /metrics ");
96+
assertThat(matcher.matches("/health")).isTrue();
97+
assertThat(matcher.matches("/metrics")).isTrue();
98+
}
99+
}

0 commit comments

Comments
 (0)