[JAVA-9604] Refactor and code clean-up for Metrics article

This commit is contained in:
Haroon Khan
2022-01-31 12:45:28 +00:00
parent b1d2619337
commit 225b992f98
25 changed files with 373 additions and 343 deletions

View File

@@ -12,3 +12,4 @@ The "REST With Spring" Classes: http://bit.ly/restwithspring
- [Health Indicators in Spring Boot](https://www.baeldung.com/spring-boot-health-indicators)
- [How to Enable All Endpoints in Spring Boot Actuator](https://www.baeldung.com/spring-boot-actuator-enable-endpoints)
- [Spring Boot Startup Actuator Endpoint](https://www.baeldung.com/spring-boot-actuator-startup)
- [Metrics for your Spring REST API](https://www.baeldung.com/spring-rest-api-metrics)

View File

@@ -23,6 +23,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
@@ -35,6 +39,16 @@
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -51,6 +65,10 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,42 @@
package com.baeldung.metrics;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.security.servlet.SecurityRequestMatchersManagementContextConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.context.request.RequestContextListener;
import javax.servlet.ServletContext;
@EnableScheduling
@ComponentScan("com.baeldung.metrics")
@SpringBootApplication
public class MetricsApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(MetricsApplication.class);
}
@Override
public void onStartup(ServletContext sc) {
// Manages the lifecycle of the root application context
sc.addListener(new RequestContextListener());
}
public static void main(final String[] args) {
// only load properties for this application
System.setProperty("spring.config.location", "classpath:application-metrics.properties");
SpringApplication.run(MetricsApplication.class, args);
}
}

View File

@@ -0,0 +1,31 @@
package com.baeldung.metrics;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@ComponentScan("com.baeldung.metrics")
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver() {
final InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/view/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
@Override
public void addViewControllers(final ViewControllerRegistry registry) {
registry.addViewController("/metrics/graph.html");
registry.addViewController("/metrics/homepage.html");
}
}

View File

@@ -0,0 +1,41 @@
package com.baeldung.metrics.controller;
import com.baeldung.metrics.service.InMemoryMetricService;
import com.baeldung.metrics.service.MetricService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Map;
@Controller
@RequestMapping(value = "/metrics")
@ResponseBody
public class MetricsController {
@Autowired
private InMemoryMetricService metricService;
// change the qualifier to use the in-memory implementation
@Autowired
@Qualifier("customActuatorMetricService")
private MetricService graphMetricService;
@GetMapping(value = "/metric")
public Map<String, Map<Integer, Integer>> getMetric() {
return metricService.getFullMetric();
}
@GetMapping(value = "/status-metric")
public Map<Integer, Integer> getStatusMetric() {
return metricService.getStatusMetric();
}
@GetMapping(value = "/metric-graph-data")
public Object[][] getMetricData() {
return graphMetricService.getGraphData();
}
}

View File

@@ -0,0 +1,51 @@
package com.baeldung.metrics.filter;
import com.baeldung.metrics.service.CustomActuatorMetricService;
import com.baeldung.metrics.service.InMemoryMetricService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class MetricFilter implements Filter {
@Autowired
private InMemoryMetricService metricService;
@Autowired
private CustomActuatorMetricService actMetricService;
@Override
public void init(final FilterConfig config) {
if (metricService == null || actMetricService == null) {
WebApplicationContext appContext = WebApplicationContextUtils
.getRequiredWebApplicationContext(config.getServletContext());
metricService = appContext.getBean(InMemoryMetricService.class);
actMetricService = appContext.getBean(CustomActuatorMetricService.class);
}
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws java.io.IOException, ServletException {
final HttpServletRequest httpRequest = ((HttpServletRequest) request);
final String req = httpRequest.getMethod() + " " + httpRequest.getRequestURI();
chain.doFilter(request, response);
final int status = ((HttpServletResponse) response).getStatus();
metricService.increaseCount(req, status);
actMetricService.increaseCount(status);
}
}

View File

@@ -0,0 +1,105 @@
package com.baeldung.metrics.service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class ActuatorMetricService implements MetricService {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
@Autowired
private MeterRegistry publicMetrics;
private final List<List<Integer>> statusMetricsByMinute;
private final List<String> statusList;
public ActuatorMetricService() {
statusMetricsByMinute = new ArrayList<>();
statusList = new ArrayList<>();
}
@Override
public Object[][] getGraphData() {
final Date current = new Date();
final int colCount = statusList.size() + 1;
final int rowCount = statusMetricsByMinute.size() + 1;
final Object[][] result = new Object[rowCount][colCount];
result[0][0] = "Time";
int j = 1;
for (final String status : statusList) {
result[0][j] = status;
j++;
}
for (int i = 1; i < rowCount; i++) {
result[i][0] = DATE_FORMAT.format(new Date(current.getTime() - (60000L * (rowCount - i))));
}
List<Integer> minuteOfStatuses;
List<Integer> last = new ArrayList<>();
for (int i = 1; i < rowCount; i++) {
minuteOfStatuses = statusMetricsByMinute.get(i - 1);
for (j = 1; j <= minuteOfStatuses.size(); j++) {
result[i][j] = minuteOfStatuses.get(j - 1) - (last.size() >= j ? last.get(j - 1) : 0);
}
while (j < colCount) {
result[i][j] = 0;
j++;
}
last = minuteOfStatuses;
}
return result;
}
@Scheduled(fixedDelayString = "${fixedDelay.in.milliseconds:60000}")
private void exportMetrics() {
final List<Integer> lastMinuteStatuses = initializeStatuses(statusList.size());
for (final Meter counterMetric : publicMetrics.getMeters()) {
updateMetrics(counterMetric, lastMinuteStatuses);
}
statusMetricsByMinute.add(lastMinuteStatuses);
}
private List<Integer> initializeStatuses(int size) {
List<Integer> counterList = new ArrayList<>();
for (int i = 0; i < size; i++) {
counterList.add(0);
}
return counterList;
}
private void updateMetrics(Meter counterMetric, List<Integer> statusCount) {
String metricName = counterMetric.getId().getName();
if (metricName.contains("counter.status.")) {
// example 404, 200
String status = metricName.substring(15, 18);
appendStatusIfNotExist(status, statusCount);
int index = statusList.indexOf(status);
int oldCount = statusCount.get(index) == null ? 0 : statusCount.get(index);
statusCount.set(index, (int)((Counter) counterMetric).count() + oldCount);
}
}
private void appendStatusIfNotExist(String status, List<Integer> statusCount) {
if (!statusList.contains(status)) {
statusList.add(status);
statusCount.add(0);
}
}
}

View File

@@ -0,0 +1,86 @@
package com.baeldung.metrics.service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.search.Search;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class CustomActuatorMetricService implements MetricService {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
@Autowired
private MeterRegistry registry;
private final List<List<Integer>> statusMetricsByMinute;
private final List<String> statusList;
public CustomActuatorMetricService() {
statusMetricsByMinute = new ArrayList<>();
statusList = new ArrayList<>();
}
public void increaseCount(int status) {
String counterName = "counter.status." + status;
registry.counter(counterName).increment();
if (!statusList.contains(counterName)) {
statusList.add(counterName);
}
}
@Override
public Object[][] getGraphData() {
final Date current = new Date();
final int colCount = statusList.size() + 1;
final int rowCount = statusMetricsByMinute.size() + 1;
final Object[][] result = new Object[rowCount][colCount];
result[0][0] = "Time";
int j = 1;
for (final String status : statusList) {
result[0][j] = status;
j++;
}
for (int i = 1; i < rowCount; i++) {
result[i][0] = DATE_FORMAT.format(new Date(current.getTime() - (60000L * (rowCount - i))));
}
List<Integer> minuteOfStatuses;
for (int i = 1; i < rowCount; i++) {
minuteOfStatuses = statusMetricsByMinute.get(i - 1);
for (j = 1; j <= minuteOfStatuses.size(); j++) {
result[i][j] = minuteOfStatuses.get(j - 1);
}
while (j < colCount) {
result[i][j] = 0;
j++;
}
}
return result;
}
@Scheduled(fixedDelayString = "${fixedDelay.in.milliseconds:60000}")
private void exportMetrics() {
List<Integer> statusCount = new ArrayList<>();
for (final String status : statusList) {
Search search = registry.find(status);
Counter counter = search.counter();
if (counter == null) {
statusCount.add(0);
} else {
statusCount.add((int) counter.count());
registry.remove(counter);
}
}
statusMetricsByMinute.add(statusCount);
}
}

View File

@@ -0,0 +1,112 @@
package com.baeldung.metrics.service;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class InMemoryMetricService implements MetricService {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
private final Map<String, Map<Integer, Integer>> metricMap;
private final Map<Integer, Integer> statusMetric;
private final Map<String, Map<Integer, Integer>> timeMap;
public InMemoryMetricService() {
metricMap = new ConcurrentHashMap<>();
statusMetric = new ConcurrentHashMap<>();
timeMap = new ConcurrentHashMap<>();
}
public void increaseCount(String request, int status) {
increaseMainMetric(request, status);
increaseStatusMetric(status);
updateTimeMap(status);
}
public Map<String, Map<Integer, Integer>> getFullMetric() {
return metricMap;
}
public Map<Integer, Integer> getStatusMetric() {
return statusMetric;
}
public Object[][] getGraphData() {
final int colCount = statusMetric.keySet().size() + 1;
final Set<Integer> allStatus = statusMetric.keySet();
final int rowCount = timeMap.keySet().size() + 1;
final Object[][] result = new Object[rowCount][colCount];
result[0][0] = "Time";
int j = 1;
for (final int status : allStatus) {
result[0][j] = status;
j++;
}
int i = 1;
Map<Integer, Integer> tempMap;
for (final Entry<String, Map<Integer, Integer>> entry : timeMap.entrySet()) {
result[i][0] = entry.getKey();
tempMap = entry.getValue();
for (j = 1; j < colCount; j++) {
result[i][j] = tempMap.get((Integer) result[0][j]);
if (result[i][j] == null) {
result[i][j] = 0;
}
}
i++;
}
for (int k = 1; k < result[0].length; k++) {
result[0][k] = result[0][k].toString();
}
return result;
}
private void increaseMainMetric(String request, int status) {
Map<Integer, Integer> statusMap = metricMap.get(request);
if (statusMap == null) {
statusMap = new ConcurrentHashMap<>();
}
Integer count = statusMap.get(status);
if (count == null) {
count = 1;
} else {
count++;
}
statusMap.put(status, count);
metricMap.put(request, statusMap);
}
private void increaseStatusMetric(int status) {
statusMetric.merge(status, 1, Integer::sum);
}
private void updateTimeMap(int status) {
final String time = DATE_FORMAT.format(new Date());
Map<Integer, Integer> statusMap = timeMap.get(time);
if (statusMap == null) {
statusMap = new ConcurrentHashMap<>();
}
Integer count = statusMap.get(status);
if (count == null) {
count = 1;
} else {
count++;
}
statusMap.put(status, count);
timeMap.put(time, statusMap);
}
}

View File

@@ -0,0 +1,7 @@
package com.baeldung.metrics.service;
public interface MetricService {
Object[][] getGraphData();
}

View File

@@ -0,0 +1,9 @@
management.endpoints.web.exposure.include=info,health,metrics
# JPA and Security is not required for Metrics application
spring.autoconfigure.exclude= org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, \
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, \
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration, \
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, \
org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration, \
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd" >
</beans>

View File

@@ -0,0 +1,43 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>Metric Graph</title>
<script
src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("visualization", "1", {
packages : [ "corechart" ]
});
function drawChart() {
$.get("<c:url value="/metrics/metric-graph-data"/>",
function(mydata) {
var data = google.visualization.arrayToDataTable(mydata);
var options = {
title : 'Website Metric',
hAxis : {
title : 'Time',
titleTextStyle : {
color : '#333'
}
},
vAxis : {
minValue : 0
}
};
var chart = new google.visualization.AreaChart(document
.getElementById('chart_div'));
chart.draw(data, options);
});
}
</script>
</head>
<body onload="drawChart()">
<div id="chart_div" style="width: 900px; height: 500px;"></div>
</body>
</html>

View File

@@ -0,0 +1,7 @@
<html>
<head></head>
<body>
<h1>This is the body of the sample view</h1>
</body>
</html>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"
>
<display-name>Spring REST Application</display-name>
<!-- Spring root -->
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.baeldung.metrics</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Spring child -->
<servlet>
<servlet-name>api</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>api</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- Metric filter -->
<filter>
<filter-name>metricFilter</filter-name>
<filter-class>com.baeldung.metrics.filter.MetricFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>metricFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>

View File

@@ -0,0 +1,64 @@
package com.baeldung.metrics;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.ActiveProfiles;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@SpringBootTest(
classes = MetricsApplication.class,
webEnvironment = RANDOM_PORT,
properties = {"fixedDelay.in.milliseconds=2000"}
)
@ActiveProfiles("metrics")
class MetricsApplicationIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void givenStatuses_WhenScheduledMethodExecuted_ExpectCountsAreAggregated() {
restTemplate.getForObject("/metrics/metric/notFound", String.class);
restTemplate.getForObject("/metrics/metric", String.class);
await().untilAsserted(() -> {
Object[][] statusCounts = restTemplate.getForObject("/metrics/metric-graph-data", Object[][].class);
assertThat(statusCounts[0]).contains("counter.status.200", "counter.status.404");
List<Integer> requestCounts = getRequestCounts(statusCounts);
verify404RequestFrom(requestCounts);
verify200RequestsFrom(requestCounts);
});
}
private static void verify200RequestsFrom(List<Integer> requestCounts) {
assertThat(requestCounts.size()).isGreaterThan(1);
}
private static void verify404RequestFrom(List<Integer> requestCounts) {
assertThat(requestCounts).contains(1);
}
private static List<Integer> getRequestCounts(Object[][] statusCounts) {
List<Integer> requestCounts = new ArrayList<>();
for (int i = 1; i < statusCounts.length; i++) {
for (int j = 1; j < statusCounts[i].length; j++) {
Integer count = (Integer) statusCounts[i][j];
if (count >= 1) {
requestCounts.add(count);
}
}
}
return requestCounts;
}
}