Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate WebMvcTags to ServerRequestObservationConvention #586

Merged
merged 37 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1270277
Initial test case
Laurens-W Sep 4, 2024
6f27d4e
WIP
Laurens-W Sep 5, 2024
1dbb42e
Merge branch 'main' into spring-webmvctags-to-observations
timtebeek Sep 6, 2024
9320b73
Apply suggestions from code review
Laurens-W Sep 9, 2024
5261925
WIP
Laurens-W Sep 9, 2024
b2a9ef5
Apply suggestions from code review
Laurens-W Sep 9, 2024
9cf1904
WIP
Laurens-W Sep 9, 2024
723355c
WIP
Laurens-W Sep 10, 2024
458653f
WIP
Laurens-W Sep 10, 2024
8c96e1e
WIP
Laurens-W Sep 11, 2024
124c91c
WIP
Laurens-W Sep 11, 2024
84fe00d
Suggestions
Laurens-W Sep 11, 2024
ef0f44c
WIP
Laurens-W Sep 19, 2024
64860a1
Working setup
Laurens-W Sep 19, 2024
957d8ea
Merge branch 'main' into spring-webmvctags-to-observations
Laurens-W Sep 19, 2024
ad130cf
Apply suggestions from code review
Laurens-W Sep 19, 2024
c8dfc23
Format and apply suggestion
Laurens-W Sep 19, 2024
f54c69e
More progress
Laurens-W Sep 24, 2024
534b043
Naming and imports
Laurens-W Sep 24, 2024
2c06389
Adopt classpathFromResources & add tomcat-embedded-core
timtebeek Sep 24, 2024
2391101
Fix most warnings and issues
Laurens-W Sep 24, 2024
7f8985d
Fix type issues and warnings
Laurens-W Sep 25, 2024
0375470
More edge cases
Laurens-W Sep 25, 2024
3c39aa3
More edge cases, extra test coverage
Laurens-W Sep 25, 2024
d93762b
Merge branch 'main' into spring-webmvctags-to-observations
Laurens-W Sep 25, 2024
1aa0d1a
Remove jetbrains notnull
Laurens-W Sep 25, 2024
2198911
Reduce document example test case and add additional test cases for e…
Laurens-W Sep 26, 2024
8338048
Apply review feedback
Laurens-W Sep 27, 2024
ac6e98b
Merge branch 'main' into spring-webmvctags-to-observations
timtebeek Sep 30, 2024
bb2c9f6
Apply formatter to tests
timtebeek Sep 30, 2024
daf9e1e
Merge branch 'main' into spring-webmvctags-to-observations
timtebeek Sep 30, 2024
2e55442
Reduce imports and classpath
Laurens-W Sep 30, 2024
34553b4
Use method matcher
Laurens-W Oct 4, 2024
e4632a5
By default use high cardinality keys as low might cause issues with i…
Laurens-W Oct 7, 2024
c46349d
Consistently use the same version suffix for classpath resources
timtebeek Oct 8, 2024
8031c75
Demonstrate failure on any empty method
timtebeek Oct 8, 2024
e246ced
Fix class cast issue
Laurens-W Oct 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ dependencies {
"testWithSpringBoot_2_7RuntimeOnly"("jakarta.xml.bind:jakarta.xml.bind-api:2.3.3")

"testWithSpringBoot_3_0RuntimeOnly"("org.springframework.boot:spring-boot-starter:3.0.+")
"testWithSpringBoot_3_0RuntimeOnly"("org.springframework.boot:spring-boot-starter-actuator:3.0.+")
"testWithSpringBoot_3_0RuntimeOnly"("org.springframework.boot:spring-boot-starter-test:3.0.+")
"testWithSpringBoot_3_0RuntimeOnly"("org.springframework:spring-context:6.0.+")
"testWithSpringBoot_3_0RuntimeOnly"("org.springframework:spring-web:6.0.+")
Expand All @@ -245,6 +246,7 @@ dependencies {
"testWithSpringBoot_3_0RuntimeOnly"("org.springframework.security:spring-security-config:6.0.+")
"testWithSpringBoot_3_0RuntimeOnly"("org.springframework.security:spring-security-web:6.0.+")
"testWithSpringBoot_3_0RuntimeOnly"("org.springframework.security:spring-security-ldap:6.0.+")
"testWithSpringBoot_3_0RuntimeOnly"("org.apache.tomcat.embed:tomcat-embed-core:10.1.+")

"testWithSpringBoot_3_2RuntimeOnly"("org.springframework.boot:spring-boot-starter:3.2.+")
"testWithSpringBoot_3_2RuntimeOnly"("org.springframework.boot:spring-boot-starter-test:3.2.+")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.spring.boot3;

import org.openrewrite.*;
Laurens-W marked this conversation as resolved.
Show resolved Hide resolved
import org.openrewrite.internal.ListUtils;
import org.openrewrite.java.*;
import org.openrewrite.java.search.UsesType;
import org.openrewrite.java.tree.*;
Laurens-W marked this conversation as resolved.
Show resolved Hide resolved
Laurens-W marked this conversation as resolved.
Show resolved Hide resolved
import org.openrewrite.marker.Markers;
Laurens-W marked this conversation as resolved.
Show resolved Hide resolved

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;

public class MigrateWebMvcTagsToObservationConvention extends Recipe {
Laurens-W marked this conversation as resolved.
Show resolved Hide resolved

private static final String WEBMVCTAGSPROVIDER_FQ = "org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider";
private static final String WEBMVCTAGS_FQ = "org.springframework.boot.actuate.metrics.web.servlet.WebMvcTags";
private static final String DEFAULTSERVERREQUESTOBSERVATIONCONVENTION = "DefaultServerRequestObservationConvention";
private static final String DEFAULTSERVERREQUESTOBSERVATIONCONVENTION_FQ = "org.springframework.http.server.observation.DefaultServerRequestObservationConvention";
private static final String SERVERREQUESTOBSERVATIONCONVENTION_FQ = "org.springframework.http.server.observation.ServerRequestObservationContext";
private static final String KEYVALUES_FQ = "io.micrometer.common.KeyValues";
private static final String KEYVALUE_FQ = "io.micrometer.common.KeyValue";
private static final String HTTPSERVLETREQUEST_FQ = "jakarta.servlet.http.HttpServletRequest";
private static final String HTTPSERVLETRESPONSE_FQ = "jakarta.servlet.http.HttpServletResponse";
private static final String TAGS_FQ = "io.micrometer.core.instrument.Tags";
private static final String TAG_FQ = "io.micrometer.core.instrument.Tag";
private static final MethodMatcher TAGS_AND = new MethodMatcher("io.micrometer.core.instrument.Tags and(java.lang.String, java.lang.String)");
private static final MethodMatcher TAGS_OF = new MethodMatcher("io.micrometer.core.instrument.Tags of(io.micrometer.core.instrument.Tag[])");

private static boolean addedHttpServletRequest;
private static boolean addedHttpServletResponse;
private static J.Identifier keyValuesIdentifier;

@Override
public @NlsRewrite.DisplayName String getDisplayName() {
return "Migrate `WebMvcTagsProvider` to `DefaultServerRequestObservationConvention`";
Laurens-W marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public @NlsRewrite.Description String getDescription() {
return "Migrate `WebMvcTagsProvider` to `DefaultServerRequestObservationConvention` as part of Spring Boot 3.2 removals.";
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(new UsesType<>(WEBMVCTAGSPROVIDER_FQ, true), new JavaVisitor<ExecutionContext>() {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
J.ClassDeclaration c = classDecl;
if (classDecl.getImplements() != null) {
for (TypeTree type : classDecl.getImplements()) {
if (TypeUtils.isOfClassType(type.getType(), WEBMVCTAGSPROVIDER_FQ)) {
maybeRemoveImport(WEBMVCTAGSPROVIDER_FQ);
maybeAddImport(DEFAULTSERVERREQUESTOBSERVATIONCONVENTION_FQ);
c = classDecl.withImplements(null)
.withExtends(TypeTree.build(DEFAULTSERVERREQUESTOBSERVATIONCONVENTION)
.withType(JavaType.buildType(DEFAULTSERVERREQUESTOBSERVATIONCONVENTION_FQ))
.withPrefix(Space.SINGLE_SPACE));
c = (J.ClassDeclaration) super.visitClassDeclaration(c, ctx);
}
}
}
maybeRemoveImport(HTTPSERVLETREQUEST_FQ);
maybeRemoveImport(HTTPSERVLETRESPONSE_FQ);
return maybeAutoFormat(classDecl, c, ctx, getCursor());
}

@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
J.MethodDeclaration m = method;
boolean isOverride = false;
for (J.Annotation anno : m.getLeadingAnnotations()) {
if (TypeUtils.isOfType(anno.getType(), JavaType.buildType("java.lang.Override"))) {
isOverride = true;
}
}
if (isOverride) {
if (method.getName().getSimpleName().equals("getTags")) {
J.VariableDeclarations methodArg = JavaTemplate.builder("ServerRequestObservationContext context")
.contextSensitive()
.imports(SERVERREQUESTOBSERVATIONCONVENTION_FQ)
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "spring-web-6.+"))
.build()
.<J.MethodDeclaration>apply(getCursor(), method.getCoordinates().replaceParameters())
.getParameters().get(0).withPrefix(Space.EMPTY);

Statement keyValuesInitializer = JavaTemplate.builder("KeyValues values = super.getLowCardinalityKeyValues(#{any()});")
.contextSensitive()
.imports(KEYVALUES_FQ)
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "micrometer-commons-1.11.+"))
.build()
.<J.MethodDeclaration>apply(getCursor(), method.getBody().getCoordinates().firstStatement(), methodArg.getVariables().get(0).getName())
.getBody().getStatements().get(0);

maybeAddImport(SERVERREQUESTOBSERVATIONCONVENTION_FQ);
maybeAddImport(KEYVALUES_FQ);
JavaType.Method met = m.getMethodType();
met = met.withName("getLowCardinalityKeyValues").withReturnType(JavaType.buildType(KEYVALUES_FQ)).withParameterNames(singletonList("context")).withParameterTypes(singletonList(methodArg.getType()));
m = m.withName(m.getName().withSimpleName("getLowCardinalityKeyValues"))
.withReturnTypeExpression(TypeTree.build("KeyValues").withType(JavaType.buildType(KEYVALUES_FQ)))
.withParameters(singletonList(methodArg))
.withBody(m.getBody().withStatements(ListUtils.insert(m.getBody().getStatements(), keyValuesInitializer, 0)))
.withMethodType(met);
keyValuesIdentifier = ((J.VariableDeclarations)m.getBody().getStatements().get(0)).getVariables().get(0).getName();
}
}
m = (J.MethodDeclaration) super.visitMethodDeclaration(m, ctx);
J.VariableDeclarations methodParam = (J.VariableDeclarations) m.getParameters().get(0);
J.Identifier methodParamIdentifier = methodParam.getVariables().get(0).getName();
Boolean addHttpServletRequest = getCursor().pollMessage("addHttpServletRequest");
Boolean addHttpServletResponse = getCursor().pollMessage("addHttpServletResponse");
if (Boolean.TRUE.equals(addHttpServletRequest)) {
m = JavaTemplate.builder("HttpServletRequest request = #{any()}.get(HttpServletRequest.class);")
.imports(HTTPSERVLETREQUEST_FQ, SERVERREQUESTOBSERVATIONCONVENTION_FQ)
.javaParser(JavaParser.fromJavaVersion().classpath("tomcat-embed-core"))
.build()
.apply(updateCursor(m), m.getBody().getCoordinates().firstStatement(), methodParamIdentifier);
addedHttpServletRequest = true;
}
if (Boolean.TRUE.equals(addHttpServletResponse)) {
m = JavaTemplate.builder("HttpServletResponse response = #{any()}.get(HttpServletResponse.class);")
.imports(HTTPSERVLETRESPONSE_FQ, SERVERREQUESTOBSERVATIONCONVENTION_FQ)
.javaParser(JavaParser.fromJavaVersion().classpath("tomcat-embed-core"))
.build()
.apply(updateCursor(m), m.getBody().getCoordinates().firstStatement(), methodParamIdentifier);
addedHttpServletResponse = true;
}
return m;
}

@Override
public Statement visitStatement(Statement statement, ExecutionContext ctx) {
Statement s = (Statement) super.visitStatement(statement, ctx);
if (s instanceof J.VariableDeclarations) {
J.VariableDeclarations m = (J.VariableDeclarations) s;
if (TypeUtils.isOfType(m.getType(), JavaType.buildType(TAGS_FQ))) {
J.MethodInvocation init = ((J.MethodInvocation) m.getVariables().get(0).getInitializer());
if (TAGS_OF.matches(init)) {
maybeRemoveImport(TAGS_FQ);
maybeRemoveImport(WEBMVCTAGS_FQ);
return null;
}
}
return m;
}
if (s instanceof J.Assignment) {
J.Assignment a = (J.Assignment) s;
if (TypeUtils.isOfType(a.getType(), JavaType.buildType(TAGS_FQ))) {
J.MethodInvocation init = ((J.MethodInvocation) a.getAssignment());

maybeAddImport(KEYVALUE_FQ);
maybeRemoveImport(TAG_FQ);
J.MethodInvocation createKeyValue = JavaTemplate.builder("KeyValue.of(#{any(java.lang.String)}, #{any(java.lang.String)})")
.imports(KEYVALUE_FQ)
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "micrometer-commons-1.11.+"))
.build()
.apply(getCursor(), a.getCoordinates().replace(), init.getArguments().get(0), init.getArguments().get(1));

return JavaTemplate.builder("values.and(#{any(io.micrometer.common.KeyValue)})")
.javaParser(JavaParser.fromJavaVersion())
.build()
.apply(getCursor(), a.getCoordinates().replace(), createKeyValue);
}
return a;
}
return s;
}

