diff --git a/.gitignore b/.gitignore index 6ff06dc..39ec585 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ open_telemetry_decorator-*.tar # Intellij nonsense .idea/ -*.iml \ No newline at end of file +*.iml +.history/ diff --git a/.tool-versions b/.tool-versions index 60f0fd0..6efbcf8 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.14.4 -erlang 25.3.1 \ No newline at end of file +elixir 1.15.7-otp-25 +erlang 25.3.2.7 diff --git a/lib/open_telemetry_decorator.ex b/lib/open_telemetry_decorator.ex index e48ac51..2eae002 100644 --- a/lib/open_telemetry_decorator.ex +++ b/lib/open_telemetry_decorator.ex @@ -40,13 +40,16 @@ defmodule OpenTelemetryDecorator do """ def with_span(span_name, opts \\ [], body, context) do include = Keyword.get(opts, :include, []) + kind = get_kind(opts) + decorator_attributes = Keyword.get(opts, :attributes, []) + Validator.validate_args(span_name, include) quote location: :keep do require OpenTelemetry.Tracer, as: Tracer require OpenTelemetry.Span, as: Span - Tracer.with_span unquote(span_name) do + Tracer.with_span unquote(span_name), %{kind: unquote(kind)} do span_context = Tracer.current_span_ctx() input_params = @@ -68,6 +71,7 @@ defmodule OpenTelemetryDecorator do # Called functions can mess up Tracer's current span context, so ensure we at least write to ours Attributes.set(span_context, attrs) + Attributes.set(span_context, unquote(decorator_attributes)) result rescue @@ -83,4 +87,11 @@ defmodule OpenTelemetryDecorator do target = "#{inspect(context.module)}.#{context.name}/#{context.arity} @decorate telemetry" reraise %ArgumentError{message: "#{target} #{e.message}"}, __STACKTRACE__ end + + def get_kind(opts) do + case Keyword.get(opts, :kind, :internal) do + kind when kind in [:internal, :server, :client, :producer, :consumer] -> kind + _ -> :internal + end + end end diff --git a/test/open_telemetry_decorator/attributes_test.exs b/test/open_telemetry_decorator/attributes_test.exs index d3eb269..e6c2ba9 100644 --- a/test/open_telemetry_decorator/attributes_test.exs +++ b/test/open_telemetry_decorator/attributes_test.exs @@ -1,5 +1,5 @@ defmodule OpenTelemetryDecorator.AttributesTest do - use ExUnit.Case, async: true + use ExUnit.Case, async: false use OtelHelper alias OpenTelemetryDecorator.Attributes diff --git a/test/open_telemetry_decorator_test.exs b/test/open_telemetry_decorator_test.exs index 1a5ef08..02e95f6 100644 --- a/test/open_telemetry_decorator_test.exs +++ b/test/open_telemetry_decorator_test.exs @@ -1,5 +1,5 @@ defmodule OpenTelemetryDecoratorTest do - use ExUnit.Case, async: true + use ExUnit.Case, async: false use OtelHelper doctest OpenTelemetryDecorator @@ -63,6 +63,21 @@ defmodule OpenTelemetryDecoratorTest do @decorate with_span("Example.with_error") def with_error, do: OpenTelemetryDecorator.Attributes.set(:error, "ruh roh!") + + @decorate with_span("Example.with_attributes", attributes: [foo: "bar", baz: "qux"]) + def with_attributes, do: :ok + + @decorate with_span("Example.with_attrs_and_include", + attributes: [foo: "bar", baz: "qux"], + include: [:opts] + ) + def with_attrs_and_include(opts), do: {:ok, opts} + + @decorate with_span("Example.with_attrs_and_conflicts", + attributes: [foo: "bar"], + include: [:foo] + ) + def with_attrs_and_conflicts(foo), do: {:ok, foo} end test "does not modify inputs or function result" do @@ -239,5 +254,80 @@ defmodule OpenTelemetryDecoratorTest do expected = %{"error" => "ruh roh!"} assert get_span_attributes(attrs) == expected end + + test "can set the span.kind on the span" do + defmodule SpanKinds do + use OpenTelemetryDecorator + + @decorate with_span("SpanKinds.producer", kind: :producer) + def producer do + :ok + end + + @decorate with_span("SpanKinds.consumer", kind: :consumer) + def consumer do + :ok + end + + @decorate with_span("SpanKinds.internal", kind: :internal) + def internal do + :ok + end + + @decorate with_span("SpanKinds.client", kind: :client) + def client do + :ok + end + + @decorate with_span("SpanKinds.server", kind: :server) + def server do + :ok + end + + @decorate with_span("SpanKinds.invalid", kind: :invalid) + def invalid do + :ok + end + end + + SpanKinds.producer() + assert_receive {:span, span(name: "SpanKinds.producer", kind: :producer)} + + SpanKinds.consumer() + assert_receive {:span, span(name: "SpanKinds.consumer", kind: :consumer)} + + SpanKinds.client() + assert_receive {:span, span(name: "SpanKinds.client", kind: :client)} + + SpanKinds.server() + assert_receive {:span, span(name: "SpanKinds.server", kind: :server)} + + SpanKinds.internal() + assert_receive {:span, span(name: "SpanKinds.internal", kind: :internal)} + + # using an invalid span.kind will default to :internal + SpanKinds.invalid() + assert_receive {:span, span(name: "SpanKinds.invalid", kind: :internal)} + end + + test "can set attributes on the span" do + Example.with_attributes() + assert_receive {:span, span(name: "Example.with_attributes", attributes: attrs)} + assert %{"app.baz" => "qux", "app.foo" => "bar"} == get_span_attributes(attrs) + end + + test "can set attributes and input params on the span" do + Example.with_attrs_and_include(:include_me) + assert_receive {:span, span(name: "Example.with_attrs_and_include", attributes: attrs)} + + assert %{"app.baz" => "qux", "app.foo" => "bar", "app.opts" => ":include_me"} == + get_span_attributes(attrs) + end + + test "can set attributes and input params on the span, where attributes win with conflicting names" do + Example.with_attrs_and_conflicts("not_bar") + assert_receive {:span, span(name: "Example.with_attrs_and_conflicts", attributes: attrs)} + assert %{"app.foo" => "bar"} == get_span_attributes(attrs) + end end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..6b8ecd6 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,3 @@ +Application.stop(:opentelemetry) +Application.unload(:opentelemetry) ExUnit.start()