Skip to content

Commit 59ba7f5

Browse files
bvn13jzheaux
authored andcommitted
Shorten Observation Event Names
Closes gh-12811
1 parent b3c8344 commit 59ba7f5

File tree

2 files changed

+174
-2
lines changed

2 files changed

+174
-2
lines changed

web/src/main/java/org/springframework/security/web/ObservationFilterChainDecorator.java

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818

1919
import java.io.IOException;
2020
import java.util.ArrayList;
21+
import java.util.HashMap;
2122
import java.util.List;
23+
import java.util.Map;
2224
import java.util.concurrent.atomic.AtomicInteger;
2325
import java.util.concurrent.atomic.AtomicReference;
2426

@@ -36,6 +38,36 @@
3638
import org.apache.commons.logging.LogFactory;
3739

3840
import org.springframework.core.log.LogMessage;
41+
import org.springframework.security.web.access.ExceptionTranslationFilter;
42+
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
43+
import org.springframework.security.web.access.intercept.AuthorizationFilter;
44+
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
45+
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
46+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
47+
import org.springframework.security.web.authentication.logout.LogoutFilter;
48+
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
49+
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
50+
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
51+
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
52+
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
53+
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
54+
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
55+
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
56+
import org.springframework.security.web.context.SecurityContextHolderFilter;
57+
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
58+
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
59+
import org.springframework.security.web.csrf.CsrfFilter;
60+
import org.springframework.security.web.header.HeaderWriterFilter;
61+
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
62+
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
63+
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
64+
import org.springframework.security.web.session.ConcurrentSessionFilter;
65+
import org.springframework.security.web.session.DisableEncodeUrlFilter;
66+
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
67+
import org.springframework.security.web.session.SessionManagementFilter;
68+
import org.springframework.util.Assert;
69+
import org.springframework.util.StringUtils;
70+
import org.springframework.web.filter.CorsFilter;
3971

