您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)如何將Arthas集成進(jìn)Spring Boot監(jiān)控平臺(tái)中,小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
Arthas 是 Alibaba 開(kāi)源的 Java 診斷工具,具有實(shí)時(shí)查看系統(tǒng)的運(yùn)行狀況;查看函數(shù)調(diào)用參數(shù)、返回值和異常;在線熱更新代碼;秒解決類沖突問(wèn)題;定位類加載路徑;生成熱點(diǎn);通過(guò)網(wǎng)頁(yè)診斷線上應(yīng)用。如今在各大廠都有廣泛應(yīng)用,也延伸出很多產(chǎn)品。
為了方便,SpringBoot Admin 簡(jiǎn)稱為 SBA(版本:1.5.x)。
1.5 版本的 SBA 如果要開(kāi)發(fā)插件比較麻煩,需要下載 SBA 的源碼包,再按照 Spring-boot-admin-server-ui-hystrix的形式 Copy 一份,由于 JS 使用的是 Angular,本人嘗試了很久,雖然掌握了如何開(kāi)發(fā)插件,奈何不會(huì) Angular,遂放棄????
版本:2.x 2.x 版本的 SBA 插件開(kāi)發(fā),官網(wǎng)有介紹如何開(kāi)發(fā),JS 使用 Vue,方便很多,由于我們項(xiàng)目還在使用 1.5,所以并沒(méi)有使用該版本,請(qǐng)讀者自行嘗試。
不能使用 SBA 的插件進(jìn)行集成,那還有什么辦法呢?????
鄙人的辦法是將 Arthas 的相關(guān)文件直接 Copy 到 Admin 服務(wù)中,這些文件都來(lái)自 Arthas-all 項(xiàng)目 Tunnel-server。
admin 目錄結(jié)構(gòu)
該包下存放的是所有 Arthas 的 Java 文件。
Endpoint 包下的文件可以都注釋掉,沒(méi)多大用。
ArthasController 這個(gè)文件是我自己新建的,用來(lái)獲取所有注冊(cè)到 Arthas 的客戶端,這在后面是有用的。
其他文件直接 Copy 過(guò)來(lái)就行。
@RequestMapping("/api/arthas") @RestController public class ArthasController { @Autowired private TunnelServer tunnelServer; @RequestMapping(value = "/clients", method = RequestMethod.GET) public Set<String> getClients() { Map<String, AgentInfo> agentInfoMap = tunnelServer.getAgentInfoMap(); return agentInfoMap.keySet(); } }
spring-boot-admin-server-ui
該文件建在 Resources.META-INF 下,Admin 會(huì)在啟動(dòng)的時(shí)候加載該目錄下的文件。
index.html 覆蓋 SBA 原來(lái)的首頁(yè),在其中添加一個(gè) Arthas 導(dǎo)航
<!DOCTYPE html> <html class="no-js"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Spring Boot Admin</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width"> <link rel="shortcut icon" type="image/x-icon" href="img/favicon.png"/> <link rel="stylesheet" type="text/css" href="core.css"/> <link rel="stylesheet" type="text/css" href="all-modules.css"/> </head> <body> <header class="navbar header--navbar desktop-only"> <div class="navbar-inner"> <div class="container-fluid"> <div class="spring-logo--container"> <a class="spring-logo" href="#"><span></span></a> </div> <div class="spring-logo--container"> <a class="spring-boot-logo" href="#"><span></span></a> </div> <ul class="nav pull-right"> <!--增加Arthas導(dǎo)航--> <li class="navbar-link ng-scope"> <a class="ng-binding" href="arthas/arthas.html">Arthas</a> </li> <li ng-repeat="view in mainViews" class="navbar-link" ng-class="{active: $state.includes(view.state)}"> <a ui-sref="{{view.state}}" ng-bind-html="view.title"></a> </li> </ul> </div> </div> </header> <div ui-view></div> <footer class="footer"> <ul class="inline"> <li><a href="https://codecentric.github.io/spring-boot-admin/@project.version@" target="_blank">Reference Guide</a></li> <li>-</li> <li><a href="https://github.com/codecentric/spring-boot-admin" target="_blank">Sources</a></li> <li>-</li> <li>Code licensed under <a href="http://www.apache.org/licenses/LICENSE-2.0" target="_blank">Apache License 2.0</a></li> </ul> </footer> <script src="dependencies.js" type="text/javascript"></script> <script type="text/javascript"> sbaModules = []; </script> <script src="core.js" type="text/javascript"></script> <script src="all-modules.js" type="text/javascript"></script> <script type="text/javascript"> angular.element(document).ready(function () { angular.bootstrap(document, sbaModules.slice(0), { strictDi: true }); }); </script> </body> </html>
Arthas.html
新建頁(yè)面,用于顯示 Arthas 控制臺(tái)頁(yè)面。
這個(gè)文件中有兩個(gè)隱藏文本域,這兩個(gè)用于連接 Arthas 服務(wù)端,在頁(yè)面加載的時(shí)候會(huì)自動(dòng)將 Admin 的 Url 賦值給 Ip。
<input type="hidden" id="ip" name="ip" value="127.0.0.1"> <input type="hidden" id="port" name="port" value="19898">
<!DOCTYPE html> <html class="no-js"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Spring Boot Admin</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width"> <link rel="shortcut icon" type="image/x-icon" href="../img/favicon.png"/> <link rel="stylesheet" type="text/css" href="../core.css"/> <link rel="stylesheet" type="text/css" href="../all-modules.css"/> <script src="js/jquery-3.3.1.min.js"></script> <script src="js/popper-1.14.6.min.js"></script> <script src="js/xterm.js"></script> <script src="js/web-console.js"></script> <script src="js/arthas.js"></script> <link href="js/xterm.css" rel="stylesheet" /> <script type="text/javascript"> window.addEventListener('resize', function () { var terminalSize = getTerminalSize(); ws.send(JSON.stringify({ action: 'resize', cols: terminalSize.cols, rows: terminalSize.rows })); xterm.resize(terminalSize.cols, terminalSize.rows); }); </script> </head> <body> <header class="navbar header--navbar desktop-only"> <div class="navbar-inner"> <div class="container-fluid"> <div class="spring-logo--container"> <a class="spring-logo" href="#"><span></span></a> </div> <div class="spring-logo--container"> <a class="spring-boot-logo" href="#"><span></span></a> </div> <ul class="nav pull-right"> <li class="navbar-link ng-scope"> <a class="ng-binding" href="arthas.html">Arthas</a> </li> <li class="navbar-link ng-scope"> <a class="ng-binding" href="../">Applications</a> </li> <li class="navbar-link ng-scope"> <a class="ng-binding" href="../#/turbine">Turbine</a> </li> <li class="navbar-link ng-scope"> <a class="ng-binding" href="../#/events">Journal</a> </li> <li class="navbar-link ng-scope"> <a class="ng-binding" href="../#/about">About</a> </li> <li class="navbar-link ng-scope"> <a class="ng-binding" href="../#/logout"><i class="fa fa-2x fa-sign-out" aria-hidden="true"></i></a> </li> </ul> </div> </div> </header> <div ui-view> <div class="container-fluid"> <form class="form-inline"> <input type="hidden" id="ip" name="ip" value="127.0.0.1"> <input type="hidden" id="port" name="port" value="19898"> Select Application: <select id="selectServer"></select> <button class="btn" onclick="startConnect()" type="button"><i class="fa fa-connectdevelop"></i> Connect</button> <button class="btn" onclick="disconnect()" type="button"><i class="fa fa-search-minus"></i> Disconnect</button> <button class="btn" onclick="release()" type="button"><i class="fa fa-search-minus"></i> Release</button> </form> <div id="terminal-card"> <div id="terminal"></div> </div> </div> </div> </body> </html>
Arthas.js 存儲(chǔ)頁(yè)面控制的 js
var registerApplications = null; var applications = null; $(document).ready(function () { reloadRegisterApplications(); reloadApplications(); }); /** * 獲取注冊(cè)的arthas客戶端 */ function reloadRegisterApplications() { var result = reqSync("/api/arthas/clients", "get"); registerApplications = result; initSelect("#selectServer", registerApplications, ""); } /** * 獲取注冊(cè)的應(yīng)用 */ function reloadApplications() { applications = reqSync("/api/applications", "get"); console.log(applications) } /** * 初始化下拉選擇框 */ function initSelect(uiSelect, list, key) { $(uiSelect).html(''); var server; for (var i = 0; i < list.length; i++) { server = list[i].toLowerCase().split("@"); if ("phantom-admin" === server[0]) continue; $(uiSelect).append("<option value=" + list[i].toLowerCase() + ">" + server[0] + "</option>"); } } /** * 重置配置文件 */ function release() { var currentServer = $("#selectServer").text(); for (var i = 0; i < applications.length; i++) { serverId = applications[i].id; serverName = applications[i].name.toLowerCase(); console.log(serverId + "/" + serverName); if (currentServer === serverName) { var result = reqSync("/api/applications/" +serverId+ "/env/reset", "post"); alert("env reset success"); } } } function reqSync(url, method) { var result = null; $.ajax({ url: url, type: method, async: false, //使用同步的方式,true為異步方式 headers: { 'Content-Type': 'application/json;charset=utf8;', }, success: function (data) { // console.log(data); result = data; }, error: function (data) { console.log("error"); } }); return result; }
Web-console.js
修改了連接部分代碼,參考一下。
var ws; var xterm; /**有修改**/ $(function () { var url = window.location.href; var ip = getUrlParam('ip'); var port = getUrlParam('port'); var agentId = getUrlParam('agentId'); if (ip != '' && ip != null) { $('#ip').val(ip); } else { $('#ip').val(window.location.hostname); } if (port != '' && port != null) { $('#port').val(port); } if (agentId != '' && agentId != null) { $('#selectServer').val(agentId); } // startConnect(true); }); /** get params in url **/ function getUrlParam (name, url) { if (!url) url = window.location.href; name = name.replace(/[\[\]]/g, '\\$&'); var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), results = regex.exec(url); if (!results) return null; if (!results[2]) return ''; return decodeURIComponent(results[2].replace(/\+/g, ' ')); } function getCharSize () { var tempDiv = $('<div />').attr({'role': 'listitem'}); var tempSpan = $('<div />').html('qwertyuiopasdfghjklzxcvbnm'); tempDiv.append(tempSpan); $("html body").append(tempDiv); var size = { width: tempSpan.outerWidth() / 26, height: tempSpan.outerHeight(), left: tempDiv.outerWidth() - tempSpan.outerWidth(), top: tempDiv.outerHeight() - tempSpan.outerHeight(), }; tempDiv.remove(); return size; } function getWindowSize () { var e = window; var a = 'inner'; if (!('innerWidth' in window )) { a = 'client'; e = document.documentElement || document.body; } var terminalDiv = document.getElementById("terminal-card"); var terminalDivRect = terminalDiv.getBoundingClientRect(); return { width: terminalDivRect.width, height: e[a + 'Height'] - terminalDivRect.top }; } function getTerminalSize () { var charSize = getCharSize(); var windowSize = getWindowSize(); console.log('charsize'); console.log(charSize); console.log('windowSize'); console.log(windowSize); return { cols: Math.floor((windowSize.width - charSize.left) / 10), rows: Math.floor((windowSize.height - charSize.top) / 17) }; } /** init websocket **/ function initWs (ip, port, agentId) { var protocol= location.protocol === 'https:' ? 'wss://' : 'ws://'; var path = protocol + ip + ':' + port + '/ws?method=connectArthas&id=' + agentId; ws = new WebSocket(path); } /** init xterm **/ function initXterm (cols, rows) { xterm = new Terminal({ cols: cols, rows: rows, screenReaderMode: true, rendererType: 'canvas', convertEol: true }); } /** 有修改 begin connect **/ function startConnect (silent) { var ip = $('#ip').val(); var port = $('#port').val(); var agentId = $('#selectServer').val(); if (ip == '' || port == '') { alert('Ip or port can not be empty'); return; } if (agentId == '') { if (silent) { return; } alert('AgentId can not be empty'); return; } if (ws != null) { alert('Already connected'); return; } // init webSocket initWs(ip, port, agentId); ws.onerror = function () { ws.close(); ws = null; !silent && alert('Connect error'); }; ws.onclose = function (message) { if (message.code === 2000) { alert(message.reason); } }; ws.onopen = function () { console.log('open'); $('#fullSc').show(); var terminalSize = getTerminalSize() console.log('terminalSize') console.log(terminalSize) // init xterm initXterm(terminalSize.cols, terminalSize.rows) ws.onmessage = function (event) { if (event.type === 'message') { var data = event.data; xterm.write(data); } }; xterm.open(document.getElementById('terminal')); xterm.on('data', function (data) { ws.send(JSON.stringify({action: 'read', data: data})) }); ws.send(JSON.stringify({action: 'resize', cols: terminalSize.cols, rows: terminalSize.rows})); window.setInterval(function () { if (ws != null && ws.readyState === 1) { ws.send(JSON.stringify({action: 'read', data: ""})); } }, 30000); } } function disconnect () { try { ws.close(); ws.onmessage = null; ws.onclose = null; ws = null; xterm.destroy(); $('#fullSc').hide(); alert('Connection was closed successfully!'); } catch (e) { alert('No connection, please start connect first.'); } } /** full screen show **/ function xtermFullScreen () { var ele = document.getElementById('terminal-card'); requestFullScreen(ele); } function requestFullScreen (element) { var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen; if (requestMethod) { requestMethod.call(element); } else if (typeof window.ActiveXObject !== "undefined") { var wscript = new ActiveXObject("WScript.Shell"); if (wscript !== null) { wscript.SendKeys("{F11}"); } } }
其他文件
jquery-3.3.1.min.js 新加 Js
copy 過(guò)來(lái)的 js
popper-1.14.6.min.js
web-console.js
xterm.css
xterm.js
bootstrap.yml
# arthas端口 arthas: server: port: 9898
這樣子,admin 端的配置完成了。
在配置中心加入配置
#arthas服務(wù)端域名 arthas.tunnel-server = ws://admin域名/ws #客戶端id,應(yīng)用名@隨機(jī)值,js會(huì)截取前面的應(yīng)用名 arthas.agent-id = ${spring.application.name}@${random.value} #arthas開(kāi)關(guān),可以在需要調(diào)式的時(shí)候開(kāi)啟,不需要的時(shí)候關(guān)閉 spring.arthas.enabled = false
需要自動(dòng) Attach 的應(yīng)用中引入 Arthas-spring-boot-starter 需要對(duì) Starter 進(jìn)行部分修改,要將注冊(cè) Arthas 的部分移除,下面是修改后的文件。
這里是將修改后的文件重新打包成 Jar 包,上傳到私服,但有些應(yīng)用會(huì)有無(wú)法加載 ArthasConfigMap 的情況,可以將這兩個(gè)文件單獨(dú)放到項(xiàng)目的公共包中。
@EnableConfigurationProperties({ ArthasProperties.class }) public class ArthasConfiguration { private static final Logger logger = LoggerFactory.getLogger(ArthasConfiguration.class); @ConfigurationProperties(prefix = "arthas") @ConditionalOnMissingBean @Bean public HashMap<String, String> arthasConfigMap() { return new HashMap<String, String>(); } }
@ConfigurationProperties(prefix = "arthas") public class ArthasProperties { private String ip; private int telnetPort; private int httpPort; private String tunnelServer; private String agentId; /** * report executed command */ private String statUrl; /** * session timeout seconds */ private long sessionTimeout; private String home; /** * when arthas agent init error will throw exception by default. */ private boolean slientInit = false; public String getHome() { return home; } public void setHome(String home) { this.home = home; } public boolean isSlientInit() { return slientInit; } public void setSlientInit(boolean slientInit) { this.slientInit = slientInit; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public int getTelnetPort() { return telnetPort; } public void setTelnetPort(int telnetPort) { this.telnetPort = telnetPort; } public int getHttpPort() { return httpPort; } public void setHttpPort(int httpPort) { this.httpPort = httpPort; } public String getTunnelServer() { return tunnelServer; } public void setTunnelServer(String tunnelServer) { this.tunnelServer = tunnelServer; } public String getAgentId() { return agentId; } public void setAgentId(String agentId) { this.agentId = agentId; } public String getStatUrl() { return statUrl; } public void setStatUrl(String statUrl) { this.statUrl = statUrl; } public long getSessionTimeout() { return sessionTimeout; } public void setSessionTimeout(long sessionTimeout) { this.sessionTimeout = sessionTimeout; } }
實(shí)現(xiàn)開(kāi)關(guān)效果
為了實(shí)現(xiàn)開(kāi)關(guān)效果,還需要一個(gè)文件用來(lái)監(jiān)聽(tīng)配置文件的改變。
我這里使用的是在 SBA 中改變環(huán)境變量,對(duì)應(yīng)服務(wù)監(jiān)聽(tīng)到變量改變,當(dāng)監(jiān)聽(tīng) spring.arthas.enabled 為 true 的時(shí)候,注冊(cè) Arthas,到下面是代碼。
@Component public class EnvironmentChangeListener implements ApplicationListener<EnvironmentChangeEvent> { @Autowired private Environment env; @Autowired private Map<String, String> arthasConfigMap; @Autowired private ArthasProperties arthasProperties; @Autowired private ApplicationContext applicationContext; @Override public void onApplicationEvent(EnvironmentChangeEvent event) { Set<String> keys = event.getKeys(); for (String key : keys) { if ("spring.arthas.enabled".equals(key)) { if ("true".equals(env.getProperty(key))) { registerArthas(); } } } } private void registerArthas() { DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); String bean = "arthasAgent"; if (defaultListableBeanFactory.containsBean(bean)) { ((ArthasAgent)defaultListableBeanFactory.getBean(bean)).init(); return; } defaultListableBeanFactory.registerSingleton(bean, arthasAgentInit()); } private ArthasAgent arthasAgentInit() { arthasConfigMap = StringUtils.removeDashKey(arthasConfigMap); // 給配置全加上前綴 Map<String, String> mapWithPrefix = new HashMap<String, String>(arthasConfigMap.size()); for (Map.Entry<String, String> entry : arthasConfigMap.entrySet()) { mapWithPrefix.put("arthas." + entry.getKey(), entry.getValue()); } final ArthasAgent arthasAgent = new ArthasAgent(mapWithPrefix, arthasProperties.getHome(), arthasProperties.isSlientInit(), null); arthasAgent.init(); return arthasAgent; } }
到此可以愉快的在 SBA 中調(diào)式應(yīng)用了,看看最后的頁(yè)面。
調(diào)式流程
流程如下:
開(kāi)啟 Arthas
在 Select Application 中選擇應(yīng)用
Connect 連接應(yīng)用
DisConnect 斷開(kāi)應(yīng)用
Release 釋放配置文件
一些缺陷:
使用 jar 包的方式引入應(yīng)用,具有一定的侵略性,如果 Arthas 無(wú)法啟動(dòng),會(huì)導(dǎo)致應(yīng)用也無(wú)法啟動(dòng)。
如果使用 Docker,需要適當(dāng)調(diào)整 JVM 內(nèi)存,防止開(kāi)啟 Arthas、調(diào)試的時(shí)候,內(nèi)存炸了。
沒(méi)有使用 SBA 插件的方式集成如上集成僅供參考,請(qǐng)根據(jù)自己企業(yè)的情況來(lái)集成。
關(guān)于“如何將Arthas集成進(jìn)Spring Boot監(jiān)控平臺(tái)中”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺(jué)得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。