├── .gitignore ├── README.md ├── config └── config.exs ├── lib ├── apollo_tracing.ex └── apollo_tracing │ ├── middleware │ ├── caching.ex │ └── tracing.ex │ ├── phase │ ├── accumulate_result.ex │ ├── add_cache_hints.ex │ ├── add_extension.ex │ └── create_tracing.ex │ ├── pipeline.ex │ └── schema.ex ├── mix.exs ├── mix.lock └── test ├── apollo_tracing_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApolloTracing (for Elixir) 2 | 3 | ApolloTracing adds data to your GraphQL query response so that an Apollo Engine can provide insights into your [Absinthe](http://absinthe-graphql.org)-based GraphQL service. 4 | 5 | ## Supported Apollo Features 6 | 7 | - [Performance Tracing](https://www.apollographql.com/docs/platform/performance/#traces) 8 | - [Response Caching](https://www.apollographql.com/docs/references/engine-proxy/#2-add-cache-hints-to-your-responses) 9 | 10 | ## Installation 11 | 12 | Add `:apollo_tracing` to your deps 13 | ```elixir 14 | def deps do 15 | [ 16 | {:apollo_tracing, "~> 0.4.0"} 17 | ] 18 | end 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### Register the Middlewares 24 | 25 | *ApolloTracing uses the Absinthe's middleware functionality to track field-level resolution times. In order to register our custom middleware, you have a few options:* 26 | 27 | **Add `use ApolloTracing` to your schema file:** 28 | 29 | ```elixir 30 | def MyApp.Schema do 31 | use Absinthe.Schema 32 | use ApolloTracing 33 | end 34 | ``` 35 | 36 | **If you have a custom middleware stack, add the apollo tracing middlewares to the beginning of your middleware stack:** 37 | 38 | ```elixir 39 | def middleware(middleware, _field, _object), 40 | do: [ApolloTracing.Middleware.Tracing, ApolloTracing.Middleware.Caching] ++ [...your other middlewares] 41 | ``` 42 | 43 | **If you prefer to only add tracing to some fields, you can selectively add tracing information:** 44 | 45 | ```elixir 46 | field :selected_field, :string do 47 | middleware ApolloTracing.Middleware # Has to be the first middleware 48 | resolve fn _, _ -> {:ok, "this field is now added to be traced"} end 49 | end 50 | ``` 51 | 52 | ### Register the Pipeline 53 | 54 | *ApolloTracing currently requires you to use a custom Pipeline in order to register 'Phases' in the correct order during resolution. Phases are used for measuring overall query times as well as appending the custom data to the response (including cache hints).* 55 | 56 | **Specify the pipeline in your Absinthe.Plug endpoint:** 57 | 58 | ```elixir 59 | forward "/graphql", Absinthe.Plug, 60 | schema: MyApp.Schema, 61 | pipeline: {ApolloTracing.Pipeline, :plug} 62 | ``` 63 | 64 | **If you have your own pipeline function, you can add the phases directly:** 65 | 66 | ```elixir 67 | def my_pipeline_creator(config, pipeline_opts) do 68 | config.schema_mod 69 | |> Absinthe.Pipeline.for_document(pipeline_opts) 70 | |> add_my_phases() # w.e your custom phases are 71 | |> ApolloTracing.Pipeline.add_phases() # Add apollo at the end 72 | end 73 | ``` 74 | 75 | **When you want to just call run a query with tracing, but without going through a Plug endpoint:** 76 | 77 | ```elixir 78 | def custom_absinthe_runner(query, opts \\ []) do 79 | pipeline = ApolloTracing.Pipeline.default(YourSchema, opts) 80 | case Absinthe.Pipeline.run(query, pipeline) do 81 | {:ok, %{result: result}, _} -> {:ok, result} 82 | {:error, err, _} -> {:ok, err} 83 | end 84 | end 85 | 86 | """ 87 | query { 88 | fielda 89 | fieldb 90 | } 91 | """ 92 | |> custom_absinthe_runner() 93 | ``` 94 | 95 | ### Add Cache Metadata 96 | 97 | **You can configure caching by adding metadata to your Absinthe objects:** 98 | 99 | ```elixir 100 | object :user do 101 | meta :cache, max_age: 30 102 | end 103 | 104 | # or 105 | 106 | object :user, meta: [cache: [max_age: 30]] do 107 | # ... 108 | end 109 | ``` 110 | 111 | **To ensure that the object is not cached across users, you can mark it as private:** 112 | 113 | ```elixir 114 | object :user do 115 | meta :cache, max_age: 30, scope: :private 116 | end 117 | ``` 118 | 119 | See the [Apollo docs](https://www.apollographql.com/docs/apollo-server/features/caching/#defining-cache-hints) for more information about cache scope. 120 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :apollo_tracing, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:apollo_tracing, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/apollo_tracing.ex: -------------------------------------------------------------------------------- 1 | defmodule ApolloTracing do 2 | @moduledoc """ 3 | Documentation for ApolloTracing. 4 | """ 5 | 6 | def version, do: 1 7 | 8 | defmacro __using__(_) do 9 | quote do 10 | def middleware(middleware, _, %{identifier: :subscription}), do: middleware 11 | def middleware(middleware, _, %{identifier: :mutation}), 12 | do: [ApolloTracing.Middleware.Tracing] ++ middleware 13 | def middleware(middleware, _, _), 14 | do: [ApolloTracing.Middleware.Tracing, ApolloTracing.Middleware.Caching] ++ middleware 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/apollo_tracing/middleware/caching.ex: -------------------------------------------------------------------------------- 1 | defmodule ApolloTracing.Middleware.Caching do 2 | @behaviour Absinthe.Middleware 3 | 4 | def call(res, _config) do 5 | path = 6 | res 7 | |> Absinthe.Resolution.path 8 | |> Enum.filter(&is_bitstring(&1)) 9 | 10 | hint = 11 | res.schema 12 | |> Absinthe.Schema.lookup_type(res.definition.schema_node.type) 13 | |> Absinthe.Type.meta() 14 | |> Map.get(:cache, []) 15 | |> Enum.into(%{}) 16 | |> put_in([:path], path) 17 | 18 | 19 | res.acc 20 | |> Map.get(:apollo_caching) 21 | |> case do 22 | %{hints: hints} -> 23 | put_in(res.acc.apollo_caching.hints, [hint | hints]) 24 | _ -> 25 | acc = 26 | res.acc 27 | |> Map.put(:apollo_caching, %{hints: [hint]}) 28 | %{res | acc: acc} 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/apollo_tracing/middleware/tracing.ex: -------------------------------------------------------------------------------- 1 | defmodule ApolloTracing.Middleware.Tracing do 2 | @behaviour Absinthe.Middleware 3 | 4 | alias ApolloTracing.Schema.Execution.Resolver 5 | alias Absinthe.Resolution 6 | 7 | # Called before resolving 8 | # if there isn't an `ApolloTracing` flag set then we aren't actually doing any tracing 9 | def call(%Resolution{acc: %{apollo_tracing_start_time: start_mono_time}, state: :unresolved} = res, _config) do 10 | now = System.monotonic_time() 11 | 12 | resolver = %Resolver{ 13 | path: Absinthe.Resolution.path(res), 14 | parentType: res.parent_type.name, 15 | fieldName: res.definition.name, 16 | returnType: Absinthe.Type.name(res.definition.schema_node.type, res.schema), 17 | startOffset: now - start_mono_time, 18 | } 19 | 20 | %{res | 21 | extensions: Map.put(res.extensions, __MODULE__, resolver), 22 | middleware: res.middleware ++ [{{__MODULE__, :after_field}, [start_time: now]}] 23 | } 24 | end 25 | 26 | def call(res, _) do 27 | res 28 | end 29 | 30 | # Called after each resolution to calculate the duration 31 | def after_field(%Resolution{state: :resolved} = res, [start_time: start_time]) do 32 | %{extensions: %{__MODULE__ => resolver}} = res 33 | 34 | updated_resolver = %Resolver{resolver | 35 | duration: System.monotonic_time() - start_time 36 | } |> Map.from_struct() 37 | 38 | update_in(res.acc.apollo_tracing.execution.resolvers, &[updated_resolver | &1]) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/apollo_tracing/phase/accumulate_result.ex: -------------------------------------------------------------------------------- 1 | defmodule ApolloTracer.Phase.AccumulateResult do 2 | use Absinthe.Phase 3 | 4 | def run(bp, _options \\ []) do 5 | %{apollo_tracing_start_time: start_mono_time, 6 | apollo_tracing: apollo_tracing} 7 | = bp.execution.acc 8 | final_tracing = %{bp.execution.acc.apollo_tracing | 9 | endTime: DateTime.utc_now() |> DateTime.to_iso8601(), 10 | duration: System.monotonic_time() - start_mono_time, 11 | execution: %{ 12 | resolvers: Enum.reverse(apollo_tracing.execution.resolvers) 13 | } 14 | } |> Map.from_struct() 15 | {:ok, put_in(bp.execution.acc.apollo_tracing, final_tracing)} 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/apollo_tracing/phase/add_cache_hints.ex: -------------------------------------------------------------------------------- 1 | defmodule ApolloTracer.Phase.AddCacheHints do 2 | use Absinthe.Phase 3 | 4 | def run(bp, _options \\ []) do 5 | case get_in(bp.execution.acc, [:apollo_caching, :hints]) do 6 | nil -> 7 | {:ok, bp} 8 | found -> 9 | {:ok, put_in(bp.execution.acc.apollo_caching, %{version: 1, hints: hints(found, [])})} 10 | end 11 | end 12 | 13 | defp hints([], result), do: result 14 | 15 | defp hints([%{max_age: max_age, path: path} = head | tail], result) do 16 | hint(max_age, Map.get(head, :scope), path) 17 | |> case do 18 | nil -> hints(tail, result) 19 | h -> hints(tail, [h | result]) 20 | end 21 | end 22 | defp hints([_ | tail], result), do: hints(tail, result) 23 | 24 | defp hint(nil, _, _), do: nil 25 | defp hint(max_age, :private, path), do: %{path: path, maxAge: max_age, scope: "PRIVATE"} 26 | defp hint(max_age, _, path), do: %{path: path, maxAge: max_age, scope: "PUBLIC"} 27 | end 28 | -------------------------------------------------------------------------------- /lib/apollo_tracing/phase/add_extension.ex: -------------------------------------------------------------------------------- 1 | defmodule ApolloTracer.Phase.AddExtension do 2 | use Absinthe.Phase 3 | 4 | def run(bp, _options \\ []) do 5 | extensions = Map.get(bp.result, :extensions, %{}) 6 | 7 | extensions = case Map.get(bp.execution.acc, :apollo_tracing) do 8 | nil -> extensions 9 | tracing -> 10 | Map.put(extensions, :tracing, tracing) 11 | end 12 | 13 | extensions = case Map.get(bp.execution.acc, :apollo_caching) do 14 | nil -> extensions 15 | cache -> 16 | Map.put(extensions, :cacheControl, cache) 17 | end 18 | 19 | result = Map.put(bp.result, :extensions, extensions) 20 | 21 | {:ok, %{bp | result: result}} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/apollo_tracing/phase/create_tracing.ex: -------------------------------------------------------------------------------- 1 | defmodule ApolloTracer.Phase.CreateTracing do 2 | use Absinthe.Phase 3 | 4 | def run(bp, _options \\ []) do 5 | tracing = %ApolloTracing.Schema{ 6 | version: ApolloTracing.version(), 7 | startTime: DateTime.utc_now() |> DateTime.to_iso8601(), 8 | endTime: nil, 9 | duration: nil, 10 | execution: %ApolloTracing.Schema.Execution{ 11 | resolvers: [] 12 | } 13 | } 14 | acc = 15 | bp.execution.acc 16 | |> Map.put(:apollo_tracing, tracing) 17 | |> Map.put(:apollo_tracing_start_time, System.monotonic_time()) 18 | {:ok, put_in(bp.execution.acc, acc)} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/apollo_tracing/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule ApolloTracing.Pipeline do 2 | def default(schema, pipeline_opts \\ []) do 3 | schema 4 | |> Absinthe.Pipeline.for_document(pipeline_opts) 5 | |> add_phases() 6 | end 7 | 8 | if Code.ensure_loaded?(Absinthe.Plug) do 9 | def plug(config, pipeline_opts \\ []) do 10 | config 11 | |> Absinthe.Plug.default_pipeline(pipeline_opts) 12 | |> add_phases() 13 | end 14 | else 15 | def plug(_, _ \\ []) do 16 | raise RuntimeError, """ 17 | You don't have Plug loaded, please use 18 | ApolloTracing.Pipeline.default(absinthe_schema, pipeline_opts) 19 | to produce a pipeline without Plug specific phases 20 | """ 21 | end 22 | end 23 | 24 | def add_phases(pipeline) do 25 | pipeline 26 | |> Absinthe.Pipeline.insert_after( 27 | Absinthe.Phase.Blueprint, 28 | ApolloTracer.Phase.CreateTracing 29 | ) 30 | |> Absinthe.Pipeline.insert_before( 31 | Absinthe.Phase.Document.Result, 32 | ApolloTracer.Phase.AccumulateResult 33 | ) 34 | |> Absinthe.Pipeline.insert_before( 35 | Absinthe.Phase.Document.Result, 36 | ApolloTracer.Phase.AddCacheHints 37 | ) 38 | |> Absinthe.Pipeline.insert_after( 39 | Absinthe.Phase.Document.Result, 40 | ApolloTracer.Phase.AddExtension 41 | ) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/apollo_tracing/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule ApolloTracing.Schema do 2 | @moduledoc """ 3 | Tracing Schema 4 | """ 5 | @derive Jason.Encoder 6 | defstruct [:version, 7 | :startTime, 8 | :endTime, 9 | :duration, 10 | :execution] 11 | 12 | @type t :: %__MODULE__{ 13 | version: pos_integer, 14 | startTime: DateTime.t, 15 | endTime: DateTime.t | nil, 16 | duration: pos_integer | nil, 17 | execution: __MODULE__.Execution.t 18 | } 19 | 20 | defmodule Execution do 21 | @derive Jason.Encoder 22 | defstruct [:resolvers] 23 | 24 | @type t :: %__MODULE__{ 25 | resolvers: [__MODULE__.Resolver.t] 26 | } 27 | 28 | defmodule Resolver do 29 | @derive Jason.Encoder 30 | defstruct [:path, 31 | :parentType, 32 | :fieldName, 33 | :returnType, 34 | :startOffset, 35 | :duration, 36 | :meta] 37 | 38 | @type path_elem :: String.t | integer 39 | 40 | @type t :: %__MODULE__{ 41 | path: [path_elem], 42 | parentType: String.t, 43 | fieldName: String.t, 44 | returnType: String.t, 45 | startOffset: pos_integer, 46 | duration: pos_integer, 47 | meta: KeywordList.t, 48 | } 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ApolloTracing.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.4.4" 5 | 6 | def project do 7 | [ 8 | app: :apollo_tracing, 9 | version: @version, 10 | elixir: "~> 1.5-rc", 11 | start_permanent: Mix.env() == :prod, 12 | package: package(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | defp package do 18 | [ 19 | description: "Apollo Tracing middleware for Absinthe", 20 | files: ["lib", "mix.exs", "README*"], 21 | maintainers: ["Sikanhe"], 22 | licenses: ["MIT"], 23 | links: %{github: "https://github.com/sikanhe/apollo-tracing-elixir"} 24 | ] 25 | end 26 | 27 | def application do 28 | [ 29 | extra_applications: [:logger] 30 | ] 31 | end 32 | 33 | defp deps do 34 | [ 35 | {:absinthe, "~> 1.4"}, 36 | {:absinthe_plug, "~> 1.4", optional: true}, 37 | {:jason, "~> 1.1", optional: true}, 38 | {:ex_doc, ">= 0.0.0", only: :dev} 39 | ] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "absinthe": {:hex, :absinthe, "1.4.13", "81eb2ff41f1b62cd6e992955f62c22c042d1079b7936c27f5f7c2c806b8fc436", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "ddb55fba830d5a25add2d707a81d11b40096bf6f2cd43b95e117610e7bb1df1a"}, 3 | "absinthe_plug": {:hex, :absinthe_plug, "1.4.6", "ac5d2d3d02acf52fda0f151b294017ab06e2ed1c6c15334e06aac82c94e36e08", [:mix], [{:absinthe, "~> 1.4.11", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "fd7656de35c5db29bff0ee90ac860cec197f2863f44b280486b586ff4d2974c5"}, 4 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, 5 | "ex_doc": {:hex, :ex_doc, "0.19.2", "6f4081ccd9ed081b6dc0bd5af97a41e87f5554de469e7d76025fba535180565f", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "4eae888633d2937e0a8839ae6002536d459c22976743c9dc98dd05941a06c016"}, 6 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, 7 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5fbc8e549aa9afeea2847c0769e3970537ed302f93a23ac612602e805d9d1e7f"}, 8 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "adf0218695e22caeda2820eaba703fa46c91820d53813a2223413da3ef4ba515"}, 9 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, 10 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm", "5c040b8469c1ff1b10093d3186e2e10dbe483cd73d79ec017993fb3985b8a9b3"}, 11 | "plug": {:hex, :plug, "1.7.1", "8516d565fb84a6a8b2ca722e74e2cd25ca0fc9d64f364ec9dbec09d33eb78ccd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm", "daa5fee4209c12c3c48b05a96cf88c320b627c9575f987554dcdc1fdcdf2c15e"}, 12 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm", "73c1682f0e414cfb5d9b95c8e8cd6ffcfdae699e3b05e1db744e58b7be857759"}, 13 | } 14 | -------------------------------------------------------------------------------- /test/apollo_tracing_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SharedTestCase do 2 | defmacro define_tests(do: block) do 3 | quote do 4 | defmacro __using__(options) do 5 | block = unquote(Macro.escape(block)) 6 | 7 | quote do 8 | use ExUnit.Case 9 | 10 | @moduletag unquote(options) 11 | unquote(block) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | 18 | defmodule ApolloTracingSharedTests do 19 | import SharedTestCase 20 | 21 | define_tests do 22 | @query """ 23 | query { 24 | getPerson { 25 | name 26 | age 27 | cars { make model } 28 | } 29 | } 30 | """ 31 | 32 | def get_result(schema, query) do 33 | pipeline = ApolloTracing.Pipeline.default(schema, []) 34 | 35 | query 36 | |> Absinthe.Pipeline.run(pipeline) 37 | |> case do 38 | {:ok, %{result: result}, _} -> result 39 | error -> error 40 | end 41 | end 42 | 43 | test "should have :tracing in extension", %{schema: schema} do 44 | result = get_result(schema, @query) 45 | assert result.extensions.tracing 46 | end 47 | 48 | test "should have start and end times in tracing", %{schema: schema} do 49 | result = get_result(schema, @query) 50 | assert result.extensions.tracing.startTime 51 | assert result.extensions.tracing.endTime 52 | assert result.extensions.tracing.duration 53 | end 54 | 55 | test "should have 6 resolvers", %{schema: schema} do 56 | result = get_result(schema, @query) 57 | assert length(result.extensions.tracing.execution.resolvers) == 6 58 | end 59 | 60 | test "each resolver should have path, start_offset and duration", %{schema: schema} do 61 | result = get_result(schema, @query) 62 | 63 | for resolver <- result.extensions.tracing.execution.resolvers do 64 | assert resolver.path 65 | assert resolver.startOffset 66 | assert resolver.duration 67 | end 68 | end 69 | 70 | test "includes cache hints", %{schema: schema} do 71 | result = get_result(schema, @query) 72 | assert result.extensions.cacheControl.version == 1 73 | 74 | assert result.extensions.cacheControl.hints == [ 75 | %{ 76 | path: ["getPerson"], 77 | maxAge: 30, 78 | scope: "PRIVATE" 79 | }, 80 | %{ 81 | path: ["getPerson", "cars"], 82 | maxAge: 600, 83 | scope: "PUBLIC" 84 | } 85 | ] 86 | end 87 | end 88 | end 89 | 90 | defmodule ApolloTracingTest do 91 | defmodule TestSchema do 92 | use Absinthe.Schema 93 | use ApolloTracing 94 | 95 | object :person do 96 | meta(:cache, max_age: 30, scope: :private) 97 | 98 | field(:name, :string) 99 | field(:age, non_null(:integer)) 100 | field(:cars, list_of(:car)) 101 | end 102 | 103 | object :car do 104 | meta(:cache, max_age: 600) 105 | 106 | field(:make, non_null(:string)) 107 | field(:model, non_null(:string)) 108 | end 109 | 110 | query do 111 | field :get_person, list_of(non_null(:person)) do 112 | resolve(fn _, _ -> 113 | {:ok, 114 | [ 115 | %{ 116 | name: "sikan", 117 | age: 20, 118 | cars: [%{make: "Honda", model: "Accord"}] 119 | } 120 | ]} 121 | end) 122 | end 123 | end 124 | end 125 | 126 | use ApolloTracingSharedTests, schema: TestSchema 127 | end 128 | 129 | defmodule ApolloTracingInlineTest do 130 | defmodule TestSchema do 131 | use Absinthe.Schema 132 | use ApolloTracing 133 | 134 | object :person, meta: [cache: [max_age: 30, scope: :private]] do 135 | field(:name, :string) 136 | field(:age, non_null(:integer)) 137 | field(:cars, list_of(:car)) 138 | end 139 | 140 | object :car, meta: [cache: [max_age: 600]] do 141 | field(:make, non_null(:string)) 142 | field(:model, non_null(:string)) 143 | end 144 | 145 | query do 146 | field :get_person, list_of(non_null(:person)) do 147 | resolve(fn _, _ -> 148 | {:ok, 149 | [ 150 | %{ 151 | name: "sikan", 152 | age: 20, 153 | cars: [%{make: "Honda", model: "Accord"}] 154 | } 155 | ]} 156 | end) 157 | end 158 | end 159 | end 160 | 161 | use ApolloTracingSharedTests, schema: TestSchema 162 | end 163 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | 2 | ExUnit.start() --------------------------------------------------------------------------------