@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, ctx);
if (method.getMethodType() != null && TypeUtils.isOfType(method.getMethodType().getDeclaringType(), JavaType.buildType(HTTPSERVLETREQUEST_FQ)) && !addedHttpServletRequest) {
getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, "addHttpServletRequest", true);
}
if (method.getMethodType() != null && TypeUtils.isOfType(method.getMethodType().getDeclaringType(), JavaType.buildType(HTTPSERVLETRESPONSE_FQ)) && !addedHttpServletResponse) {
getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, "addHttpServletResponse", true);
}
return m;
}

@Override
public J visitReturn(J.Return return_, ExecutionContext ctx) {
J.Return ret = (J.Return) super.visitReturn(return_, ctx);
if (TypeUtils.isOfType(ret.getExpression().getType(), JavaType.buildType(TAGS_FQ))) {
return ret.withExpression(keyValuesIdentifier);
}
return ret;
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.spring.boot3;

import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;

class MigrateWebMvcTagsToObservationConventionTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new MigrateWebMvcTagsToObservationConvention()).parser(JavaParser.fromJavaVersion().classpath( "micrometer-core", "spring-boot", "spring-context", "spring-beans", "spring-web", "tomcat-embed-core"));
}

@DocumentExample
@Test
void ShouldMigrateWebMvcTagsProviderToDefaultServerRequestObservationConvention() {
//language=java
rewriteRun(
java(
"""
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTags;
import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider;
import org.springframework.stereotype.Component;

@Component
class CustomWebMvcTagsProvider implements WebMvcTagsProvider {
@Override
public Iterable<Tag> getTags(HttpServletRequest request, HttpServletResponse response, Object handler, Throwable exception) {
Tags tags = Tags.of(WebMvcTags.method(request), WebMvcTags.uri(request, response), WebMvcTags.status(response), WebMvcTags.outcome(response));

String customHeader = request.getHeader("X-Custom-Header");
if (customHeader != null) {
tags = tags.and("custom.header", customHeader);
}

return tags;
}
}
""",
"""
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationContext;
import org.springframework.stereotype.Component;

@Component
class CustomWebMvcTagsProvider extends DefaultServerRequestObservationConvention {
@Override
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
HttpServletRequest request = context.get(HttpServletRequest.class);
KeyValues values = super.getLowCardinalityKeyValues(context);

String customHeader = request.getHeader("X-Custom-Header");
if (customHeader != null) {
values.and(KeyValue.of("custom.header", customHeader));
}

return values;
}
}
"""));
}

}
Loading