From e155b55a4f61119abb78480e64203bbd716eff22 Mon Sep 17 00:00:00 2001 From: Greg Mefford Date: Wed, 27 Nov 2024 15:26:22 -0500 Subject: [PATCH 1/6] inspect the Elixir exception struct --- apps/opentelemetry_api/lib/open_telemetry/span.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/opentelemetry_api/lib/open_telemetry/span.ex b/apps/opentelemetry_api/lib/open_telemetry/span.ex index 59bce947..a0a3f639 100644 --- a/apps/opentelemetry_api/lib/open_telemetry/span.ex +++ b/apps/opentelemetry_api/lib/open_telemetry/span.ex @@ -135,10 +135,8 @@ 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 = [ - {:"exception.type", exception_type}, + {:"exception.type", inspect(exception.__struct__)}, {:"exception.message", Exception.message(exception)}, {:"exception.stacktrace", Exception.format_stacktrace(trace)} ] From 14a1765160453157ea4432ee7169eb3f8a186f60 Mon Sep 17 00:00:00 2001 From: Greg Mefford Date: Wed, 27 Nov 2024 15:26:52 -0500 Subject: [PATCH 2/6] Add some testing infrastructure to make it possible to test this change --- apps/opentelemetry_api/mix.exs | 4 + .../src/otel_span_mailbox.erl | 76 +++++++++++++++++++ .../src/otel_tracer_test.erl | 71 +++++++++++++++++ .../test/open_telemetry_test.exs | 24 ++++-- 4 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 apps/opentelemetry_api/src/otel_span_mailbox.erl create mode 100644 apps/opentelemetry_api/src/otel_tracer_test.erl diff --git a/apps/opentelemetry_api/mix.exs b/apps/opentelemetry_api/mix.exs index 52f4e8f8..0e36d912 100644 --- a/apps/opentelemetry_api/mix.exs +++ b/apps/opentelemetry_api/mix.exs @@ -10,6 +10,7 @@ defmodule OpenTelemetry.MixProject do description: to_string(Keyword.fetch!(desc, :description)), elixir: "~> 1.8", start_permanent: Mix.env() == :prod, + elixirc_paths: elixirc_paths(Mix.env()), deps: [ {:eqwalizer_support, git: "https://github.com/whatsapp/eqwalizer.git", @@ -35,6 +36,9 @@ defmodule OpenTelemetry.MixProject do def application, do: [] + defp elixirc_paths(:test), do: ["test/support", "lib"] + defp elixirc_paths(_), do: ["lib"] + defp package() do [ description: "OpenTelemetry API", diff --git a/apps/opentelemetry_api/src/otel_span_mailbox.erl b/apps/opentelemetry_api/src/otel_span_mailbox.erl new file mode 100644 index 00000000..6ee6bd2b --- /dev/null +++ b/apps/opentelemetry_api/src/otel_span_mailbox.erl @@ -0,0 +1,76 @@ +%%%------------------------------------------------------------------------ +%% Copyright 2024, OpenTelemetry Authors +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% @doc +%% This module implements the `otel_span` API, but sends the data to the current +%% process's mailbox rather than storing it anywhere, so that it can be inspected +%% in tests. +%% @end +%%%------------------------------------------------------------------------- +-module(otel_span_mailbox). + +-export([ + end_span/1, + end_span/2, + set_attribute/3, + set_attributes/2, + add_event/3, + add_events/2, + set_status/2, + update_name/2]). + +-include("opentelemetry.hrl"). + +-spec end_span(opentelemetry:span_ctx() | undefined) -> boolean(). +end_span(SpanCtx) -> + self() ! {?FUNCTION_NAME, SpanCtx}, + true. + +-spec end_span(opentelemetry:span_ctx() | undefined, + integer() | undefined) -> boolean(). +end_span(SpanCtx, Timestamp) -> + self() ! {?FUNCTION_NAME, SpanCtx, Timestamp}, + true. + +-spec set_attribute(opentelemetry:span_ctx() | undefined, + opentelemetry:attribute_key(), + opentelemetry:attribute_value()) -> boolean(). +set_attribute(SpanCtx, Key, Value) -> + self() ! {?FUNCTION_NAME, SpanCtx, Key, Value}, + true. + +-spec set_attributes(opentelemetry:span_ctx() | undefined, opentelemetry:attributes_map()) -> boolean(). +set_attributes(SpanCtx, NewAttributes) -> + self() ! {?FUNCTION_NAME, SpanCtx, NewAttributes}, + true. + +-spec add_event(opentelemetry:span_ctx() | undefined, unicode:unicode_binary(), opentelemetry:attributes_map()) -> boolean(). +add_event(SpanCtx, Name, Attributes) -> + self() ! {?FUNCTION_NAME, SpanCtx, Name, Attributes}, + true. + +-spec add_events(opentelemetry:span_ctx() | undefined, [opentelemetry:event()]) -> boolean(). +add_events(SpanCtx, NewEvents) -> + self() ! {?FUNCTION_NAME, SpanCtx, NewEvents}, + true. + +-spec set_status(opentelemetry:span_ctx() | undefined, opentelemetry:status()) -> boolean(). +set_status(SpanCtx, Status) -> + self() ! {?FUNCTION_NAME, SpanCtx, Status}, + true. + +-spec update_name(opentelemetry:span_ctx() | undefined, opentelemetry:span_name()) -> boolean(). +update_name(SpanCtx, Name) -> + self() ! {?FUNCTION_NAME, SpanCtx, Name}, + true. diff --git a/apps/opentelemetry_api/src/otel_tracer_test.erl b/apps/opentelemetry_api/src/otel_tracer_test.erl new file mode 100644 index 00000000..9db741b2 --- /dev/null +++ b/apps/opentelemetry_api/src/otel_tracer_test.erl @@ -0,0 +1,71 @@ +%%%------------------------------------------------------------------------ +%% Copyright 2024, OpenTelemetry Authors +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% @doc +%% This module is used for testing the API by arranging for the calls that would +%% have been made to the SDK instead be send to the current process as +%% messages. This allows the test process to inspect and assert on them. +%% @end +%%%------------------------------------------------------------------------- +-module(otel_tracer_test). + +-behaviour(otel_tracer). + +-export([set_default/0, + start_span/4, + with_span/5, + end_span/2]). + +-include("opentelemetry.hrl"). +-include("otel_tracer.hrl"). + +%% @doc This helper acts as a default tracer provider that can be used in tests +%% to point to this tracer, either using Elixir or Erlang, since Elixir won't +%% have access to the necessary Erlang macro. +set_default() -> + opentelemetry:set_default_tracer(?GLOBAL_TRACER_PROVIDER_NAME, {?MODULE, []}), + ok. + +-spec start_span(otel_ctx:t(), opentelemetry:tracer(), opentelemetry:span_name(), + otel_span:start_config()) -> opentelemetry:span_ctx(). +start_span(Ctx, Tracer, Name, Opts) -> + self() ! {?FUNCTION_NAME, Ctx, Tracer, Name, Opts}, + #span_ctx{trace_id=rand:uniform(2 bsl 127 - 1), %% 2 shifted left by 127 == 2 ^ 128 + span_id=rand:uniform(2 bsl 63 - 1), %% 2 shifted left by 63 == 2 ^ 64 + trace_flags=0, + tracestate=otel_tracestate:new(), + is_valid=true, + is_recording=true, + %% The critical functionality here is to set the span_sdk to the + %% one that sends messages to the process mailbox. + span_sdk={otel_span_mailbox, []}}. + +-spec with_span(otel_ctx:t(), opentelemetry:tracer(), opentelemetry:span_name(), + otel_span:start_config(), otel_tracer:traced_fun(T)) -> T. +with_span(Ctx, Tracer, SpanName, Opts, Fun) -> + self() ! {?FUNCTION_NAME, Ctx, Tracer, SpanName, Opts, Fun}, + SpanCtx = start_span(Ctx, Tracer, SpanName, Opts), + Ctx1 = otel_tracer:set_current_span(Ctx, SpanCtx), + otel_ctx:attach(Ctx1), + try + Fun(SpanCtx) + after + otel_ctx:attach(Ctx) + end. + +-spec end_span(opentelemetry:tracer(), opentelemetry:span_ctx()) + -> boolean() | {error, term()}. +end_span(Tracer, Ctx) -> + self() ! {?FUNCTION_NAME, Tracer, Ctx}, + true. diff --git a/apps/opentelemetry_api/test/open_telemetry_test.exs b/apps/opentelemetry_api/test/open_telemetry_test.exs index fefa9b06..cd8e543a 100644 --- a/apps/opentelemetry_api/test/open_telemetry_test.exs +++ b/apps/opentelemetry_api/test/open_telemetry_test.exs @@ -7,11 +7,12 @@ defmodule OpenTelemetryTest do require OpenTelemetry.Ctx, as: Ctx require Record - @fields Record.extract(:span_ctx, from_lib: "opentelemetry_api/include/opentelemetry.hrl") - Record.defrecordp(:span_ctx, @fields) + Record.defrecordp(:span_ctx, Record.extract(:span_ctx, from_lib: "opentelemetry_api/include/opentelemetry.hrl")) + Record.defrecordp(:status, Record.extract(:status, from_lib: "opentelemetry_api/include/opentelemetry.hrl")) - @fields Record.extract(:status, from_lib: "opentelemetry_api/include/opentelemetry.hrl") - Record.defrecordp(:status, @fields) + setup_all do + :otel_tracer_test.set_default() + end test "current_span tracks last set_span" do span_ctx1 = Tracer.start_span("span-1") @@ -103,8 +104,8 @@ defmodule OpenTelemetryTest do Tracer.with_span "span-1" do span = Tracer.current_span_ctx() - assert Span.hex_trace_id(span) == "00000000000000000000000000000000" - assert Span.hex_span_id(span) == "0000000000000000" + assert Span.hex_trace_id(span) =~ ~r/[0-9a-f]{32}/ + assert Span.hex_span_id(span) =~ ~r/[0-9a-f]{16}/ end end @@ -144,4 +145,15 @@ defmodule OpenTelemetryTest do Ctx.detach(token) assert %{"a" => {"b", []}} = Baggage.get_all() end + + test "Span.record_exception" do + Tracer.with_span("exceptional span") do + Tracer.record_exception(%RuntimeError{message: "too awesome"}) + end + + assert_received {:add_event, _span_ctx, :exception, attributes} + assert %{"exception.type": "RuntimeError", "exception.stacktrace": stacktrace, "exception.message": "too awesome"} = attributes + assert is_binary(stacktrace) + assert String.contains?(stacktrace, "\n") + end end From 407d14627c534ba07f761753d621b2c871198c14 Mon Sep 17 00:00:00 2001 From: Greg Mefford Date: Wed, 27 Nov 2024 15:37:55 -0500 Subject: [PATCH 3/6] mix format --- .../test/open_telemetry_test.exs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/opentelemetry_api/test/open_telemetry_test.exs b/apps/opentelemetry_api/test/open_telemetry_test.exs index cd8e543a..8616230b 100644 --- a/apps/opentelemetry_api/test/open_telemetry_test.exs +++ b/apps/opentelemetry_api/test/open_telemetry_test.exs @@ -7,8 +7,10 @@ defmodule OpenTelemetryTest do require OpenTelemetry.Ctx, as: Ctx require Record - Record.defrecordp(:span_ctx, Record.extract(:span_ctx, from_lib: "opentelemetry_api/include/opentelemetry.hrl")) - Record.defrecordp(:status, Record.extract(:status, from_lib: "opentelemetry_api/include/opentelemetry.hrl")) + + @otel_include "opentelemetry_api/include/opentelemetry.hrl" + Record.defrecordp(:span_ctx, Record.extract(:span_ctx, from_lib: @otel_include)) + Record.defrecordp(:status, Record.extract(:status, from_lib: @otel_include)) setup_all do :otel_tracer_test.set_default() @@ -147,12 +149,18 @@ defmodule OpenTelemetryTest do end test "Span.record_exception" do - Tracer.with_span("exceptional span") do + Tracer.with_span "exceptional span" do Tracer.record_exception(%RuntimeError{message: "too awesome"}) end assert_received {:add_event, _span_ctx, :exception, attributes} - assert %{"exception.type": "RuntimeError", "exception.stacktrace": stacktrace, "exception.message": "too awesome"} = attributes + + assert %{ + "exception.type": "RuntimeError", + "exception.stacktrace": stacktrace, + "exception.message": "too awesome" + } = attributes + assert is_binary(stacktrace) assert String.contains?(stacktrace, "\n") end From 926e093d3b8e304dde3f654f1ba4346e1cf1d0da Mon Sep 17 00:00:00 2001 From: Greg Mefford Date: Wed, 27 Nov 2024 15:44:00 -0500 Subject: [PATCH 4/6] Simplify record_exception pattern-matching --- apps/opentelemetry_api/lib/open_telemetry/span.ex | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/opentelemetry_api/lib/open_telemetry/span.ex b/apps/opentelemetry_api/lib/open_telemetry/span.ex index a0a3f639..c2eed5af 100644 --- a/apps/opentelemetry_api/lib/open_telemetry/span.ex +++ b/apps/opentelemetry_api/lib/open_telemetry/span.ex @@ -120,12 +120,6 @@ 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. @@ -134,9 +128,9 @@ defmodule OpenTelemetry.Span do @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 + def record_exception(span_ctx, %type{__exception__: true} = exception, trace, attributes) do exception_attributes = [ - {:"exception.type", inspect(exception.__struct__)}, + {:"exception.type", inspect(type)}, {:"exception.message", Exception.message(exception)}, {:"exception.stacktrace", Exception.format_stacktrace(trace)} ] From 6e2b46c16c1e6f12860ae1ac90fdc05423549ce9 Mon Sep 17 00:00:00 2001 From: Greg Mefford Date: Wed, 27 Nov 2024 15:44:45 -0500 Subject: [PATCH 5/6] Fix typo --- apps/opentelemetry_api/lib/open_telemetry/span.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/opentelemetry_api/lib/open_telemetry/span.ex b/apps/opentelemetry_api/lib/open_telemetry/span.ex index c2eed5af..3bc8b11c 100644 --- a/apps/opentelemetry_api/lib/open_telemetry/span.ex +++ b/apps/opentelemetry_api/lib/open_telemetry/span.ex @@ -121,7 +121,7 @@ defmodule OpenTelemetry.Span do defdelegate add_events(span_ctx, events), to: :otel_span @doc """ - Record an exception as an event, following the semantics convetions for exceptions. + Record an exception as an event, following the semantic conventions for exceptions. If trace is not provided, the stacktrace is retrieved from `Process.info/2` """ From a56e1345bf6750d29abb90843ae9103ecf16c7ca Mon Sep 17 00:00:00 2001 From: Greg Mefford Date: Wed, 27 Nov 2024 16:00:38 -0500 Subject: [PATCH 6/6] Fix the top-level test --- test/otel_tests.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/otel_tests.exs b/test/otel_tests.exs index 677bda51..5aceb008 100644 --- a/test/otel_tests.exs +++ b/test/otel_tests.exs @@ -241,7 +241,7 @@ defmodule OtelTests do attributes = :otel_attributes.new( [ - {:"exception.type", "Elixir.RuntimeError"}, + {:"exception.type", "RuntimeError"}, {:"exception.message", "my error message"}, {:"exception.stacktrace", stacktrace} ],