diff --git a/.versions.yml b/.versions.yml new file mode 100644 index 0000000..6a55d9a --- /dev/null +++ b/.versions.yml @@ -0,0 +1,3 @@ +files: + - path: mix.exs + pattern: '@version \"{version}\"' diff --git a/CHANGELOG.md b/CHANGELOG.md index c3d3c45..cf48e75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +--- + +## [2.2.2-rc.0] - 2024-04-18 + +### Added + +- Allow attaching to `[:absinthe, :subscription, :publish]` (both `:start` and `:stop`) given a `trace_subscription: true` config +- New `graphql.event.type` trace attribute, with value `operation` or `publish` + ## [2.2.1] - 2024-02-21 ### Changed @@ -64,7 +73,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 If you're upgrading to opentelemetry_absinthe 1.1.0, it is therefore recommended to also upgrade to OpenTelemetry API 1.1.0 in order to keep the opentelemetry log metadata. -[Unreleased]: https://github.com/primait/opentelemetry_absinthe/compare/2.0.1...HEAD + +[Unreleased]: https://github.com/primait/opentelemetry_absinthe/compare/2.2.2-rc.0...HEAD +[2.2.2-rc.0]: https://github.com/primait/opentelemetry_absinthe/compare/2.2.1...2.2.2-rc.0 [2.0.0]: https://github.com/primait/opentelemetry_absinthe/compare/2.0.0...2.0.1 [2.0.0]: https://github.com/primait/opentelemetry_absinthe/compare/1.1.0...2.0.0 [1.1.0]: https://github.com/primait/opentelemetry_absinthe/compare/1.0.0...1.1.0 diff --git a/lib/instrumentation.ex b/lib/instrumentation.ex index b219740..1d00c27 100644 --- a/lib/instrumentation.ex +++ b/lib/instrumentation.ex @@ -15,8 +15,6 @@ defmodule OpentelemetryAbsinthe.Instrumentation do require Logger require Record - @telemetry [:opentelemetry_absinthe, :graphql, :handled] - @type graphql_handled_event_metadata :: %{ operation_name: String.t() | nil, operation_type: :query | :mutation, @@ -54,7 +52,8 @@ defmodule OpentelemetryAbsinthe.Instrumentation do trace_request_variables: false, trace_request_selections: true, trace_response_result: false, - trace_response_errors: false + trace_response_errors: false, + trace_subscriptions: false ] def setup(instrumentation_opts \\ []) do @@ -68,27 +67,45 @@ defmodule OpentelemetryAbsinthe.Instrumentation do Logger.warning("The opentelemetry_absinthe span_name option is deprecated and will be removed in the future") end - :telemetry.attach( + telemetry_provider().attach( {__MODULE__, :operation_start}, [:absinthe, :execute, :operation, :start], - &__MODULE__.handle_operation_start/4, - config + &__MODULE__.handle_start/4, + Map.put(config, :type, :operation) ) - :telemetry.attach( + telemetry_provider().attach( {__MODULE__, :operation_stop}, [:absinthe, :execute, :operation, :stop], - &__MODULE__.handle_operation_stop/4, - config + &__MODULE__.handle_stop/4, + Map.put(config, :type, :operation) ) + + if config.trace_subscriptions do + telemetry_provider().attach( + {__MODULE__, :publish_start}, + [:absinthe, :subscription, :publish, :start], + &__MODULE__.handle_start/4, + Map.put(config, :type, :publish) + ) + + telemetry_provider().attach( + {__MODULE__, :publish_stop}, + [:absinthe, :subscription, :publish, :stop], + &__MODULE__.handle_stop/4, + Map.put(config, :type, :publish) + ) + end end def teardown do - :telemetry.detach({__MODULE__, :operation_start}) - :telemetry.detach({__MODULE__, :operation_stop}) + telemetry_provider().detach({__MODULE__, :operation_start}) + telemetry_provider().detach({__MODULE__, :operation_stop}) + telemetry_provider().detach({__MODULE__, :publish_start}) + telemetry_provider().detach({__MODULE__, :publish_stop}) end - def handle_operation_start(_event_name, _measurements, metadata, config) do + def handle_start(_event_name, _measurements, metadata, config) do document = metadata.blueprint.input variables = metadata |> Map.get(:options, []) |> Keyword.get(:variables, %{}) @@ -99,6 +116,7 @@ defmodule OpentelemetryAbsinthe.Instrumentation do {:"graphql.request.variables", Jason.encode!(variables)} ) |> put_if(config.trace_request_query, {@graphql_document, document}) + |> List.insert_at(0, {:"graphql.event.type", config.type}) save_parent_ctx() @@ -108,7 +126,7 @@ defmodule OpentelemetryAbsinthe.Instrumentation do Tracer.set_current_span(new_ctx) end - def handle_operation_stop(_event_name, measurements, data, config) do + def handle_stop(_event_name, measurements, data, config) do operation_type = get_operation_type(data) operation_name = get_operation_name(data) @@ -129,10 +147,11 @@ defmodule OpentelemetryAbsinthe.Instrumentation do config.trace_request_selections, fn -> {:"graphql.request.selections", data |> get_graphql_selections() |> Jason.encode!()} end ) + |> List.insert_at(0, {:"graphql.event.type", config.type}) |> Tracer.set_attributes() - :telemetry.execute( - @telemetry, + telemetry_provider().execute( + [:opentelemetry_absinthe, :graphql, :handled], measurements, %{ operation_name: operation_name, @@ -202,4 +221,6 @@ defmodule OpentelemetryAbsinthe.Instrumentation do defp set_status(:ok), do: :ok defp set_status(:error), do: Tracer.set_status(OpenTelemetry.status(:error, "")) + + defp telemetry_provider, do: Application.get_env(:opentelemetry_absinthe, :telemetry_provider, :telemetry) end diff --git a/lib/opentelemetry_absinthe.ex b/lib/opentelemetry_absinthe.ex index af500f5..69603f3 100644 --- a/lib/opentelemetry_absinthe.ex +++ b/lib/opentelemetry_absinthe.ex @@ -20,14 +20,16 @@ defmodule OpentelemetryAbsinthe do config :opentelemetry_absinthe, trace_options: [ trace_request_query: false, - trace_response_error: true + trace_response_error: true, + ... ] ``` configuration can also be passed directly to the setup function ``` OpentelemetryAbsinthe.setup( trace_request_query: false, - trace_response_error: true + trace_response_error: true, + ... ) ``` @@ -70,6 +72,7 @@ defmodule OpentelemetryAbsinthe do * `trace_response_result`(default: #{Keyword.fetch!(@config, :trace_response_result)}): attaches the result returned by the server as an attribute * `trace_response_errors`(default: #{Keyword.fetch!(@config, :trace_response_errors)}): attaches the errors returned by the server as an attribute + * `trace_subscriptions`(default: #{Keyword.fetch!(@config, :trace_subscriptions)}): attaches to `[:absinthe, :subscription, :publish]` (`:start` and `:stop`) ## Telemetry diff --git a/mix.exs b/mix.exs index 844da0c..b839f7b 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule OpentelemetryAbsinthe.MixProject do use Mix.Project @source_url "https://github.com/primait/opentelemetry_absinthe" - @version "2.2.1" + @version "2.2.2-rc.0" def project do [ diff --git a/test/configuration_test.exs b/test/configuration_test.exs index 4578729..1d80bf0 100644 --- a/test/configuration_test.exs +++ b/test/configuration_test.exs @@ -8,6 +8,7 @@ defmodule OpentelemetryAbsintheTest.Configuration do @graphql_operation_name :"graphql.operation.name" @graphql_operation_type :"graphql.operation.type" @graphql_request_selections :"graphql.request.selections" + @graphql_event_type :"graphql.event.type" doctest OpentelemetryAbsinthe.Instrumentation @@ -19,6 +20,7 @@ defmodule OpentelemetryAbsintheTest.Configuration do assert [ @graphql_document, + @graphql_event_type, @graphql_operation_name, @graphql_operation_type, @graphql_request_selections @@ -35,6 +37,7 @@ defmodule OpentelemetryAbsintheTest.Configuration do attributes = Query.query_for_attrs(Queries.query(), variables: %{"isbn" => "A1"}) assert [ + @graphql_event_type, @graphql_operation_name, @graphql_operation_type, @graphql_request_selections, @@ -53,6 +56,7 @@ defmodule OpentelemetryAbsintheTest.Configuration do assert [ @graphql_document, + @graphql_event_type, @graphql_operation_name, @graphql_operation_type ] = attributes |> Map.keys() |> Enum.sort() diff --git a/test/event_type_test.exs b/test/event_type_test.exs new file mode 100644 index 0000000..22419b8 --- /dev/null +++ b/test/event_type_test.exs @@ -0,0 +1,69 @@ +defmodule OpentelemetryAbsintheTest.EventTypeTest do + use OpentelemetryAbsintheTest.Case + + alias OpentelemetryAbsintheTest.Support.GraphQL.Queries + alias OpentelemetryAbsintheTest.Support.Query + + alias OpentelemetryAbsintheTest.EventTypeTest.TelemetryProvider + + @graphql_event_type :"graphql.event.type" + + doctest OpentelemetryAbsinthe.Instrumentation + + test "records operation on query" do + OpentelemetryAbsinthe.Instrumentation.setup() + + attributes = Query.query_for_attrs(Queries.query(), variables: %{"isbn" => "A1"}) + + assert @graphql_event_type in Map.keys(attributes) + assert :operation == Map.get(attributes, @graphql_event_type) + end + + describe "subscription" do + test "doesn't attach to subscription if not enabled" do + Application.put_env(:opentelemetry_absinthe, :telemetry_provider, TelemetryProvider) + + on_exit(fn -> + Application.put_env(:opentelemetry_absinthe, :telemetry_provider, :telemetry) + end) + + {:ok, _} = TelemetryProvider.start_link([]) + + OpentelemetryAbsinthe.Instrumentation.setup() + + # Attaches only to operation start/stop + assert 2 == TelemetryProvider.count() + end + + test "attaches to subscription if enabled" do + Application.put_env(:opentelemetry_absinthe, :telemetry_provider, TelemetryProvider) + + on_exit(fn -> + Application.put_env(:opentelemetry_absinthe, :telemetry_provider, :telemetry) + end) + + {:ok, _} = TelemetryProvider.start_link([]) + + OpentelemetryAbsinthe.Instrumentation.setup(trace_subscriptions: true) + + # Attaches also to subscription start/stop + assert 4 == TelemetryProvider.count() + end + end +end + +defmodule OpentelemetryAbsintheTest.EventTypeTest.TelemetryProvider do + use Agent + + @me __MODULE__ + + def start_link(_opts) do + Agent.start_link(fn -> 0 end, name: @me) + end + + def count, do: Agent.get(@me, & &1) + + defp increment, do: Agent.update(@me, &(&1 + 1)) + + def attach(_, _, _, _), do: increment() +end diff --git a/test/instrumentation_test.exs b/test/instrumentation_test.exs index 97193b7..e450411 100644 --- a/test/instrumentation_test.exs +++ b/test/instrumentation_test.exs @@ -14,6 +14,7 @@ defmodule OpentelemetryAbsintheTest.Instrumentation do @trace_attributes [ :"graphql.document", + :"graphql.event.type", :"graphql.operation.name", :"graphql.operation.type", :"graphql.request.selections", @@ -22,13 +23,11 @@ defmodule OpentelemetryAbsintheTest.Instrumentation do :"graphql.response.result" ] - describe "query" do - test "doesn't crash when empty" do - OpentelemetryAbsinthe.Instrumentation.setup(@capture_all) - attrs = Query.query_for_attrs(Queries.empty_query()) + test "doesn't crash when query is empty" do + OpentelemetryAbsinthe.Instrumentation.setup(@capture_all) + attrs = Query.query_for_attrs(Queries.empty_query()) - assert @trace_attributes = attrs |> Map.keys() |> Enum.sort() - end + assert @trace_attributes = attrs |> Map.keys() |> Enum.sort() end test "handles multiple queries properly" do