Skip to content

Commit

Permalink
Add record_exceptions options to with_span
Browse files Browse the repository at this point in the history
  • Loading branch information
albertored committed Sep 1, 2023
1 parent a673c0f commit 0fb52b6
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 13 deletions.
22 changes: 21 additions & 1 deletion apps/opentelemetry/src/otel_tracer_default.erl
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,33 @@ start_span(Ctx, {_, #tracer{on_start_processors=Processors,
SpanCtx#span_ctx{span_sdk={otel_span_ets, OnEndProcessors}}.

-spec with_span(otel_ctx:t(), opentelemetry:tracer(), opentelemetry:span_name(),
otel_span:start_opts(), otel_tracer:traced_fun(T)) -> T.
otel_span:with_opts(), otel_tracer:traced_fun(T)) -> T.
with_span(Ctx, Tracer, SpanName, Opts, Fun) ->
RecordException = maps:get(record_exception, Opts, false),
SetStatusOnException = maps:get(set_status_on_exception, Opts, false),
SpanCtx = start_span(Ctx, Tracer, SpanName, Opts),
Ctx1 = otel_tracer:set_current_span(Ctx, SpanCtx),
Token = otel_ctx:attach(Ctx1),
try
Fun(SpanCtx)
catch
Class:Term:Stacktrace ->
if
RecordException ->
otel_span:record_exception(SpanCtx, Class, Term, Stacktrace, #{});
true ->
ok
end,

if
SetStatusOnException ->
Status = opentelemetry:status(?OTEL_STATUS_ERROR, <<"exception">>),
otel_span:set_status(SpanCtx, Status);
true ->
ok
end,

erlang:raise(Class, Term, Stacktrace)
after
%% passing SpanCtx directly ensures that this `end_span' ends the span started
%% in this function. If spans in `Fun()' were started and not finished properly
Expand Down
61 changes: 59 additions & 2 deletions apps/opentelemetry/test/opentelemetry_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ all() ->
{group, otel_batch_processor}].

all_cases() ->
[with_span, macros, child_spans, disabled_sdk,
[with_span, record_exception, macros, child_spans, disabled_sdk,
update_span_data, tracer_instrumentation_scope, tracer_previous_ctx, stop_temporary_app,
reset_after, attach_ctx, default_sampler, non_recording_ets_table,
root_span_sampling_always_on, root_span_sampling_always_off,
Expand Down Expand Up @@ -122,7 +122,7 @@ init_per_testcase(tracer_instrumentation_scope, Config) ->
Config1 = set_batch_tab_processor(Config),
{ok, _} = application:ensure_all_started(opentelemetry),
Config1;
init_per_testcase(multiple_tracer_providers, Config) ->
init_per_testcase(Test, Config) when Test =:= record_exception; Test =:= multiple_tracer_providers->
application:set_env(opentelemetry, processors, [{otel_batch_processor, #{exporter => {otel_exporter_pid, self()},
scheduled_delay_ms => 1}}]),
{ok, _} = application:ensure_all_started(opentelemetry),
Expand Down Expand Up @@ -465,6 +465,63 @@ with_span(Config) ->

ok.

record_exception(_Config) ->
Tracer = opentelemetry:get_tracer(),

%% ERROR
?assertException(error, badarg, otel_tracer:with_span(Tracer, <<"span-error">>, #{record_exception => true},
fun(_SpanCtx) ->
erlang:error(badarg)
end)),

receive
{span, SpanError} ->
?assertEqual(<<"span-error">>, SpanError#span.name),
[#event{name=exception, attributes=A}] = otel_events:list(SpanError#span.events),
?assertMatch(#{'exception.type' := <<"error:badarg">>, 'exception.stacktrace' := _}, otel_attributes:map(A))

after
1000 ->
ct:fail(timeout)
end,

%% THROW
?assertException(throw, value, otel_tracer:with_span(Tracer, <<"span-throw">>, #{record_exception => true, set_status_on_exception => true},
fun(_SpanCtx) ->
erlang:throw(value)
end)),

receive
{span, SpanThrow} ->
?assertEqual(<<"span-throw">>, SpanThrow#span.name),
?assertMatch(#status{code=?OTEL_STATUS_ERROR}, SpanThrow#span.status),
[#event{name=exception, attributes=A1}] = otel_events:list(SpanThrow#span.events),
?assertMatch(#{'exception.type' := <<"throw:value">>, 'exception.stacktrace' := _}, otel_attributes:map(A1))

after
1000 ->
ct:fail(timeout)
end,

%% EXIT
?assertException(exit, shutdown, otel_tracer:with_span(Tracer, <<"span-exit">>, #{record_exception => true},
fun(_SpanCtx) ->
erlang:exit(shutdown)
end)),

receive
{span, SpanExit} ->
?assertEqual(<<"span-exit">>, SpanExit#span.name),
[#event{name=exception, attributes=A2}] = otel_events:list(SpanExit#span.events),
?assertMatch(#{'exception.type' := <<"exit:shutdown">>, 'exception.stacktrace' := _}, otel_attributes:map(A2))

after
1000 ->
ct:fail(timeout)
end,

ok.

child_spans(Config) ->
Tid = ?config(tid, Config),

Expand Down
51 changes: 47 additions & 4 deletions apps/opentelemetry_api/src/otel_span.erl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
is_valid/1,
is_valid_name/1,
validate_start_opts/1,
validate_with_opts/1,
set_attribute/3,
set_attributes/2,
add_event/3,
Expand All @@ -51,7 +52,15 @@
start_time := opentelemetry:timestamp(),
kind := opentelemetry:span_kind()}.

-export_type([start_opts/0]).
-type with_opts() :: #{attributes => opentelemetry:attributes_map(),
links => [opentelemetry:link()],
is_recording => boolean(),
start_time => opentelemetry:timestamp(),
kind => opentelemetry:span_kind(),
record_exception => boolean(),
set_status_on_exception => boolean()}.

-export_type([start_opts/0, with_opts/0]).

-spec validate_start_opts(start_opts()) -> start_opts().
validate_start_opts(Opts) when is_map(Opts) ->
Expand All @@ -68,6 +77,17 @@ validate_start_opts(Opts) when is_map(Opts) ->
is_recording => IsRecording
}.

-spec validate_with_opts(with_opts()) -> with_opts().
validate_with_opts(Opts) when is_map(Opts) ->
StartOpts = validate_start_opts(Opts),
RecordException = maps:get(record_exception, Opts, false),
SetStatusOnException = maps:get(set_status_on_exception, Opts, false),
maps:merge(StartOpts, #{
record_exception => RecordException,
set_status_on_exception => SetStatusOnException
}).


-spec is_recording(SpanCtx) -> boolean() when
SpanCtx :: opentelemetry:span_ctx().
is_recording(SpanCtx) ->
Expand Down Expand Up @@ -201,11 +221,12 @@ add_events(_, _) ->
record_exception(SpanCtx, Class, Term, Stacktrace, Attributes) when is_list(Attributes) ->
record_exception(SpanCtx, Class, Term, Stacktrace, maps:from_list(Attributes));
record_exception(SpanCtx, Class, Term, Stacktrace, Attributes) when is_map(Attributes) ->
{ok, ExceptionType} = otel_utils:format_binary_string("~0tP:~0tP", [Class, 10, Term, 10], [{chars_limit, 50}]),
ExceptionType = exception_type(Class, Term),
{ok, StacktraceString} = otel_utils:format_binary_string("~0tP", [Stacktrace, 10], [{chars_limit, 50}]),
ExceptionAttributes = #{?EXCEPTION_TYPE => ExceptionType,
?EXCEPTION_STACKTRACE => StacktraceString},
add_event(SpanCtx, 'exception', maps:merge(ExceptionAttributes, Attributes));
ExceptionAttributes1 = add_elixir_message(ExceptionAttributes, Term),
add_event(SpanCtx, 'exception', maps:merge(ExceptionAttributes1, Attributes));
record_exception(_, _, _, _, _) ->
false.

Expand All @@ -219,7 +240,7 @@ record_exception(_, _, _, _, _) ->
record_exception(SpanCtx, Class, Term, Message, Stacktrace, Attributes) when is_list(Attributes) ->
record_exception(SpanCtx, Class, Term, Message, Stacktrace, maps:from_list(Attributes));
record_exception(SpanCtx, Class, Term, Message, Stacktrace, Attributes) when is_map(Attributes) ->
{ok, ExceptionType} = otel_utils:format_binary_string("~0tP:~0tP", [Class, 10, Term, 10], [{chars_limit, 50}]),
ExceptionType = exception_type(Class, Term),
{ok, StacktraceString} = otel_utils:format_binary_string("~0tP", [Stacktrace, 10], [{chars_limit, 50}]),
ExceptionAttributes = #{?EXCEPTION_TYPE => ExceptionType,
?EXCEPTION_STACKTRACE => StacktraceString,
Expand All @@ -228,6 +249,28 @@ record_exception(SpanCtx, Class, Term, Message, Stacktrace, Attributes) when is_
record_exception(_, _, _, _, _, _) ->
false.

exception_type(error, #{'__exception__' := true, '__struct__' := ElixirErrorStruct} = Term) ->
case atom_to_binary(ElixirErrorStruct) of
<<"Elixir.", ExceptionType/binary>> -> ExceptionType;
_ -> exception_type_erl(error, Term)
end;
exception_type(Class, Term) ->
exception_type_erl(Class, Term).
exception_type_erl(Class, Term) ->
{ok, ExceptionType} = otel_utils:format_binary_string("~0tP:~0tP", [Class, 10, Term, 10], [{chars_limit, 50}]),
ExceptionType.

add_elixir_message(Attributes, #{'__exception__' := true} = Exception) ->
try
Message = 'Elixir.Exception':message(Exception),
maps:put(?EXCEPTION_MESSAGE, Message, Attributes)
catch
_Class:_Exception ->
Attributes
end;
add_elixir_message(Attributes, _) ->
Attributes.

-spec set_status(SpanCtx, StatusOrCode) -> boolean() when
StatusOrCode :: opentelemetry:status() | undefined | opentelemetry:status_code(),
SpanCtx :: opentelemetry:span_ctx().
Expand Down
8 changes: 4 additions & 4 deletions apps/opentelemetry_api/src/otel_tracer.erl
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,20 @@ start_span(Ctx, Tracer={Module, _}, SpanName, Opts) ->
otel_tracer_noop:noop_span_ctx()
end.

-spec with_span(opentelemetry:tracer(), opentelemetry:span_name(), otel_span:start_opts(), traced_fun(T)) -> T.
-spec with_span(opentelemetry:tracer(), opentelemetry:span_name(), otel_span:with_opts(), traced_fun(T)) -> T.
with_span(Tracer={Module, _}, SpanName, Opts, Fun) when is_atom(Module) ->
case otel_span:is_valid_name(SpanName) of
true ->
Module:with_span(otel_ctx:get_current(), Tracer, SpanName, otel_span:validate_start_opts(Opts), Fun);
Module:with_span(otel_ctx:get_current(), Tracer, SpanName, otel_span:validate_with_opts(Opts), Fun);
false ->
Fun(otel_tracer_noop:noop_span_ctx())
end.

-spec with_span(otel_ctx:t(), opentelemetry:tracer(), opentelemetry:span_name(), otel_span:start_opts(), traced_fun(T)) -> T.
-spec with_span(otel_ctx:t(), opentelemetry:tracer(), opentelemetry:span_name(), otel_span:with_opts(), traced_fun(T)) -> T.
with_span(Ctx, Tracer={Module, _}, SpanName, Opts, Fun) when is_atom(Module) ->
case otel_span:is_valid_name(SpanName) of
true ->
Module:with_span(Ctx, Tracer, SpanName, otel_span:validate_start_opts(Opts), Fun);
Module:with_span(Ctx, Tracer, SpanName, otel_span:validate_with_opts(Opts), Fun);
false ->
Fun(otel_tracer_noop:noop_span_ctx())
end.
Expand Down
5 changes: 3 additions & 2 deletions rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@
opentelemetry_exporter_trace_service_pb,
opentelemetry_exporter_metrics_service_pb,
opentelemetry_exporter_logs_service_pb,
opentelemetry_zipkin_pb]}.

opentelemetry_zipkin_pb,

{'Elixir.Exception', message, 1}]}.

{dialyzer, [{warnings, [no_unknown]}]}.

Expand Down
103 changes: 103 additions & 0 deletions test/otel_tests.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ defmodule OtelTests do
@fields Record.extract(:span, from_lib: "opentelemetry/include/otel_span.hrl")
Record.defrecordp(:span, @fields)

@fields Record.extract(:event, from_lib: "opentelemetry/include/otel_span.hrl")
Record.defrecordp(:event, @fields)

@fields Record.extract(:tracer, from_lib: "opentelemetry/src/otel_tracer.hrl")
Record.defrecordp(:tracer, @fields)

@fields Record.extract(:span_ctx, from_lib: "opentelemetry_api/include/opentelemetry.hrl")
Record.defrecordp(:span_ctx, @fields)

@fields Record.extract(:status, from_lib: "opentelemetry_api/include/opentelemetry.hrl")
Record.defrecordp(:status, @fields)

setup do
Application.load(:opentelemetry)

Expand Down Expand Up @@ -308,4 +314,101 @@ defmodule OtelTests do
opts
)
end

describe "Tracer.with_span record exception" do
test "raise" do
assert_raise RuntimeError, "my error message", fn ->
Tracer.with_span "span-1", record_exception: true, set_status_on_exception: true do
raise RuntimeError, "my error message"
end
end

assert_receive {:span,
span(
name: "span-1",
events: {:events, _, _, _, _, [event]},
status: status(code: :error)
)}

assert event(name: :exception, attributes: {:attributes, _, _, _, received_attirbutes}) =
event

assert %{
"exception.type": "RuntimeError",
"exception.message": "my error message",
"exception.stacktrace": _
} = received_attirbutes
end

test ":erlang.error()" do
assert_raise ArgumentError, fn ->
Tracer.with_span "span-1", record_exception: true do
:erlang.error(:badarg)
end
end

assert_receive {:span,
span(
name: "span-1",
events: {:events, _, _, _, _, [event]},
status: :undefined
)}

assert event(name: :exception, attributes: {:attributes, _, _, _, received_attirbutes}) =
event

assert %{
"exception.type": "error:badarg",
"exception.stacktrace": _
} = received_attirbutes
end

test "exit" do
assert :shutdown ==
catch_exit(
Tracer.with_span "span-1", record_exception: true, set_status_on_exception: true do
exit(:shutdown)
end
)

assert_receive {:span,
span(
name: "span-1",
events: {:events, _, _, _, _, [event]},
status: status(code: :error)
)}

assert event(name: :exception, attributes: {:attributes, _, _, _, received_attirbutes}) =
event

assert %{
"exception.type": "exit:shutdown",
"exception.stacktrace": _
} = received_attirbutes
end

test "throw" do
assert :value ==
catch_throw(
Tracer.with_span "span-1", record_exception: true do
throw(:value)
end
)

assert_receive {:span,
span(
name: "span-1",
events: {:events, _, _, _, _, [event]},
status: :undefined
)}

assert event(name: :exception, attributes: {:attributes, _, _, _, received_attirbutes}) =
event

assert %{
"exception.type": "throw:value",
"exception.stacktrace": _
} = received_attirbutes
end
end
end

0 comments on commit 0fb52b6

Please sign in to comment.