Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
82 changes: 80 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,84 @@ 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),
?assertEqual(undefined, SpanError#span.status),
[#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),
?assertEqual(undefined, SpanExit#span.status),
[#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,

%% broken elixir exception
Exception = #{'__exception__' => true, '__struct__' => invalid},
?assertException(exit, Exception, otel_tracer:with_span(Tracer, <<"span-elixir">>, #{record_exception => true},
fun(_SpanCtx) ->
erlang:exit(Exception)
end)),

receive
{span, SpanElixir} ->
?assertEqual(<<"span-elixir">>, SpanElixir#span.name),
?assertEqual(undefined, SpanElixir#span.status),
[#event{name=exception, attributes=A3}] = otel_events:list(SpanElixir#span.events),
?assertMatch(#{'exception.type' := <<"exit:#{'__exception__' => true,'__struct__' => invalid}">>, 'exception.stacktrace' := _}, otel_attributes:map(A3))

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

ok.

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

Expand Down
19 changes: 9 additions & 10 deletions apps/opentelemetry_api/lib/open_telemetry/span.ex
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,15 @@ defmodule OpenTelemetry.Span do
def record_exception(span_ctx, exception, trace \\ nil, attributes \\ [])

def record_exception(span_ctx, exception, trace, attributes) when is_exception?(exception) do
exception_type = to_string(exception.__struct__)

exception_attributes = [
{OpenTelemetry.SemanticConventions.Trace.exception_type(), exception_type},
{OpenTelemetry.SemanticConventions.Trace.exception_message(), Exception.message(exception)},
{OpenTelemetry.SemanticConventions.Trace.exception_stacktrace(),
Exception.format_stacktrace(trace)}
]

add_event(span_ctx, "exception", exception_attributes ++ attributes)
trace =
if trace do
trace
else
{:current_stacktrace, t} = Process.info(self(), :current_stacktrace)
Enum.drop(t, 3)
end

:otel_span.record_exception(span_ctx, :error, exception, trace, attributes)
end

def record_exception(_, _, _, _), do: false
Expand Down
82 changes: 70 additions & 12 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,7 @@ 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}]),
{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));
do_record_exception(SpanCtx, Class, Term, undefined, Stacktrace, Attributes);
record_exception(_, _, _, _, _) ->
false.

Expand All @@ -219,15 +235,57 @@ 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}]),
{ok, StacktraceString} = otel_utils:format_binary_string("~0tP", [Stacktrace, 10], [{chars_limit, 50}]),
ExceptionAttributes = #{?EXCEPTION_TYPE => ExceptionType,
?EXCEPTION_STACKTRACE => StacktraceString,
?EXCEPTION_MESSAGE => Message},
add_event(SpanCtx, 'exception', maps:merge(ExceptionAttributes, Attributes));
do_record_exception(SpanCtx, Class, Term, Message, Stacktrace, Attributes);
record_exception(_, _, _, _, _, _) ->
false.

do_record_exception(SpanCtx, Class, Term, Message, Stacktrace, Attributes) ->
ExceptionFromElixir = exception_from_elixir(Stacktrace),
Term1 = normalize_exception(Class, Term, Stacktrace, ExceptionFromElixir),
ExceptionType = exception_type(Class, Term1),
StacktraceString = format_stacktrace(Stacktrace, ExceptionFromElixir),
ExceptionAttributes = #{?EXCEPTION_TYPE => ExceptionType,
?EXCEPTION_STACKTRACE => StacktraceString},
ExceptionAttributes1 = add_message(ExceptionAttributes, Message, Term1),
add_event(SpanCtx, exception, maps:merge(ExceptionAttributes1, Attributes)).
exception_from_elixir([{Module, _, _, _} | _]) ->
ModuleStr = atom_to_list(Module),
string:find(ModuleStr, "Elixir.") =:= ModuleStr;
exception_from_elixir(_) ->
false.
format_stacktrace(Stacktrace, false) ->
{ok, StacktraceString} = otel_utils:format_binary_string("~0tP", [Stacktrace, 10], [{chars_limit, 50}]),
StacktraceString;
format_stacktrace(Stacktrace, true) ->
'Elixir.Exception':format_stacktrace(Stacktrace).
normalize_exception(Class, Term, Stacktrace, true) ->
'Elixir.Exception':normalize(Class, Term, Stacktrace);
normalize_exception(_Class, Term, _ExceptionStacktrace, false) ->
Term.
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_message(Attributes, Message, _Exception) when Message /= undefined ->
maps:put(?EXCEPTION_MESSAGE, Message, Attributes);
add_message(Attributes, undefined, #{'__exception__' := true} = Exception) ->
try
Message = 'Elixir.Exception':message(Exception),
maps:put(?EXCEPTION_MESSAGE, Message, Attributes)
catch
_Class:_Exception ->
Attributes
end;
add_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']}.

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

Expand Down
Loading