Skip to content

Commit

Permalink
Absinthe instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
binaryseed committed Dec 31, 2024
1 parent d84ad55 commit 7625f6b
Show file tree
Hide file tree
Showing 23 changed files with 617 additions and 41 deletions.
16 changes: 8 additions & 8 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ Create the app

```
cd apps
mix new example
mix new --sup example
```

Point to the agent

```elixir
defp deps do
[
{:new_relic_agent, path: "../../../"},
{:test_support, in_umbrella: true},
# ...
]
end
defp deps do
[
{:new_relic_agent, path: "../../../"},
{:test_support, in_umbrella: true},
# ...
]
end
```
5 changes: 5 additions & 0 deletions examples/apps/absinthe_example/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
import_deps: [:absinthe, :plug]
]
27 changes: 27 additions & 0 deletions examples/apps/absinthe_example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
absinthe_example-*.tar


# Temporary files for e.g. tests
/tmp
5 changes: 5 additions & 0 deletions examples/apps/absinthe_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Absinthe

An example of an `Absinthe` GraphQL API instrumented by the New Relic Agent via `telemetry`.

This instrumentation is fully automatic.
4 changes: 4 additions & 0 deletions examples/apps/absinthe_example/config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Config

config :absinthe_example,
http_port: 4006
19 changes: 19 additions & 0 deletions examples/apps/absinthe_example/lib/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule AbsintheExample.Application do
@moduledoc false
use Application

def start(_type, _args) do
http_port = Application.get_env(:absinthe_example, :http_port)

children = [
Plug.Cowboy.child_spec(
scheme: :http,
plug: AbsintheExample.Router,
options: [port: http_port]
)
]

opts = [strategy: :one_for_one, name: AbsintheExample.Supervisor]
Supervisor.start_link(children, opts)
end
end
26 changes: 26 additions & 0 deletions examples/apps/absinthe_example/lib/resolvers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule AbsintheExample.Resolvers do
use NewRelic.Tracer

def echo(_source, %{this: this}, _res) do
{:ok, do_echo(this)}
end

@trace :do_echo
defp do_echo(this), do: this

def one(_source, _args, _res) do
Process.sleep(1)
{:ok, %{two: %{}}}
end

def three(_source, _args, _res) do
Process.sleep(2)
{:ok, do_three()}
end

@trace :do_three
def do_three() do
Process.sleep(2)
3
end
end
15 changes: 15 additions & 0 deletions examples/apps/absinthe_example/lib/router.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule AbsintheExample.Router do
use Plug.Builder
use Plug.ErrorHandler

plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
pass: ["*/*"],
json_decoder: Jason

plug Absinthe.Plug, schema: AbsintheExample.Schema

def handle_errors(conn, error) do
send_resp(conn, conn.status, "Something went wrong: #{inspect(error)}")
end
end
24 changes: 24 additions & 0 deletions examples/apps/absinthe_example/lib/schema.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule AbsintheExample.Schema do
use Absinthe.Schema

query do
field :echo, :string do
arg :this, :string
resolve &AbsintheExample.Resolvers.echo/3
end

field :one, :one_thing do
resolve &AbsintheExample.Resolvers.one/3
end
end

object :one_thing do
field :two, :two_thing
end

object :two_thing do
field :three, :integer do
resolve &AbsintheExample.Resolvers.three/3
end
end
end
34 changes: 34 additions & 0 deletions examples/apps/absinthe_example/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule AbsintheExample.MixProject do
use Mix.Project

