diff --git a/examples/README.md b/examples/README.md index e97aa2a2..6aa4400f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 ``` diff --git a/examples/apps/absinthe_example/.formatter.exs b/examples/apps/absinthe_example/.formatter.exs new file mode 100644 index 00000000..90a37a25 --- /dev/null +++ b/examples/apps/absinthe_example/.formatter.exs @@ -0,0 +1,5 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + import_deps: [:absinthe, :plug] +] diff --git a/examples/apps/absinthe_example/.gitignore b/examples/apps/absinthe_example/.gitignore new file mode 100644 index 00000000..c54e29ad --- /dev/null +++ b/examples/apps/absinthe_example/.gitignore @@ -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 diff --git a/examples/apps/absinthe_example/README.md b/examples/apps/absinthe_example/README.md new file mode 100644 index 00000000..f6cee29d --- /dev/null +++ b/examples/apps/absinthe_example/README.md @@ -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. diff --git a/examples/apps/absinthe_example/config/config.exs b/examples/apps/absinthe_example/config/config.exs new file mode 100644 index 00000000..7a7127f1 --- /dev/null +++ b/examples/apps/absinthe_example/config/config.exs @@ -0,0 +1,4 @@ +import Config + +config :absinthe_example, + http_port: 4006 diff --git a/examples/apps/absinthe_example/lib/application.ex b/examples/apps/absinthe_example/lib/application.ex new file mode 100644 index 00000000..a0897dc3 --- /dev/null +++ b/examples/apps/absinthe_example/lib/application.ex @@ -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 diff --git a/examples/apps/absinthe_example/lib/resolvers.ex b/examples/apps/absinthe_example/lib/resolvers.ex new file mode 100644 index 00000000..067f998f --- /dev/null +++ b/examples/apps/absinthe_example/lib/resolvers.ex @@ -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 diff --git a/examples/apps/absinthe_example/lib/router.ex b/examples/apps/absinthe_example/lib/router.ex new file mode 100644 index 00000000..d0f1af04 --- /dev/null +++ b/examples/apps/absinthe_example/lib/router.ex @@ -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 diff --git a/examples/apps/absinthe_example/lib/schema.ex b/examples/apps/absinthe_example/lib/schema.ex new file mode 100644 index 00000000..e0ef4b01 --- /dev/null +++ b/examples/apps/absinthe_example/lib/schema.ex @@ -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 diff --git a/examples/apps/absinthe_example/mix.exs b/examples/apps/absinthe_example/mix.exs new file mode 100644 index 00000000..464d09dc --- /dev/null +++ b/examples/apps/absinthe_example/mix.exs @@ -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 diff --git a/examples/apps/absinthe_example/test/absinthe_example_test.exs b/examples/apps/absinthe_example/test/absinthe_example_test.exs new file mode 100644 index 00000000..cf9dd3d3 --- /dev/null +++ b/examples/apps/absinthe_example/test/absinthe_example_test.exs @@ -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 diff --git a/examples/apps/absinthe_example/test/test_helper.exs b/examples/apps/absinthe_example/test/test_helper.exs new file mode 100644 index 00000000..869559e7 --- /dev/null +++ b/examples/apps/absinthe_example/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/examples/apps/test_support/lib/test_support.ex b/examples/apps/test_support/lib/test_support.ex index b4dea2be..057ce722 100644 --- a/examples/apps/test_support/lib/test_support.ex +++ b/examples/apps/test_support/lib/test_support.ex @@ -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() @@ -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 @@ -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) diff --git a/examples/mix.lock b/examples/mix.lock index aab6ad74..1c33ec98 100644 --- a/examples/mix.lock +++ b/examples/mix.lock @@ -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"}, @@ -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"}, diff --git a/lib/new_relic.ex b/lib/new_relic.ex index a894415d..602cff5c 100644 --- a/lib/new_relic.ex +++ b/lib/new_relic.ex @@ -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 @@ -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` """ diff --git a/lib/new_relic/config.ex b/lib/new_relic/config.ex index a60e57aa..6cda3ca2 100644 --- a/lib/new_relic/config.ex +++ b/lib/new_relic/config.ex @@ -143,6 +143,8 @@ defmodule NewRelic.Config do * Controls all Ecto instrumentation * `:redix_instrumentation_enabled` (default `true`) * Controls all Redix instrumentation + * `:absinthe_instrumentation_enabled` (default `true`) + * Controls all Absinthe instrumentation * `:request_queuing_metrics_enabled` * Controls collection of request queuing metrics @@ -183,6 +185,10 @@ defmodule NewRelic.Config do get(:features, :redix_instrumentation) end + def feature?(:absinthe_instrumentation) do + get(:features, :absinthe_instrumentation) + end + def feature?(:function_argument_collection) do get(:features, :function_argument_collection) end diff --git a/lib/new_relic/init.ex b/lib/new_relic/init.ex index 6f1a618e..2530f0f5 100644 --- a/lib/new_relic/init.ex +++ b/lib/new_relic/init.ex @@ -87,6 +87,11 @@ defmodule NewRelic.Init do "NEW_RELIC_REDIX_INSTRUMENTATION_ENABLED", :redix_instrumentation_enabled ), + absinthe_instrumentation: + determine_feature( + "NEW_RELIC_ABSINTHE_INSTRUMENTATION_ENABLED", + :absinthe_instrumentation_enabled + ), plug_instrumentation: determine_feature( "NEW_RELIC_PLUG_INSTRUMENTATION_ENABLED", diff --git a/lib/new_relic/telemetry/absinthe.ex b/lib/new_relic/telemetry/absinthe.ex new file mode 100644 index 00000000..e1827a77 --- /dev/null +++ b/lib/new_relic/telemetry/absinthe.ex @@ -0,0 +1,124 @@ +defmodule NewRelic.Telemetry.Absinthe do + use GenServer + + @moduledoc """ + Provides `Absinthe` instrumentation via `telemetry` + + We automatically gather: + + * Transaction name + * Distributed Trace span events + + You can opt-out of this instrumentation as a whole and specifically of + query collection via configuration. See `NewRelic.Config` for details. + """ + + alias NewRelic.Telemetry.Absinthe.Metadata + + @doc false + def start_link(_) do + enabled = NewRelic.Config.feature?(:absinthe_instrumentation) + GenServer.start_link(__MODULE__, [enabled: enabled], name: __MODULE__) + end + + @operation_start [:absinthe, :execute, :operation, :start] + @operation_stop [:absinthe, :execute, :operation, :stop] + @resolve_field_start [:absinthe, :resolve, :field, :start] + @resolve_field_stop [:absinthe, :resolve, :field, :stop] + + @events [ + @operation_start, + @operation_stop, + @resolve_field_start, + @resolve_field_stop + ] + + @doc false + def init(enabled: false), do: :ignore + + def init(enabled: true) do + config = %{ + handler_id: {:new_relic, :absinthe}, + collect_query?: NewRelic.Config.feature?(:db_query_collection) + } + + :telemetry.attach_many( + config.handler_id, + @events, + &__MODULE__.handle_event/4, + config + ) + + Process.flag(:trap_exit, true) + {:ok, config} + end + + @doc false + def terminate(_reason, %{handler_id: handler_id}) do + :telemetry.detach(handler_id) + end + + @doc false + def handle_event(@operation_start, meas, meta, config) do + query = read_query(meta.options[:document], collect: config.collect_query?) + + NewRelic.Tracer.Direct.start_span( + meta.id, + "Operation", + start_time: meas.system_time, + attributes: [ + "absinthe.query": query + ] + ) + + NewRelic.add_attributes("absinthe.query": query) + + NewRelic.incr_attributes("absinthe.operation.count": 1) + end + + def handle_event(@operation_stop, meas, meta, _config) do + operation = apply(Absinthe.Blueprint, :current_operation, [meta.blueprint]) + + NewRelic.Tracer.Direct.stop_span( + meta.id, + name: Metadata.operation_span_name(meta.blueprint.execution.result.emitter), + duration: meas.duration, + attributes: [ + "absinthe.operation.type": operation.type, + "absinthe.operation.name": operation.name + ] + ) + + NewRelic.add_attributes( + framework_name: Metadata.transaction_name(meta.blueprint.schema, operation), + "absinthe.operation.type": operation.type, + "absinthe.operation.name": operation.name + ) + end + + def handle_event(@resolve_field_start, meas, meta, _config) do + path = apply(Absinthe.Resolution, :path, [meta.resolution]) |> Enum.join(".") + type = "#{meta.resolution.definition.parent_type.name}.#{meta.resolution.definition.name}" + resolver_name = Metadata.resolver_name(meta.resolution.middleware) + + NewRelic.Tracer.Direct.start_span( + meta.id, + {resolver_name, path}, + start_time: meas.system_time, + attributes: [ + "absinthe.field.path": path, + "absinthe.field.type": type + ] + ) + end + + def handle_event(@resolve_field_stop, meas, meta, _config) do + NewRelic.Tracer.Direct.stop_span( + meta.id, + duration: meas.duration + ) + end + + defp read_query(query, collect: true), do: query + defp read_query(_query, collect: false), do: "[NOT_COLLECTED]" +end diff --git a/lib/new_relic/telemetry/absinthe/metadata.ex b/lib/new_relic/telemetry/absinthe/metadata.ex new file mode 100644 index 00000000..c2490d2f --- /dev/null +++ b/lib/new_relic/telemetry/absinthe/metadata.ex @@ -0,0 +1,64 @@ +defmodule NewRelic.Telemetry.Absinthe.Metadata do + def resolver_name(middleware) do + Enum.find_value(middleware, fn + {{Absinthe.Resolution, :call}, resolver_fn} -> + info = Function.info(resolver_fn) + + case Keyword.get(info, :type) do + :external -> inspect(resolver_fn) + :local -> "&#{inspect(info[:module])}.anonymous/#{info[:arity]}" + end + + {{middleware, :call}, _options} -> + inspect({middleware, :call}) + + _ -> + nil + end) + end + + def operation_span_name(%{type: type, name: name}) when is_binary(name) do + "#{to_string(type)}:#{name}" + end + + def operation_span_name(%{type: type}) do + "#{to_string(type)}" + end + + def transaction_name(schema, operation) do + "Absinthe/#{inspect(schema)}/#{operation.type}/" <> + Enum.join(collect_deepest_path(operation), ".") + end + + def collect_deepest_path(%{type: :mutation, selections: [%{name: name} | _]}) do + [name] + end + + def collect_deepest_path(%{type: :subscription, selections: [%{name: name} | _]}) do + [name] + end + + def collect_deepest_path(%{type: :query, selections: [selection]}) do + collect_deepest_path(selection, []) + end + + def collect_deepest_path(%{type: :query}) do + [] + end + + def collect_deepest_path(%{selections: selections, name: name}, acc) do + selections + |> Enum.reject(fn + %{name: "__typename"} -> true + _ -> false + end) + |> case do + [selection] -> collect_deepest_path(selection, acc ++ [name]) + _ -> acc ++ [name] + end + end + + def collect_deepest_path(_, acc) do + acc + end +end diff --git a/lib/new_relic/telemetry/supervisor.ex b/lib/new_relic/telemetry/supervisor.ex index b3a0d417..2afcc7ae 100644 --- a/lib/new_relic/telemetry/supervisor.ex +++ b/lib/new_relic/telemetry/supervisor.ex @@ -12,7 +12,8 @@ defmodule NewRelic.Telemetry.Supervisor do NewRelic.Telemetry.Ecto.Supervisor, NewRelic.Telemetry.Redix, NewRelic.Telemetry.Plug, - NewRelic.Telemetry.Phoenix + NewRelic.Telemetry.Phoenix, + NewRelic.Telemetry.Absinthe ] Supervisor.init(children, strategy: :one_for_one) diff --git a/lib/new_relic/tracer/direct.ex b/lib/new_relic/tracer/direct.ex new file mode 100644 index 00000000..b7f4b643 --- /dev/null +++ b/lib/new_relic/tracer/direct.ex @@ -0,0 +1,95 @@ +defmodule NewRelic.Tracer.Direct do + @moduledoc false + + @max_open_span_count Application.compile_env(:new_relic, :max_open_span_count, 20) + + def start_span(id, name, options) do + case Process.get(:nr_open_span_count, 0) do + over when over >= @max_open_span_count -> + Process.put(:nr_open_span_count, over + 1) + NewRelic.incr_attributes(skipped_span_count: 1) + + under -> + Process.put(:nr_open_span_count, under + 1) + + start_time = Keyword.get(options, :start_time, System.system_time()) + attributes = Keyword.get(options, :attributes, []) |> Map.new() + + {span, previous_span, previous_span_attrs} = + NewRelic.DistributedTrace.set_current_span( + label: id, + ref: make_ref() + ) + + Process.put( + {:nr_span, id}, + {name, start_time, attributes, {span, previous_span, previous_span_attrs}} + ) + end + + :ok + end + + def stop_span(id, options) do + case Process.get({:nr_span, id}) do + nil -> + :no_such_span + + {name, start_time, start_attributes, {span, previous_span, previous_span_attrs}} -> + Process.put(:nr_open_span_count, Process.get(:nr_open_span_count) - 1) + + {duration, duration_s} = + case Keyword.get(options, :duration) do + nil -> + duration = System.system_time() - start_time + {duration, System.convert_time_unit(duration, :native, :millisecond) / 1_000} + + duration -> + {duration, System.convert_time_unit(duration, :native, :millisecond) / 1_000} + end + + name = Keyword.get(options, :name, name) + stop_attributes = Keyword.get(options, :attributes, []) |> Map.new() + attributes = Map.merge(start_attributes, stop_attributes) + timestamp_ms = System.convert_time_unit(start_time, :native, :millisecond) + + {primary_name, secondary_name} = + case name do + {primary_name, secondary_name} -> {primary_name, secondary_name} + name -> {name, ""} + end + + NewRelic.Transaction.Reporter.add_trace_segment(%{ + primary_name: primary_name, + secondary_name: secondary_name, + attributes: attributes, + pid: self(), + id: span, + parent_id: previous_span || :root, + system_time: start_time, + duration: duration + }) + + NewRelic.report_span( + timestamp_ms: timestamp_ms, + duration_s: duration_s, + name: primary_name, + edge: [span: span, parent: previous_span || :root], + category: "generic", + attributes: attributes + ) + + IO.inspect( + {:report_span, + %{name: name, attributes: attributes, span: span, previous_span: previous_span}} + ) + + NewRelic.DistributedTrace.reset_span( + previous_span: previous_span, + previous_span_attrs: previous_span_attrs + ) + end + + :ok + end +end diff --git a/mix.exs b/mix.exs index 73f8294f..1e0cac1b 100644 --- a/mix.exs +++ b/mix.exs @@ -51,7 +51,8 @@ defmodule NewRelic.Mixfile do {:phoenix, ">= 1.5.5", optional: true}, {:ecto_sql, ">= 3.4.0", optional: true}, {:ecto, ">= 3.9.5", optional: true}, - {:redix, ">= 0.11.0", optional: true} + {:redix, ">= 0.11.0", optional: true}, + {:absinthe, ">= 1.6.0", optional: true} ] end diff --git a/mix.lock b/mix.lock index e4c29ac4..1ce1f46a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,30 +1,32 @@ %{ - "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, - "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, + "absinthe": {:hex, :absinthe, "1.7.8", "43443d12ad2b4fcce60e257ac71caf3081f3d5c4ddd5eac63a02628bcaf5b556", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c4085df201892a498384f997649aedb37a4ce8a726c170d5b5617ed3bf45d40b"}, + "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, + "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, - "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, - "ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"}, - "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, - "ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, + "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, + "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "phoenix": {:hex, :phoenix, "1.7.9", "9a2b873e2cb3955efdd18ad050f1818af097fa3f5fc3a6aaba666da36bdd3f02", [: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", "83e32da028272b4bfd076c61a964e6d2b9d988378df2f1276a0ed21b13b5e997"}, + "phoenix": {:hex, :phoenix, "1.7.18", "5310c21443514be44ed93c422e15870aef254cf1b3619e4f91538e7529d2b2e4", [: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.7", [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", "1797fcc82108442a66f2c77a643a62980f342bfeb63d6c9a515ab8294870004e"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, - "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, - "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, - "redix": {:hex, :redix, "1.3.0", "f4121163ff9d73bf72157539ff23b13e38422284520bb58c05e014b19d6f0577", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "60d483d320c77329c8cbd3df73007e51b23f3fae75b7693bc31120d83ab26131"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "redix": {:hex, :redix, "1.5.2", "ab854435a663f01ce7b7847f42f5da067eea7a3a10c0a9d560fa52038fd7ab48", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "78538d184231a5d6912f20567d76a49d1be7d3fca0e1aaaa20f4df8e1142dcb8"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, }