diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c349c4..9ffcd5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [2.3.1] - 2024-05-24 + +### Added + +- Support custom metadata from integrators. Use `OpentelemetryAbsinthe.TelemetryMetadata` to add metadata to your context which will then be broadcast. + ## [2.3.0-rc.0] - 2024-04-18 ### Added diff --git a/lib/instrumentation.ex b/lib/instrumentation.ex index 1d00c27..6c88dfa 100644 --- a/lib/instrumentation.ex +++ b/lib/instrumentation.ex @@ -9,6 +9,7 @@ defmodule OpentelemetryAbsinthe.Instrumentation do code, it just won't do anything.) """ alias Absinthe.Blueprint + alias OpentelemetryAbsinthe.TelemetryMetadata require OpenTelemetry.Tracer, as: Tracer require OpenTelemetry.SemanticConventions.Trace, as: Conventions @@ -16,11 +17,12 @@ defmodule OpentelemetryAbsinthe.Instrumentation do require Record @type graphql_handled_event_metadata :: %{ - operation_name: String.t() | nil, - operation_type: :query | :mutation, - schema: Absinthe.Schema.t(), - errors: [graphql_handled_event_error()] | nil, - status: :ok | :error + required(:operation_name) => String.t() | nil, + required(:operation_type) => :query | :mutation, + required(:schema) => Absinthe.Schema.t(), + required(:errors) => [graphql_handled_event_error()] | nil, + required(:status) => :ok | :error, + optional(atom()) => any() } @type graphql_handled_event_error :: %{ @@ -153,13 +155,16 @@ defmodule OpentelemetryAbsinthe.Instrumentation do telemetry_provider().execute( [:opentelemetry_absinthe, :graphql, :handled], measurements, - %{ - operation_name: operation_name, - operation_type: operation_type, - schema: data.blueprint.schema, - errors: errors, - status: status - } + Map.merge( + %{ + operation_name: operation_name, + operation_type: operation_type, + schema: data.blueprint.schema, + errors: errors, + status: status + }, + TelemetryMetadata.from_context(data.blueprint.execution.context) + ) ) Tracer.end_span() diff --git a/lib/telemetry_metadata.ex b/lib/telemetry_metadata.ex new file mode 100644 index 0000000..b279024 --- /dev/null +++ b/lib/telemetry_metadata.ex @@ -0,0 +1,20 @@ +defmodule OpentelemetryAbsinthe.TelemetryMetadata do + @moduledoc """ + A helper module to allow integrators to add custom data to their context + which will then be added to the [:opentelemetry_absinthe, :graphql, :handled] + event + """ + @key __MODULE__ + + @type absinthe_context :: map() + @type telemetry_metadata :: %{ + optional(atom()) => any() + } + + @spec update_context(absinthe_context(), telemetry_metadata()) :: absinthe_context() + def update_context(%{} = context, %{} = metadata), + do: Map.update(context, @key, metadata, &Map.merge(&1, metadata)) + + @spec from_context(absinthe_context()) :: telemetry_metadata() + def from_context(%{} = context), do: Map.get(context, @key, %{}) +end diff --git a/mix.exs b/mix.exs index a1a75ba..aefd7ab 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.3.0-rc.0" + @version "2.3.1" def project do [ diff --git a/test/instrumentation_test.exs b/test/instrumentation_test.exs index e450411..602e2ff 100644 --- a/test/instrumentation_test.exs +++ b/test/instrumentation_test.exs @@ -1,6 +1,8 @@ -defmodule OpentelemetryAbsintheTest.Instrumentation do +defmodule OpentelemetryAbsintheTest.InstrumentationTest do use OpentelemetryAbsintheTest.Case + alias OpentelemetryAbsinthe.Instrumentation + alias OpentelemetryAbsinthe.TelemetryMetadata alias OpentelemetryAbsintheTest.Support.GraphQL.Queries alias OpentelemetryAbsintheTest.Support.Query @@ -36,4 +38,74 @@ defmodule OpentelemetryAbsintheTest.Instrumentation do assert @trace_attributes = attrs |> Map.keys() |> Enum.sort() end + + describe "handles metadata" do + setup do + config = + Instrumentation.default_config() + |> Keyword.put(:type, :operation) + |> Enum.into(%{}) + + %{config: config} + end + + test "standard values are returned when no metadata in context", ctx do + assert :ok = + :telemetry.attach( + ctx.test, + [:opentelemetry_absinthe, :graphql, :handled], + fn _telemetry_event, _measurements, metadata, _config -> + send(self(), metadata) + end, + nil + ) + + assert :ok = + Instrumentation.handle_stop( + "Test", + %{}, + %{blueprint: BlueprintArchitect.blueprint(schema: __MODULE__)}, + ctx.config + ) + + assert_receive %{ + operation_name: "TestOperation", + operation_type: :query, + schema: __MODULE__, + errors: nil, + status: :ok + }, + 10 + end + + test "standard values are returned alongside the metadata from context", ctx do + context = TelemetryMetadata.update_context(%{}, %{source: "TestSource", user_agent: "Insomnia"}) + + blueprint = + BlueprintArchitect.blueprint(schema: __MODULE__, execution: BlueprintArchitect.execution(context: context)) + + assert :ok = + :telemetry.attach( + ctx.test, + [:opentelemetry_absinthe, :graphql, :handled], + fn _telemetry_event, _measurements, metadata, _config -> + send(self(), metadata) + end, + nil + ) + + assert :ok = Instrumentation.handle_stop("Test", %{}, %{blueprint: blueprint}, ctx.config) + + assert_receive %{ + operation_name: "TestOperation", + operation_type: :query, + schema: __MODULE__, + errors: nil, + status: :ok, + user_agent: "Insomnia", + source: "TestSource" + }, + 10 + end + end end diff --git a/test/support/blueprint_architect.ex b/test/support/blueprint_architect.ex new file mode 100644 index 0000000..3b1b016 --- /dev/null +++ b/test/support/blueprint_architect.ex @@ -0,0 +1,32 @@ +defmodule BlueprintArchitect do + @moduledoc false + + alias Absinthe.Blueprint + + @spec blueprint(keyword()) :: Blueprint.t() + def blueprint(overrides \\ []) do + %{ + operations: [operation()] + } + |> Map.merge(Enum.into(overrides, %{})) + |> then(&struct!(Blueprint, &1)) + end + + @spec operation(keyword()) :: Blueprint.Document.Operation.t() + def operation(overrides \\ []) do + %{ + name: "TestOperation", + type: :query, + current: true + } + |> Map.merge(Enum.into(overrides, %{})) + |> then(&struct!(Blueprint.Document.Operation, &1)) + end + + @spec execution(keyword()) :: Blueprint.Execution.t() + def execution(overrides \\ []) do + %{} + |> Map.merge(Enum.into(overrides, %{})) + |> then(&struct!(Blueprint.Execution, &1)) + end +end diff --git a/test/telemetry_metadata_test.exs b/test/telemetry_metadata_test.exs new file mode 100644 index 0000000..d70099a --- /dev/null +++ b/test/telemetry_metadata_test.exs @@ -0,0 +1,20 @@ +defmodule OpentelemetryAbsinthe.TelemetryMetadataTest do + use ExUnit.Case + + alias OpentelemetryAbsinthe.TelemetryMetadata + + test "should return an empty metadata when context is empty" do + assert %{} == TelemetryMetadata.from_context(%{}) + end + + test "should return an empty metadata when context does not contain metadata" do + assert %{} == TelemetryMetadata.from_context(%{foo: :bar}) + end + + test "should return same metadata that was stored" do + assert %{user_agent: :test} == + %{} + |> TelemetryMetadata.update_context(%{user_agent: :test}) + |> TelemetryMetadata.from_context() + end +end