Skip to content

Commit

Permalink
Merge pull request #138 from gugahoa/add-record-exception
Browse files Browse the repository at this point in the history
Add RecordException to Elixir's Span module
  • Loading branch information
tsloughter authored Feb 11, 2021
2 parents 3995f77 + 37f5559 commit 3d26b62
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 1 deletion.
43 changes: 42 additions & 1 deletion apps/opentelemetry/test/opentelemetry_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ all() ->
all_testcases() ->
[with_span, macros, child_spans, update_span_data, tracer_instrumentation_library,
tracer_previous_ctx, stop_temporary_app, reset_after, attach_ctx, default_sampler,
record_but_not_sample].
record_but_not_sample, record_exception_works, record_exception_with_message_works].

groups() ->
[{w3c, [], [propagation]},
Expand Down Expand Up @@ -456,6 +456,47 @@ record_but_not_sample(Config) ->

ok.

record_exception_works(Config) ->
Tid = ?config(tid, Config),
SpanCtx = ?start_span(<<"span-1">>),
try throw(my_error) of
_ ->
ok
catch
Class:Term:Stacktrace ->
otel_span:record_exception(SpanCtx, Class, Term, Stacktrace, [{"some-attribute", "value"}]),
?end_span(SpanCtx),
[Span] = assert_exported(Tid, SpanCtx),
[Event] = Span#span.events,
?assertEqual(<<"exception">>, Event#event.name),
?assertEqual([{<<"exception.type">>, <<"throw:my_error">>},
{<<"exception.stacktrace">>, list_to_binary(io_lib:format("~p", [Stacktrace], [{chars_limit, 50}]))},
{"some-attribute","value"}],
Event#event.attributes),
ok
end.

record_exception_with_message_works(Config) ->
Tid = ?config(tid, Config),
SpanCtx = ?start_span(<<"span-1">>),
try throw(my_error) of
_ ->
ok
catch
Class:Term:Stacktrace ->
otel_span:record_exception(SpanCtx, Class, Term, "My message", Stacktrace, [{"some-attribute", "value"}]),
?end_span(SpanCtx),
[Span] = assert_exported(Tid, SpanCtx),
[Event] = Span#span.events,
?assertEqual(<<"exception">>, Event#event.name),
?assertEqual([{<<"exception.type">>, <<"throw:my_error">>},
{<<"exception.stacktrace">>, list_to_binary(io_lib:format("~p", [Stacktrace], [{chars_limit, 50}]))},
{<<"exception.message">>, "My message"},
{"some-attribute","value"}],
Event#event.attributes),
ok
end.

%%

assert_all_exported(Tid, SpanCtxs) ->
Expand Down
28 changes: 28 additions & 0 deletions apps/opentelemetry_api/lib/open_telemetry/span.ex
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,34 @@ defmodule OpenTelemetry.Span do
@spec add_events(OpenTelemetry.span_ctx(), [OpenTelemetry.event()]) :: boolean()
defdelegate add_events(span_ctx, events), to: :otel_span

defguardp is_exception?(term)
when is_map(term) and :erlang.is_map_key(:__struct__, term) and
is_atom(:erlang.map_get(:__struct__, term)) and
:erlang.is_map_key(:__exception__, term) and
:erlang.map_get(:__exception__, term) == true

@doc """
Record an exception as an event, following the semantics convetions for exceptions.
If trace is not provided, the stacktrace is retrieved from `Process.info/2`
"""
@spec record_exception(OpenTelemetry.span_ctx(), Exception.t()) :: boolean()
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 = [
{"exception.type", exception_type},
{"exception.message", Exception.message(exception)},
{"exception.stacktrace", Exception.format_stacktrace(trace)}
]

add_event(span_ctx, "exception", exception_attributes ++ attributes)
end

def record_exception(_, _, _, _), do: false

@doc """
Sets the Status of the currently active Span.
Expand Down
26 changes: 26 additions & 0 deletions apps/opentelemetry_api/src/otel_span.erl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
set_attributes/2,
add_event/3,
add_events/2,
record_exception/5,
record_exception/6,
set_status/2,
update_name/2,
end_span/1]).
Expand Down Expand Up @@ -105,6 +107,30 @@ add_events(SpanCtx=#span_ctx{span_sdk={Module, _}}, Events) when ?is_recording(S
add_events(_, _) ->
false.

-spec record_exception(SpanCtx, Class, Term, Stacktrace, Attributes) -> boolean() when
SpanCtx :: opentelemetry:span_ctx(),
Class :: atom(),
Term :: term(),
Stacktrace :: list(any()),
Attributes :: opentelemetry:attributes().
record_exception(SpanCtx, Class, Term, Stacktrace, Attributes) ->
ExceptionAttributes = [{<<"exception.type">>, iolist_to_binary(io_lib:format("~0tP:~0tP", [Class, 10, Term, 10], [{chars_limit, 50}]))},
{<<"exception.stacktrace">>, iolist_to_binary(io_lib:format("~0tP", [Stacktrace, 10], [{chars_limit, 50}]))}],
add_event(SpanCtx, <<"exception">>, ExceptionAttributes ++ Attributes).

-spec record_exception(SpanCtx, Class, Term, Message, Stacktrace, Attributes) -> boolean() when
SpanCtx :: opentelemetry:span_ctx(),
Class :: atom(),
Term :: term(),
Message :: unicode:unicode_binary(),
Stacktrace :: list(any()),
Attributes :: opentelemetry:attributes().
record_exception(SpanCtx, Class, Term, Message, Stacktrace, Attributes) ->
ExceptionAttributes = [{<<"exception.type">>, iolist_to_binary(io_lib:format("~0tP:~0tP", [Class, 10, Term, 10], [{chars_limit, 50}]))},
{<<"exception.stacktrace">>, iolist_to_binary(io_lib:format("~0tP", [Stacktrace, 10], [{chars_limit, 50}]))},
{<<"exception.message">>, Message}],
add_event(SpanCtx, <<"exception">>, ExceptionAttributes ++ Attributes).

-spec set_status(SpanCtx, Status) -> boolean() when
Status :: opentelemetry:status(),
SpanCtx :: opentelemetry:span_ctx().
Expand Down
40 changes: 40 additions & 0 deletions test/otel_tests.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ defmodule OtelTests do
require Record
@fields Record.extract(:span, from_lib: "opentelemetry/include/otel_span.hrl")
Record.defrecordp(:span, @fields)

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

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

test "use Tracer to set current active Span's attributes" do
:otel_batch_processor.set_exporter(:otel_exporter_pid, self())
OpenTelemetry.register_tracer(:test_tracer, "0.1.0")
Expand Down Expand Up @@ -200,4 +204,40 @@ defmodule OtelTests do
parent_span_id: :undefined
)}
end

test "Span.record_exception/4 should return false if passed an invalid exception" do
Tracer.with_span "span-3" do
refute OpenTelemetry.Span.record_exception(Tracer.current_span_ctx(), :not_an_exception)
end
end

test "Span.record_exception/4 should add an exception event to the span" do
:otel_batch_processor.set_exporter(:otel_exporter_pid, self())
s = Tracer.start_span("span-4")

try do
raise RuntimeError, "my error message"
rescue
ex ->
assert Span.record_exception(s, ex, __STACKTRACE__)
assert Span.end_span(s)

stacktrace = Exception.format_stacktrace(__STACKTRACE__)

assert_receive {:span,
span(
name: "span-4",
events: [
event(
name: "exception",
attributes: [
{"exception.type", "Elixir.RuntimeError"},
{"exception.message", "my error message"},
{"exception.stacktrace", ^stacktrace}
]
)
]
)}
end
end
end

0 comments on commit 3d26b62

Please sign in to comment.