Skip to content

Instantly share code, notes, and snippets.

@percygrunwald
Last active January 5, 2021 00:12
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save percygrunwald/6dd1d9132855b0372785e75df4a26669 to your computer and use it in GitHub Desktop.
Save percygrunwald/6dd1d9132855b0372785e75df4a26669 to your computer and use it in GitHub Desktop.
Scout Absinthe (GraphQL) Instrumentation
defmodule ScoutApm.Absinthe.Plug do
alias ScoutApm.Internal.Layer
require Logger
@error_prefix "GraphQL query document could not be parsed"
@endpoint_prefix "GraphQL"
@default_action_name "unknown"
def init(default), do: default
def call(conn, _default) do
ScoutApm.TrackedRequest.start_layer("Controller", endpoint_name(conn))
conn
|> Plug.Conn.register_before_send(&before_send/1)
end
defp before_send(conn) do
endpoint_name = endpoint_name(conn)
uri = "#{conn.request_path}/#{action_name(conn)}"
ScoutApm.TrackedRequest.stop_layer(fn layer ->
layer
|> Layer.update_name(endpoint_name)
|> Layer.update_uri(uri)
end)
conn
end
defp endpoint_name(conn), do: "#{@endpoint_prefix}##{action_name(conn)}"
defp action_name(%{params: %{"operationName" => operation_name}} = _conn) when is_binary(operation_name) do
operation_name
end
defp action_name(%{params: %{"query" => query_bin}} = _conn) when is_binary(query_bin) do
query_chars = :erlang.binary_to_list(query_bin)
with \
{:ok, tokens, _line_count} <- :absinthe_lexer.string(query_chars),
{:ok, query_doc} <- :absinthe_parser.parse(tokens)
do
get_first_field_from_query_doc(query_doc)
else
{:error, raw_error} ->
log_raw_parse_error(raw_error)
@default_action_name
end
end
defp action_name(_conn), do: @default_action_name
defp get_first_field_from_query_doc(query_doc) do
query_doc
|> Map.get(:definitions, [%{}])
|> List.first()
|> Map.get(:selection_set, %{})
|> Map.get(:selections, [%{}])
|> List.first()
|> Map.get(:name, @default_action_name)
end
defp log_raw_parse_error({_line, :absinthe_parser, msgs}) do
message = msgs |> Enum.map(&to_string/1) |> Enum.join("")
Logger.error("#{@error_prefix}: #{message}")
end
defp log_raw_parse_error({_line, :absinthe_lexer, {problem, field}}) do
message = "#{problem}: #{field}"
Logger.error("#{@error_prefix}: #{message}")
end
defp log_raw_parse_error(_unknown), do: Logger.error("#{@error_prefix}: unknown error")
end
@cpursley
Copy link

cpursley commented Dec 11, 2018

For even more detail, I customized the context by adding the GraphQL query that was run for the endpoint/action by adding ScoutApm.Context.add("GraphQL Query", String.replace(query_bin, ~r/ +|\n/, " ")) to line 38 in the action_name function.

@ryancurtin
Copy link

ryancurtin commented Jan 5, 2021

For anyone who is using Absinthe 1.5.x (https://github.com/absinthe-graphql/absinthe/blob/37d72813e1d89b05413e6f58f4563f89a84c94f8/CHANGELOG.md#v150-alpha), :absinthe_lexer is no longer used. You'll have to make a slight modification to #action_name/2. Check out my fork, which has been updated to work with Absinthe 1.5.x: https://gist.github.com/ryancurtin/60c6e147a346f9155befa82ff1a1b199

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment