Skip to content

Commit 826a5ce

Browse files
committed
Add record_exceptions options to with_span
1 parent 0ec0e86 commit 826a5ce

File tree

6 files changed

+258
-13
lines changed

6 files changed

+258
-13
lines changed

apps/opentelemetry/src/otel_tracer_default.erl

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,33 @@ start_span(Ctx, {_, #tracer{on_start_processors=Processors,
3838
SpanCtx#span_ctx{span_sdk={otel_span_ets, OnEndProcessors}}.
3939

4040
-spec with_span(otel_ctx:t(), opentelemetry:tracer(), opentelemetry:span_name(),
41-
otel_span:start_opts(), otel_tracer:traced_fun(T)) -> T.
41+
otel_span:with_opts(), otel_tracer:traced_fun(T)) -> T.
4242
with_span(Ctx, Tracer, SpanName, Opts, Fun) ->
43+
RecordException = maps:get(record_exception, Opts, false),
44+
SetStatusOnException = maps:get(set_status_on_exception, Opts, false),
4345
SpanCtx = start_span(Ctx, Tracer, SpanName, Opts),
4446
Ctx1 = otel_tracer:set_current_span(Ctx, SpanCtx),
4547
Token = otel_ctx:attach(Ctx1),
4648
try
4749
Fun(SpanCtx)
50+
catch
51+
Class:Term:Stacktrace ->
52+
if
53+
RecordException ->
54+
otel_span:record_exception(SpanCtx, Class, Term, Stacktrace, #{});
55+
true ->
56+
ok
57+
end,
58+
59+
if
60+
SetStatusOnException ->
61+
Status = opentelemetry:status(?OTEL_STATUS_ERROR, <<"exception">>),
62+
otel_span:set_status(SpanCtx, Status);
63+
true ->
64+
ok
65+
end,
66+
67+
erlang:raise(Class, Term, Stacktrace)
4868
after
4969
%% passing SpanCtx directly ensures that this `end_span' ends the span started
5070
%% in this function. If spans in `Fun()' were started and not finished properly

apps/opentelemetry/test/opentelemetry_SUITE.erl

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ all() ->
3333
{group, otel_batch_processor}].
3434

3535
all_cases() ->
36-
[with_span, macros, child_spans, disabled_sdk,
36+
[with_span, record_exception, macros, child_spans, disabled_sdk,
3737
update_span_data, tracer_instrumentation_scope, tracer_previous_ctx, stop_temporary_app,
3838
reset_after, attach_ctx, default_sampler, non_recording_ets_table,
3939
root_span_sampling_always_on, root_span_sampling_always_off,
@@ -122,7 +122,7 @@ init_per_testcase(tracer_instrumentation_scope, Config) ->
122122
Config1 = set_batch_tab_processor(Config),
123123
{ok, _} = application:ensure_all_started(opentelemetry),
124124
Config1;
125-
init_per_testcase(multiple_tracer_providers, Config) ->
125+
init_per_testcase(Test, Config) when Test =:= record_exception; Test =:= multiple_tracer_providers->
126126
application:set_env(opentelemetry, processors, [{otel_batch_processor, #{exporter => {otel_exporter_pid, self()},
127127
scheduled_delay_ms => 1}}]),
128128
{ok, _} = application:ensure_all_started(opentelemetry),
@@ -465,6 +465,84 @@ with_span(Config) ->
465465

466466
ok.
467467

468+
record_exception(_Config) ->
469+
Tracer = opentelemetry:get_tracer(),
470+
471+
%% ERROR
472+
?assertException(error, badarg, otel_tracer:with_span(Tracer, <<"span-error">>, #{record_exception => true},
473+
fun(_SpanCtx) ->
474+
erlang:error(badarg)
475+
end)),
476+
477+
receive
478+
{span, SpanError} ->
479+
?assertEqual(<<"span-error">>, SpanError#span.name),
480+
?assertEqual(undefined, SpanError#span.status),
481+
[#event{name=exception, attributes=A}] = otel_events:list(SpanError#span.events),
482+
?assertMatch(#{'exception.type' := <<"error:badarg">>, 'exception.stacktrace' := _}, otel_attributes:map(A))
483+
484+
after
485+
1000 ->
486+
ct:fail(timeout)
487+
end,
488+
489+
%% THROW
490+
?assertException(throw, value, otel_tracer:with_span(Tracer, <<"span-throw">>, #{record_exception => true, set_status_on_exception => true},
491+
fun(_SpanCtx) ->
492+
erlang:throw(value)
493+
end)),
494+
495+
receive
496+
{span, SpanThrow} ->
497+
?assertEqual(<<"span-throw">>, SpanThrow#span.name),
498+
?assertMatch(#status{code=?OTEL_STATUS_ERROR}, SpanThrow#span.status),
499+
[#event{name=exception, attributes=A1}] = otel_events:list(SpanThrow#span.events),
500+
?assertMatch(#{'exception.type' := <<"throw:value">>, 'exception.stacktrace' := _}, otel_attributes:map(A1))
501+
502+
after
503+
1000 ->
504+
ct:fail(timeout)
505+
end,
506+
507+
%% EXIT
508+
?assertException(exit, shutdown, otel_tracer:with_span(Tracer, <<"span-exit">>, #{record_exception => true},
509+
fun(_SpanCtx) ->
510+
erlang:exit(shutdown)
511+
end)),
512+
513+
receive
514+
{span, SpanExit} ->
515+
?assertEqual(<<"span-exit">>, SpanExit#span.name),
516+
?assertEqual(undefined, SpanExit#span.status),
517+
[#event{name=exception, attributes=A2}] = otel_events:list(SpanExit#span.events),
518+
?assertMatch(#{'exception.type' := <<"exit:shutdown">>, 'exception.stacktrace' := _}, otel_attributes:map(A2))
519+
520+
after
521+
1000 ->
522+
ct:fail(timeout)
523+
end,
524+
525+
%% broken elixir exception
526+
Exception = #{'__exception__' => true, '__struct__' => invalid},
527+
?assertException(exit, Exception, otel_tracer:with_span(Tracer, <<"span-elixir">>, #{record_exception => true},
528+
fun(_SpanCtx) ->
529+
erlang:exit(Exception)
530+
end)),
531+
532+
receive
533+
{span, SpanElixir} ->
534+
?assertEqual(<<"span-elixir">>, SpanElixir#span.name),
535+
?assertEqual(undefined, SpanElixir#span.status),
536+
[#event{name=exception, attributes=A3}] = otel_events:list(SpanElixir#span.events),
537+
?assertMatch(#{'exception.type' := <<"exit:#{'__exception__' => true,'__struct__' => invalid}">>, 'exception.stacktrace' := _}, otel_attributes:map(A3))
538+
539+
after
540+
1000 ->
541+
ct:fail(timeout)
542+
end,
543+
544+
ok.
545+
468546
child_spans(Config) ->
469547
Tid = ?config(tid, Config),
470548

apps/opentelemetry_api/src/otel_span.erl

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
is_valid/1,
2929
is_valid_name/1,
3030
validate_start_opts/1,
31+
validate_with_opts/1,
3132
set_attribute/3,
3233
set_attributes/2,
3334
add_event/3,
@@ -51,7 +52,15 @@
5152
start_time := opentelemetry:timestamp(),
5253
kind := opentelemetry:span_kind()}.
5354

54-
-export_type([start_opts/0]).
55+
-type with_opts() :: #{attributes => opentelemetry:attributes_map(),
56+
links => [opentelemetry:link()],
57+
is_recording => boolean(),
58+
start_time => opentelemetry:timestamp(),
59+
kind => opentelemetry:span_kind(),
60+
record_exception => boolean(),
61+
set_status_on_exception => boolean()}.
62+
63+
-export_type([start_opts/0, with_opts/0]).
5564

5665
-spec validate_start_opts(start_opts()) -> start_opts().
5766
validate_start_opts(Opts) when is_map(Opts) ->
@@ -68,6 +77,17 @@ validate_start_opts(Opts) when is_map(Opts) ->
6877
is_recording => IsRecording
6978
}.
7079

80+
-spec validate_with_opts(with_opts()) -> with_opts().
81+
validate_with_opts(Opts) when is_map(Opts) ->
82+
StartOpts = validate_start_opts(Opts),
83+
RecordException = maps:get(record_exception, Opts, false),
84+
SetStatusOnException = maps:get(set_status_on_exception, Opts, false),
85+
maps:merge(StartOpts, #{
86+
record_exception => RecordException,
87+
set_status_on_exception => SetStatusOnException
88+
}).
89+
90+
7191
-spec is_recording(SpanCtx) -> boolean() when
7292
SpanCtx :: opentelemetry:span_ctx().
7393
is_recording(SpanCtx) ->
@@ -201,11 +221,12 @@ add_events(_, _) ->
201221
record_exception(SpanCtx, Class, Term, Stacktrace, Attributes) when is_list(Attributes) ->
202222
record_exception(SpanCtx, Class, Term, Stacktrace, maps:from_list(Attributes));
203223
record_exception(SpanCtx, Class, Term, Stacktrace, Attributes) when is_map(Attributes) ->
204-
{ok, ExceptionType} = otel_utils:format_binary_string("~0tP:~0tP", [Class, 10, Term, 10], [{chars_limit, 50}]),
224+
ExceptionType = exception_type(Class, Term),
205225
{ok, StacktraceString} = otel_utils:format_binary_string("~0tP", [Stacktrace, 10], [{chars_limit, 50}]),
206226
ExceptionAttributes = #{?EXCEPTION_TYPE => ExceptionType,
207227
?EXCEPTION_STACKTRACE => StacktraceString},
208-
add_event(SpanCtx, 'exception', maps:merge(ExceptionAttributes, Attributes));
228+
ExceptionAttributes1 = add_elixir_message(ExceptionAttributes, Term),
229+
add_event(SpanCtx, 'exception', maps:merge(ExceptionAttributes1, Attributes));
209230
record_exception(_, _, _, _, _) ->
210231
false.
211232

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

252+
exception_type(error, #{'__exception__' := true, '__struct__' := ElixirErrorStruct} = Term) ->
253+
case atom_to_binary(ElixirErrorStruct) of
254+
<<"Elixir.", ExceptionType/binary>> -> ExceptionType;
255+
_ -> exception_type_erl(error, Term)
256+
end;
257+
exception_type(Class, Term) ->
258+
exception_type_erl(Class, Term).
259+
exception_type_erl(Class, Term) ->
260+
{ok, ExceptionType} = otel_utils:format_binary_string("~0tP:~0tP", [Class, 10, Term, 10], [{chars_limit, 50}]),
261+
ExceptionType.
262+
263+
add_elixir_message(Attributes, #{'__exception__' := true} = Exception) ->
264+
try
265+
Message = 'Elixir.Exception':message(Exception),
266+
maps:put(?EXCEPTION_MESSAGE, Message, Attributes)
267+
catch
268+
_Class:_Exception ->
269+
Attributes
270+
end;
271+
add_elixir_message(Attributes, _) ->
272+
Attributes.
273+
231274
-spec set_status(SpanCtx, StatusOrCode) -> boolean() when
232275
StatusOrCode :: opentelemetry:status() | undefined | opentelemetry:status_code(),
233276
SpanCtx :: opentelemetry:span_ctx().

apps/opentelemetry_api/src/otel_tracer.erl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,20 @@ start_span(Ctx, Tracer={Module, _}, SpanName, Opts) ->
6666
otel_tracer_noop:noop_span_ctx()
6767
end.
6868

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

78-
-spec with_span(otel_ctx:t(), opentelemetry:tracer(), opentelemetry:span_name(), otel_span:start_opts(), traced_fun(T)) -> T.
78+
-spec with_span(otel_ctx:t(), opentelemetry:tracer(), opentelemetry:span_name(), otel_span:with_opts(), traced_fun(T)) -> T.
7979
with_span(Ctx, Tracer={Module, _}, SpanName, Opts, Fun) when is_atom(Module) ->
8080
case otel_span:is_valid_name(SpanName) of
8181
true ->
82-
Module:with_span(Ctx, Tracer, SpanName, otel_span:validate_start_opts(Opts), Fun);
82+
Module:with_span(Ctx, Tracer, SpanName, otel_span:validate_with_opts(Opts), Fun);
8383
false ->
8484
Fun(otel_tracer_noop:noop_span_ctx())
8585
end.

rebar.config

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@
5858
opentelemetry_exporter_trace_service_pb,
5959
opentelemetry_exporter_metrics_service_pb,
6060
opentelemetry_exporter_logs_service_pb,
61-
opentelemetry_zipkin_pb]}.
62-
61+
opentelemetry_zipkin_pb,
62+
63+
{'Elixir.Exception', message, 1}]}.
6364

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

test/otel_tests.exs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ defmodule OtelTests do
99
@fields Record.extract(:span, from_lib: "opentelemetry/include/otel_span.hrl")
1010
Record.defrecordp(:span, @fields)
1111

12+
@fields Record.extract(:event, from_lib: "opentelemetry/include/otel_span.hrl")
13+
Record.defrecordp(:event, @fields)
14+
1215
@fields Record.extract(:tracer, from_lib: "opentelemetry/src/otel_tracer.hrl")
1316
Record.defrecordp(:tracer, @fields)
1417

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

21+
@fields Record.extract(:status, from_lib: "opentelemetry_api/include/opentelemetry.hrl")
22+
Record.defrecordp(:status, @fields)
23+
1824
setup do
1925
Application.load(:opentelemetry)
2026

@@ -308,4 +314,101 @@ defmodule OtelTests do
308314
opts
309315
)
310316
end
317+
318+
describe "Tracer.with_span record exception" do
319+
test "raise" do
320+
assert_raise RuntimeError, "my error message", fn ->
321+
Tracer.with_span "span-1", record_exception: true, set_status_on_exception: true do
322+
raise RuntimeError, "my error message"
323+
end
324+
end
325+
326+
assert_receive {:span,
327+
span(
328+
name: "span-1",
329+
events: {:events, _, _, _, _, [event]},
330+
status: status(code: :error)
331+
)}
332+
333+
assert event(name: :exception, attributes: {:attributes, _, _, _, received_attirbutes}) =
334+
event
335+
336+
assert %{
337+
"exception.type": "RuntimeError",
338+
"exception.message": "my error message",
339+
"exception.stacktrace": _
340+
} = received_attirbutes
341+
end
342+
343+
test ":erlang.error()" do
344+
assert_raise ArgumentError, fn ->
345+
Tracer.with_span "span-1", record_exception: true do
346+
:erlang.error(:badarg)
347+
end
348+
end
349+
350+
assert_receive {:span,
351+
span(
352+
name: "span-1",
353+
events: {:events, _, _, _, _, [event]},
354+
status: :undefined
355+
)}
356+
357+
assert event(name: :exception, attributes: {:attributes, _, _, _, received_attirbutes}) =
358+
event
359+
360+
assert %{
361+
"exception.type": "error:badarg",
362+
"exception.stacktrace": _
363+
} = received_attirbutes
364+
end
365+
366+
test "exit" do
367+
assert :shutdown ==
368+
catch_exit(
369+
Tracer.with_span "span-1", record_exception: true, set_status_on_exception: true do
370+
exit(:shutdown)
371+
end
372+
)
373+
374+
assert_receive {:span,
375+
span(
376+
name: "span-1",
377+
events: {:events, _, _, _, _, [event]},
378+
status: status(code: :error)
379+
)}
380+
381+
assert event(name: :exception, attributes: {:attributes, _, _, _, received_attirbutes}) =
382+
event
383+
384+
assert %{
385+
"exception.type": "exit:shutdown",
386+
"exception.stacktrace": _
387+
} = received_attirbutes
388+
end
389+
390+
test "throw" do
391+
assert :value ==
392+
catch_throw(
393+
Tracer.with_span "span-1", record_exception: true do
394+
throw(:value)
395+
end
396+
)
397+
398+
assert_receive {:span,
399+
span(
400+
name: "span-1",
401+
events: {:events, _, _, _, _, [event]},
402+
status: :undefined
403+
)}
404+
405+
assert event(name: :exception, attributes: {:attributes, _, _, _, received_attirbutes}) =
406+
event
407+
408+
assert %{
409+
"exception.type": "throw:value",
410+
"exception.stacktrace": _
411+
} = received_attirbutes
412+
end
413+
end
311414
end

0 commit comments

Comments
 (0)