def project do
[
app: :absinthe_example,
version: "0.1.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> 1.9",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

def application do
[
extra_applications: [:logger],
mod: {AbsintheExample.Application, []}
]
end

defp deps do
[
{:new_relic_agent, path: "../../../"},
{:test_support, in_umbrella: true},
{:absinthe, "~> 1.6"},
{:absinthe_plug, "~> 1.5"},
{:plug_cowboy, "~> 2.0"}
]
end
end
95 changes: 95 additions & 0 deletions examples/apps/absinthe_example/test/absinthe_example_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
defmodule AbsintheExampleTest do
use ExUnit.Case

alias NewRelic.Harvest.TelemetrySdk
alias NewRelic.Harvest.Collector

setup_all context, do: TestSupport.simulate_agent_run(context, trace_mode: :infinite)
setup_all context, do: TestSupport.simulate_agent_enabled(context)

test "Absinthe instrumentation" do
TestSupport.restart_harvest_cycle(Collector.Metric.HarvestCycle)
TestSupport.restart_harvest_cycle(TelemetrySdk.Spans.HarvestCycle)

{:ok, %{body: _body}} = request("query TestQuery { one { two { three } } }")

metrics = TestSupport.gather_harvest(Collector.Metric.Harvester)

assert TestSupport.find_metric(metrics, "WebTransaction")

assert TestSupport.find_metric(
metrics,
"WebTransactionTotalTime/Absinthe/AbsintheExample.Schema/query/one.two.three"
)

[%{spans: spans}] = TestSupport.gather_harvest(TelemetrySdk.Spans.Harvester)

spansaction =
Enum.find(spans, fn %{attributes: attr} ->
attr[:name] == "Absinthe/AbsintheExample.Schema/query/one.two.three"
end)

tx_root_process =
Enum.find(spans, fn %{attributes: attr} ->
attr[:name] == "Transaction Root Process"
end)

process =
Enum.find(spans, fn %{attributes: attr} ->
attr[:name] == "Process"
end)

operation =
Enum.find(spans, fn %{attributes: attr} ->
attr[:name] == "query:TestQuery"
end)

one_resolver =
Enum.find(spans, fn %{attributes: attr} ->
attr[:name] == "&AbsintheExample.Resolvers.one/3"
end)

three_resolver =
Enum.find(spans, fn %{attributes: attr} ->
attr[:name] == "&AbsintheExample.Resolvers.three/3"
end)

do_three_fn_trace =
Enum.find(spans, fn %{attributes: attr} ->
attr[:name] == "AbsintheExample.Resolvers.do_three/0"
end)

assert operation.attributes[:"absinthe.operation.name"] == "TestQuery"
assert operation.attributes[:"absinthe.operation.type"] == "query"

assert spansaction.attributes[:"absinthe.operation.name"] == "TestQuery"
assert spansaction.attributes[:"absinthe.operation.type"] == "query"

assert one_resolver.attributes[:"absinthe.field.path"] == "one"
assert three_resolver.attributes[:"absinthe.field.path"] == "one.two.three"

assert one_resolver.attributes[:"parent.id"] == operation.id
assert three_resolver.attributes[:"parent.id"] == operation.id
assert do_three_fn_trace.attributes[:"parent.id"] == three_resolver.id
assert operation.attributes[:"parent.id"] == process.id
assert process.attributes[:"parent.id"] == tx_root_process.id
assert tx_root_process.attributes[:"parent.id"] == spansaction.id
assert spansaction.attributes[:"nr.entryPoint"] == true

Enum.each(spans, fn span ->
assert span[:"trace.id"] == spansaction[:"trace.id"]
assert span.attributes[:transactionId] == spansaction.attributes[:transactionId]
end)
end

defp request(query) do
http_port = Application.get_env(:absinthe_example, :http_port)
body = Jason.encode!(%{query: query})
request = {'http://localhost:#{http_port}/graphql', [], 'application/json', body}

with {:ok, {{_, status_code, _}, _headers, body}} <-
:httpc.request(:post, request, [], []) do
{:ok, %{status_code: status_code, body: to_string(body)}}
end
end
end
1 change: 1 addition & 0 deletions examples/apps/absinthe_example/test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()
18 changes: 7 additions & 11 deletions examples/apps/test_support/lib/test_support.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
defmodule TestSupport do
# def trigger_report(module) do
# Process.sleep(300)
# GenServer.call(module, :report)
# end

def gather_harvest(harvester) do
Process.sleep(300)
harvester.gather_harvest()
Expand All @@ -14,10 +9,6 @@ defmodule TestSupport do
GenServer.call(harvest_cycle, :restart)
end

# def pause_harvest_cycle(harvest_cycle) do
# GenServer.call(harvest_cycle, :pause)
# end

def find_metric(metrics, name, call_count \\ 1)

def find_metric(metrics, {name, scope}, call_count) do
Expand All @@ -41,8 +32,13 @@ defmodule TestSupport do
:ok
end

def simulate_agent_run(_context) do
reset_config = update(:nr_config, license_key: "dummy_key", harvest_enabled: true)
def simulate_agent_run(_context, extra_config \\ []) do
reset_config =
update(
:nr_config,
Keyword.merge([license_key: "dummy_key", harvest_enabled: true], extra_config)
)

reset_agent_run = update(:nr_agent_run, trusted_account_key: "190")
send(NewRelic.DistributedTrace.BackoffSampler, :reset)

Expand Down
3 changes: 3 additions & 0 deletions examples/mix.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
%{
"absinthe": {:hex, :absinthe, "1.6.4", "d2958908b72ce146698de8ccbc03622630471eb0e354e06823aaef183e5067bd", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e9c1cf36d86c704cb9a9c78db62d1c2676b03e0f61a28a23fc42749e8cd41ae"},
"absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"},
"bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
"castore": {:hex, :castore, "0.1.9", "eb08a94c12ebff92a92d844c6ccd90728dc7662aab9bdc8b3b785ba653c499d5", [:mix], [], "hexpm", "99c3a38ad9c0bab03fee1418c98390da1a31f3b85e317db5840d51a1443d26c8"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"},
Expand All @@ -22,6 +24,7 @@
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"myxql": {:hex, :myxql, "0.6.4", "1502ea37ee23c31b79725b95d4cc3553693c2bda7421b1febc50722fd988c918", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a3307f4671f3009d3708283649adf205bfe280f7e036fc8ef7f16dbf821ab8e9"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"},
"phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},
Expand Down
26 changes: 25 additions & 1 deletion lib/new_relic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ defmodule NewRelic do
defdelegate stop_transaction(), to: NewRelic.OtherTransaction

@doc """
Define an "Other" transaction with the given block. The return value of
Record an "Other" transaction within the given block. The return value of
the block is returned.
See `start_transaction/2` and `stop_transaction/0` for more details about
Expand Down Expand Up @@ -207,6 +207,30 @@ defmodule NewRelic do
"""
defdelegate distributed_trace_headers(type), to: NewRelic.DistributedTrace

@type name :: binary() | {primary_name :: binary(), secondary_name :: binary()}

@doc """
Record a "Span" within the given block. The return value of the block is returned.
```elixir
NewRelic.span("do.some_work", user_id: "abc123") do
# do some work
end
```
Note: You can also use `@trace` annotations to instrument functions without modifying code.
"""
@spec span(name :: name, attributes :: Keyword.t()) :: term()
defmacro span(name, attributes \\ [], do: block) do
quote do
id = make_ref()
NewRelic.Tracer.Direct.start_span(id, name: unquote(name), attributes: unquote(attributes))
res = unquote(block)
NewRelic.Tracer.Direct.stop_span(id)
res
end
end

@doc """
See: `NewRelic.distributed_trace_headers/1`
"""
Expand Down
Loading

0 comments on commit 7625f6b

Please sign in to comment.