From a7d423e11db297ae711670acdc41eb63ee262b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=CC=81ter=20Go=CC=88mo=CC=88ri?= Date: Sun, 13 Aug 2017 23:45:01 +0200 Subject: [PATCH 1/3] Split Cowboy 1.x specific code from generic REST handler --- apps/xprof_gui/src/xprof_gui_app.erl | 26 +- .../src/xprof_gui_cowboy1_handler.erl | 226 +++--------------- apps/xprof_gui/src/xprof_gui_rest.erl | 221 +++++++++++++++++ 3 files changed, 267 insertions(+), 206 deletions(-) create mode 100644 apps/xprof_gui/src/xprof_gui_rest.erl diff --git a/apps/xprof_gui/src/xprof_gui_app.erl b/apps/xprof_gui/src/xprof_gui_app.erl index 6270cf5e..90498b3b 100644 --- a/apps/xprof_gui/src/xprof_gui_app.erl +++ b/apps/xprof_gui/src/xprof_gui_app.erl @@ -10,6 +10,7 @@ -define(APP, xprof_gui). -define(DEF_WEB_IF_PORT, 7890). +-define(LISTENER, xprof_http_listener). %% Application callbacks @@ -29,17 +30,18 @@ stop(_State) -> start_cowboy() -> Port = application:get_env(?APP, port, ?DEF_WEB_IF_PORT), - Dispatch = cowboy_router:compile(cowboy_routes()), - cowboy:start_http(xprof_http_listener, 100, [{port, Port}], - [{env, [{dispatch, Dispatch}]}]). - -cowboy_routes() -> - [{'_', [{"/api/:what", xprof_gui_cowboy1_handler, []}, - {"/build/[...]", cowboy_static, {priv_dir, ?APP, "build"}}, - {"/styles/[...]", cowboy_static, {priv_dir, ?APP, "styles"}}, - {"/img/[...]", cowboy_static, {priv_dir, ?APP, "img"}}, - {"/", cowboy_static, {priv_file, ?APP, "index.html"}} - ]}]. + Dispatch = cowboy_dispatch(xprof_gui_cowboy1_handler), + xprof_gui_cowboy1_handler:start_listener(?LISTENER, Port, Dispatch). + +cowboy_dispatch(Mod) -> + Routes = + [{'_', [{"/api/:what", Mod, []}, + {"/build/[...]", cowboy_static, {priv_dir, ?APP, "build"}}, + {"/styles/[...]", cowboy_static, {priv_dir, ?APP, "styles"}}, + {"/img/[...]", cowboy_static, {priv_dir, ?APP, "img"}}, + {"/", cowboy_static, {priv_file, ?APP, "index.html"}} + ]}], + cowboy_router:compile(Routes). stop_cowboy() -> - cowboy:stop_listener(xprof_http_listener). + cowboy:stop_listener(?LISTENER). diff --git a/apps/xprof_gui/src/xprof_gui_cowboy1_handler.erl b/apps/xprof_gui/src/xprof_gui_cowboy1_handler.erl index 1a570d5d..ba6ae4e4 100644 --- a/apps/xprof_gui/src/xprof_gui_cowboy1_handler.erl +++ b/apps/xprof_gui/src/xprof_gui_cowboy1_handler.erl @@ -1,210 +1,48 @@ +%%% @doc Cowboy 1.x compatible HTTP handler -module(xprof_gui_cowboy1_handler). --export([init/3,handle/2,terminate/3]). - -behavior(cowboy_http_handler). +%% xprof_gui_app callback +-export([start_listener/3]). + +%% Cowboy 1.x callbacks +-export([init/3, + handle/2, + terminate/3 + ]). + +-define(HDR_JSON, [{<<"content-type">>, <<"application/json">>}]). + %% In case an XHR receives no content with no content-type Firefox will emit %% the following error: "XML Parsing Error: no root element found..." %% As a workaround always return a content-type of octet-stream with %% 204 No Content responses --define(HDR_NO_CONTENT, [{<<"Content-type">>, <<"application/octet-stream">>}]). - -%% Cowboy callbacks - -init(_Type, Req, _Opts) -> - {ok, Req, no_state}. - -handle(Req, State) -> - {What,_} = cowboy_req:binding(what, Req), - handle_req(What, Req, State). - -terminate(_Reason, _Req, _State) -> - ok. - -%% Private - -%% Handling different HTTP requests - -handle_req(<<"funs">>, Req, State) -> - Query = get_query(Req), - - Funs = xprof_core:get_matching_mfas_pp(Query), - Json = jsone:encode(Funs), - - lager:debug("Returning ~b functions matching phrase \"~s\"", - [length(Funs), Query]), - - {ok, Req2} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, - Req), - {ok, Req2, State}; - -handle_req(<<"mon_start">>, Req, State) -> - Query = get_query(Req), - lager:info("Starting monitoring via web on '~s'~n", [Query]), - - {ok, ResReq} = - case xprof_core:monitor_pp(Query) of - ok -> - cowboy_req:reply(204, ?HDR_NO_CONTENT, Req); - {error, already_traced} -> - cowboy_req:reply(204, ?HDR_NO_CONTENT, Req); - _Error -> - cowboy_req:reply(400, Req) - end, - {ok, ResReq, State}; - - -handle_req(<<"mon_stop">>, Req, State) -> - MFA = {M, F, A} = get_mfa(Req), +-define(HDR_NO_CONTENT, [{<<"content-type">>, <<"application/octet-stream">>}]). - lager:info("Stopping monitoring via web on ~w:~w/~w~n",[M, F, A]), +%% xprof_gui_app callback - xprof_core:demonitor(MFA), - {ok, ResReq} = cowboy_req:reply(204, ?HDR_NO_CONTENT, Req), - {ok, ResReq, State}; +start_listener(Name, Port, Dispatch) -> + cowboy:start_http(Name, 100, [{port, Port}], + [{env, [{dispatch, Dispatch}]}]). -handle_req(<<"mon_get_all">>, Req, State) -> - Funs = xprof_core:get_all_monitored(), - FunsArr = [[Mod, Fun, Arity, Query] - || {{Mod, Fun, Arity}, Query} <- Funs], - Json = jsone:encode(FunsArr), - {ok, ResReq} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req), - {ok, ResReq, State}; +%% Cowboy 1.x callbacks -handle_req(<<"data">>, Req, State) -> - MFA = get_mfa(Req), - {LastTS, _} = cowboy_req:qs_val(<<"last_ts">>, Req, <<"0">>), - - {ok, ResReq} = - case xprof_core:get_data(MFA, binary_to_integer(LastTS)) of - {error, not_found} -> - cowboy_req:reply(404, Req); - Vals -> - Json = jsone:encode([{Val} || Val <- Vals]), - - cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req) - end, - {ok, ResReq, State}; - -handle_req(<<"get_callees">>, Req, State) -> - MFA = get_mfa(Req), - Callees = xprof_core:get_called_funs_pp(MFA), - Json = jsone:encode(Callees), - {ok, ResReq} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req), - {ok, ResReq, State}; - -handle_req(<<"trace_set">>, Req, State) -> - {Spec, _} = cowboy_req:qs_val(<<"spec">>, Req), - - {ok, ResReq} = case lists:member(Spec, [<<"all">>, <<"pause">>]) of - true -> - xprof_core:trace(list_to_atom(binary_to_list(Spec))), - cowboy_req:reply(204, ?HDR_NO_CONTENT, Req); - false -> - lager:info("Wrong spec for tracing: ~p",[Spec]), - cowboy_req:reply(400, Req) - end, - {ok, ResReq, State}; - -handle_req(<<"trace_status">>, Req, State) -> - {_, Status} = xprof_core:get_trace_status(), - Json = jsone:encode({[{status, Status}]}), - {ok, ResReq} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req), - - {ok, ResReq, State}; - -handle_req(<<"capture">>, Req, State) -> - MFA = {M,F,A} = get_mfa(Req), - {ThresholdStr, _} = cowboy_req:qs_val(<<"threshold">>, Req), - {LimitStr, _} = cowboy_req:qs_val(<<"limit">>, Req), - Threshold = binary_to_integer(ThresholdStr), - Limit = binary_to_integer(LimitStr), - - lager:info("Capture ~b calls to ~w:~w/~w~n exceeding ~b ms", - [Limit, M, F, A, Threshold]), - - {ok, CaptureId} = xprof_core:capture(MFA, Threshold, Limit), - Json = jsone:encode({[{capture_id, CaptureId}]}), - - {ok, ResReq} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], Json, Req), - {ok, ResReq, State}; - -handle_req(<<"capture_stop">>, Req, State) -> - MFA = get_mfa(Req), - - lager:info("Stopping slow calls capturing for ~p", [MFA]), - - {ok, ResReq} = - case xprof_core:capture_stop(MFA) of - ok -> - cowboy_req:reply(204, ?HDR_NO_CONTENT, Req); - {error, not_found} -> - cowboy_req:reply(404, Req) - end, - {ok, ResReq, State}; - -handle_req(<<"capture_data">>, Req, State) -> - MFA = get_mfa(Req), - {OffsetStr, _} = cowboy_req:qs_val(<<"offset">>, Req), - Offset = binary_to_integer(OffsetStr), +init(_Type, Req, _Opts) -> + {ok, Req, no_state}. - {ok, ResReq} = - case xprof_core:get_captured_data_pp(MFA, Offset) of - {error, not_found} -> - cowboy_req:reply(404, Req); - {ok, {Id, Threshold, Limit, HasMore}, Items} -> - Json = jsone:encode({[{capture_id, Id}, - {threshold, Threshold}, - {limit, Limit}, - {items, Items}, - {has_more, HasMore}]}), - cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req) +handle(Req0, State) -> + {What, _} = cowboy_req:binding(what, Req0), + {Params, _} = cowboy_req:qs_vals(Req0), + {ok, Req} = + case xprof_gui_rest:handle_req(What, Params) of + {StatusCode, Json} when is_integer(StatusCode), is_binary(Json) -> + cowboy_req:reply(StatusCode, ?HDR_JSON, Json, Req0); + StatusCode when is_integer(StatusCode) -> + cowboy_req:reply(StatusCode, ?HDR_NO_CONTENT, Req0) end, - {ok, ResReq, State}; - -handle_req(<<"mode">>, Req, State) -> - Mode = xprof_core:get_mode(), - Json = jsone:encode({[{mode, Mode}]}), - {ok, ResReq} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req), - {ok, ResReq, State}. - -%% Helpers --spec get_mfa(cowboy:req()) -> xprof_core:mfa_id(). -get_mfa(Req) -> - {Params, _} = cowboy_req:qs_vals(Req), - {list_to_atom(binary_to_list(proplists:get_value(<<"mod">>, Params))), - list_to_atom(binary_to_list(proplists:get_value(<<"fun">>, Params))), - case proplists:get_value(<<"arity">>, Params) of - <<"_">> -> '_'; - Arity -> binary_to_integer(Arity) - end}. + {ok, Req, State}. --spec get_query(cowboy:req()) -> binary(). -get_query(Req) -> - {Query, _} = cowboy_req:qs_val(<<"query">>, Req, <<"">>), - Query. +terminate(_Reason, _Req, _State) -> + ok. diff --git a/apps/xprof_gui/src/xprof_gui_rest.erl b/apps/xprof_gui/src/xprof_gui_rest.erl new file mode 100644 index 00000000..27f847cb --- /dev/null +++ b/apps/xprof_gui/src/xprof_gui_rest.erl @@ -0,0 +1,221 @@ +%%% @doc HTTP server independent part of the REST API implementation +-module(xprof_gui_rest). + +-export([handle_req/2]). + + +-spec handle_req(binary(), [{binary(), binary()}]) -> integer() | {integer(), binary()}. + +%% @doc "/api/funs" +%% Returns: +%% - 200: ["MFA"] +%% Get loaded modules and functions (MFAs) that match the query string. +%% Used for autocomplete suggestions on the GUI. +handle_req(<<"funs">>, Params) -> + Query = get_query(Params), + + Funs = xprof_core:get_matching_mfas_pp(Query), + Json = jsone:encode(Funs), + + lager:debug("Returning ~b functions matching phrase \"~s\"", [length(Funs), Query]), + + {200, Json}; + +%% @doc "/api/get_callees" +%% Params: +%% - "mod" +%% - "fun" +%% - "arity" +%% Returns: +%% - 200: ["MFA"] +%% Get list of functions (MFAs) that are called by the specified function +%% (MFA) based on static analysis (ie. not based on runtime information). +handle_req(<<"get_callees">>, Req) -> + MFA = get_mfa(Req), + Callees = xprof_core:get_called_funs_pp(MFA), + Json = jsone:encode(Callees), + {200, Json}; + +%% @doc "/api/mon_start" +%% Params: +%% - "query" (""): the query string represening a XProf-flavoured match-spec +%% Returns: +%% - 204: "" +%% - 400: "" +%% Start monitoring based on the specified query string. +handle_req(<<"mon_start">>, Params) -> + Query = get_query(Params), + + lager:info("Starting monitoring via web on '~s'~n", [Query]), + + case xprof_core:monitor_pp(Query) of + ok -> + 204; + {error, already_traced} -> + 204; + _Error -> + 400 + end; + +%% @doc "/api/mon_stop" +%% Params: +%% - "mod" +%% - "fun" +%% - "arity" +%% Returns: +%% - 204: "" +%% Stop monitoring the specified function (MFA). +handle_req(<<"mon_stop">>, Params) -> + MFA = {M, F, A} = get_mfa(Params), + + lager:info("Stopping monitoring via web on ~w:~w/~w~n",[M, F, A]), + + xprof_core:demonitor(MFA), + 204; + +%% @doc "/api/mon_get_all" +%% Returns: +%% - 200: [["mod", "fun", "arity", "query"]] +%% Return list of monitored functions. +%% (The values of "mod", "fun" and "arity" can be used as params to calls to eg +%% "/api/mon_stop" while "query" can be used to display the original query +%% string). +handle_req(<<"mon_get_all">>, _Params) -> + Funs = xprof_core:get_all_monitored(), + FunsArr = [[Mod, Fun, Arity, Query] + || {{Mod, Fun, Arity}, Query} <- Funs], + Json = jsone:encode(FunsArr), + {200, Json}; + +%% @doc "/api/data" +%% Params: +%% - "mod" +%% - "fun" +%% - "arity" +%% - "last_ts" +%% Returns: +%% - 200: [{"time": timestamp, "hitkey": number}] (where "histkey" is one of: +%% min, mean, median, max, stddev, +%% p25, p50, p75, p90, p99, p9999999, memsize, count) +%% - 404: "" (the requested MFA is not monitored) +%% Return metrics gathered for the given function since the given +%% timestamp. Each item contains a timestamp and the corresponding histogram +%% metrics values. +handle_req(<<"data">>, Params) -> + MFA = get_mfa(Params), + LastTS = get_int(<<"last_ts">>, Params, 0), + + case xprof_core:get_data(MFA, LastTS) of + {error, not_found} -> + 404; + Vals -> + Json = jsone:encode([{Val} || Val <- Vals]), + {200, Json} + end; + +%% @doc "/api/trace_set" +%% Params: +%% - "spec" ("all"/"pause") +%% Returns: +%% - 204: "" +%% - 400: "" (if spec has invalid value) +%% Turn on or pause tracing of all processes. +handle_req(<<"trace_set">>, Params) -> + case proplists:get_value(<<"spec">>, Params) of + <<"all">> -> + xprof_core:trace(all), + 204; + <<"pause">> -> + xprof_core:trace(pause), + 204; + Spec -> + lager:info("Wrong spec for tracing: ~p",[Spec]), + 400 + end; + +%% @doc "/api/trace_status" +%% Returns: +%% - 200: {"status": "initialized"/"running"/"paused"/"overflow" +%% Return current tracing state. +%% (The `initialized' status is basically the same as `paused', additionally +%% meaning that no tracing was started yet since xprof was started) +handle_req(<<"trace_status">>, _Params) -> + {_, Status} = xprof_core:get_trace_status(), + Json = jsone:encode({[{status, Status}]}), + {200, Json}; + +handle_req(<<"capture">>, Params) -> + MFA = {M, F, A} = get_mfa(Params), + Threshold = get_int(<<"threshold">>, Params), + Limit = get_int(<<"limit">>, Params), + + lager:info("Capture ~b calls to ~w:~w/~w~n exceeding ~b ms", + [Limit, M, F, A, Threshold]), + + {ok, CaptureId} = xprof_core:capture(MFA, Threshold, Limit), + Json = jsone:encode({[{capture_id, CaptureId}]}), + + {200, Json}; + +handle_req(<<"capture_stop">>, Params) -> + MFA = get_mfa(Params), + + lager:info("Stopping slow calls capturing for ~p", [MFA]), + + case xprof_core:capture_stop(MFA) of + ok -> + 204; + {error, not_found} -> + 404 + end; + +handle_req(<<"capture_data">>, Params) -> + MFA = get_mfa(Params), + Offset = get_int(<<"offset">>, Params, 0), + + case xprof_core:get_captured_data_pp(MFA, Offset) of + {error, not_found} -> + 404; + {ok, {Id, Threshold, Limit, HasMore}, Items} -> + Json = jsone:encode({[{capture_id, Id}, + {threshold, Threshold}, + {limit, Limit}, + {items, Items}, + {has_more, HasMore}]}), + {200, Json} + end; + +handle_req(<<"mode">>, _Params) -> + Mode = xprof_core:get_mode(), + Json = jsone:encode({[{mode, Mode}]}), + {200, Json}. + +%% Helpers + +-spec get_mfa([{binary(), binary() | true}]) -> xprof_core:mfa_id(). +get_mfa(Params) -> + {binary_to_atom(proplists:get_value(<<"mod">>, Params), latin1), + binary_to_atom(proplists:get_value(<<"fun">>, Params), latin1), + case proplists:get_value(<<"arity">>, Params) of + <<"_">> -> '_'; + Arity -> binary_to_integer(Arity) + end}. + +-spec get_query([{binary(), binary() | true}]) -> binary(). +get_query(Params) -> + proplists:get_value(<<"query">>, Params, <<"">>). + +-spec get_int(binary(), [{binary(), binary() | true}]) -> integer(). +get_int(Key, Params) -> + {_, BinValue} = lists:keyfind(Key, 1, Params), + binary_to_integer(BinValue). + +-spec get_int(binary(), [{binary(), binary() | true}], integer()) -> integer(). +get_int(Key, Params, Default) -> + case lists:keyfind(Key, 1, Params) of + {_, BinValue} -> + binary_to_integer(BinValue); + _ -> + Default + end. + From 2cc4192eb703745918653935fc48854193ac47c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20G=C3=B6m=C3=B6ri?= Date: Wed, 15 Nov 2017 19:52:26 +0100 Subject: [PATCH 2/3] Improve error handling of capture functions Handle cases when MFA is not monitored or not captured. As an exception in case of `get_captured_data` return default values (-1) if MFA is monitored but not captured as this is polled by the frontend and used as a kind of capture status. This could be improved in the future. --- apps/xprof_core/src/xprof_core.erl | 4 +- .../src/xprof_core_trace_handler.erl | 57 ++++++++++++------- apps/xprof_core/test/xprof_tracing_SUITE.erl | 39 ++++++++++++- apps/xprof_gui/src/xprof_gui_rest.erl | 12 +++- apps/xprof_gui/test/xprof_http_e2e_SUITE.erl | 20 +++++++ 5 files changed, 104 insertions(+), 28 deletions(-) diff --git a/apps/xprof_core/src/xprof_core.erl b/apps/xprof_core/src/xprof_core.erl index 1bc074ff..94946fa9 100644 --- a/apps/xprof_core/src/xprof_core.erl +++ b/apps/xprof_core/src/xprof_core.erl @@ -136,12 +136,12 @@ get_trace_status() -> %% @doc Start capturing arguments and return values of function calls that %% lasted longer than the specified time threshold in ms. -spec capture(xprof_core:mfa_id(), non_neg_integer(), non_neg_integer()) -> - {ok, CaptureId :: non_neg_integer()}. + {ok, CaptureId :: non_neg_integer()} | {error, not_found}. capture(MFA, Threshold, Limit) -> xprof_core_trace_handler:capture(MFA, Threshold, Limit). %% @doc Stop capturing long calls of the given function. --spec capture_stop(xprof_core:mfa_id()) -> ok | {error, not_found}. +-spec capture_stop(xprof_core:mfa_id()) -> ok | {error, not_found | not_captured}. capture_stop(MFA) -> xprof_core_trace_handler:capture_stop(MFA). diff --git a/apps/xprof_core/src/xprof_core_trace_handler.erl b/apps/xprof_core/src/xprof_core_trace_handler.erl index 6bd0e8a0..23888a97 100644 --- a/apps/xprof_core/src/xprof_core_trace_handler.erl +++ b/apps/xprof_core/src/xprof_core_trace_handler.erl @@ -55,15 +55,20 @@ data(MFA, FromEpoch) -> %% @doc Starts capturing args and results from function calls that lasted longer %% than specified time threshold. -spec capture(xprof_core:mfa_id(), non_neg_integer(), non_neg_integer()) -> - {ok, non_neg_integer()}. + {ok, non_neg_integer()} | {error, not_found}. capture(MFA = {M,F,A}, Threshold, Limit) -> lager:info("Capturing ~p calls to ~w:~w/~w that exceed ~p ms:", [Limit, M, F, A, Threshold]), Name = xprof_core_lib:mfa2atom(MFA), - gen_server:call(Name, {capture, Threshold, Limit}). + try + gen_server:call(Name, {capture, Threshold, Limit}) + catch + exit:{noproc, _} -> + {error, not_found} + end. --spec capture_stop(xprof_core:mfa_id()) -> ok | {error, not_found}. +-spec capture_stop(xprof_core:mfa_id()) -> ok | {error, not_found | not_captured}. capture_stop(MFA) -> Name = xprof_core_lib:mfa2atom(MFA), try @@ -75,27 +80,34 @@ capture_stop(MFA) -> %% @doc -spec get_captured_data(xprof_core:mfa_id(), non_neg_integer()) -> - empty | {ok, - {Index :: non_neg_integer(), - Threshold :: non_neg_integer(), - OrigLimit :: non_neg_integer(), - HasMore :: boolean() - }, [tuple()]}. + {ok, + {Index :: non_neg_integer(), + Threshold :: non_neg_integer(), + OrigLimit :: non_neg_integer(), + HasMore :: boolean() + }, [tuple()]} | + {error, not_found}. get_captured_data(MFA, Offset) when Offset >= 0 -> Name = xprof_core_lib:mfa2atom(MFA), try - Items = lists:sort(ets:select(Name, - [{ - {{args_res, '$1'}, - {'$2', '$3','$4','$5'}}, - [{'>','$1',Offset}], - [{{'$1', '$2', '$3', '$4', '$5'}}] - }])), - - Res = ets:lookup(Name, capture_spec), - [{capture_spec, Index, Threshold, Limit, OrigLimit}] = Res, - HasMore = Offset + length(Items) < Limit, - {ok, {Index, Threshold, OrigLimit, HasMore}, Items} + [{capture_spec, Index, Threshold, Limit, OrigLimit}] = + ets:lookup(Name, capture_spec), + case Index of + -1 -> + %% no capturing was done for this MFA yet, + %% no need to traverse the table + {ok, {Index, Threshold, OrigLimit, _HasMore = false}, _Items = []}; + _ -> + MS = [{ + {{args_res, '$1'}, + {'$2', '$3','$4','$5'}}, + [{'>','$1',Offset}], + [{{'$1', '$2', '$3', '$4', '$5'}}] + }], + Items = lists:sort(ets:select(Name, MS)), + HasMore = Offset + length(Items) < Limit, + {ok, {Index, Threshold, OrigLimit, HasMore}, Items} + end catch error:badarg -> {error, not_found} end. @@ -127,6 +139,9 @@ handle_call({capture, Threshold, Limit}, _From, capture_args_trace_on(MFA), {Timeout, NewState2} = maybe_make_snapshot(NewState), {reply, {ok, NewId}, NewState2, Timeout}; +handle_call(capture_stop, _From, State = #state{capture_spec = undefined}) -> + {Timeout, NewState} = maybe_make_snapshot(State), + {reply, {error, not_captured}, NewState, Timeout}; handle_call(capture_stop, _From, State = #state{mfa = MFA}) -> capture_args_trace_off(MFA), #state{capture_spec = {Threshold, Limit}, diff --git a/apps/xprof_core/test/xprof_tracing_SUITE.erl b/apps/xprof_core/test/xprof_tracing_SUITE.erl index 6e6cee41..834eede3 100644 --- a/apps/xprof_core/test/xprof_tracing_SUITE.erl +++ b/apps/xprof_core/test/xprof_tracing_SUITE.erl @@ -13,7 +13,10 @@ ]). %% Test cases --export([monitor_many_funs/1, +-export([not_found_error/1, + already_traced_error/1, + not_captured_error/1, + monitor_many_funs/1, monitor_recursive_fun/1, monitor_keep_recursive_fun/1, monitor_crashing_fun/1, @@ -32,7 +35,10 @@ %% CT funs all() -> - [monitor_many_funs, + [not_found_error, + already_traced_error, + not_captured_error, + monitor_many_funs, monitor_recursive_fun, monitor_keep_recursive_fun, monitor_crashing_fun, @@ -133,6 +139,35 @@ dead_proc_tracing(_Config) -> xprof_core:get_trace_status()), ok. +not_found_error(_Config) -> + MFA = {?MODULE, no_such_fun, 1}, + ?assertEqual(ok, xprof_core:demonitor(MFA)), + ?assertEqual({error, not_found}, xprof_core:get_data(MFA, 0)), + ?assertEqual({error, not_found}, xprof_core:capture(MFA, 1, 1)), + ?assertEqual({error, not_found}, xprof_core:capture_stop(MFA)), + ?assertEqual({error, not_found}, xprof_core:get_captured_data(MFA, 0)), + ok. + +already_traced_error(_Config) -> + MFA = {?MODULE, test_fun, 1}, + ok = xprof_core:monitor(MFA), + try + ?assertEqual({error, already_traced}, xprof_core:monitor(MFA)) + after + xprof_core:demonitor(MFA) + end. + +not_captured_error(_Config) -> + MFA = {?MODULE, test_fun, 1}, + ok = xprof_core:monitor(MFA), + try + ?assertEqual({error, not_captured}, xprof_core:capture_stop(MFA)), + ?assertEqual({ok, {-1,-1,-1,false}, []}, + xprof_core:get_captured_data(MFA, 0)) + after + xprof_core:demonitor(MFA) + end. + monitor_crashing_fun(_Config) -> xprof_core:monitor(MFA = {?MODULE, maybe_crash_test_fun, 1}), ok = xprof_core:trace(self()), diff --git a/apps/xprof_gui/src/xprof_gui_rest.erl b/apps/xprof_gui/src/xprof_gui_rest.erl index 27f847cb..870f71c6 100644 --- a/apps/xprof_gui/src/xprof_gui_rest.erl +++ b/apps/xprof_gui/src/xprof_gui_rest.erl @@ -152,10 +152,14 @@ handle_req(<<"capture">>, Params) -> lager:info("Capture ~b calls to ~w:~w/~w~n exceeding ~b ms", [Limit, M, F, A, Threshold]), - {ok, CaptureId} = xprof_core:capture(MFA, Threshold, Limit), - Json = jsone:encode({[{capture_id, CaptureId}]}), + case xprof_core:capture(MFA, Threshold, Limit) of + {ok, CaptureId} -> + Json = jsone:encode({[{capture_id, CaptureId}]}), - {200, Json}; + {200, Json}; + {error, not_found} -> + 404 + end; handle_req(<<"capture_stop">>, Params) -> MFA = get_mfa(Params), @@ -166,6 +170,8 @@ handle_req(<<"capture_stop">>, Params) -> ok -> 204; {error, not_found} -> + 404; + {error, not_captured} -> 404 end; diff --git a/apps/xprof_gui/test/xprof_http_e2e_SUITE.erl b/apps/xprof_gui/test/xprof_http_e2e_SUITE.erl index 8371ae33..75c3bd2d 100644 --- a/apps/xprof_gui/test/xprof_http_e2e_SUITE.erl +++ b/apps/xprof_gui/test/xprof_http_e2e_SUITE.erl @@ -28,9 +28,11 @@ stop_monitoring/1, get_data_for_not_traced_fun/1, get_data_for_traced_fun/1, + error_when_starting_not_traced_fun/1, no_capture_data_when_not_traced/1, capture_data_when_traced_test/1, capture_data_with_formatted_exception_test/1, + error_when_stopping_not_traced_fun/1, error_when_stopping_not_started_capture/1, dont_receive_new_capture_data_after_stop/1, in_this_project_we_should_detect_erlang/1, @@ -66,9 +68,11 @@ groups() -> stop_monitoring, get_data_for_not_traced_fun, get_data_for_traced_fun, + error_when_starting_not_traced_fun, no_capture_data_when_not_traced, capture_data_when_traced_test, capture_data_with_formatted_exception_test, + error_when_stopping_not_traced_fun, error_when_stopping_not_started_capture, dont_receive_new_capture_data_after_stop, in_this_project_we_should_detect_erlang, @@ -223,6 +227,15 @@ get_data_for_traced_fun(_Config) -> ])), ok. +error_when_starting_not_traced_fun(_Config) -> + Params = [{"mod", "xprof_http_e2e_SUITE"}, + {"fun", "long_function"}, + {"arity", "0"}, + {"threshold", "1"}, + {"limit", "1"}], + ?assertMatch({404, _}, make_get_request("api/capture", Params)), + ok. + no_capture_data_when_not_traced(_Config) -> ?assertMatch({404, _}, make_get_request("api/capture_data", [ {"mod", "xprof_http_e2e_SUITE"}, @@ -232,8 +245,15 @@ no_capture_data_when_not_traced(_Config) -> ])), ok. +error_when_stopping_not_traced_fun(_Config) -> + MFA = [{"mod", "xprof_http_e2e_SUITE"}, {"fun", "long_function"}, {"arity", "0"}], + ?assertMatch({404, _}, make_get_request("api/capture_stop", MFA)), + ok. + error_when_stopping_not_started_capture(_Config) -> MFA = [{"mod", "xprof_http_e2e_SUITE"}, {"fun", "long_function"}, {"arity", "0"}], + given_traced("xprof_http_e2e_SUITE:long_function/0"), + ?assertMatch({404, _}, make_get_request("api/capture_stop", MFA)), ok. From 2d68421ae8604807fd3ca8edfc521784057af9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20G=C3=B6m=C3=B6ri?= Date: Wed, 15 Nov 2017 19:56:17 +0100 Subject: [PATCH 3/3] Document the REST API Currently only in edoc, but in the future it could be moved to some more declarative format (eg OpenAPI/Swagger). --- apps/xprof_gui/src/xprof_gui_rest.erl | 283 ++++++++++++++++++++------ 1 file changed, 219 insertions(+), 64 deletions(-) diff --git a/apps/xprof_gui/src/xprof_gui_rest.erl b/apps/xprof_gui/src/xprof_gui_rest.erl index 870f71c6..3815cb84 100644 --- a/apps/xprof_gui/src/xprof_gui_rest.erl +++ b/apps/xprof_gui/src/xprof_gui_rest.erl @@ -1,16 +1,229 @@ %%% @doc HTTP server independent part of the REST API implementation +%%% +%%% == Autocomplete == +%%% +%%% === /api/funs === +%%% +%%% Returns: +%%%
    +%%%
  • 200: ["MFA"]
  • +%%%
+%%% +%%% Get loaded modules and functions (MFAs) that match the query string. +%%% Used for autocomplete suggestions on the GUI. +%%% +%%% === /api/get_callees === +%%% +%%% Params: +%%%
    +%%%
  • "mod"
  • +%%%
  • "fun"
  • +%%%
  • "arity"
  • +%%%
+%%% +%%% Returns: +%%%
    +%%%
  • 200: ["MFA"]
  • +%%%
+%%% +%%% Get list of functions (MFAs) that are called by the specified function +%%% (MFA) based on static analysis (ie. not based on runtime information). +%%% +%%% +%%% == Monitoring functions == +%%% +%%% === /api/mon_start === +%%% +%%% Params: +%%%
    +%%%
  • "query" (""): the query string representing a XProf-flavoured match-spec
  • +%%%
+%%% +%%% Returns: +%%%
    +%%%
  • 204: ""
  • +%%%
  • 400: ""
  • +%%%
+%%% +%%% Start monitoring based on the specified query string. +%%% +%%% === /api/mon_stop === +%%% +%%% Params: +%%%
    +%%%
  • "mod"
  • +%%%
  • "fun"
  • +%%%
  • "arity"
  • +%%%
+%%% +%%% Returns: +%%%
    +%%%
  • 204: ""
  • +%%%
+%%% +%%% Stop monitoring the specified function (MFA). +%%% +%%% === /api/mon_get_all === +%%% +%%% Returns: +%%%
    +%%%
  • 200: [["mod", "fun", "arity", "query"]]
  • +%%%
+%%% +%%% Return list of monitored functions. +%%% (The values of "mod", "fun" and "arity" can be used as params to calls to eg +%%% "/api/mon_stop" while "query" can be used to display the original query +%%% string). +%%% +%%% === /api/data === +%%% +%%% Params: +%%%
    +%%%
  • "mod"
  • +%%%
  • "fun"
  • +%%%
  • "arity"
  • +%%%
  • "last_ts"
  • +%%%
+%%% +%%% Returns: +%%%
    +%%%
  • 200: [{"time": timestamp, "hitkey": number}] (where "histkey" is one of: +%%% min, mean, median, max, stddev, +%%% p25, p50, p75, p90, p99, p9999999, memsize, count)
  • +%%%
  • 404: "" (the requested MFA is not monitored)
  • +%%%
+%%% +%%% Return metrics gathered for the given function since the given +%%% timestamp. Each item contains a timestamp and the corresponding histogram +%%% metrics values. +%%% +%%% == Global trace status == +%%% +%%% === /api/trace_set === +%%% +%%% Params: +%%%
    +%%%
  • "spec" ("all"/"pause")
  • +%%%
+%%% +%%% Returns: +%%%
    +%%%
  • 204: ""
  • +%%%
  • 400: "" (if spec has invalid value)
  • +%%%
+%%% +%%% Turn on or pause tracing of all processes. +%%% +%%% === /api/trace_status === +%%% +%%% Returns: +%%%
    +%%%
  • 200: {"status": "initialized"/"running"/"paused"/"overflow"}
  • +%%%
+%%% +%%% Return current tracing state. +%%% (The `initialized' status is basically the same as `paused', additionally +%%% meaning that no tracing was started yet since xprof was started) +%%% +%%% == Long call capturing == +%%% +%%% === /api/capture === +%%% +%%% Params: +%%%
    +%%%
  • "mod"
  • +%%%
  • "fun"
  • +%%%
  • "arity"
  • +%%%
  • "threshold"
  • +%%%
  • "limit"
  • +%%%
+%%% +%%% Returns: +%%%
    +%%%
  • 202: {"capture_id": integer}
  • +%%%
  • 404: "" (the requested MFA is not monitored)
  • +%%%
+%%% +%%% Start capturing arguments and return values of function calls that lasted +%%% longer than the specified time threshold in ms. Stop after `limit' number of +%%% captured calls. +%%% +%%% === /api/capture_stop === +%%% +%%% Params: +%%%
    +%%%
  • "mod"
  • +%%%
  • "fun"
  • +%%%
  • "arity"
  • +%%%
+%%% +%%% Returns: +%%%
    +%%%
  • 204: ""
  • +%%%
  • 404: "" (the requested MFA is not monitored or hasn't been captured yet)
  • +%%%
+%%% +%%% Stop capturing long calls of the given function (MFA). +%%% +%%% === /api/capture_data === +%%% +%%% Params: +%%%
    +%%%
  • "mod"
  • +%%%
  • "fun"
  • +%%%
  • "arity"
  • +%%%
  • "offset"
  • +%%%
+%%% +%%% Returns: +%%%
    +%%%
  • 200: {"capture_id": integer, +%%% "threshold": integer, +%%% "limit": integer, +%%% "items": [Item], +%%% "has_more": boolean} +%%% where `Item' is +%%% {"id": number, +%%% "pid": string, +%%% "call_time": number, +%%% "args": string, +%%% "res": string}
  • +%%%
  • 404: "" (the requested MFA is not monitored)
  • +%%%
+%%% +%%% Return captured arguments and return values. +%%% +%%% The `Offset' argument is the item index last seen by the caller, only items +%%% newer than that will be returned. An offset of 0 will return all data. +%%% +%%% The returned `HasMore' indicates whether capturing is still ongoing or it has +%%% been stopped either manually or by reaching the limit. +%%% +%%% == Syntax mode == +%%% +%%% === /api/mode === +%%% +%%% Returns: +%%%
    +%%%
  • 200: {"mode": "erlang"/"elixir"}
  • +%%%
+%%% +%%% Get syntax mode, if not set explicitely in the backend, it will be +%%% autodetected. +%%% +%%% @end + -module(xprof_gui_rest). -export([handle_req/2]). --spec handle_req(binary(), [{binary(), binary()}]) -> integer() | {integer(), binary()}. +-spec handle_req(Path :: binary(), Params :: [{binary(), binary()}]) + -> StatusCode | {StatusCode, Body} + when StatusCode :: integer(), + Body :: binary(). -%% @doc "/api/funs" -%% Returns: -%% - 200: ["MFA"] -%% Get loaded modules and functions (MFAs) that match the query string. -%% Used for autocomplete suggestions on the GUI. +%% @doc handle_req(<<"funs">>, Params) -> Query = get_query(Params), @@ -21,28 +234,12 @@ handle_req(<<"funs">>, Params) -> {200, Json}; -%% @doc "/api/get_callees" -%% Params: -%% - "mod" -%% - "fun" -%% - "arity" -%% Returns: -%% - 200: ["MFA"] -%% Get list of functions (MFAs) that are called by the specified function -%% (MFA) based on static analysis (ie. not based on runtime information). handle_req(<<"get_callees">>, Req) -> MFA = get_mfa(Req), Callees = xprof_core:get_called_funs_pp(MFA), Json = jsone:encode(Callees), {200, Json}; -%% @doc "/api/mon_start" -%% Params: -%% - "query" (""): the query string represening a XProf-flavoured match-spec -%% Returns: -%% - 204: "" -%% - 400: "" -%% Start monitoring based on the specified query string. handle_req(<<"mon_start">>, Params) -> Query = get_query(Params), @@ -57,14 +254,6 @@ handle_req(<<"mon_start">>, Params) -> 400 end; -%% @doc "/api/mon_stop" -%% Params: -%% - "mod" -%% - "fun" -%% - "arity" -%% Returns: -%% - 204: "" -%% Stop monitoring the specified function (MFA). handle_req(<<"mon_stop">>, Params) -> MFA = {M, F, A} = get_mfa(Params), @@ -73,13 +262,6 @@ handle_req(<<"mon_stop">>, Params) -> xprof_core:demonitor(MFA), 204; -%% @doc "/api/mon_get_all" -%% Returns: -%% - 200: [["mod", "fun", "arity", "query"]] -%% Return list of monitored functions. -%% (The values of "mod", "fun" and "arity" can be used as params to calls to eg -%% "/api/mon_stop" while "query" can be used to display the original query -%% string). handle_req(<<"mon_get_all">>, _Params) -> Funs = xprof_core:get_all_monitored(), FunsArr = [[Mod, Fun, Arity, Query] @@ -87,20 +269,6 @@ handle_req(<<"mon_get_all">>, _Params) -> Json = jsone:encode(FunsArr), {200, Json}; -%% @doc "/api/data" -%% Params: -%% - "mod" -%% - "fun" -%% - "arity" -%% - "last_ts" -%% Returns: -%% - 200: [{"time": timestamp, "hitkey": number}] (where "histkey" is one of: -%% min, mean, median, max, stddev, -%% p25, p50, p75, p90, p99, p9999999, memsize, count) -%% - 404: "" (the requested MFA is not monitored) -%% Return metrics gathered for the given function since the given -%% timestamp. Each item contains a timestamp and the corresponding histogram -%% metrics values. handle_req(<<"data">>, Params) -> MFA = get_mfa(Params), LastTS = get_int(<<"last_ts">>, Params, 0), @@ -113,13 +281,6 @@ handle_req(<<"data">>, Params) -> {200, Json} end; -%% @doc "/api/trace_set" -%% Params: -%% - "spec" ("all"/"pause") -%% Returns: -%% - 204: "" -%% - 400: "" (if spec has invalid value) -%% Turn on or pause tracing of all processes. handle_req(<<"trace_set">>, Params) -> case proplists:get_value(<<"spec">>, Params) of <<"all">> -> @@ -133,12 +294,6 @@ handle_req(<<"trace_set">>, Params) -> 400 end; -%% @doc "/api/trace_status" -%% Returns: -%% - 200: {"status": "initialized"/"running"/"paused"/"overflow" -%% Return current tracing state. -%% (The `initialized' status is basically the same as `paused', additionally -%% meaning that no tracing was started yet since xprof was started) handle_req(<<"trace_status">>, _Params) -> {_, Status} = xprof_core:get_trace_status(), Json = jsone:encode({[{status, Status}]}),