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

Instrument webmvc #27936

3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ configure(allprojects) { project ->
mavenBom "io.projectreactor:reactor-bom:2020.0.15"
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR12"
mavenBom "io.rsocket:rsocket-bom:1.1.1"
mavenBom "io.micrometer:micrometer-bom:2.0.0-SNAPSHOT"
mavenBom "io.micrometer:micrometer-tracing-bom:1.0.0-SNAPSHOT"
mavenBom "org.eclipse.jetty:jetty-bom:11.0.7"
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.6.10"
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.0"
Expand Down Expand Up @@ -249,6 +251,7 @@ configure(allprojects) { project ->
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/libs-spring-framework-build" }
}
}
Expand Down
1 change: 1 addition & 0 deletions spring-core/spring-core.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies {
optional("io.reactivex.rxjava3:rxjava")
optional("io.smallrye.reactive:mutiny")
optional("io.netty:netty-buffer")
optional("io.micrometer:micrometer-api")
testImplementation("jakarta.annotation:jakarta.annotation-api")
testImplementation("jakarta.xml.bind:jakarta.xml.bind-api")
testImplementation("com.google.code.findbugs:jsr305")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2012-2022 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.springframework.core.observability;

import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;

import io.micrometer.api.annotation.Timed;
import io.micrometer.api.instrument.Timer;
import io.micrometer.api.instrument.Timer.Builder;

import org.springframework.util.CollectionUtils;

/**
* Strategy that can be used to apply {@link Timer Timers} automatically instead of using
* {@link Timed @Timed}.
*
* @author Tadaya Tsuyukubo
* @author Stephane Nicoll
* @author Phillip Webb
* @since 6.0.0
*/
@FunctionalInterface
public interface AutoTimer {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this class be better in io.micrometer? Other than CollectionUtils there's no direct Spring connection.

Copy link
Member Author

@jonatan-ivanov jonatan-ivanov Jan 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something we need to figure out. I think based on what we have right now, it would be but we need to check if we can/want to provide a different disable mechanism (for Meters this mechanism is provided by MeterFilters but that won't work in this use-case).


/**
* An {@link AutoTimer} implementation that is enabled but applies no additional
* customizations.
*/
AutoTimer ENABLED = builder -> {
};

/**
* An {@link AutoTimer} implementation that is disabled and will not record metrics.
*/
AutoTimer DISABLED = new AutoTimer() {

@Override
public boolean isEnabled() {
return false;
}

@Override
public void apply(Builder builder) {
throw new IllegalStateException("AutoTimer is disabled");
}

};

/**
* Return if the auto-timer is enabled and metrics should be recorded.
* @return if the auto-timer is enabled
*/
default boolean isEnabled() {
return true;
}

/**
* Factory method to create a new {@link Builder Timer.Builder} with auto-timer
* settings {@link #apply(Builder) applied}.
* @param name the name of the timer
* @return a new builder instance with auto-settings applied
*/
default Builder builder(String name) {
return builder(() -> Timer.builder(name));
}

/**
* Factory method to create a new {@link Builder Timer.Builder} with auto-timer
* settings {@link #apply(Builder) applied}.
* @param supplier the builder supplier
* @return a new builder instance with auto-settings applied
*/
default Builder builder(Supplier<Builder> supplier) {
Builder builder = supplier.get();
apply(builder);
return builder;
}

/**
* Called to apply any auto-timer settings to the given {@link Builder Timer.Builder}.
* @param builder the builder to apply settings to
*/
void apply(Builder builder);

static void apply(AutoTimer autoTimer, String metricName, Set<Timed> annotations, Consumer<Builder> action) {
if (!CollectionUtils.isEmpty(annotations)) {
for (Timed annotation : annotations) {
action.accept(Timer.builder(annotation, metricName));
}
}
else {
if (autoTimer != null && autoTimer.isEnabled()) {
action.accept(autoTimer.builder(metricName));
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2012-2022 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.springframework.core.observability.annotation;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import io.micrometer.api.annotation.Timed;

import org.springframework.core.annotation.MergedAnnotationCollectors;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.ConcurrentReferenceHashMap;

/**
* Utility used to obtain {@link Timed @Timed} annotations from bean methods.
*
* @author Phillip Webb
* @since 6.0.0
*/
public final class TimedAnnotations {

private static final Map<AnnotatedElement, Set<Timed>> cache = new ConcurrentReferenceHashMap<>();

private TimedAnnotations() {
}

/**
* Return {@link Timed} annotations that should be used for the given {@code method}
* and {@code type}.
* @param method the source method
* @param type the source type
* @return the {@link Timed} annotations to use or an empty set
*/
public static Set<Timed> get(Method method, Class<?> type) {
Set<Timed> methodAnnotations = findTimedAnnotations(method);
if (!methodAnnotations.isEmpty()) {
return methodAnnotations;
}
return findTimedAnnotations(type);
}

private static Set<Timed> findTimedAnnotations(AnnotatedElement element) {
if (element == null) {
return Collections.emptySet();
}
Set<Timed> result = cache.get(element);
if (result != null) {
return result;
}
MergedAnnotations annotations = MergedAnnotations.from(element);
result = (!annotations.isPresent(Timed.class)) ? Collections.emptySet()
: annotations.stream(Timed.class).collect(MergedAnnotationCollectors.toAnnotationSet());
cache.put(element, result);
return result;
}

}
1 change: 1 addition & 0 deletions spring-test/spring-test.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ dependencies {
testImplementation("org.apache.httpcomponents:httpclient")
testImplementation("io.projectreactor.netty:reactor-netty-http")
testImplementation("de.bechte.junit:junit-hierarchicalcontextrunner")
testImplementation("io.micrometer:micrometer-tracing-integration-test")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine") {
exclude group: "junit", module: "junit"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright 2002-2022 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.springframework.test.web.servlet;

import java.time.Duration;
import java.util.function.BiConsumer;

import io.micrometer.api.annotation.Timed;
import io.micrometer.api.instrument.MeterRegistry;
import io.micrometer.api.instrument.Timer;
import io.micrometer.api.instrument.TimerRecordingHandler;
import io.micrometer.api.instrument.simple.SimpleMeterRegistry;
import io.micrometer.api.instrument.transport.http.context.HttpServerHandlerContext;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.test.SampleTestRunner;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.observability.DefaultWebMvcTagsProvider;
import org.springframework.web.servlet.observability.WebMvcObservabilityFilter;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;

/**
* Just a demo to try MVC instrumentation out with Zipkin, will be deleted later.
*/
public class ObservabilityPlaygroundTests extends SampleTestRunner {
private final SimpleMeterRegistry meterRegistry;
private final MockMvc mockMvc;

public ObservabilityPlaygroundTests() {
this.meterRegistry = new SimpleMeterRegistry();
this.meterRegistry.config().timerRecordingHandler(new TestTimerRecordingHandler());
this.mockMvc = standaloneSetup(new TestController())
.addFilters(new WebMvcObservabilityFilter(this.meterRegistry, new DefaultWebMvcTagsProvider(), "http.server.rq", null))
.build();
}

@Override
protected MeterRegistry getMeterRegistry() {
return this.meterRegistry;
}

@Override
protected SampleRunnerConfig getSampleRunnerConfig() {
return SampleRunnerConfig.builder().build();
}

@Override
public BiConsumer<Tracer, MeterRegistry> yourCode() {
return ((tracer, registry) -> {
try {
mockMvc.perform(get("/"));
mockMvc.perform(get("/api/people/12345"));
mockMvc.perform(get("/oops"));
}
catch (Exception e) {
e.printStackTrace();
}

System.out.println(meterRegistry.getMetersAsString());
});
}

@Timed
@RestController
static class TestController {
@GetMapping("/")
String hello() {
return "hello";
}

@GetMapping("/api/people/{id}")
String personById(@PathVariable String id) {
return id;
}

@GetMapping("/oops")
ResponseEntity<String> oops() {
return ResponseEntity.badRequest().body("oops");
}
}

static class TestTimerRecordingHandler implements TimerRecordingHandler<HttpServerHandlerContext> {
@Override
public void onStart(Timer.Sample sample, HttpServerHandlerContext context) {
System.out.println(sample + " started " + context);
}

@Override
public void onError(Timer.Sample sample, HttpServerHandlerContext context, Throwable throwable) {
System.out.println(sample + " failed " + context);
}

@Override
public void onStop(Timer.Sample sample, HttpServerHandlerContext context, Timer timer, Duration duration) {
System.out.println(sample + " stopped " + context);
}

@Override
public void onScopeOpened(Timer.Sample sample, HttpServerHandlerContext context) {
System.out.println(sample + " scope opened " + context);
}

@Override
public void onScopeClosed(Timer.Sample sample, HttpServerHandlerContext context) {
System.out.println(sample + " scope closed " + context);
}

@Override
public boolean supportsContext(Timer.HandlerContext context) {
return context instanceof HttpServerHandlerContext;
}
}

}
1 change: 1 addition & 0 deletions spring-webmvc/spring-webmvc.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies {
optional("org.jetbrains.kotlin:kotlin-stdlib")
optional("org.reactivestreams:reactive-streams")
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
optional("io.micrometer:micrometer-api")
testImplementation(testFixtures(project(":spring-beans")))
testImplementation(testFixtures(project(":spring-core")))
testImplementation(testFixtures(project(":spring-context")))
Expand Down
Loading