Skip to content

Commit

Permalink
add a behaviour around metadata plugins to give integrators an option…
Browse files Browse the repository at this point in the history
… to add to the telemetry metadata
  • Loading branch information
matt-hobbs-prima committed May 23, 2024
1 parent 189fe09 commit 661ed3b
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 19 deletions.
31 changes: 13 additions & 18 deletions lib/instrumentation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule OpentelemetryAbsinthe.Instrumentation do
code, it just won't do anything.)
"""
alias Absinthe.Blueprint
alias OpenTelemetryAbsinthe.StandardMetadataPlugin

require OpenTelemetry.Tracer, as: Tracer
require OpenTelemetry.SemanticConventions.Trace, as: Conventions
Expand Down Expand Up @@ -53,7 +54,8 @@ defmodule OpentelemetryAbsinthe.Instrumentation do
trace_request_selections: true,
trace_response_result: false,
trace_response_errors: false,
trace_subscriptions: false
trace_subscriptions: false,
metadata_plugins: []
]

def setup(instrumentation_opts \\ []) do
Expand Down Expand Up @@ -127,8 +129,8 @@ defmodule OpentelemetryAbsinthe.Instrumentation do
end

def handle_stop(_event_name, measurements, data, config) do
operation_type = get_operation_type(data)
operation_name = get_operation_name(data)
operation_type = StandardMetadataPlugin.get_operation_type(data.blueprint)
operation_name = StandardMetadataPlugin.get_operation_name(data.blueprint)

operation_type
|> span_name(operation_name, config.span_name)
Expand All @@ -150,16 +152,17 @@ defmodule OpentelemetryAbsinthe.Instrumentation do
|> List.insert_at(0, {:"graphql.event.type", config.type})
|> Tracer.set_attributes()

plugins = [StandardMetadataPlugin | config.metadata_plugins]

metadata =
Enum.reduce(plugins, %{}, fn plugin, metadata ->
Map.merge(metadata, plugin.metadata(data.blueprint))
end)

telemetry_provider().execute(
[:opentelemetry_absinthe, :graphql, :handled],
measurements,
%{
operation_name: operation_name,
operation_type: operation_type,
schema: data.blueprint.schema,
errors: errors,
status: status
}
metadata
)

Tracer.end_span()
Expand All @@ -181,14 +184,6 @@ defmodule OpentelemetryAbsinthe.Instrumentation do
@default_config
end

defp get_operation_type(%{blueprint: %Blueprint{} = blueprint}) do
blueprint |> Absinthe.Blueprint.current_operation() |> Kernel.||(%{}) |> Map.get(:type)
end

defp get_operation_name(%{blueprint: %Blueprint{} = blueprint}) do
blueprint |> Absinthe.Blueprint.current_operation() |> Kernel.||(%{}) |> Map.get(:name)
end

defp span_name(_, _, name) when is_binary(name), do: name
defp span_name(nil, _, _), do: @default_operation_span
defp span_name(op_type, nil, _), do: Atom.to_string(op_type)
Expand Down
23 changes: 23 additions & 0 deletions lib/metadata_plugin.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule OpenTelemetryAbsinthe.MetadataPlugin do
@moduledoc """
A MetadataPlugin is used to allow library integrators to add their own
metadata to the broadcasted telemetry events.
Note: plugins are run after `OpenTelemetryAbsinthe.MetadataPlugin.StandardMetadata`
so they should avoid the keys:
```
operation_name
operation_type
schema
errors
status
```
"""
alias Absinthe.Blueprint

@type metadata :: %{
atom() => any()
}

@callback metadata(Blueprint.t()) :: metadata()
end
48 changes: 48 additions & 0 deletions lib/standard_metadata_plugin.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
defmodule OpenTelemetryAbsinthe.StandardMetadataPlugin do
@moduledoc """
An implementation of `OpenTelemetryAbsinthe.MetadataPlugin` behaviour that
returns standard metadata:
```
operation_name
operation_type
schema
errors
status
```
"""
@behaviour OpenTelemetryAbsinthe.MetadataPlugin

alias Absinthe.Blueprint

@impl OpenTelemetryAbsinthe.MetadataPlugin
def metadata(%Blueprint{} = blueprint) do
operation_type = get_operation_type(blueprint)
operation_name = get_operation_name(blueprint)

errors = blueprint.result[:errors]
status = status(errors)

%{
operation_name: operation_name,
operation_type: operation_type,
schema: blueprint.schema,
errors: errors,
status: status
}
end

defp status(nil), do: :ok
defp status([]), do: :ok
defp status(_error), do: :error

@spec get_operation_type(Absinthe.Blueprint.t()) :: any()
def get_operation_type(%Blueprint{} = blueprint) do
blueprint |> Absinthe.Blueprint.current_operation() |> Kernel.||(%{}) |> Map.get(:type)
end

@spec get_operation_name(Absinthe.Blueprint.t()) :: any()
def get_operation_name(%Blueprint{} = blueprint) do
blueprint |> Absinthe.Blueprint.current_operation() |> Kernel.||(%{}) |> Map.get(:name)
end
end
87 changes: 86 additions & 1 deletion test/instrumentation_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule OpentelemetryAbsintheTest.Instrumentation do
defmodule OpentelemetryAbsintheTest.InstrumentationTest do
use OpentelemetryAbsintheTest.Case

alias OpentelemetryAbsinthe.Instrumentation
alias OpentelemetryAbsintheTest.Support.GraphQL.Queries
alias OpentelemetryAbsintheTest.Support.Query

Expand Down Expand Up @@ -36,4 +37,88 @@ defmodule OpentelemetryAbsintheTest.Instrumentation do

assert @trace_attributes = attrs |> Map.keys() |> Enum.sort()
end

describe "metadata plugins" do
setup do
config =
Instrumentation.default_config()
|> Keyword.put(:type, :operation)
|> Enum.into(%{})

%{config: config}
end

test "standard values are returned when no plugins specified", 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 those from plugins", 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__, source: "TestSource")},
Map.put(ctx.config, :metadata_plugins, [__MODULE__.TestMetadataPlugin])
)

assert_receive %{
operation_name: "TestOperation",
operation_type: :query,
schema: __MODULE__,
errors: nil,
status: :ok,
plugin_name: __MODULE__.TestMetadataPlugin,
source: "TestSource"
},
10
end
end
end

defmodule OpentelemetryAbsintheTest.InstrumentationTest.TestMetadataPlugin do
@behaviour OpenTelemetryAbsinthe.MetadataPlugin

alias Absinthe.Blueprint

def metadata(%Blueprint{} = blueprint) do
%{
plugin_name: __MODULE__,
source: blueprint.source
}
end
end
76 changes: 76 additions & 0 deletions test/standard_metadata_plugin_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
defmodule OpenTelemetryAbsinthe.StandardMetadataPluginTest do
use ExUnit.Case

alias OpenTelemetryAbsinthe.StandardMetadataPlugin

test "should include operation name" do
assert %{operation_name: "FindByUUID"} =
[operations: [BlueprintArchitect.operation(name: "FindByUUID")]]
|> BlueprintArchitect.blueprint()
|> StandardMetadataPlugin.metadata()
end

test "should include operation type" do
assert %{operation_type: :mutation} =
[operations: [BlueprintArchitect.operation(type: :mutation)]]
|> BlueprintArchitect.blueprint()
|> StandardMetadataPlugin.metadata()
end

test "should include schema" do
assert %{schema: __MODULE__} =
[schema: __MODULE__]
|> BlueprintArchitect.blueprint()
|> StandardMetadataPlugin.metadata()
end

test "should include nil errors" do
assert %{errors: nil} =
[result: %{}]
|> BlueprintArchitect.blueprint()
|> StandardMetadataPlugin.metadata()

assert %{errors: nil} =
[result: %{errors: nil}]
|> BlueprintArchitect.blueprint()
|> StandardMetadataPlugin.metadata()
end

test "should include empty errors" do
assert %{errors: []} =
[result: %{errors: []}]
|> BlueprintArchitect.blueprint()
|> StandardMetadataPlugin.metadata()
end

test "should include errors" do
assert %{errors: [:stuff_went_wrong, :wrong_number]} =
[result: %{errors: [:stuff_went_wrong, :wrong_number]}]
|> BlueprintArchitect.blueprint()
|> StandardMetadataPlugin.metadata()
end

test "should include ok status" do
assert %{status: :ok} =
[result: %{}]
|> BlueprintArchitect.blueprint()
|> StandardMetadataPlugin.metadata()

assert %{status: :ok} =
[result: %{errors: nil}]
|> BlueprintArchitect.blueprint()
|> StandardMetadataPlugin.metadata()

assert %{status: :ok} =
[result: %{errors: []}]
|> BlueprintArchitect.blueprint()
|> StandardMetadataPlugin.metadata()
end

test "should include error status when there are errors" do
assert %{status: :error} =
[result: %{errors: [:stuff_went_wrong, :wrong_number]}]
|> BlueprintArchitect.blueprint()
|> StandardMetadataPlugin.metadata()
end
end
25 changes: 25 additions & 0 deletions test/support/blueprint_architect.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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
end

0 comments on commit 661ed3b

Please sign in to comment.