diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/LiveMetricsModel.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/LiveMetricsModel.java new file mode 100644 index 000000000..52541a054 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/LiveMetricsModel.java @@ -0,0 +1,7 @@ +package org.springframework.ide.vscode.boot.java.livehover.v2; + +public interface LiveMetricsModel { + + RequestMappingMetrics getRequestMappingMetrics(LiveRequestMapping rm); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/RequestMappingMetrics.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/RequestMappingMetrics.java new file mode 100644 index 000000000..473146fc8 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/RequestMappingMetrics.java @@ -0,0 +1,61 @@ +package org.springframework.ide.vscode.boot.java.livehover.v2; + +import java.util.concurrent.TimeUnit; + +import org.json.JSONArray; +import org.json.JSONObject; + +public interface RequestMappingMetrics { + + long getCallsCount(); + + double getTotalTime(); + + double getMaxTime(); + + TimeUnit getTimeUnit(); + + static RequestMappingMetrics parse(String metricsData) { + final JSONObject obj = new JSONObject(metricsData); + final TimeUnit timeUnit = TimeUnit.valueOf(obj.getString("baseUnit").toUpperCase()); + final JSONArray measurements = obj.getJSONArray("measurements"); + return new RequestMappingMetrics() { + + @Override + public double getTotalTime() { + Double d = findStatistic(measurements, "TOTAL_TIME"); + return d == null ? 0 : d.doubleValue(); + } + + @Override + public double getMaxTime() { + Double d = findStatistic(measurements, "MAX"); + return d == null ? 0 : d.doubleValue(); + } + + + @Override + public long getCallsCount() { + Double d = findStatistic(measurements, "COUNT"); + return d == null ? 0 : d.longValue(); + } + + @Override + public TimeUnit getTimeUnit() { + return timeUnit == null ? TimeUnit.SECONDS : timeUnit; + } + + @SuppressWarnings("unchecked") + private T findStatistic(JSONArray measurements, String statistic) { + for (int i = 0; i < measurements.length(); i++) { + JSONObject entry = measurements.getJSONObject(i); + if (statistic.equals(entry.getString("statistic"))) { + return (T) entry.get("value"); + } + } + return null; + } + }; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveData.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveData.java index 7cd164a2e..7d56ca306 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveData.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveData.java @@ -28,10 +28,12 @@ public class SpringProcessLiveData { private final LiveRequestMapping[] requestMappings; private final LiveConditional[] conditionals; private final LiveProperties properties; + private final LiveMetricsModel metrics; public SpringProcessLiveData(String processName, String processID, String contextPath, String urlScheme, String port, String host, LiveBeansModel beansModel, String[] activeProfiles, - LiveRequestMapping[] requestMappings, LiveConditional[] conditionals, LiveProperties properties) { + LiveRequestMapping[] requestMappings, LiveConditional[] conditionals, LiveProperties properties, + LiveMetricsModel metrics) { super(); this.processName = processName; this.processID = processID; @@ -44,6 +46,7 @@ public class SpringProcessLiveData { this.requestMappings = requestMappings; this.conditionals = conditionals; this.properties = properties; + this.metrics = metrics; } public String getProcessName() { @@ -89,5 +92,9 @@ public class SpringProcessLiveData { public LiveProperties getLiveProperties() { return this.properties; } + + public LiveMetricsModel getLiveMterics() { + return this.metrics; + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java index 1874ab7b8..6da80bc1a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java @@ -90,6 +90,7 @@ public class SpringProcessLiveDataExtractorOverJMX { LiveConditional[] conditionals = getConditionals(connection, domain, processID, processName); LiveRequestMapping[] requestMappings = getRequestMappings(connection, domain); LiveBeansModel beans = getBeans(connection, domain); + LiveMetricsModel metrics = getMetrics(connection, domain); if (contextPath == null) { contextPath = getContextPath(connection, domain, environment); @@ -110,7 +111,8 @@ public class SpringProcessLiveDataExtractorOverJMX { activeProfiles, requestMappings, conditionals, - properties); + properties, + metrics); } catch (Exception e) { log.error("error reading live data from: " + processID + " - " + processName, e); @@ -119,6 +121,35 @@ public class SpringProcessLiveDataExtractorOverJMX { return null; } + private LiveMetricsModel getMetrics(MBeanServerConnection connection, String domain) { + + return new LiveMetricsModel() { + + @Override + public RequestMappingMetrics getRequestMappingMetrics(LiveRequestMapping rm) { + try { + List tags = new ArrayList<>(); + if (rm.getSplitPath().length == 0) { + return null; + } + tags.add("uri:" + rm.getSplitPath()[0]); + if (!rm.getRequestMethods().isEmpty()) { + tags.add("methods:" + String.join(",", rm.getRequestMethods())); + } + Object metricsData = getActuatorDataFromOperation(connection, getObjectName(domain, "type=Endpoint,name=Metrics"), "metric", "http.server.requests", tags); + if (metricsData instanceof String) { + return RequestMappingMetrics.parse((String) metricsData); + } else { + return RequestMappingMetrics.parse(gson.toJson(metricsData)); + } + } catch (Exception e) { + log.error("", e); + return null; + } + } + }; + } + public String getProcessID(MBeanServerConnection connection) { try { RuntimeMXBean runtime = ManagementFactory.getPlatformMXBean(connection, RuntimeMXBean.class); @@ -379,6 +410,18 @@ public class SpringProcessLiveDataExtractorOverJMX { return null; } + private Object getActuatorDataFromOperation(MBeanServerConnection connection, ObjectName objectName, String operation, Object... parameters) throws Exception { + if (objectName != null) { + try { + return connection.invoke(objectName, operation, parameters, null); + } + catch (InstanceNotFoundException|IOException e) { + return null; + } + } + return null; + } + private ObjectName getObjectName(String domain, String keyProperties) throws Exception { if (StringUtil.hasText(domain) && StringUtil.hasText(keyProperties)) { String fullName = domain + ":" + keyProperties; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java index 82d2f1ae0..b53597330 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java @@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; import org.springframework.ide.vscode.boot.java.livehover.LiveHoverUtils; import org.springframework.ide.vscode.boot.java.livehover.v2.LiveRequestMapping; +import org.springframework.ide.vscode.boot.java.livehover.v2.RequestMappingMetrics; import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveData; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.util.Renderable; @@ -256,11 +257,12 @@ public class RequestMappingHoverProvider implements HoverProvider { Tuple2 mappingMethod = mappingMethods.get(i); SpringProcessLiveData liveData = mappingMethod.getT2(); + LiveRequestMapping requestMapping = mappingMethod.getT1(); String urlScheme = liveData.getUrlScheme(); String port = liveData.getPort(); String host = liveData.getHost(); - String[] paths = mappingMethod.getT1().getSplitPath(); + String[] paths = requestMapping.getSplitPath(); if (paths==null || paths.length==0) { //Technically, this means the path 'predicate' is unconstrained, meaning any path matches. //So this is not quite the same as the case where path=""... but... @@ -276,6 +278,13 @@ public class RequestMappingHoverProvider implements HoverProvider { .collect(Collectors.toList()); Renderable urlRenderables = Renderables.concat(renderableUrls); + + RequestMappingMetrics metrics = liveData.getLiveMterics().getRequestMappingMetrics(requestMapping); + if (metrics != null) { + Renderable metricsRenderable = Renderables.bold("Count: " + metrics.getCallsCount() + " Total Time: " + metrics.getTotalTime()); + urlRenderables = Renderables.concat(urlRenderables, Renderables.text("\n\n"), metricsRenderable); + } + Renderable processSection = Renderables.concat( urlRenderables, Renderables.mdBlob(LiveHoverUtils.niceAppName(liveData)) diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/metrics/test/RequestMappingMetricsTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/metrics/test/RequestMappingMetricsTest.java new file mode 100644 index 000000000..1719c5b6d --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/metrics/test/RequestMappingMetricsTest.java @@ -0,0 +1,21 @@ +package org.springframework.ide.vscode.boot.java.metrics.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.springframework.ide.vscode.boot.java.livehover.v2.RequestMappingMetrics; + +public class RequestMappingMetricsTest { + + @Test + public void testParser1() throws Exception { + RequestMappingMetrics mappingMetrics = RequestMappingMetrics.parse("{\"name\":\"http.server.requests\",\"description\":null,\"baseUnit\":\"seconds\",\"measurements\":[{\"statistic\":\"COUNT\",\"value\":1.0},{\"statistic\":\"TOTAL_TIME\",\"value\":0.03465965},{\"statistic\":\"MAX\",\"value\":0.47461985}],\"availableTags\":[{\"tag\":\"exception\",\"values\":[\"None\"]},{\"tag\":\"outcome\",\"values\":[\"SUCCESS\"]},{\"tag\":\"status\",\"values\":[\"200\"]}]}"); + assertEquals(TimeUnit.SECONDS, mappingMetrics.getTimeUnit()); + assertEquals(1, mappingMetrics.getCallsCount()); + assertEquals(0.47461985, mappingMetrics.getMaxTime()); + assertEquals(0.03465965, mappingMetrics.getTotalTime()); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/SpringProcessLiveDataBuilder.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/SpringProcessLiveDataBuilder.java index 2480989f7..0c0179ec1 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/SpringProcessLiveDataBuilder.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/SpringProcessLiveDataBuilder.java @@ -19,6 +19,7 @@ import org.springframework.ide.vscode.boot.java.livehover.v2.LiveBeansModel; import org.springframework.ide.vscode.boot.java.livehover.v2.LiveConditional; import org.springframework.ide.vscode.boot.java.livehover.v2.LiveConditionalParser; import org.springframework.ide.vscode.boot.java.livehover.v2.LiveContextPathUtil; +import org.springframework.ide.vscode.boot.java.livehover.v2.LiveMetricsModel; import org.springframework.ide.vscode.boot.java.livehover.v2.LiveProperties; import org.springframework.ide.vscode.boot.java.livehover.v2.LiveRequestMapping; import org.springframework.ide.vscode.boot.java.livehover.v2.LiveRequestMappingBoot1xRequestMapping; @@ -42,6 +43,7 @@ public class SpringProcessLiveDataBuilder { private LiveRequestMapping[] requestMappings; private LiveConditional[] conditionals; private LiveProperties properties; + private LiveMetricsModel metrics; public SpringProcessLiveDataBuilder processName(String processName) { this.processName = processName; @@ -122,8 +124,13 @@ public class SpringProcessLiveDataBuilder { return this; } + public SpringProcessLiveDataBuilder liveMetrics(LiveMetricsModel metrics) { + this.metrics = metrics; + return this; + } + public SpringProcessLiveData build() { - return new SpringProcessLiveData(processName, processID, contextPath, urlScheme, port, host, beansModel, activeProfiles, requestMappings, conditionals, properties); + return new SpringProcessLiveData(processName, processID, contextPath, urlScheme, port, host, beansModel, activeProfiles, requestMappings, conditionals, properties, metrics); } }