Align Filter Chain Observability Lineage

Closes gh-12849
This commit is contained in:
Josh Cummings
2023-03-24 17:32:11 -06:00
parent 834e361898
commit 6db2b0dcd0
2 changed files with 299 additions and 12 deletions

View File

@@ -16,15 +16,22 @@
package org.springframework.security.web.server;
import java.util.ArrayList;
import java.util.List;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
@@ -64,4 +71,147 @@ public class ObservationWebFilterChainDecoratorTests {
verifyNoInteractions(handler);
}
// gh-12849
@Test
void decorateWhenCustomAfterFilterThenObserves() {
AccumulatingObservationHandler handler = new AccumulatingObservationHandler();
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(handler);
ObservationWebFilterChainDecorator decorator = new ObservationWebFilterChainDecorator(registry);
WebFilter mock = mock(WebFilter.class);
given(mock.filter(any(), any())).willReturn(Mono.empty());
WebFilterChain chain = mock(WebFilterChain.class);
given(chain.filter(any())).willReturn(Mono.empty());
WebFilterChain decorated = decorator.decorate(chain,
List.of((e, c) -> c.filter(e).then(Mono.deferContextual((context) -> {
Observation parentObservation = context.getOrDefault(ObservationThreadLocalAccessor.KEY, null);
Observation observation = Observation.createNotStarted("custom", registry)
.parentObservation(parentObservation).contextualName("custom").start();
return Mono.just("3").doOnSuccess((v) -> observation.stop()).doOnCancel(observation::stop)
.doOnError((t) -> {
observation.error(t);
observation.stop();
}).then(Mono.empty());
}))));
Observation http = Observation.start("http", registry).contextualName("http");
try {
decorated.filter(MockServerWebExchange.from(MockServerHttpRequest.get("/").build()))
.contextWrite((context) -> context.put(ObservationThreadLocalAccessor.KEY, http)).block();
}
finally {
http.stop();
}
handler.assertSpanStart(0, "http", null);
handler.assertSpanStart(1, "spring.security.filterchains", "http");
handler.assertSpanStop(2, "security filterchain before");
handler.assertSpanStart(3, "secured request", "security filterchain before");
handler.assertSpanStop(4, "secured request");
handler.assertSpanStart(5, "spring.security.filterchains", "http");
handler.assertSpanStart(6, "custom", "spring.security.filterchains");
handler.assertSpanStop(7, "custom");
handler.assertSpanStop(8, "security filterchain after");
handler.assertSpanStop(9, "http");
}
static class AccumulatingObservationHandler implements ObservationHandler<Observation.Context> {
List<Event> contexts = new ArrayList<>();
@Override
public boolean supportsContext(Observation.Context context) {
return true;
}
@Override
public void onStart(Observation.Context context) {
this.contexts.add(new Event("start", context));
}
@Override
public void onError(Observation.Context context) {
this.contexts.add(new Event("error", context));
}
@Override
public void onEvent(Observation.Event event, Observation.Context context) {
this.contexts.add(new Event("event", context));
}
@Override
public void onScopeOpened(Observation.Context context) {
this.contexts.add(new Event("opened", context));
}
@Override
public void onScopeClosed(Observation.Context context) {
this.contexts.add(new Event("closed", context));
}
@Override
public void onScopeReset(Observation.Context context) {
this.contexts.add(new Event("reset", context));
}
@Override
public void onStop(Observation.Context context) {
this.contexts.add(new Event("stop", context));
}
private void assertSpanStart(int index, String name, String parentName) {
Event event = this.contexts.get(index);
assertThat(event.event).isEqualTo("start");
if (event.contextualName == null) {
assertThat(event.name).isEqualTo(name);
}
else {
assertThat(event.contextualName).isEqualTo(name);
}
if (parentName == null) {
return;
}
if (event.parentContextualName == null) {
assertThat(event.parentName).isEqualTo(parentName);
}
else {
assertThat(event.parentContextualName).isEqualTo(parentName);
}
}
private void assertSpanStop(int index, String name) {
Event event = this.contexts.get(index);
assertThat(event.event).isEqualTo("stop");
if (event.contextualName == null) {
assertThat(event.name).isEqualTo(name);
}
else {
assertThat(event.contextualName).isEqualTo(name);
}
}
static class Event {
String event;
String name;
String contextualName;
String parentName;
String parentContextualName;
Event(String event, Observation.Context context) {
this.event = event;
this.name = context.getName();
this.contextualName = context.getContextualName();
if (context.getParentObservation() != null) {
this.parentName = context.getParentObservation().getContextView().getName();
this.parentContextualName = context.getParentObservation().getContextView().getContextualName();
}
}
}
}
}