From 7a01f23a73dcec7d1ca4f12f802a8c7fecbb2500 Mon Sep 17 00:00:00 2001 From: gongdewei Date: Fri, 7 Aug 2020 16:09:19 +0800 Subject: [PATCH] HTTP API support one-time command execution and add HTTP API docs (#1408) --- .../session/impl/SessionManagerImpl.java | 15 +- .../shell/term/impl/http/api/ApiRequest.java | 22 +- .../term/impl/http/api/HttpApiHandler.java | 168 +++-- site/src/site/sphinx/en/http-api.md | 711 ++++++++++++++++++ site/src/site/sphinx/en/index.md | 1 + site/src/site/sphinx/http-api.md | 630 ++++++++++++++++ site/src/site/sphinx/index.md | 1 + 7 files changed, 1452 insertions(+), 96 deletions(-) create mode 100644 site/src/site/sphinx/en/http-api.md create mode 100644 site/src/site/sphinx/http-api.md diff --git a/core/src/main/java/com/taobao/arthas/core/shell/session/impl/SessionManagerImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/session/impl/SessionManagerImpl.java index 5a86f4154..bbce73603 100644 --- a/core/src/main/java/com/taobao/arthas/core/shell/session/impl/SessionManagerImpl.java +++ b/core/src/main/java/com/taobao/arthas/core/shell/session/impl/SessionManagerImpl.java @@ -29,7 +29,8 @@ public class SessionManagerImpl implements SessionManager { private final InternalCommandManager commandManager; private final Instrumentation instrumentation; private final JobController jobController; - private final long timeoutMillis; + private final long sessionTimeoutMillis; + private final int consumerTimeoutMillis; private final long reaperInterval; private final Map sessions; private final long pid; @@ -42,7 +43,8 @@ public class SessionManagerImpl implements SessionManager { this.commandManager = commandManager; this.jobController = jobController; this.sessions = new ConcurrentHashMap(); - this.timeoutMillis = options.getSessionTimeout(); + this.sessionTimeoutMillis = options.getSessionTimeout(); + this.consumerTimeoutMillis = 5 * 60 * 1000; // 5 minutes this.reaperInterval = options.getReaperInterval(); this.instrumentation = options.getInstrumentation(); this.pid = options.getPid(); @@ -62,9 +64,6 @@ public class SessionManagerImpl implements SessionManager { String sessionId = UUID.randomUUID().toString(); session.put(Session.ID, sessionId); - //Result Distributor - session.setResultDistributor(new SharingResultDistributorImpl(session)); - sessions.put(sessionId, session); return session; } @@ -135,7 +134,7 @@ public class SessionManagerImpl implements SessionManager { // do not close if there is still job running, // e.g. trace command might wait for a long time before condition is met //TODO check background job size - if (now - session.getLastAccessTime() > timeoutMillis && session.getForegroundJob() == null) { + if (now - session.getLastAccessTime() > sessionTimeoutMillis && session.getForegroundJob() == null) { toClose.add(session); } evictConsumers(session); @@ -146,7 +145,7 @@ public class SessionManagerImpl implements SessionManager { if (job != null) { job.interrupt(); } - long timeOutInMinutes = timeoutMillis / 1000 / 60; + long timeOutInMinutes = sessionTimeoutMillis / 1000 / 60; String reason = "session is inactive for " + timeOutInMinutes + " min(s)."; session.getResultDistributor().appendResult(new MessageModel(reason)); this.removeSession(session.getSessionId()); @@ -166,7 +165,7 @@ public class SessionManagerImpl implements SessionManager { long now = System.currentTimeMillis(); for (ResultConsumer consumer : consumers) { long inactiveTime = now - consumer.getLastAccessTime(); - if (inactiveTime > 30000) { + if (inactiveTime > consumerTimeoutMillis) { //inactive duration must be large than pollTimeLimit logger.info("Removing inactive consumer from session, sessionId: {}, consumerId: {}, inactive duration: {}", session.getSessionId(), consumer.getConsumerId(), inactiveTime); diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/http/api/ApiRequest.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/http/api/ApiRequest.java index a3d2da354..545c3165a 100644 --- a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/http/api/ApiRequest.java +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/http/api/ApiRequest.java @@ -13,8 +13,7 @@ public class ApiRequest { private String requestId; private String sessionId; private String consumerId; - private Integer timeout; - private Map options; + private Integer execTimeout; @Override public String toString() { @@ -24,8 +23,7 @@ public class ApiRequest { ", requestId='" + requestId + '\'' + ", sessionId='" + sessionId + '\'' + ", consumerId='" + consumerId + '\'' + - ", timeout=" + timeout + - ", options=" + options + + ", execTimeout=" + execTimeout + '}'; } @@ -45,14 +43,6 @@ public class ApiRequest { this.command = command; } - public Map getOptions() { - return options; - } - - public void setOptions(Map options) { - this.options = options; - } - public String getRequestId() { return requestId; } @@ -77,11 +67,11 @@ public class ApiRequest { this.consumerId = consumerId; } - public Integer getTimeout() { - return timeout; + public Integer getExecTimeout() { + return execTimeout; } - public void setTimeout(Integer timeout) { - this.timeout = timeout; + public void setExecTimeout(Integer execTimeout) { + this.execTimeout = execTimeout; } } diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/http/api/HttpApiHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/http/api/HttpApiHandler.java index 66c5ee364..a5d1ee3a0 100644 --- a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/http/api/HttpApiHandler.java +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/http/api/HttpApiHandler.java @@ -8,8 +8,10 @@ import com.taobao.arthas.core.command.model.*; import com.taobao.arthas.core.distribution.PackingResultDistributor; import com.taobao.arthas.core.distribution.ResultConsumer; import com.taobao.arthas.core.distribution.ResultDistributor; +import com.taobao.arthas.core.distribution.SharingResultDistributor; import com.taobao.arthas.core.distribution.impl.PackingResultDistributorImpl; import com.taobao.arthas.core.distribution.impl.ResultConsumerImpl; +import com.taobao.arthas.core.distribution.impl.SharingResultDistributorImpl; import com.taobao.arthas.core.shell.cli.CliToken; import com.taobao.arthas.core.shell.cli.CliTokens; import com.taobao.arthas.core.shell.cli.Completion; @@ -53,7 +55,6 @@ public class HttpApiHandler { private static final Logger logger = LoggerFactory.getLogger(HttpApiHandler.class); public static final int DEFAULT_EXEC_TIMEOUT = 30000; private final SessionManager sessionManager; - private final AtomicInteger requestIdGenerator = new AtomicInteger(0); private static HttpApiHandler instance; private final InternalCommandManager commandManager; private final JobController jobController; @@ -84,13 +85,13 @@ public class HttpApiHandler { ApiResponse result; String requestBody = null; - String requestId = "req_" + requestIdGenerator.addAndGet(1); + String requestId = null; try { HttpMethod method = request.method(); if (HttpMethod.POST.equals(method)) { requestBody = getBody(request); ApiRequest apiRequest = parseRequest(requestBody); - apiRequest.setRequestId(requestId); + requestId = apiRequest.getRequestId(); result = processRequest(apiRequest); } else { result = createResponse(ApiState.REFUSED, "Unsupported http method: " + method.name()); @@ -210,15 +211,20 @@ public class HttpApiHandler { } //required session + Session session = null; + boolean allowNullSession = ApiAction.EXEC.equals(action); String sessionId = apiRequest.getSessionId(); if (StringUtils.isBlank(sessionId)) { - throw new ApiException("'sessionId' is required"); + if (!allowNullSession) { + throw new ApiException("'sessionId' is required"); + } + } else { + session = sessionManager.getSession(sessionId); + if (session == null) { + throw new ApiException("session not found: " + sessionId); + } + sessionManager.updateAccessTime(session); } - Session session = sessionManager.getSession(sessionId); - if (session == null) { - throw new ApiException("session not found: " + sessionId); - } - sessionManager.updateAccessTime(session); //dispatch requests ApiResponse response = dispatchRequest(action, apiRequest, session); @@ -266,11 +272,14 @@ public class HttpApiHandler { Session session = sessionManager.createSession(); if (session != null) { + //Result Distributor + SharingResultDistributorImpl resultDistributor = new SharingResultDistributorImpl(session); //create consumer ResultConsumer resultConsumer = new ResultConsumerImpl(); - session.getResultDistributor().addConsumer(resultConsumer); + resultDistributor.addConsumer(resultConsumer); + session.setResultDistributor(resultDistributor); - session.getResultDistributor().appendResult(new MessageModel("Welcome to arthas!")); + resultDistributor.appendResult(new MessageModel("Welcome to arthas!")); //welcome message WelcomeModel welcomeModel = new WelcomeModel(); @@ -279,7 +288,7 @@ public class HttpApiHandler { welcomeModel.setTutorials(ArthasBanner.tutorials()); welcomeModel.setPid(PidUtils.currentPid()); welcomeModel.setTime(DateUtils.getCurrentDate()); - session.getResultDistributor().appendResult(welcomeModel); + resultDistributor.appendResult(welcomeModel); //allow input updateSessionInputStatus(session, InputStatus.ALLOW_INPUT); @@ -300,7 +309,10 @@ public class HttpApiHandler { * @param inputStatus */ private void updateSessionInputStatus(Session session, InputStatus inputStatus) { - session.getResultDistributor().appendResult(new InputStatusModel(inputStatus)); + SharingResultDistributor resultDistributor = session.getResultDistributor(); + if (resultDistributor != null) { + resultDistributor.appendResult(new InputStatusModel(inputStatus)); + } } private ApiResponse processJoinSessionRequest(ApiRequest apiRequest, Session session) { @@ -347,78 +359,90 @@ public class HttpApiHandler { * @return */ private ApiResponse processExecRequest(ApiRequest apiRequest, Session session) { - String commandLine = apiRequest.getCommand(); - Map body = new TreeMap(); - body.put("command", commandLine); - - ApiResponse response = new ApiResponse(); - response.setSessionId(session.getSessionId()) - .setBody(body); - - if (!session.tryLock()) { - response.setState(ApiState.REFUSED) - .setMessage("Another command is executing."); - return response; + boolean oneTimeAccess = false; + if (session == null) { + oneTimeAccess = true; + session = sessionManager.createSession(); } - int lock = session.getLock(); - PackingResultDistributor packingResultDistributor = null; - Job job = null; try { - Job foregroundJob = session.getForegroundJob(); - if (foregroundJob != null) { + String commandLine = apiRequest.getCommand(); + Map body = new TreeMap(); + body.put("command", commandLine); + + ApiResponse response = new ApiResponse(); + response.setSessionId(session.getSessionId()) + .setBody(body); + + if (!session.tryLock()) { response.setState(ApiState.REFUSED) - .setMessage("Another job is running."); - logger.info("Another job is running, jobId: {}", foregroundJob.id()); + .setMessage("Another command is executing."); return response; } - //distribute result message both to origin session channel and request channel by CompositeResultDistributor - packingResultDistributor = new PackingResultDistributorImpl(session); - //ResultDistributor resultDistributor = new CompositeResultDistributorImpl(packingResultDistributor, session.getResultDistributor()); - job = this.createJob(commandLine, session, packingResultDistributor); - session.setForegroundJob(job); - updateSessionInputStatus(session, InputStatus.ALLOW_INTERRUPT); + int lock = session.getLock(); + PackingResultDistributor packingResultDistributor = null; + Job job = null; + try { + Job foregroundJob = session.getForegroundJob(); + if (foregroundJob != null) { + response.setState(ApiState.REFUSED) + .setMessage("Another job is running."); + logger.info("Another job is running, jobId: {}", foregroundJob.id()); + return response; + } - job.run(); + packingResultDistributor = new PackingResultDistributorImpl(session); + //distribute result message both to origin session channel and request channel by CompositeResultDistributor + //ResultDistributor resultDistributor = new CompositeResultDistributorImpl(packingResultDistributor, session.getResultDistributor()); + job = this.createJob(commandLine, session, packingResultDistributor); + session.setForegroundJob(job); + updateSessionInputStatus(session, InputStatus.ALLOW_INTERRUPT); - } catch (Throwable e) { - logger.error("Exec command failed:" + e.getMessage() + ", command:" + commandLine, e); - response.setState(ApiState.FAILED).setMessage("Exec command failed:" + e.getMessage()); + job.run(); + + } catch (Throwable e) { + logger.error("Exec command failed:" + e.getMessage() + ", command:" + commandLine, e); + response.setState(ApiState.FAILED).setMessage("Exec command failed:" + e.getMessage()); + return response; + } finally { + if (session.getLock() == lock) { + session.unLock(); + } + } + + //wait for job completed or timeout + Integer timeout = apiRequest.getExecTimeout(); + if (timeout == null || timeout <= 0) { + timeout = DEFAULT_EXEC_TIMEOUT; + } + boolean timeExpired = !waitForJob(job, timeout); + if (timeExpired) { + logger.warn("Job is exceeded time limit, force interrupt it, jobId: {}", job.id()); + job.interrupt(); + response.setState(ApiState.INTERRUPTED).setMessage("The job is exceeded time limit, force interrupt"); + } else { + response.setState(ApiState.SUCCEEDED); + } + + //packing results + body.put("jobId", job.id()); + body.put("jobStatus", job.status()); + body.put("timeExpired", timeExpired); + if (timeExpired) { + body.put("timeout", timeout); + } + body.put("results", packingResultDistributor.getResults()); + + response.setSessionId(session.getSessionId()) + //.setConsumerId(consumerId) + .setBody(body); return response; } finally { - if (session.getLock() == lock) { - session.unLock(); + if (oneTimeAccess) { + sessionManager.removeSession(session.getSessionId()); } } - - //wait for job completed or timeout - Integer timeout = apiRequest.getTimeout(); - if (timeout == null || timeout <= 0) { - timeout = DEFAULT_EXEC_TIMEOUT; - } - boolean timeExpired = !waitForJob(job, timeout); - if (timeExpired) { - logger.warn("Job is exceeded time limit, force interrupt it, jobId: {}", job.id()); - job.interrupt(); - response.setState(ApiState.INTERRUPTED).setMessage("The job is exceeded time limit, force interrupt"); - } else { - response.setState(ApiState.SUCCEEDED); - } - - //packing results - body.put("jobId", job.id()); - body.put("jobStatus", job.status()); - body.put("timeExpired", timeExpired); - if (timeExpired) { - body.put("timeout", timeout); - } - body.put("results", packingResultDistributor.getResults()); - - response.setSessionId(session.getSessionId()) - //.setConsumerId(consumerId) - .setBody(body); - return response; } /** diff --git a/site/src/site/sphinx/en/http-api.md b/site/src/site/sphinx/en/http-api.md new file mode 100644 index 000000000..beb7a3c8a --- /dev/null +++ b/site/src/site/sphinx/en/http-api.md @@ -0,0 +1,711 @@ +Http API +======== + +### Overview + +Http API provides a RESTful-like interactive interface, and both +requests and responses data in JSON format. Compared with +Telnet/WebConsole's output unstructured text data, Http API can provide +structured data and support more complex interactive functions, such as +a series of diagnostic operations in specific application scenarios. + + +#### Access address + +The Http API address is: `http://ip:port/api`, the request parameters +must be submitted using `POST`. Such as POST +`http://127.0.0.1:8563/api`. + +Note: The telnet port `3658` has compatibility issues with the Chrome +browser. It is recommended to use the http port `8563` to access the +http api. + + +#### Request data format + +``` +{ + "action": "exec", + "requestId": "req112" + "sessionId": "94766d3c-8b39-42d3-8596-98aee3ccbefb" + "consumerId": "955dbd1325334a84972b0f3ac19de4f7_2" + "command": "version", + "execTimeout": "10000" +} +``` +Request data format description: + +* `action` : The requested action/behavior, please refer to "Request + Actions" for optional values. +* `requestId` : Optional request ID, generated by the client. +* `sessionId` : Arthas session ID, one-time command does not need to + set the session ID. +* `consumerId` : Arthas consumer ID, used for multi-person sharing + sessions. +* `command` : Arthas command line +* `execTimeout` : Timeout for executing commands (ms), default value is 30000. + +Note: Different actions use different parameters. Set the parameters +according to the specific action. + +#### Request Actions + +Currently supported request actions are as follows: + +* `exec` : The command is executed synchronously, and the command + results is returned after the command execution end or interrupted. +* `async_exec` : The command is executed asynchronously, and the + scheduling result of the command is returned immediately. The command + execution result is obtained through `pull_results` action. +* `interrupt_job` : To interrupt the foreground command of the session, + similar to the function of Telnet `Ctrl + c`. +* `pull_results` : Get the result of the command executed + asynchronously, and execute it repeatedly in http long-polling mode. +* `init_session` : Create new session. +* `join_session` : Join the session, used to support multiple people + sharing the same Arthas session. +* `close_session` : Close the session. + +#### Response status + +The state attribute in the response indicates the request processing +state, and its value is as follows: + +* `SCHEDULED`: When the command is executed asynchronously, it means that + the job has been created, and may not be executed yet or is being + executed; +* `SUCCEEDED`: The request is processed successfully (completed status); +* `FAILED`: Request processing failed (completed status), usually + accompanied by a message explaining the reason; +* `REFUSED`: The request is rejected (completed status), usually + accompanied by a message explaining the reason; + +### One-time command + +Similar to executing batch commands, the one-time commands are executed +synchronously. No need to create a session, no need to set the +`sessionId` option. + +``` +{ + "action": "exec", + "command": "" +} +``` + +For example, get the Arthas version number: + +``` +curl -Ss -XPOST http://localhost:8563/api -d ' +{ + "action":"exec", + "command":"version" +} +' +``` +The response is as follows: + +``` +{ + "state" : "SUCCEEDED", + "sessionId" : "ee3bc004-4586-43de-bac0-b69d6db7a869", + "body" : { + "results" : [ + { + "type" : "version", + "version" : "3.3.8-SNAPSHOT", + "jobId" : 5 + }, + { + "jobId" : 5, + "statusCode" : 0, + "type" : "status" + } + ], + "timeExpired" : false, + "command" : "version", + "jobStatus" : "TERMINATED", + "jobId" : 5 + } +} +``` +Response data format description: + +* `state`: Request processing status, refer to the description of + "Response Status". +* `sessionId `: Arthas session ID, one-time command to automatically + create and destroy temporary sessions. +* `body.jobId`: The job ID of the command, all output results of the + same job are the same jobId. +* `body.jobStatus`: The job status of the command. +* `body.timeExpired`: Whether the job execution timed out. +* `body/results`: Command execution results. + +**Command result format description** + +``` + [{ + "type" : "version", + "version" : "3.3.8-SNAPSHOT", + "jobId" : 5 + }, + { + "jobId" : 5, + "statusCode" : 0, + "type" : "status" + }] +``` + +* `type` : The command result type, except for the special ones such as + `status`, the others remain the same as the Arthas command name. + Please refer to the section + "[Special command results](#special_command_results)". +* `jobId` : The job ID of the command. +* Other fields are the data of each different command. + +Note: You can also use a one-time command to execute continuous output +commands such as watch/trace, but you can't interrupt the command +execution, and there may be hang up for a long time. Please refer to the +example in the +"[Make watch command output a map object](#change_watch_value_to_map)" +section. + +Please try to deal with it in the following way: + +* Set a reasonable `execTimeout` to forcibly interrupt the command + execution after the timeout period is reached to avoid a long hang. +* Use the `-n` parameter to specify a smaller number of executions. +* Ensure the methods of the command matched can be successfully hit and + the `condition-express` is written correctly. If the `watch/trace` does + not hit, even if `-n 1` is specified, it will hang and wait until the + execution timeout. + + + +### Session interaction + +Users create and manage Arthas sessions, which are suitable for complex +interactive processes. The access process is as follows: + +* Create a session +* Join the session (optional) +* Pull command results +* Execute a series of commands +* Interrupt command execution +* Close the session + +#### Create session + +``` +curl -Ss -XPOST http://localhost:8563/api -d ' +{ + "action":"init_session" +} +' +``` +Response result: + +``` +{ + "sessionId" : "b09f1353-202c-407b-af24-701b744f971e", + "consumerId" : "5ae4e5fbab8b4e529ac404f260d4e2d1_1", + "state" : "SUCCEEDED" +} +``` +The new session ID is: `b09f1353-202c-407b-af24-701b744f971e`, and +consumer ID is: `5ae4e5fbab8b4e529ac404f260d4e2d1_1`. + + +#### Join session + +Specify the session ID to join, and the server will assign a new +consumer ID. Multiple consumers can receive the same command results of +target session. This interface is used to support multiple people +sharing the same session or refreshing the page to retrieve the session +history. + +``` +curl -Ss -XPOST http://localhost:8563/api -d ' +{ + "action":"join_session", + "sessionId" : "b09f1353-202c-407b-af24-701b744f971e" +} +' +``` +Response result: + +``` +{ + "consumerId" : "8f7f6ad7bc2d4cb5aa57a530927a95cc_2", + "sessionId" : "b09f1353-202c-407b-af24-701b744f971e", + "state" : "SUCCEEDED" +} +``` +The new consumer ID is `8f7f6ad7bc2d4cb5aa57a530927a95cc_2 ` . + +#### Pull command results + +The action of pulling the command result message is `pull_results`. +Please use the Http long-polling method to periodically pull the result +messages. The consumer's timeout period is 5 minutes. After the timeout, +you need to call `join_session` to allocate a new consumer. + +Each consumer is allocated a cache queue separately, and the pull order +does not affect the content received by the consumer. + + +The request parameters require session ID and consumer ID: + +``` +curl -Ss -XPOST http://localhost:8563/api -d ' +{ + "action":"pull_results", + "sessionId" : "b09f1353-202c-407b-af24-701b744f971e", + "consumerId" : "8f7f6ad7bc2d4cb5aa57a530927a95cc_2" +} +' +``` + +Use Bash scripts to regularly pull results messages: + +``` +while true; do curl -Ss -XPOST http://localhost:8563/api -d ' +{ + "action":"pull_results", + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a", + "consumerId" : "8ecb9cb7c7804d5d92e258b23d5245cc_1" +} +' | json_pp; sleep 2; done +``` + +Note: The `json_pp` tool formats the output content as pretty json. + +The response content is as follows: + +``` +{ + "body" : { + "results" : [ + { + "inputStatus" : "DISABLED", + "jobId" : 0, + "type" : "input_status" + }, + { + "type" : "message", + "jobId" : 0, + "message" : "Welcome to arthas!" + }, + { + "tutorials" : "https://alibaba.github.io/arthas/arthas-tutorials", + "time" : "2020-08-06 15:56:43", + "type" : "welcome", + "jobId" : 0, + "pid" : "7909", + "wiki" : "https://alibaba.github.io/arthas", + "version" : "3.3.8-SNAPSHOT" + }, + { + "inputStatus" : "ALLOW_INPUT", + "type" : "input_status", + "jobId" : 0 + } + ] + }, + "sessionId" : "b09f1353-202c-407b-af24-701b744f971e", + "consumerId" : "8f7f6ad7bc2d4cb5aa57a530927a95cc_2", + "state" : "SUCCEEDED" +} + +``` + + +#### Execute commands asynchronously + +``` +curl -Ss -XPOST http://localhost:8563/api -d ''' +{ + "action":"async_exec", + "command":"watch demo.MathGame primeFactors \"{params, returnObj, throwExp}\" ", + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a" +} +''' +``` + +Response of `async_exec`: + +``` +{ + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a", + "state" : "SCHEDULED", + "body" : { + "jobStatus" : "READY", + "jobId" : 3, + "command" : "watch demo.MathGame primeFactors \"{params, returnObj, throwExp}\" " + } +} +``` + +* `state` : The status of `SCHEDULED` means that the command has been + parsed and generated the job, but the execution has not started. +* `body.jobId` : The job id of command execution, filter the command + results output in `pull_results` according to this job ID. +* `body.jobStatus` : The job status `READY` means that execution has not started. + +The shell output of the script that continuously pulls the result message: + +``` +{ + "body" : { + "results" : [ + { + "type" : "command", + "jobId" : 3, + "state" : "SCHEDULED", + "command" : "watch demo.MathGame primeFactors \"{params, returnObj, throwExp}\" " + }, + { + "inputStatus" : "ALLOW_INTERRUPT", + "jobId" : 0, + "type" : "input_status" + }, + { + "success" : true, + "jobId" : 3, + "effect" : { + "listenerId" : 3, + "cost" : 24, + "classCount" : 1, + "methodCount" : 1 + }, + "type" : "enhancer" + }, + { + "sizeLimit" : 10485760, + "expand" : 1, + "jobId" : 3, + "type" : "watch", + "cost" : 0.071499, + "ts" : 1596703453237, + "value" : [ + [ + -170365 + ], + null, + { + "stackTrace" : [ + { + "className" : "demo.MathGame", + "classLoaderName" : "app", + "methodName" : "primeFactors", + "nativeMethod" : false, + "lineNumber" : 46, + "fileName" : "MathGame.java" + }, + ... + ], + "localizedMessage" : "number is: -170365, need >= 2", + "@type" : "java.lang.IllegalArgumentException", + "message" : "number is: -170365, need >= 2" + } + ] + }, + { + "type" : "watch", + "cost" : 0.033375, + "jobId" : 3, + "ts" : 1596703454241, + "value" : [ + [ + 1 + ], + [ + 2, + 2, + 2, + 2, + 13, + 491 + ], + null + ], + "sizeLimit" : 10485760, + "expand" : 1 + } + ] + }, + "consumerId" : "8ecb9cb7c7804d5d92e258b23d5245cc_1", + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a", + "state" : "SUCCEEDED" +} +``` + + +The `value` of the watch command result is the value of watch-experss, +and the above command is `{params, returnObj, throwExp}`, so the value +of the watch result is an array of length 3, and each element +corresponds to the expression in the corresponding order. + +Please refer to the section "[Make watch command output a map object](#change_watch_value_to_map)". + +#### Interrupt command execution + +Interrupt the running foreground job of the session: + +``` +curl -Ss -XPOST http://localhost:8563/api -d ''' +{ + "action":"interrupt_job", + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a" +} +''' +``` + +``` +{ + "state" : "SUCCEEDED", + "body" : { + "jobStatus" : "TERMINATED", + "jobId" : 3 + } +} +``` + +#### Close session + +Specify the session ID to close the session. + +``` +curl -Ss -XPOST http://localhost:8563/api -d ''' +{ + "action":"close_session", + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a" +} +''' +``` + +``` +{ + "state" : "SUCCEEDED" +} +``` + +### Web UI + +A Web UI based on the Http API interface, the access address is: +[http://127.0.0.1:8563/ui](http://127.0.0.1:8563/ui) . + +Completed functions: + +* Create a session +* Copy and open the url to join the session, share the session with + multiple people +* Continuously pull session command result messages +* Refresh the web page or join the session to pull command messages + history +* Control input or interrupt command status + + +Pending function: + +* Improve the readability of command result messages +* Support automatic completion of input commands and command templates +* Provide command help +* Support personal profile settings + + +### Special command results + +#### status + +``` +{ + "jobId" : 5, + "statusCode" : 0, + "type" : "status" +} +``` + +`type` is `status` to indicate the command execution status: + + +After each command is executed, there is a unique status result. If the +`statusCode` is 0, it means the execution is successful, and the +`statusCode` is a non-zero value that means the execution failed, +similar to the process exit code. + +When the command execution fails, an error message is generally provided, such as: + +``` +{ + "jobId":3, + "message":"The argument 'class-pattern' is required", + "statusCode":-10, + "type":"status" +} +``` + +#### input_status + +``` + { + "inputStatus" : "ALLOW_INPUT", + "type" : "input_status", + "jobId" : 0 + } +``` + +`type` is `input_status` to indicate input status: + + +It is used to control user input during UI interaction, and a change +message will be sent before and after each command is executed. + +Possible values ​​of `inputStatus`: + +* `ALLOW_INPUT` : Allow users to enter commands, which means that the + session has no foreground command being executed and can accept new + command. +* `ALLOW_INTERRUPT` : Allow the user to interrupt the command execution, + indicating that a command is currently being executed, and the user + can send `interrupt_job` to interrupt the execution. +* `DISABLED` : In the disabled state, commands cannot be entered or + interrupted. + + +#### command + +``` + { + "type" : "command", + "jobId" : 3, + "state" : "SCHEDULED", + "command" : "watch demo.MathGame primeFactors \"{params, returnObj, throwExp}\" " + } +``` +`type` is `command` to indicate the input command data: + +It is used for the interactive UI to echo the commands entered by the +user. The pulled session command message history will contain messages +of type `command`, which can be processed in order. + +#### enhancer + +``` + { + "success" : true, + "jobId" : 3, + "effect" : { + "listenerId" : 3, + "cost" : 24, + "classCount" : 1, + "methodCount" : 1 + }, + "type" : "enhancer" + } +``` + +`type` is `enhancer` to indicate the result of class enhancement: + +Commands such as `trace/watch/jad/tt` need to enhance the class and will +receive this `enhancer` result. It may happen that the result of +`enhancer` is successful, but there is no hit method. The client can +prompt the user according to the result of `enhancer`. + +### Others + + +#### Make watch command output a map object + +The result value of `watch` is generated by calculating the +`watch-express` ognl expression. You can change the ognl expression to +generate the desired value, please refer to +[OGNL document](https://commons.apache.org/proper/commons-ognl/language-guide.html). + +> Maps can also be created using a special syntax. +> +>#{ "foo" : "foo value", "bar" : "bar value" } +> +>This creates a Map initialized with mappings for "foo" and "bar". + +The following command generates values ​​in map format: + +``` +watch *MathGame prime* '#{ "params" : params, "returnObj" : returnObj, "throwExp": throwExp}' -x 2 -n 5 +``` + +Execute the above command in Telnet shell/WebConsole, the output result: + +``` +ts=2020-08-06 16:57:20; [cost=0.241735ms] result=@LinkedHashMap[ + @String[params]:@Object[][ + @Integer[1], + ], + @String[returnObj]:@ArrayList[ + @Integer[2], + @Integer[241], + @Integer[379], + ], + @String[throwExp]:null, +] +``` + +Execute the above command with Http api, pay attention to escaping the JSON double quotes: + +``` +curl -Ss -XPOST http://localhost:8563/api -d @- << EOF +{ + "action":"exec", + "execTimeout": 30000, + "command":"watch *MathGame prime* '#{ \"params\" : params, \"returnObj\" : returnObj, \"throwExp\": throwExp}' -n 3 " +} +EOF +``` + +Http api execution result: + +``` +{ + "body": { + ... + "results": [ + ... + { + ... + "type": "watch", + "value": { + "params": [ + 1 + ], + "returnObj": [ + 2, + 5, + 17, + 23, + 23 + ] + } + }, + { + ... + "type": "watch", + "value": { + "params": [ + -98278 + ], + "throwExp": { + "@type": "java.lang.IllegalArgumentException", + "localizedMessage": "number is: -98278, need >= 2", + "message": "number is: -98278, need >= 2", + "stackTrace": [ + ... + ] + } + } + }, + ... +} +``` + +You can see that the value of the watch result becomes a map object, and +the program can read value through a key . diff --git a/site/src/site/sphinx/en/index.md b/site/src/site/sphinx/en/index.md index 4b94ecab1..c74b19423 100644 --- a/site/src/site/sphinx/en/index.md +++ b/site/src/site/sphinx/en/index.md @@ -46,6 +46,7 @@ Contents * [Advanced usage](advanced-use.md) * [Commands](commands.md) * [WebConsole](web-console.md) +* [Http API](http-api.md) * [Docker](docker.md) * [Arthas Spring Boot Starter](spring-boot-starter.md) * [User cases](https://github.com/alibaba/arthas/issues?q=label%3Auser-case) diff --git a/site/src/site/sphinx/http-api.md b/site/src/site/sphinx/http-api.md new file mode 100644 index 000000000..34f72d860 --- /dev/null +++ b/site/src/site/sphinx/http-api.md @@ -0,0 +1,630 @@ +Http API +======== + +### 概览 + +Http API +提供类似RESTful的交互接口,请求和响应均为JSON格式的数据。相对于Telnet/WebConsole的输出非结构化文本数据,Http +API可以提供结构化的数据,支持更复杂的交互功能,比如特定应用场景的一系列诊断操作。 + + +#### 访问地址 + +Http API接口地址为:`http://ip:port/api`,必须使用POST方式提交请求参数。如POST +`http://127.0.0.1:8563/api` 。 + +注意:telnet服务的3658端口与Chrome浏览器有兼容性问题,建议使用http端口8563来访问http接口。 + +#### 请求数据格式 + +``` +{ + "action": "exec", + "requestId": "req112" + "sessionId": "94766d3c-8b39-42d3-8596-98aee3ccbefb" + "consumerId": "955dbd1325334a84972b0f3ac19de4f7_2" + "command": "version", + "execTimeout": "10000" +} +``` +请求数据格式说明: + +* `action` : 请求的动作/行为,可选值请参考"请求Action"小节。 +* `requestId` : 可选请求ID,由客户端生成。 +* `sessionId` : Arthas会话ID,一次性命令不需要设置会话ID。 +* `consumerId` : Arthas消费者ID,用于多人共享会话。 +* `command` : Arthas command line 。 +* `execTimeout` : 命令同步执行的超时时间(ms),默认为30000。 + +注意: 不同的action使用到参数不同,根据具体的action来设置参数。 + +#### 请求Action + +目前支持的请求Action如下: + +* `exec` : 同步执行命令,命令正常结束或者超时后中断命令执行后返回命令的执行结果。 +* `async_exec` : 异步执行命令,立即返回命令的调度结果,命令执行结果通过`pull_results`获取。 +* `interrupt_job` : 中断会话当前的命令,类似Telnet `Ctrl + c`的功能。 +* `pull_results` : 获取异步执行的命令的结果,以http 长轮询(long-polling)方式重复执行 +* `init_session` : 创建会话 +* `join_session` : 加入会话,用于支持多人共享同一个Arthas会话 +* `close_session` : 关闭会话 + +#### 响应状态 + +响应中的state属性表示请求处理状态,取值如下: + +* `SCHEDULED`:异步执行命令时表示已经创建job并已提交到命令执行队列,命令可能还没开始执行或者执行中; +* `SUCCEEDED`:请求处理成功(完成状态); +* `FAILED`:请求处理失败(完成状态),通常附带message说明原因; +* `REFUSED`:请求被拒绝(完成状态),通常附带message说明原因; + +### 一次性命令 + +与执行批处理命令类似,一次性命令以同步方式执行。不需要创建会话,不需要设置`sessionId`选项。 + +``` +{ + "action": "exec", + "command": "" +} +``` + +比如获取Arthas版本号: + +``` +curl -Ss -XPOST http://localhost:8563/api -d ' +{ + "action":"exec", + "command":"version" +} +' +``` +响应内容如下: + +``` +{ + "state" : "SUCCEEDED", + "sessionId" : "ee3bc004-4586-43de-bac0-b69d6db7a869", + "body" : { + "results" : [ + { + "type" : "version", + "version" : "3.3.8-SNAPSHOT", + "jobId" : 5 + }, + { + "jobId" : 5, + "statusCode" : 0, + "type" : "status" + } + ], + "timeExpired" : false, + "command" : "version", + "jobStatus" : "TERMINATED", + "jobId" : 5 + } +} +``` +响应数据解析: + +* `state`: 请求处理状态,参考“接口响应状态”说明 +* `sessionId `: Arthas会话ID,一次性命令自动创建及销毁临时会话 +* `body.jobId`: 命令的任务ID,同一任务输出的所有Result都是相同的jobId +* `body.jobStatus`: 任务状态,同步执行正常结束为`TERMINATED ` +* `body.timeExpired`: 任务执行是否超时 +* `body/results`: 命令执行的结果列表 + +**命令结果格式说明** + +``` + [{ + "type" : "version", + "version" : "3.3.8-SNAPSHOT", + "jobId" : 5 + }, + { + "jobId" : 5, + "statusCode" : 0, + "type" : "status" + }] +``` + +* `type` : 命令结果类型,除了`status`等特殊的几个外,其它的保持与Arthas命令名称一致。请参考"[特殊命令结果](#special_command_results)"小节。 +* `jobId` : 处理命令的任务ID。 +* 其它字段为每个不同命令的数据。 + +注意:也可以使用一次性命令的方式执行watch/trace等连续输出的命令,但不能中断命令执行,可能出现长时间没有结束的问题。请参考"[watch命令输出map对象](#change_watch_value_to_map)"小节的示例。 + +请尽量按照以下方式处理: + +* 设置合理的`execTimeout`,到达超时时间后强制中断命令执行,避免长时间挂起。 +* 通过`-n`参数指定较少的执行次数。 +* 保证命令匹配的方法可以成功命中和condition-express编写正确,如果watch/trace没有命中就算指定`-n + 1`也会挂起等待到执行超时。 + +### 会话交互 + +由用户创建及管理Arthas会话,适用于复杂的交互过程。访问流程如下: + +* 创建会话 +* 加入会话(可选) +* 拉取命令结果 +* 执行一系列命令 +* 中断命令执行 +* 关闭会话 + +#### 创建会话 + +``` +curl -Ss -XPOST http://localhost:8563/api -d ' +{ + "action":"init_session" +} +' +``` +响应结果: + +``` +{ + "sessionId" : "b09f1353-202c-407b-af24-701b744f971e", + "consumerId" : "5ae4e5fbab8b4e529ac404f260d4e2d1_1", + "state" : "SUCCEEDED" +} +``` +当前会话ID为: `b09f1353-202c-407b-af24-701b744f971e`, 当前消费者ID为:`5ae4e5fbab8b4e529ac404f260d4e2d1_1 `。 + +#### 加入会话 + +指定要加入的会话ID,服务端将分配一个新的消费者ID。多个消费者可以接收到同一个会话的命令结果。本接口用于支持多人共享同一个会话或刷新页面后重新拉取会话历史记录。 + +``` +curl -Ss -XPOST http://localhost:8563/api -d ' +{ + "action":"join_session", + "sessionId" : "b09f1353-202c-407b-af24-701b744f971e" +} +' +``` +响应结果: + +``` +{ + "consumerId" : "8f7f6ad7bc2d4cb5aa57a530927a95cc_2", + "sessionId" : "b09f1353-202c-407b-af24-701b744f971e", + "state" : "SUCCEEDED" +} +``` +新的消费者ID为`8f7f6ad7bc2d4cb5aa57a530927a95cc_2 ` 。 + +#### 拉取命令结果 + +拉取命令结果消息的action为`pull_results`。请使用Http long-polling方式,定时循环拉取结果消息。 +消费者的超时时间为5分钟,超时后需要调用`join_session`分配新的消费者。每个消费者单独分配一个缓存队列,按顺序拉取命令结果,不会影响到其它消费者。 + +请求参数需要指定会话ID及消费者ID: + +``` +curl -Ss -XPOST http://localhost:8563/api -d ' +{ + "action":"pull_results", + "sessionId" : "b09f1353-202c-407b-af24-701b744f971e", + "consumerId" : "8f7f6ad7bc2d4cb5aa57a530927a95cc_2" +} +' +``` + +用Bash脚本定时拉取结果消息: + +``` +while true; do curl -Ss -XPOST http://localhost:8563/api -d ' +{ + "action":"pull_results", + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a", + "consumerId" : "8ecb9cb7c7804d5d92e258b23d5245cc_1" +} +' | json_pp; sleep 2; done +``` +注: `json_pp` 工具将输出内容格式化为pretty json。 + +响应内容如下: + +``` +{ + "body" : { + "results" : [ + { + "inputStatus" : "DISABLED", + "jobId" : 0, + "type" : "input_status" + }, + { + "type" : "message", + "jobId" : 0, + "message" : "Welcome to arthas!" + }, + { + "tutorials" : "https://alibaba.github.io/arthas/arthas-tutorials", + "time" : "2020-08-06 15:56:43", + "type" : "welcome", + "jobId" : 0, + "pid" : "7909", + "wiki" : "https://alibaba.github.io/arthas", + "version" : "3.3.8-SNAPSHOT" + }, + { + "inputStatus" : "ALLOW_INPUT", + "type" : "input_status", + "jobId" : 0 + } + ] + }, + "sessionId" : "b09f1353-202c-407b-af24-701b744f971e", + "consumerId" : "8f7f6ad7bc2d4cb5aa57a530927a95cc_2", + "state" : "SUCCEEDED" +} + +``` + + +#### 异步执行命令 + +``` +curl -Ss -XPOST http://localhost:8563/api -d ''' +{ + "action":"async_exec", + "command":"watch demo.MathGame primeFactors \"{params, returnObj, throwExp}\" ", + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a" +} +''' +``` + +`async_exec` 的结果: + +``` +{ + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a", + "state" : "SCHEDULED", + "body" : { + "jobStatus" : "READY", + "jobId" : 3, + "command" : "watch demo.MathGame primeFactors \"{params, returnObj, throwExp}\" " + } +} +``` + +* `state` : `SCHEDULED` 状态表示已经解析命令生成任务,但未开始执行。 +* `body.jobId` : + 异步执行命令的任务ID,可以根据此任务ID来过滤在`pull_results`输出的命令结果。 +* `body.jobStatus` : 任务状态`READY`表示未开始执行。 + + +查看上面自动拉取结果消息脚本的shell输出: + +``` +{ + "body" : { + "results" : [ + { + "type" : "command", + "jobId" : 3, + "state" : "SCHEDULED", + "command" : "watch demo.MathGame primeFactors \"{params, returnObj, throwExp}\" " + }, + { + "inputStatus" : "ALLOW_INTERRUPT", + "jobId" : 0, + "type" : "input_status" + }, + { + "success" : true, + "jobId" : 3, + "effect" : { + "listenerId" : 3, + "cost" : 24, + "classCount" : 1, + "methodCount" : 1 + }, + "type" : "enhancer" + }, + { + "sizeLimit" : 10485760, + "expand" : 1, + "jobId" : 3, + "type" : "watch", + "cost" : 0.071499, + "ts" : 1596703453237, + "value" : [ + [ + -170365 + ], + null, + { + "stackTrace" : [ + { + "className" : "demo.MathGame", + "classLoaderName" : "app", + "methodName" : "primeFactors", + "nativeMethod" : false, + "lineNumber" : 46, + "fileName" : "MathGame.java" + }, + ... + ], + "localizedMessage" : "number is: -170365, need >= 2", + "@type" : "java.lang.IllegalArgumentException", + "message" : "number is: -170365, need >= 2" + } + ] + }, + { + "type" : "watch", + "cost" : 0.033375, + "jobId" : 3, + "ts" : 1596703454241, + "value" : [ + [ + 1 + ], + [ + 2, + 2, + 2, + 2, + 13, + 491 + ], + null + ], + "sizeLimit" : 10485760, + "expand" : 1 + } + ] + }, + "consumerId" : "8ecb9cb7c7804d5d92e258b23d5245cc_1", + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a", + "state" : "SUCCEEDED" +} +``` + +watch命令结果的`value`为watch-experss的值,上面命令中为`{params, returnObj, +throwExp}`,所以watch结果的value为一个长度为3的数组,每个元素分别对应相应顺序的表达式。 +请参考"[watch命令输出map对象](#change_watch_value_to_map)"小节。 + +#### 中断命令执行 + +中断会话正在运行的前台Job(前台任务): + +``` +curl -Ss -XPOST http://localhost:8563/api -d ''' +{ + "action":"interrupt_job", + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a" +} +''' +``` + +``` +{ + "state" : "SUCCEEDED", + "body" : { + "jobStatus" : "TERMINATED", + "jobId" : 3 + } +} +``` + +#### 关闭会话 +指定会话ID,关闭会话。 + +``` +curl -Ss -XPOST http://localhost:8563/api -d ''' +{ + "action":"close_session", + "sessionId" : "2b085b5d-883b-4914-ab35-b2c5c1d5aa2a" +} +''' +``` + +``` +{ + "state" : "SUCCEEDED" +} +``` + +### Web UI + +一个基于Http API接口实现的Web UI,访问地址为: [http://127.0.0.1:8563/ui](http://127.0.0.1:8563/ui) 。 + +已实现功能: + +* 创建会话 +* 复制并打开url加入会话,多人共享会话 +* 周期性拉取会话命令结果消息 +* 刷新页面或者加入会话拉取会话历史命令消息 +* 输入命令/中断命令状态控制 + +待开发功能: + +* 改进将命令结果消息可读性 +* 命令输入支持自动完成及命令模板 +* 提供命令帮助 +* 支持个人选项设置 + + +### 特殊命令结果 + +#### status + +``` +{ + "jobId" : 5, + "statusCode" : 0, + "type" : "status" +} +``` +`type`为`status`表示命令执行状态: + +每个命令执行结束后都有唯一一个status结果。`statusCode` +为0表示执行成功,`statusCode` 为非0值表示执行失败,类似进程退出码(exit code)。 + +命令执行失败时一般会提供错误消息,如: + +``` +{ + "jobId":3, + "message":"The argument 'class-pattern' is required", + "statusCode":-10, + "type":"status" +} +``` + +#### input_status + +``` + { + "inputStatus" : "ALLOW_INPUT", + "type" : "input_status", + "jobId" : 0 + } +``` + +`type`为`input_status`表示输入状态: + +用于UI交互时控制用户输入,每次执行命令前后会发送改变的消息。 +`inputStatus` 的值说明: + +* `ALLOW_INPUT` : + 允许用户输入命令,表示会话没有在执行的前台命令,可以接受新的命令。 +* `ALLOW_INTERRUPT` : + 允许用户中断命令执行,表示当前正在执行命令,用户可以发送`interrupt_job`中断执行。 +* `DISABLED` : 禁用状态,不能输入命令也不能中断命令。 + + +#### command + +``` + { + "type" : "command", + "jobId" : 3, + "state" : "SCHEDULED", + "command" : "watch demo.MathGame primeFactors \"{params, returnObj, throwExp}\" " + } +``` +`type` 为`command`表示输入的命令数据: + +用于交互UI回显用户输入的命令,拉取的会话命令消息历史会包含`command`类型的消息,按顺序处理即可。 + + +#### enhancer + +``` + { + "success" : true, + "jobId" : 3, + "effect" : { + "listenerId" : 3, + "cost" : 24, + "classCount" : 1, + "methodCount" : 1 + }, + "type" : "enhancer" + } +``` +`type`为`enhancer`表示类增强结果: + +`trace/watch/jad/tt`等命令需要对类进行增强,会接收到这个`enhancer`结果。可能出现`enhancer`结果成功,但没有命中方法的情况,客户端可以根据`enhancer`结果提示用户。 + +### 其它 + + +#### watch命令输出map对象 + +watch的结果值由计算`watch-express` ognl表达式产生,可以通过改变ognl表达式来生成想要的值,请参考[OGNL文档](https://commons.apache.org/proper/commons-ognl/language-guide.html)。 + +> Maps can also be created using a special syntax. +> +>#{ "foo" : "foo value", "bar" : "bar value" } +> +>This creates a Map initialized with mappings for "foo" and "bar". + +下面的命令生成map格式的值: + +``` +watch *MathGame prime* '#{ "params" : params, "returnObj" : returnObj, "throwExp": throwExp}' -x 2 -n 5 +``` + +在Telnet shell/WebConsole 中执行上面的命令,输出的结果: + +``` +ts=2020-08-06 16:57:20; [cost=0.241735ms] result=@LinkedHashMap[ + @String[params]:@Object[][ + @Integer[1], + ], + @String[returnObj]:@ArrayList[ + @Integer[2], + @Integer[241], + @Integer[379], + ], + @String[throwExp]:null, +] +``` + +用Http api 执行上面的命令,注意对JSON双引号转义: + +``` +curl -Ss -XPOST http://localhost:8563/api -d @- << EOF +{ + "action":"exec", + "execTimeout": 30000, + "command":"watch *MathGame prime* '#{ \"params\" : params, \"returnObj\" : returnObj, \"throwExp\": throwExp}' -n 3 " +} +EOF +``` + +Http api 执行结果: + +``` +{ + "body": { + ... + "results": [ + ... + { + ... + "type": "watch", + "value": { + "params": [ + 1 + ], + "returnObj": [ + 2, + 5, + 17, + 23, + 23 + ] + } + }, + { + ... + "type": "watch", + "value": { + "params": [ + -98278 + ], + "throwExp": { + "@type": "java.lang.IllegalArgumentException", + "localizedMessage": "number is: -98278, need >= 2", + "message": "number is: -98278, need >= 2", + "stackTrace": [ + ... + ] + } + } + }, + ... +} +``` + +可以看到watch结果的value变成map对象,程序可以通过key读取结果。 \ No newline at end of file diff --git a/site/src/site/sphinx/index.md b/site/src/site/sphinx/index.md index 7a3beada3..7b943ab60 100644 --- a/site/src/site/sphinx/index.md +++ b/site/src/site/sphinx/index.md @@ -37,6 +37,7 @@ Contents * [进阶使用](advanced-use.md) * [命令列表](commands.md) * [WebConsole](web-console.md) +* [Http API](http-api.md) * [Docker](docker.md) * [Arthas Spring Boot Starter](spring-boot-starter.md) * [用户案例](https://github.com/alibaba/arthas/issues?q=label%3Auser-case)