From 1bd6b30dddcbd073541216781b03bdf4a682f7bb Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 21 May 2024 20:01:17 +0200 Subject: [PATCH] Allow ServerHttpObservationFilter to be extended This commit allows to extend the `ServerHttpObservationFilter` when the observation scope is opened. This typically allows to add the current traceId as a response header. Closes gh-30632 --- .../filter/ServerHttpObservationFilter.java | 14 +++++++- .../ServerHttpObservationFilterTests.java | 33 +++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java index effaf0553de..a297c16da90 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -106,6 +106,7 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter { Observation observation = createOrFetchObservation(request, response); try (Observation.Scope scope = observation.openScope()) { + onScopeOpened(scope, request, response); filterChain.doFilter(request, response); } catch (Exception ex) { @@ -125,6 +126,17 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter { } } + /** + * Notifies this filter that a new {@link Observation.Scope} is opened for the observation that was just created. + * @param scope the newly opened observation scope + * @param request the HTTP client request + * @param response the filter's response@ + * @since 6.2.0 + **/ + protected void onScopeOpened(Observation.Scope scope, HttpServletRequest request, HttpServletResponse response) { + + } + private Observation createOrFetchObservation(HttpServletRequest request, HttpServletResponse response) { Observation observation = (Observation) request.getAttribute(CURRENT_OBSERVATION_ATTRIBUTE); if (observation == null) { diff --git a/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java index 0a3459e7af0..2fcdf919199 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java @@ -16,15 +16,19 @@ package org.springframework.web.filter; +import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.http.server.observation.ServerRequestObservationContext; +import org.springframework.util.Assert; import org.springframework.web.testfixture.servlet.MockFilterChain; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; @@ -41,14 +45,14 @@ class ServerHttpObservationFilterTests { private final TestObservationRegistry observationRegistry = TestObservationRegistry.create(); - private final ServerHttpObservationFilter filter = new ServerHttpObservationFilter(this.observationRegistry); - private final MockFilterChain mockFilterChain = new MockFilterChain(); private final MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/resource/test"); private final MockHttpServletResponse response = new MockHttpServletResponse(); + private ServerHttpObservationFilter filter = new ServerHttpObservationFilter(this.observationRegistry); + @Test void filterShouldFillObservationContext() throws Exception { @@ -65,7 +69,7 @@ class ServerHttpObservationFilterTests { @Test void filterShouldAcceptNoOpObservationContext() throws Exception { - ServerHttpObservationFilter filter = new ServerHttpObservationFilter(ObservationRegistry.NOOP); + this.filter = new ServerHttpObservationFilter(ObservationRegistry.NOOP); filter.doFilter(this.request, this.response, this.mockFilterChain); ServerRequestObservationContext context = (ServerRequestObservationContext) this.request @@ -109,9 +113,32 @@ class ServerHttpObservationFilterTests { .hasLowCardinalityKeyValue("status", "500"); } + @Test + void customFilterShouldCallScopeOpened() throws Exception { + this.filter = new CustomObservationFilter(this.observationRegistry); + this.filter.doFilter(this.request, this.response, this.mockFilterChain); + + assertThat(this.response.getHeader("X-Trace-Id")).isEqualTo("badc0ff33"); + } + private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() { return TestObservationRegistryAssert.assertThat(this.observationRegistry) .hasObservationWithNameEqualTo("http.server.requests").that(); } + static class CustomObservationFilter extends ServerHttpObservationFilter { + + public CustomObservationFilter(ObservationRegistry observationRegistry) { + super(observationRegistry); + } + + @Override + protected void onScopeOpened(Observation.Scope scope, HttpServletRequest request, HttpServletResponse response) { + Assert.notNull(scope, "scope must not be null"); + Assert.notNull(request, "request must not be null"); + Assert.notNull(response, "response must not be null"); + response.setHeader("X-Trace-Id", "badc0ff33"); + } + } + }