4072
/**
4173
* A {@link org.springframework.security.web.FilterChainProxy.FilterChainDecorator} that
@@ -48,6 +80,12 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F
4880

4981
private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
5082

83+
private static final int OPENTELEMETRY_MAX_NAME_LENGTH = 63;
84+
85+
private static final int MAX_OBSERVATION_NAME_LENGTH = OPENTELEMETRY_MAX_NAME_LENGTH - ".before".length();
86+
87+
private static final Map<String, String> OBSERVATION_NAMES = new HashMap<>();
88+
5189
private static final String ATTRIBUTE = ObservationFilterChainDecorator.class + ".observation";
5290

5391
static final String UNSECURED_OBSERVATION_NAME = "spring.security.http.unsecured.requests";
@@ -98,10 +136,83 @@ private List<ObservationFilter> wrap(List<Filter> filters) {
98136
return observableFilters;
99137
}
100138

139+
static {
140+
registerName(DisableEncodeUrlFilter.class, "session.encode-url.disable");
141+
registerName(ForceEagerSessionCreationFilter.class, "session.create");
142+
registerName(ChannelProcessingFilter.class, "web.request.delivery.ensure");
143+
registerName(WebAsyncManagerIntegrationFilter.class, "web-async-manager.join.security-context");
144+
registerName(SecurityContextHolderFilter.class, "security-context.hold");
145+
registerName(SecurityContextPersistenceFilter.class, "security-context.persist");
146+
registerName(HeaderWriterFilter.class, "web.response.header.set");
147+
registerName(CorsFilter.class, "cors.process");
148+
registerName(CsrfFilter.class, "csrf.protect");
149+
registerName(LogoutFilter.class, "principal.logout");
150+
registerName("org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
151+
"web.request.oauth2.redirect");
152+
registerName(
153+
"org.springframework.security.saml2.provider.service.web." + "Saml2WebSsoAuthenticationRequestFilter",
154+
"web.request.saml2.redirect");
155+
registerName(X509AuthenticationFilter.class, "web.request.x509.auth");
156+
registerName(AbstractPreAuthenticatedProcessingFilter.class, "web.request.pre-auth.base.process");
157+
registerName("org.springframework.security.cas.web.CasAuthenticationFilter", "web.request.sas.auth");
158+
registerName("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
159+
"web.response.oauth2.process");
160+
registerName("org.springframework.security.saml2.provider.service.web.authentication"
161+
+ ".Saml2WebSsoAuthenticationFilter", "web.request.saml2.auth");
162+
registerName(UsernamePasswordAuthenticationFilter.class, "web.request.username-password.auth");
163+
registerName(DefaultLoginPageGeneratingFilter.class, "web.login-page.default.generate");
164+
registerName(DefaultLogoutPageGeneratingFilter.class, "web.logout-page.default.generate");
165+
registerName(ConcurrentSessionFilter.class, "session.refresh");
166+
registerName(DigestAuthenticationFilter.class, "web.request.digest.auth");
167+
registerName("org.springframework.security.oauth2.server.resource.web.authentication."
168+
+ "BearerTokenAuthenticationFilter", "web.request.bearer.auth");
169+
registerName(BasicAuthenticationFilter.class, "web.request.basic.auth");
170+
registerName(RequestCacheAwareFilter.class, "web.request.cache.extract");
171+
registerName(SecurityContextHolderAwareRequestFilter.class, "web.request.security.wrap");
172+
registerName(JaasApiIntegrationFilter.class, "web.request.jass.auth");
173+
registerName(RememberMeAuthenticationFilter.class, "web.request.remember-me.auth");
174+
registerName(AnonymousAuthenticationFilter.class, "web.request.anonymous.auth");
175+
registerName("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
176+
"web.response.oauth2.code-grant.process");
177+
registerName(SessionManagementFilter.class, "session.manage");
178+
registerName(ExceptionTranslationFilter.class, "exception.translate");
179+
registerName(FilterSecurityInterceptor.class, "web.response.security.intercept");
180+
registerName(AuthorizationFilter.class, "web.access.auth.restrict");
181+
registerName(SwitchUserFilter.class, "session.switch");
182+
}
183+
184+
public static void registerName(Class clazz, String name) {
185+
String keyName = clazz.getName();
186+
checkAlreadyRegistered(keyName);
187+
OBSERVATION_NAMES.put(keyName, limitLength(name));
188+
}
189+
190+
public static void registerName(String className, String name) {
191+
checkAlreadyRegistered(className);
192+
OBSERVATION_NAMES.put(className, name);
193+
}
194+
101195
static AroundFilterObservation observation(HttpServletRequest request) {
102196
return (AroundFilterObservation) request.getAttribute(ATTRIBUTE);
103197
}
104198

199+
private static String getObservationName(String className) {
200+
if (OBSERVATION_NAMES.containsKey(className)) {
201+
return OBSERVATION_NAMES.get(className);
202+
}
203+
throw new IllegalArgumentException("Class not registered for observation: " + className);
204+
}
205+
206+
private static String limitLength(String s) {
207+
Assert.isTrue(s.length() <= MAX_OBSERVATION_NAME_LENGTH,
208+
"The name must be less than MAX_OBSERVATION_NAME_LENGTH=" + MAX_OBSERVATION_NAME_LENGTH);
209+
return s;
210+
}
211+
212+
private static void checkAlreadyRegistered(String keyName) {
213+
Assert.isTrue(!OBSERVATION_NAMES.containsKey(keyName), "Observation name is registered already: " + keyName);
214+
}
215+
105216
private static final class VirtualFilterChain implements FilterChain {
106217

107218
private final FilterChain originalChain;
@@ -145,6 +256,8 @@ static final class ObservationFilter implements Filter {
145256

146257
private final String name;
147258

259+
private final String observationName;
260+
148261
private final int position;
149262

150263
private final int size;
@@ -155,12 +268,30 @@ static final class ObservationFilter implements Filter {
155268
this.name = filter.getClass().getSimpleName();
156269
this.position = position;
157270
this.size = size;
271+
String tempObservationName;
272+
try {
273+
tempObservationName = ObservationFilterChainDecorator.getObservationName(filter.getClass().getName());
274+
}
275+
catch (IllegalArgumentException ex) {
276+
tempObservationName = compressName(this.name);
277+
logger.warn(
278+
"Class " + filter.getClass().getName()
279+
+ " is not registered for observation and will have name " + tempObservationName
280+
+ ". Please consider of registering this class with "
281+
+ ObservationFilterChainDecorator.class.getSimpleName() + ".registerName(class, name).",
282+
ex);
283+
}
284+
this.observationName = tempObservationName;
158285
}
159286

160287
String getName() {
161288
return this.name;
162289
}
163290

291+
String getObservationName() {
292+
return this.observationName;
293+
}
294+
164295
@Override
165296
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
166297
throws IOException, ServletException {
@@ -181,15 +312,17 @@ private void wrapFilter(ServletRequest request, ServletResponse response, Filter
181312
parentBefore.setFilterName(this.name);
182313
parentBefore.setChainPosition(this.position);
183314
}
184-
parent.before().event(Observation.Event.of(this.name + ".before", "before " + this.name));
315+
parent.before().event(Observation.Event.of(this.observationName + ".before",
316+
"before " + this.name));
185317
this.filter.doFilter(request, response, chain);
186318
parent.start();
187319
if (parent.after().getContext() instanceof FilterChainObservationContext parentAfter) {
188320
parentAfter.setChainSize(this.size);
189321
parentAfter.setFilterName(this.name);
190322
parentAfter.setChainPosition(this.size - this.position + 1);
191323
}
192-
parent.after().event(Observation.Event.of(this.name + ".after", "after " + this.name));
324+
parent.after().event(Observation.Event.of(this.observationName + ".after",
325+
"after " + this.name));
193326
}
194327

195328
private AroundFilterObservation parent(HttpServletRequest request) {
@@ -202,6 +335,24 @@ private AroundFilterObservation parent(HttpServletRequest request) {
202335
return parent;
203336
}
204337

338+
private String compressName(String className) {
339+
if (className.length() >= MAX_OBSERVATION_NAME_LENGTH) {
340+
return maximalCompressClassName(className, MAX_OBSERVATION_NAME_LENGTH);
341+
}
342+
return className;
343+
}
344+
345+
private String maximalCompressClassName(String className, int maxLength) {
346+
String[] names = className.split("(?=\\p{Lu})");
347+
for (int j = 0; j < names.length; j++) {
348+
final int maxPortionLength = maxLength / names.length;
349+
if (names[j].length() > maxPortionLength) {
350+
names[j] = names[j].substring(0, maxPortionLength);
351+
}
352+
}
353+
return StringUtils.arrayToDelimitedString(names, "");
354+
}
355+
205356
}
206357

207358
interface AroundFilterObservation extends FilterObservation {

web/src/test/java/org/springframework/security/web/ObservationFilterChainDecoratorTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.security.web;
1818

19+
import java.lang.reflect.Field;
20+
import java.lang.reflect.Method;
1921
import java.util.List;
2022

2123
import io.micrometer.observation.Observation;
@@ -78,6 +80,7 @@ void decorateFiltersWhenDefaultsThenObserves() throws Exception {
7880
FilterChain chain = mock(FilterChain.class);
7981
Filter filter = mock(Filter.class);
8082
FilterChain decorated = decorator.decorate(chain, List.of(filter));
83+
assertCompressedName(decorated);
8184
decorated.doFilter(new MockHttpServletRequest("GET", "/"), new MockHttpServletResponse());
8285
verify(handler, times(2)).onStart(any());
8386
ArgumentCaptor<Observation.Event> event = ArgumentCaptor.forClass(Observation.Event.class);
@@ -87,4 +90,22 @@ void decorateFiltersWhenDefaultsThenObserves() throws Exception {
8790
assertThat(events.get(1).getName()).isEqualTo(filter.getClass().getSimpleName() + ".after");
8891
}
8992

93+
void assertCompressedName(FilterChain filterChain) throws Exception {
94+
assertThat(filterChain.getClass().getSimpleName()).isEqualTo("VirtualFilterChain");
95+
Field field = filterChain.getClass().getDeclaredField("additionalFilters");
96+
field.setAccessible(true);
97+
List<ObservationFilterChainDecorator.ObservationFilter> additionalFilters =
98+
(List<ObservationFilterChainDecorator.ObservationFilter>) field.get(filterChain);
99+
assertThat(additionalFilters.size()).isEqualTo(1);
100+
final ObservationFilterChainDecorator.ObservationFilter observationFilter = additionalFilters.get(0);
101+
assertThat(observationFilter.getObservationName()).isEqualTo(observationFilter.getName());
102+
Method method = observationFilter.getClass().getDeclaredMethod("compressName", String.class);
103+
method.setAccessible(true);
104+
String compressed = (String) method.invoke(observationFilter, "ObservationFilterChainDecoratorTests");
105+
assertThat(compressed).isEqualTo("ObservationFilterChainDecoratorTests");
106+
String fakeCompressed = (String) method.invoke(observationFilter,
107+
"ObservationFilterChainDecoratorTestsObservationFilterChainDecoratorTests");
108+
assertThat(fakeCompressed).isEqualTo("ObserFilteChainDecorTestsObserFilteChainDecorTests");
109+
}
110+
90111
}

0 commit comments

Comments
 (0)