Skip to content

Commit 1f49a93

Browse files
Merge pull request #230 from samuelmanzanera/feat/500_error_page
feat: Display 500 error by tracing steps of execution
2 parents b746b9b + 903c740 commit 1f49a93

File tree

6 files changed

+174
-21
lines changed

6 files changed

+174
-21
lines changed

src/dev_meta.erl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ adopt_node_message(Request, NodeMsg) ->
199199
%% After execution, we run the node's `postprocessor' message on the result of
200200
%% the request before returning the result it grants back to the user.
201201
handle_resolve(Req, Msgs, NodeMsg) ->
202+
TracePID = maps:get(trace, NodeMsg),
202203
% Apply the pre-processor to the request.
203204
case resolve_processor(<<"preprocess">>, preprocessor, Req, Msgs, NodeMsg) of
204205
{ok, PreProcessedMsg} ->
@@ -216,7 +217,7 @@ handle_resolve(Req, Msgs, NodeMsg) ->
216217
try
217218
hb_ao:resolve_many(
218219
PreProcessedMsg,
219-
HTTPOpts#{ force_message => true }
220+
HTTPOpts#{ force_message => true, trace => TracePID }
220221
)
221222
catch
222223
throw:{necessary_message_not_found, MsgID} ->

src/hb_ao.erl

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ do_resolve_many([Msg1, Msg2 | MsgList], Opts) ->
189189
resolved_message_of_many,
190190
{msg3, Msg3},
191191
{opts, Opts}
192-
}
192+
},
193+
Opts
193194
),
194195
do_resolve_many([Msg3 | MsgList], Opts);
195196
Res ->
@@ -272,7 +273,7 @@ resolve_stage(2, Msg1, Msg2, Opts) ->
272273
% only return a result if it is already in the cache).
273274
case hb_cache_control:maybe_lookup(Msg1, Msg2, Opts) of
274275
{ok, Msg3} ->
275-
?event(ao_core, {stage, 2, cache_hit, {msg3, Msg3}, {opts, Opts}}),
276+
?event(ao_core, {stage, 2, cache_hit, {msg3, Msg3}, {opts, Opts}}, Opts),
276277
{ok, Msg3};
277278
{continue, NewMsg1, NewMsg2} ->
278279
resolve_stage(3, NewMsg1, NewMsg2, Opts);
@@ -357,7 +358,7 @@ resolve_stage(4, Msg1, Msg2, Opts) ->
357358
end
358359
end.
359360
resolve_stage(5, Msg1, Msg2, ExecName, Opts) ->
360-
?event(ao_core, {stage, 5, device_lookup}),
361+
?event(ao_core, {stage, 5, device_lookup}, Opts),
361362
% Device lookup: Find the Erlang function that should be utilized to
362363
% execute Msg2 on Msg1.
363364
{ResolvedFunc, NewOpts} =
@@ -409,7 +410,8 @@ resolve_stage(5, Msg1, Msg2, ExecName, Opts) ->
409410
{exec_exception, Exception},
410411
{exec_stacktrace, Stacktrace},
411412
{opts, Opts}
412-
}
413+
},
414+
Opts
413415
),
414416
% If the device cannot be loaded, we alert the caller.
415417
error_execution(
@@ -426,7 +428,7 @@ resolve_stage(6, Func, Msg1, Msg2, ExecName, Opts) ->
426428
% Execution.
427429
% First, determine the arguments to pass to the function.
428430
% While calculating the arguments we unset the add_key option.
429-
UserOpts1 = maps:without(?TEMP_OPTS, Opts),
431+
UserOpts1 = maps:remove(trace, maps:without(?TEMP_OPTS, Opts)),
430432
% Unless the user has explicitly requested recursive spawning, we
431433
% unset the spawn_worker option so that we do not spawn a new worker
432434
% for every resulting execution.
@@ -479,7 +481,8 @@ resolve_stage(6, Func, Msg1, Msg2, ExecName, Opts) ->
479481
{exec_exception, ExecException},
480482
{exec_stacktrace, erlang:process_info(self(), backtrace)},
481483
{opts, Opts}
482-
}
484+
},
485+
Opts
483486
),
484487
% If the function call fails, we raise an error in the manner
485488
% indicated by caller's `#Opts'.
@@ -691,7 +694,7 @@ error_invalid_intermediate_status(Msg1, Msg2, Msg3, RemainingPath, Opts) ->
691694
error_execution(ExecGroup, Msg2, Whence, {Class, Exception, Stacktrace}, Opts) ->
692695
Error = {error, Whence, {Class, Exception, Stacktrace}},
693696
hb_persistent:unregister_notify(ExecGroup, Msg2, Error, Opts),
694-
?event(ao_core, {handle_error, Error, {opts, Opts}}),
697+
?event(ao_core, {handle_error, Error, {opts, Opts}}, Opts),
695698
case hb_opts:get(error_strategy, throw, Opts) of
696699
throw -> erlang:raise(Class, Exception, Stacktrace);
697700
_ -> Error
@@ -954,7 +957,8 @@ message_to_fun(Msg, Key, Opts) ->
954957
{key, Key},
955958
{is_exported, Exported},
956959
{opts, Opts}
957-
}
960+
},
961+
Opts
958962
),
959963
% Does the device have an explicit handler function?
960964
case {maps:find(handler, Info), Exported} of

