Monitor Spring Boot API Latency with Actuator, AOP, and Prometheus
This tutorial shows how to instrument Spring Boot APIs using Actuator, a custom @Monitor annotation with AOP, and Prometheus to collect and visualize method execution times, providing a complete end‑to‑end monitoring solution.
1. Introduction
This article explains how to monitor arbitrary API call latency in a Spring Boot application by combining Spring Boot Actuator, a custom @Monitor annotation, AOP, and Prometheus.
2. Environment Setup
Required dependencies:
<code><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency></code>Expose all actuator endpoints:
<code>management:
endpoints:
web:
base-path: /ac
exposure:
include: '*'</code>3. Practical Example
3.1 Define the @Monitor annotation
<code>@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Monitor {
String[] tags() default {};
}</code>3.2 Implement the monitoring aspect
<code>@Component
@Aspect
public class MonitorAspect {
private final MeterRegistry meterRegistry;
private static final String API_TIMER_METER_NAME = "myapp.api.timer";
public MonitorAspect(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Pointcut("@annotation(monitor)")
private void pcMonitor(Monitor monitor) {}
@Around("pcMonitor(monitor)")
public Object around(ProceedingJoinPoint pjp, Monitor monitor) throws Throwable {
Timer.Sample sample = Timer.start(this.meterRegistry);
String[] tags = monitor.tags();
Object ret = null;
Throwable ex = null;
try {
ret = pjp.proceed();
} catch (Throwable th) {
ex = th;
throw th;
} finally {
List<String> listTags = new ArrayList<>();
listTags.addAll(Arrays.asList(tags));
if (Objects.nonNull(ex)) {
listTags.add(ex.getClass().getSimpleName());
}
Timer timer = meterRegistry.timer(API_TIMER_METER_NAME, listTags.toArray(new String[0]));
sample.stop(timer);
}
return ret;
}
}</code>3.3 Service layer
<code>@Service
public class UserService {
private static final List<User> DATAS = List.of(
new User(1L, "张三", "男", 22),
new User(2L, "李四", "男", 23),
new User(3L, "王五", "女", 22),
new User(4L, "赵六", "男", 32));
public List<User> queryUsers() {
sleep(2000);
return DATAS;
}
public User queryById(Long id) {
sleep(1000);
return DATAS.stream().filter(user -> user.getId() == id).findFirst().orElse(null);
}
private void sleep(int time) {
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(time));
} catch (InterruptedException e) {}
}
}</code>3.4 Controller layer
<code>@Monitor(tags = {"UserController", "list"})
@GetMapping("")
public List<User> list() {
return this.userService.queryUsers();
}
@Monitor(tags = {"UserController", "ById"})
@GetMapping("/{id}")
public User queryById(@PathVariable Long id) {
return this.userService.queryById(id);
}</code>Note: The tags attribute must contain an even number of strings because they are paired as key/value tags.
4. Testing and Visualization
Invoke the two endpoints repeatedly and access /ac/metrics/myapp.api.timer to view raw metrics. Prometheus scrapes the /actuator/prometheus endpoint, and you can visualize latency trends in Grafana or the Prometheus UI.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.