diff --git a/tunnel-server/pom.xml b/tunnel-server/pom.xml index b56dc0270..bb846aaf1 100644 --- a/tunnel-server/pom.xml +++ b/tunnel-server/pom.xml @@ -70,6 +70,18 @@ <artifactId>commons-lang3</artifactId> </dependency> + <dependency> + <groupId>it.ozimov</groupId> + <artifactId>embedded-redis</artifactId> + <version>0.7.3</version> + <exclusions> + <exclusion> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/configuration/ArthasProperties.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/configuration/ArthasProperties.java index a84fc6247..4b4c41ee7 100644 --- a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/configuration/ArthasProperties.java +++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/configuration/ArthasProperties.java @@ -17,6 +17,13 @@ public class ArthasProperties { private Server server; + private EmbeddedRedis embeddedRedis; + + /** + * supoort apps.html/agents.html + */ + private boolean enableDetatilPages = false; + public Server getServer() { return server; } @@ -25,6 +32,22 @@ public class ArthasProperties { this.server = server; } + public EmbeddedRedis getEmbeddedRedis() { + return embeddedRedis; + } + + public void setEmbeddedRedis(EmbeddedRedis embeddedRedis) { + this.embeddedRedis = embeddedRedis; + } + + public boolean isEnableDetatilPages() { + return enableDetatilPages; + } + + public void setEnableDetatilPages(boolean enableDetatilPages) { + this.enableDetatilPages = enableDetatilPages; + } + public static class Server { /** * tunnel server listen host @@ -81,4 +104,40 @@ public class ArthasProperties { } + /** + * for test + * + * @author hengyunabc 2020-11-03 + * + */ + public static class EmbeddedRedis { + private boolean enabled = false; + private String host = "127.0.0.1"; + private int port = 6379; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + } + } diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/configuration/EmbeddedRedisConfiguration.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/configuration/EmbeddedRedisConfiguration.java new file mode 100644 index 000000000..83452acf1 --- /dev/null +++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/configuration/EmbeddedRedisConfiguration.java @@ -0,0 +1,38 @@ +package com.alibaba.arthas.tunnel.server.app.configuration; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.arthas.tunnel.server.app.configuration.ArthasProperties.EmbeddedRedis; + +import redis.embedded.RedisServer; + +/** + * + * @author hengyunabc 2020-11-03 + * + */ +@Configuration +@AutoConfigureBefore(TunnelClusterStoreConfiguration.class) +public class EmbeddedRedisConfiguration { + + @Bean(initMethod = "start", destroyMethod = "stop") + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "arthas", name = { "embedded-redis.enabled" }) + public RedisServer embeddedRedisServer(ArthasProperties arthasProperties) { + EmbeddedRedis embeddedRedis = arthasProperties.getEmbeddedRedis(); + + RedisServer redisServer = RedisServer.builder().port(embeddedRedis.getPort()).bind(embeddedRedis.getHost()) + .build(); + return redisServer; + + } + + public static void main(String[] args) { + RedisServer redisServer = new RedisServer(); + redisServer.start(); + } +} diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/web/DetailAPIController.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/web/DetailAPIController.java new file mode 100644 index 000000000..c05fee350 --- /dev/null +++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/web/DetailAPIController.java @@ -0,0 +1,91 @@ +package com.alibaba.arthas.tunnel.server.app.web; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.alibaba.arthas.tunnel.server.AgentClusterInfo; +import com.alibaba.arthas.tunnel.server.app.configuration.ArthasProperties; +import com.alibaba.arthas.tunnel.server.cluster.TunnelClusterStore; + +/** + * + * @author hengyunabc 2020-11-03 + * + */ +@Controller +public class DetailAPIController { + + private final static Logger logger = LoggerFactory.getLogger(DetailAPIController.class); + + @Autowired + ArthasProperties arthasProperties; + + @Autowired(required = false) + private TunnelClusterStore tunnelClusterStore; + + @RequestMapping("/api/tunnelApps") + @ResponseBody + public Set<String> tunnelApps(HttpServletRequest request, Model model) { + if (!arthasProperties.isEnableDetatilPages()) { + throw new IllegalAccessError("not allow"); + } + + Set<String> result = new HashSet<String>(); + + if (tunnelClusterStore != null) { + Collection<String> agentIds = tunnelClusterStore.allAgentIds(); + + for (String id : agentIds) { + String appName = findAppNameFromAgentId(id); + if (appName != null) { + result.add(appName); + } else { + logger.warn("illegal agentId: " + id); + } + } + + } + + return result; + } + + @RequestMapping("/api/tunnelAgentInfo") + @ResponseBody + public Map<String, AgentClusterInfo> tunnelAgentIds(@RequestParam(value = "app", required = true) String appName, + HttpServletRequest request, Model model) { + if (!arthasProperties.isEnableDetatilPages()) { + throw new IllegalAccessError("not allow"); + } + + if (tunnelClusterStore != null) { + Map<String, AgentClusterInfo> agentInfos = tunnelClusterStore.agentInfo(appName); + + return agentInfos; + } + + return Collections.emptyMap(); + } + + private static String findAppNameFromAgentId(String id) { + int index = id.indexOf('_'); + if (index < 0 || index >= id.length()) { + return null; + } + + return id.substring(0, index); + } +} diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/web/StatController.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/web/StatController.java index 11cb411a8..409ef1a29 100644 --- a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/web/StatController.java +++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/web/StatController.java @@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; /** - * + * arthas agent数据回报的演示接口 * @author hengyunabc 2019-09-24 * */ diff --git a/tunnel-server/src/main/resources/application.properties b/tunnel-server/src/main/resources/application.properties index 3e6b1f8de..c56becb64 100755 --- a/tunnel-server/src/main/resources/application.properties +++ b/tunnel-server/src/main/resources/application.properties @@ -7,4 +7,9 @@ arthas.server.port=7777 management.endpoints.web.exposure.include=* # default user name -spring.security.user.name=arthas \ No newline at end of file +spring.security.user.name=arthas + + +#arthas.enable-detatil-pages=true +#arthas.embedded-redis.enabled=true +#spring.redis.host=127.0.0.1 \ No newline at end of file diff --git a/tunnel-server/src/main/resources/static/agents.html b/tunnel-server/src/main/resources/static/agents.html new file mode 100644 index 000000000..a5505c8ea --- /dev/null +++ b/tunnel-server/src/main/resources/static/agents.html @@ -0,0 +1,95 @@ +<!doctype html> +<html lang="en"> + +<head> + <!-- Required meta tags --> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + + <!-- Bootstrap CSS --> + <link rel="stylesheet" href="https://g.alicdn.com/code/lib/twitter-bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" + crossorigin="anonymous"> + + <script src="https://g.alicdn.com/code/lib/vue/2.6.4/vue.min.js" integrity="sha256-isEQDc5Dw7wea1s5iMZjBvPuYzjzMrvtlPwE6LtavFA=" crossorigin="anonymous"></script> + <script src="https://g.alicdn.com/code/lib/axios/0.18.0/axios.min.js"></script> + + <title>Arthas Tutorials</title> + + <style> + /* This is all that's required */ + .dropdown-item-checked::before { + position: absolute; + left: .4rem; + content: '✓'; + font-weight: 600; + } + </style> +</head> + +<body> + <!-- Optional JavaScript --> + <!-- jQuery first, then Popper.js, then Bootstrap JS --> + <script src="https://g.alicdn.com/code/lib/jquery/3.3.1/jquery.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> + <script src="https://g.alicdn.com/code/lib/popper.js/1.14.7/umd/popper.min.js" integrity="sha512-5WvZa4N7Jq3TVNCp4rjcBMlc6pT3lZ7gVxjtI6IkKW+uItSa+rFgtFljvZnCxQGj8SUX5DHraKE6Mn/4smK1Cg==" crossorigin="anonymous"></script> + <script src="https://g.alicdn.com/code/lib/twitter-bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" + crossorigin="anonymous"></script> + + <div id="app"> + + <table class="table table-hover issue-tracker"> + <thead> + <tr> + <th>IP</th> + <th>AgentId</th> + <th>Version</th> + </tr> + </thead> + <tbody> + <tr v-for="(agentInfo, agentId) in agentInfos"> + <td> + <a class="btn btn-primary btn-xs" v-bind:href="tunnelWebConsoleLink(agentId, agentInfo.clientConnectHost)">{{agentInfo.host}}</a> + </td> + <td> + <span class="label label-default">{{agentId}}</span> + </td> + <td> + <span class="label label-default">{{agentInfo.arthasVersion}}</span> + </td> + </tr> + </tbody> + </table> + + </div> +</body> + + +<script> + var app = new Vue({ + el: '#app', + data: { + agentInfos: [], + }, + methods: { + fetchMyApps: function () { + var vm = this; + var params = new window.URLSearchParams(window.location.search); + var appName = params.get('app'); + axios.get('/api/tunnelAgentInfo?app=' + appName) + .then(function (response) { + vm.agentInfos = response.data + }) + .catch(function (error) { + console.log('api error ' + error) + }) + }, + tunnelWebConsoleLink: function (agentId, targetServer) { + return "/?targetServer=" + targetServer + "&agentId=" + agentId; + } + }, + mounted: function() { + this.fetchMyApps() + } + }) + +</script> +</html> \ No newline at end of file diff --git a/tunnel-server/src/main/resources/static/apps.html b/tunnel-server/src/main/resources/static/apps.html new file mode 100644 index 000000000..285c3261d --- /dev/null +++ b/tunnel-server/src/main/resources/static/apps.html @@ -0,0 +1,85 @@ +<!doctype html> +<html lang="en"> + +<head> + <!-- Required meta tags --> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + + <!-- Bootstrap CSS --> + <link rel="stylesheet" href="https://g.alicdn.com/code/lib/twitter-bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" + crossorigin="anonymous"> + + <script src="https://g.alicdn.com/code/lib/vue/2.6.4/vue.min.js" integrity="sha256-isEQDc5Dw7wea1s5iMZjBvPuYzjzMrvtlPwE6LtavFA=" crossorigin="anonymous"></script> + <script src="https://g.alicdn.com/code/lib/axios/0.18.0/axios.min.js"></script> + + <title>Arthas Tutorials</title> + + <style> + /* This is all that's required */ + .dropdown-item-checked::before { + position: absolute; + left: .4rem; + content: '✓'; + font-weight: 600; + } + </style> +</head> + +<body> + <!-- Optional JavaScript --> + <!-- jQuery first, then Popper.js, then Bootstrap JS --> + <script src="https://g.alicdn.com/code/lib/jquery/3.3.1/jquery.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> + <script src="https://g.alicdn.com/code/lib/popper.js/1.14.7/umd/popper.min.js" integrity="sha512-5WvZa4N7Jq3TVNCp4rjcBMlc6pT3lZ7gVxjtI6IkKW+uItSa+rFgtFljvZnCxQGj8SUX5DHraKE6Mn/4smK1Cg==" crossorigin="anonymous"></script> + <script src="https://g.alicdn.com/code/lib/twitter-bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" + crossorigin="anonymous"></script> + + <div id="app"> + + <table class="table table-hover issue-tracker"> + <thead> + <tr> + <th>应用名</th> + </tr> + </thead> + <tbody> + <tr v-for="myApp in myApps"> + <td> + <a class="btn btn-primary btn-xs" v-bind:href="detailLink(myApp)">{{myApp}}</a> + </td> + </tr> + </tbody> + </table> + + </div> +</body> + + +<script> + var app = new Vue({ + el: '#app', + data: { + myApps: {}, + }, + methods: { + fetchMyApps: function () { + var vm = this + axios.get('/api/tunnelApps') + .then(function (response) { + vm.myApps = response.data + }) + .catch(function (error) { + console.log('api error ' + error) + }) + }, + detailLink: function (appName) { + return "agents.html?app=" + appName; + } + }, + mounted: function() { + this.fetchMyApps() + } + }) + +</script> +</html> \ No newline at end of file