src/hb_event.erl

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,34 @@ log(Topic, X, ModStr, Func, Line, Opts) ->
3939
end;
4040
true -> hb_util:debug_print(X, ModStr, Func, Line);
4141
false -> X
42-
end.
42+
end,
43+
handle_tracer(Topic, X, Opts).
44+
45+
handle_tracer(Topic, X, Opts) ->
46+
AllowedTopics = [http, ao_core, ao_result],
47+
case lists:member(Topic, AllowedTopics) of
48+
true ->
49+
case maps:get(trace, Opts, undefined) of
50+
undefined ->
51+
case tuple_to_list(X) of
52+
[_ | Rest] ->
53+
try
54+
Map = maps:from_list(Rest),
55+
TopicOpts = maps:get(opts, Map, #{}),
56+
case maps:get(trace, TopicOpts, undefined) of
57+
undefined -> ok;
58+
TracePID -> hb_tracer:record_step(TracePID, {Topic, X})
59+
end
60+
catch
61+
_:_ -> ok
62+
end;
63+
_ ->
64+
ok
65+
end;
66+
TracePID -> hb_tracer:record_step(TracePID, {Topic, X})
67+
end;
68+
_ -> ok
69+
end.
4370

4471
%% @doc Increment the counter for the given topic and message. Registers the
4572
%% counter if it doesn't exist. If the topic is `global', the message is ignored.

src/hb_http_server.erl

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -305,17 +305,35 @@ handle_request(RawReq, Body, ServerID) ->
305305
% The request is of normal AO-Core form, so we parse it and invoke
306306
% the meta@1.0 device to handle it.
307307
?event(http, {http_inbound, {cowboy_req, Req}, {body, {string, Body}}}),
308+
309+
TracePID = hb_tracer:start_trace(),
310+
308311
% Parse the HTTP request into HyerBEAM's message format.
309-
ReqSingleton = hb_http:req_to_tabm_singleton(Req, Body, NodeMsg),
310-
CommitmentCodec = hb_http:accept_to_codec(ReqSingleton, NodeMsg),
311-
?event(http, {parsed_singleton, ReqSingleton, {accept_codec, CommitmentCodec}}),
312-
{ok, Res} =
313-
dev_meta:handle(
314-
NodeMsg#{ commitment_device => CommitmentCodec },
315-
ReqSingleton
316-
),
317-
hb_http:reply(Req, ReqSingleton, Res, NodeMsg)
318-
end.
312+
try
313+
ReqSingleton = hb_http:req_to_tabm_singleton(Req, Body, NodeMsg),
314+
CommitmentCodec = hb_http:accept_to_codec(ReqSingleton, NodeMsg),
315+
?event(http, {parsed_singleton, ReqSingleton, {accept_codec, CommitmentCodec}}, #{trace => TracePID}),
316+
317+
% hb_tracer:record_step(TracePID, request_parsing),
318+
% Invoke the meta@1.0 device to handle the request.
319+
{ok, Res} =
320+
dev_meta:handle(
321+
NodeMsg#{ commitment_device => CommitmentCodec, trace => TracePID },
322+
ReqSingleton
323+
),
324+
hb_http:reply(Req, ReqSingleton, Res, NodeMsg)
325+
catch
326+
throw:_ ->
327+
Trace = hb_tracer:get_trace(TracePID),
328+
TraceString = hb_tracer:format_error_trace(Trace),
329+
hb_http:reply(Req, #{}, #{ <<"status">> => 500, <<"body">> => list_to_binary(TraceString)}, NodeMsg);
330+
331+
error:_ ->
332+
Trace = hb_tracer:get_trace(TracePID),
333+
TraceString = hb_tracer:format_error_trace(Trace),
334+
hb_http:reply(Req, #{}, #{ <<"status">> => 500, <<"body">> => list_to_binary(TraceString)}, NodeMsg)
335+
end
336+
end.
319337

320338
%% @doc Return the list of allowed methods for the HTTP server.
321339
allowed_methods(Req, State) ->

src/hb_tracer.erl

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
%%% @doc A module for tracing the flow of requests through the system.
2+
%%% This allows for tracking the lifecycle of a request from HTTP receipt through processing and response.
3+
4+
-module(hb_tracer).
5+
-export([start_trace/0, record_step/2, get_trace/1, format_error_trace/1]).
6+
7+
-include("include/hb.hrl").
8+
9+
start_trace() ->
10+
Trace = #{ steps => queue:new() },
11+
TracePID = spawn(fun() -> trace_loop(Trace) end),
12+
?event(trace, {trace_started, TracePID}),
13+
TracePID.
14+
15+
trace_loop(Trace) ->
16+
receive
17+
{record_step, Step} ->
18+
Steps = maps:get(steps, Trace),
19+
NewTrace = Trace#{steps => queue:in(Step, Steps)},
20+
?event(trace, {step_recorded, Step}),
21+
trace_loop(NewTrace);
22+
{get_trace, From} ->
23+
% Convert queue to list for the response
24+
TraceWithList = Trace#{steps => queue:to_list(maps:get(steps, Trace))},
25+
From ! {trace, TraceWithList},
26+
trace_loop(Trace)
27+
end.
28+
29+
record_step(TracePID, Step) ->
30+
TracePID! {record_step, Step}.
31+
32+
get_trace(TracePID) ->
33+
TracePID! {get_trace, self()},
34+
receive
35+
{trace, Trace} ->
36+
Trace
37+
after 5000 ->
38+
?event(trace, {trace_timeout, TracePID}),
39+
{trace, #{}}
40+
end.
41+
42+
format_error_trace(Trace) ->
43+
Steps = maps:get(steps, Trace, []),
44+
45+
TraceMap = lists:foldl(fun (TraceItem, Acc) ->
46+
case TraceItem of
47+
{http, {parsed_singleton, _ReqSingleton, _}} ->
48+
maps:put(request_parsing, true, Acc);
49+
{ao_core, {stage, Stage, _Task}} ->
50+
maps:put(resolve_stage, Stage, Acc);
51+
{ao_result, {load_device_failed, _, _, _, _, {exec_exception, Exception}, _, _}} ->
52+
maps:put(error, Exception, Acc);
53+
{ao_result, {exec_failed, _, _, _, {func, Fun}, _, {exec_exception, Error}, _, _}} ->
54+
maps:put(error, {Fun, Error}, Acc);
55+
_ -> Acc
56+
end
57+
end, #{}, Steps),
58+
59+
% Build the trace message
60+
TraceStrings = ["Oops! Something went wrong. Here's the rundown:"],
61+
62+
% Add parsing status
63+
ParsingTrace = case maps:get(request_parsing, TraceMap, false) of
64+
false ->
65+
TraceStrings ++ [[failure_emoji(), "Parsing your request"]];
66+
true ->
67+
TraceStrings ++ [[checkmark_emoji(), "Parsing your request"]]
68+
end,
69+
70+
% Add stage information
71+
StageTrace = case maps:get(resolve_stage, TraceMap, undefined) of
72+
undefined ->
73+
ParsingTrace;
74+
Stage ->
75+
ParsingTrace ++ [[stage_to_emoji(Stage), " Resolved steps of your execution"]]
76+
end,
77+
78+
% Add error information
79+
ErrorTrace = case maps:get(error, TraceMap, undefined) of
80+
undefined ->
81+
StageTrace;
82+
{Fun, Reason} ->
83+
StageTrace ++ [[failure_emoji(), io_lib:format("Error: ~p -> ~p", [Fun, Reason])]];
84+
Error ->
85+
StageTrace ++ [[failure_emoji(), io_lib:format("Error: ~p", [Error])]]
86+
end,
87+
string:join(ErrorTrace, "\n").
88+
89+
90+
checkmark_emoji() ->
91+
% Unicode for checkmark
92+
"\xE2\x9C\x85". % \xE2\x9C\x85 is the checkmark emoji in UTF-8
93+
94+
failure_emoji() ->
95+
% Unicode for failure emoji
96+
"\xE2\x9D\x8C". % \xE2\x9D\x8C is the failure emoji in UTF-8
97+
98+
% Helper function to convert stage number to emoji
99+
stage_to_emoji(Stage) when Stage >= 1, Stage =< 9 ->
100+
% Unicode for circled numbers 1-9
101+
[Stage + 48, 16#E2, 16#83, 16#A3];
102+
stage_to_emoji(_) ->
103+
"".

src/include/hb.hrl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
%%% underlying function.
2727
-define(event(X), hb_event:log(global, X, ?MODULE, ?FUNCTION_NAME, ?LINE)).
2828
-define(event(Topic, X), hb_event:log(Topic, X, ?MODULE, ?FUNCTION_NAME, ?LINE)).
29-
-define(event(Topic, X, Opts), hb_event:log(maps:get(topic, Opts, Topic), X, ?MODULE, ?FUNCTION_NAME, ?LINE), Opts).
29+
-define(event(Topic, X, Opts), hb_event:log(maps:get(topic, Opts, Topic), X, ?MODULE, ?FUNCTION_NAME, ?LINE, Opts)).
3030
-define(debug_wait(T), hb:debug_wait(T, ?MODULE, ?FUNCTION_NAME, ?LINE)).
3131
-define(debug_print(X), hb_util:debug_print(X, ?MODULE, ?FUNCTION_NAME, ?LINE)).
3232
-define(no_prod(X), hb:no_prod(X, ?MODULE, ?LINE)).

0 commit comments

Comments
 (0)