diff --git a/src/main/java/org/openrewrite/micrometer/TimerToObservation.java b/src/main/java/org/openrewrite/micrometer/TimerToObservation.java new file mode 100644 index 0000000..15cdad7 --- /dev/null +++ b/src/main/java/org/openrewrite/micrometer/TimerToObservation.java @@ -0,0 +1,143 @@ +/* + * Copyright 2023 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. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.micrometer; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.java.*; +import org.openrewrite.java.search.UsesMethod; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; + +import java.util.ArrayList; +import java.util.List; + +public class TimerToObservation extends Recipe { + private static final String TIMER = "io.micrometer.core.instrument.Timer"; + private static final String OBSERVATION = "io.micrometer.observation.Observation"; + + @Override + public String getDisplayName() { + return "Convert Micrometer Timer to Observations"; + } + + @Override + public String getDescription() { + return "Convert Micrometer Timer to Observations."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.and( + Preconditions.or( + new UsesMethod<>(TIMER + " record*(..)", false), + new UsesMethod<>(TIMER + " wrap(..)", false) + ), + Preconditions.and( + Preconditions.not(new UsesMethod<>(TIMER + " record(java.time.Duration)", false)), + Preconditions.not(new UsesMethod<>(TIMER + " record(long, java.util.concurrent.TimeUnit)", false)) + ) + ), + new JavaIsoVisitor() { + private final MethodMatcher builderMatcher = new MethodMatcher(OBSERVATION + " builder(String)"); + private final MethodMatcher registerMatcher = new MethodMatcher(TIMER + ".Builder register(io.micrometer.observation.ObservationRegistry)"); + private final MethodMatcher tagMatcher = new MethodMatcher(TIMER + ".Builder tag(String, String)"); + private final MethodMatcher tagsMatcher = new MethodMatcher(TIMER + ".Builder tags(..)"); + private final MethodMatcher tagsIterableMatcher = new MethodMatcher(TIMER + ".Builder tags(Iterable)"); + + private final ChangeType changeTypeRegistry = new ChangeType("io.micrometer.core.instrument.MeterRegistry", "io.micrometer.observation.ObservationRegistry", null); + private final ChangeType changeTypeTimer = new ChangeType("io.micrometer.core.instrument.Timer","io.micrometer.observation.Observation", null); + private final ChangeMethodName changeRecord = new ChangeMethodName(OBSERVATION + " record*(..)", "observe", null, null); + + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit compilationUnit, ExecutionContext executionContext) { + J.CompilationUnit cu = compilationUnit; + cu = (J.CompilationUnit) changeTypeRegistry.getVisitor().visit(cu, executionContext); + cu = (J.CompilationUnit) changeTypeTimer.getVisitor().visit(cu, executionContext); + cu = (J.CompilationUnit) changeRecord.getVisitor().visit(cu, executionContext); + assert cu != null; + return super.visitCompilationUnit(cu, executionContext); + } + + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, ExecutionContext executionContext) { + if (registerMatcher.matches(mi)) { + Expression timerName = null; + Expression registry = mi.getArguments().get(0); + + List builder = new ArrayList<>(); + List parameters = new ArrayList<>(); + + Expression maybeBuilder = mi.getSelect(); + while (maybeBuilder instanceof J.MethodInvocation) { + J.MethodInvocation builderMethod = (J.MethodInvocation) maybeBuilder; + if (builderMatcher.matches(maybeBuilder)) { + timerName = builderMethod.getArguments().get(0); + } + else if (tagMatcher.matches(maybeBuilder)) { + builder.add("\n.highCardinalityKeyValue(#{any(String)}, #{any(String)})"); + parameters.add(builderMethod.getArguments().get(0)); + parameters.add(builderMethod.getArguments().get(1)); + } + else if (tagsIterableMatcher.matches(maybeBuilder)) { + builder.add("\n.highCardinalityKeyValues(KeyValues.of(#{any(Iterable)}, Tag::getKey, Tag::getValue))"); + parameters.addAll(builderMethod.getArguments()); + maybeAddImport("io.micrometer.common.KeyValues"); + maybeAddImport("io.micrometer.core.instrument.Tag"); + } + else if (tagsMatcher.matches(maybeBuilder)) { + String args = StringUtils.repeat("#{any(String)},", builderMethod.getArguments().size()); + args = args.substring(0, args.length() - 1); + builder.add("\n.highCardinalityKeyValues(KeyValues.of(" + args + "))"); + parameters.addAll(builderMethod.getArguments()); + maybeAddImport("io.micrometer.common.KeyValues"); + } + maybeBuilder = ((J.MethodInvocation) maybeBuilder).getSelect(); + } + if (timerName != null) { + parameters.add(0, timerName); + parameters.add(1, registry); + + maybeAddImport("io.micrometer.observation.Observation"); + maybeRemoveImport("io.micrometer.core.instrument.Timer"); + + JavaTemplate template = JavaTemplate + .builder("Observation.createNotStarted(#{any(java.lang.String)}, #{any()})" + + String.join("", builder)) + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion() + .classpathFromResources(executionContext, "micrometer-observation", "micrometer-commons", "micrometer-core")) + .imports("io.micrometer.observation.Observation") + .imports("io.micrometer.common.KeyValues") + .imports("io.micrometer.core.instrument.Tag") + .build(); + + mi = autoFormat( + template.apply(updateCursor(mi), mi.getCoordinates().replace(), parameters.toArray()), + executionContext + ); + } + } + return super.visitMethodInvocation(mi, executionContext); + } + + }); + } +} diff --git a/src/main/java/org/openrewrite/micrometer/package-info.java b/src/main/java/org/openrewrite/micrometer/package-info.java new file mode 100644 index 0000000..e313075 --- /dev/null +++ b/src/main/java/org/openrewrite/micrometer/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 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. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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. + */ +@NonNullApi @NonNullFields +package org.openrewrite.micrometer; + +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.internal.lang.NonNullFields; \ No newline at end of file diff --git a/src/main/resources/META-INF/rewrite/classpath/micrometer-commons-1.11.3.jar b/src/main/resources/META-INF/rewrite/classpath/micrometer-commons-1.11.3.jar new file mode 100644 index 0000000..73feb1e Binary files /dev/null and b/src/main/resources/META-INF/rewrite/classpath/micrometer-commons-1.11.3.jar differ diff --git a/src/main/resources/META-INF/rewrite/classpath/micrometer-core-1.11.3.jar b/src/main/resources/META-INF/rewrite/classpath/micrometer-core-1.11.3.jar new file mode 100644 index 0000000..8a863fa Binary files /dev/null and b/src/main/resources/META-INF/rewrite/classpath/micrometer-core-1.11.3.jar differ diff --git a/src/main/resources/META-INF/rewrite/classpath/micrometer-observation-1.11.3.jar b/src/main/resources/META-INF/rewrite/classpath/micrometer-observation-1.11.3.jar new file mode 100644 index 0000000..2baeffc Binary files /dev/null and b/src/main/resources/META-INF/rewrite/classpath/micrometer-observation-1.11.3.jar differ diff --git a/src/test/java/org/openrewrite/micrometer/TimerToObservationTest.java b/src/test/java/org/openrewrite/micrometer/TimerToObservationTest.java new file mode 100644 index 0000000..71b0784 --- /dev/null +++ b/src/test/java/org/openrewrite/micrometer/TimerToObservationTest.java @@ -0,0 +1,601 @@ +/* + * Copyright 2021 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. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.micrometer; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class TimerToObservationTest implements RewriteTest { + + public void defaults(RecipeSpec spec) { + spec.recipe(new TimerToObservation()) + .parser(JavaParser.fromJavaVersion().classpath("micrometer-core")); + } + + @Test + void recordRunnable() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + class Test { + private MeterRegistry registry; + + void test(Runnable arg) { + Timer.builder("my.timer") + .register(registry) + .record(arg); + } + } + """, + """ + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + class Test { + private ObservationRegistry registry; + + void test(Runnable arg) { + Observation.createNotStarted("my.timer", registry) + .observe(arg); + } + } + """ + ) + ); + } + + @Test + void timerVariable() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + class Test { + private MeterRegistry registry; + + void test(Runnable arg) { + Timer t = Timer.builder("my.timer") + .register(registry); + t.record(arg); + } + } + """, + """ + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + class Test { + private ObservationRegistry registry; + + void test(Runnable arg) { + Observation t = Observation.createNotStarted("my.timer", registry); + t.observe(arg); + } + } + """ + ) + ); + } + + @Test + void keepComments() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + class Test { + private MeterRegistry registry; + + void test(Runnable arg) { + // Comments on Timer + Timer.builder("my.timer") + // Comments on register + .register(registry) + // Comments on record + .record(arg); + } + } + """, + """ + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + class Test { + private ObservationRegistry registry; + + void test(Runnable arg) { + // Comments on Timer + Observation.createNotStarted("my.timer", registry) + // Comments on record + .observe(arg); + } + } + """ + ) + ); + } + + @Nested + class Tags { + @Test + void tag() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + class Test { + private MeterRegistry registry; + + void test(Runnable arg) { + Timer.builder("my.timer") + .tag("key", "value") + .register(registry) + .record(arg); + } + } + """, + """ + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + class Test { + private ObservationRegistry registry; + + void test(Runnable arg) { + Observation.createNotStarted("my.timer", registry) + .highCardinalityKeyValue("key", "value") + .observe(arg); + } + } + """ + ) + ); + } + + @Test + void tagsVarArgs() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + class Test { + private MeterRegistry registry; + + void test(Runnable arg) { + Timer.builder("my.timer") + .tags("key1", "value1", "key2", "value2") + .register(registry) + .record(arg); + } + } + """, + """ + import io.micrometer.common.KeyValues; + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + class Test { + private ObservationRegistry registry; + + void test(Runnable arg) { + Observation.createNotStarted("my.timer", registry) + .highCardinalityKeyValues(KeyValues.of("key1", "value1", "key2", "value2")) + .observe(arg); + } + } + """ + ) + ); + } + + @Test + void tagsArray() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + class Test { + private MeterRegistry registry; + + void test(Runnable arg) { + String[] tags = new String[]{"key1", "value1", "key2", "value2"}; + Timer.builder("my.timer") + .tags(tags) + .register(registry) + .record(arg); + } + } + """, + """ + import io.micrometer.common.KeyValues; + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + class Test { + private ObservationRegistry registry; + + void test(Runnable arg) { + String[] tags = new String[]{"key1", "value1", "key2", "value2"}; + Observation.createNotStarted("my.timer", registry) + .highCardinalityKeyValues(KeyValues.of(tags)) + .observe(arg); + } + } + """ + ) + ); + } + + @Test + void tagsCollection() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Tag; + import io.micrometer.core.instrument.Timer; + + import java.util.List; + + class Test { + private MeterRegistry registry; + + void test(Runnable arg) { + List tags = List.of( + Tag.of("key1", "value1"), + Tag.of("key2", "value2") + ); + Timer.builder("my.timer") + .tags(tags) + .register(registry) + .record(arg); + } + } + """, + """ + import io.micrometer.common.KeyValues; + import io.micrometer.core.instrument.Tag; + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + import java.util.List; + + class Test { + private ObservationRegistry registry; + + void test(Runnable arg) { + List tags = List.of( + Tag.of("key1", "value1"), + Tag.of("key2", "value2") + ); + Observation.createNotStarted("my.timer", registry) + .highCardinalityKeyValues(KeyValues.of(tags, Tag::getKey, Tag::getValue)) + .observe(arg); + } + } + """ + ) + ); + } + } + + + @Test + void recordSupplier() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + import java.util.function.Supplier; + + class Test { + private MeterRegistry registry; + + void test(Supplier arg) { + String result = Timer.builder("my.timer") + .register(registry) + .record(arg); + } + } + """, + """ + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + import java.util.function.Supplier; + + class Test { + private ObservationRegistry registry; + + void test(Supplier arg) { + String result = Observation.createNotStarted("my.timer", registry) + .observe(arg); + } + } + """ + ) + ); + } + + @Nested + class Callable { + @Test + void recordCallable() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + import java.util.concurrent.Callable; + + class Test { + private MeterRegistry registry; + + void test(Callable arg) { + String result = Timer.builder("my.timer") + .register(registry) + .recordCallable(arg); + } + } + """, + """ + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + import java.util.concurrent.Callable; + + class Test { + private ObservationRegistry registry; + + void test(Callable arg) { + String result = Observation.createNotStarted("my.timer", registry) + .observe(arg); + } + } + """ + ) + ); + } + } + + @Nested + class Wrap { + + @Test + void wrapRunnable() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + class Test { + private MeterRegistry registry; + + void test(Runnable arg) { + Runnable result = Timer.builder("my.timer") + .register(registry) + .wrap(arg); + } + } + """, + """ + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + class Test { + private ObservationRegistry registry; + + void test(Runnable arg) { + Runnable result = Observation.createNotStarted("my.timer", registry) + .wrap(arg); + } + } + """ + ) + ); + } + + @Test + void wrapSupplier() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + import java.util.function.Supplier; + + class Test { + private MeterRegistry registry; + + void test(Supplier arg) { + Supplier result = Timer.builder("my.timer") + .register(registry) + .wrap(arg); + } + } + """, + """ + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + import java.util.function.Supplier; + + class Test { + private ObservationRegistry registry; + + void test(Supplier arg) { + Supplier result = Observation.createNotStarted("my.timer", registry) + .wrap(arg); + } + } + """ + ) + ); + } + + @Test + void wrapCallable() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + import java.util.concurrent.Callable; + + class Test { + private MeterRegistry registry; + + void test(Callable arg) { + Callable result = Timer.builder("my.timer") + .register(registry) + .wrap(arg); + } + } + """, + """ + import io.micrometer.observation.Observation; + import io.micrometer.observation.ObservationRegistry; + + import java.util.concurrent.Callable; + + class Test { + private ObservationRegistry registry; + + void test(Callable arg) { + Callable result = Observation.createNotStarted("my.timer", registry) + .wrap(arg); + } + } + """ + ) + ); + } + } + + @Nested + class Ignore { + @Test + void noTimer() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.Counter; + import io.micrometer.core.instrument.MeterRegistry; + + class Test { + private MeterRegistry registry; + + void test() { + Counter.builder("my.counter") + .register(registry) + .increment(); + } + } + """ + ) + ); + } + + @Test + void recordDuration() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + import java.time.Duration; + + class Test { + private MeterRegistry registry; + + void test() { + Timer.builder("my.timer") + .register(registry) + .record(Duration.ofMillis(100)); + } + } + """ + ) + ); + } + + @Test + void recordTimeUnit() { + rewriteRun( + //language=java + java( + """ + import io.micrometer.core.instrument.MeterRegistry; + import io.micrometer.core.instrument.Timer; + + import java.util.concurrent.TimeUnit; + + class Test { + private MeterRegistry registry; + + void test() { + Timer.builder("my.timer") + .register(registry) + .record(100, TimeUnit.MILLISECONDS); + } + } + """ + ) + ); + } + } + +} \ No newline at end of file