spandex-project/spandex

Only leaf spans

ludwikbukowski opened this issue · 12 comments

I have the problem so that I'd like to include only bottom level spans in my Trace (only leaf spans)
So that starting from some distinguished span, only the spans that does not have childs will be forwarded.
Is it possible to achieve that without creating the fork of this library and implementing such a feature?

Can you clarify what you're trying to accomplish / why you need this feature? It sounds like you'd prefer for each trace to only display the root span (which is required as far as I know) and the most-detailed spans, but nothing in between (i.e. no intermediate calls in the flame graph).

Here are a few thoughts while we wait for more details:

  • Spandex itself doesn't create any traces or spans unless you tell it to. If you don't create the intermediate spans in the first place, they won't be there. I presume what you're looking for, though, is more control over the spans that get automatically created e.g. by spandex_phoenix and spandex_ecto.

  • You could create your own sender to plug into spandex_datadog that filters some of the spans out of the trace before sending it. It's not obvious in the docs, but then if you configure the sender: MyApp.SpecialDatadogSender option in your Tracer config, it will call that module instead of SpandexDatadog.APIServer. You could make a module that uses defdelegate to call into the real SpandexDatadog.APIServer for everything except the send_trace function. (see https://github.com/spandex-project/spandex_datadog/blob/b2dd1c0/lib/spandex_datadog/api_server.ex#L105 for how this works normally).

  • If it turns out that what you want is something that others would also want, we can figure out whether we should / how we can integrate it into Spandex or SpandexDatadog itself.

Thanks for clear and very good detailed response :)
Exactly this is what I would like to have.
The reason is that we wrote support for Absinthe library so that each resolver is span and the Trace is simply phoenix request. It was done by implementing custom middleware that calls start_span for each field (btw maybe I could share that code with you as something like spandex_absinthe)

The issue we faced is that we have quite deep flame graph trees.
The resolvers that calls other resolvers aren't very informative for us since those that do some time-consuming operations are the bottom level resolvers only.

I will look at the code you suggest and try to figure out optimal solution that does not require modifying the library itself :)

Cool! Yes we would love to have a spandex_absinthe library that people can just drop into their project. Maybe that library could have some way to “compress” the stack of spans as well. I’m not sure how Absinthe middleware works.

Agreed! Greg hit it all on the head. And absinthe is definitely something that deserves its own full integration :)

I think we should close this now, as there isn't really much sense in giving spandex an option to only send leaf traces, but rather in building an integration with absinthe separately.

Hey!
Let's suppose I'd like to contribute regarding the Absinthe support
Should I create PR into root spandex library so that you can review and in the end create separate spandex_absinthe repository?

I would recommend creating your own repository, and making changes there. Then we will review your repo, and you can transfer it to the spandex-project org. I would also put you on the core team so you could help maintain it :)

Oh right, makes much more sense :)
Then will keep you updated!

Hey @ludwikbukowski Do you still have that middleware for spandex + absinthe? I'm struggling a bit trying to get it to work. I followed this presentation (slide 37 is the main one for this) and I got spans in my Datadog traces, but they were more of a single, long spike of spans going down getting smaller and smaller, rather than the flamegraph that I'm used to.

The one thing I would point out about that slide is that it shows something like this:

  def middleware(middleware, _field, _object) do
    [MyAppWeb.Graphql.Middleware.DatadogTracing | middleware]

    middleware
  end

But that wouldn't actually add the middleware. It only "works" (partly) when I do this:

  def middleware(middleware, _field, _object) do
    [MyAppWeb.Graphql.Middleware.DatadogTracing | middleware]
  end

Oh, wow, ok, I see that's your slideshow from after you made your comments 😆

Maybe there's more to it? This is what my middleware module looks like:

defmodule MyAppWeb.Graphql.Middleware.DatadogTracing do
  @moduledoc "Allows display on a flamegraph for GraphQL fields in Datadog traces"

  @behaviour Absinthe.Middleware

  alias MyAppWeb.Tracer

  @impl Absinthe.Middleware
  def call(res, _config) do
    Tracer.start_span("#{res.parent_type.name}.#{res.definition.name}")
    %{res | middleware: res.middleware ++ [{{__MODULE__, :after_field}, []}]}
  end

  def after_field(res, _) do
    Tracer.finish_span()

    res
  end
end

And this is an example of what I see in Datadog:

Screen Shot 2020-09-07 at 11 10 08

Any updates regarding the spandex_absinthe? The code of @cheerfulstoic is great to get the flamegraphs for the absinthe resolvers, however it doesn't help in segregating the requests. Any recommendations to break apart these distinct queries, like we have in normal REST requests?

image

To whom it may concern, I made this gambiarra so now I can convert the normal resource (HTTP Verb + Path) into the query/mutation name.

MyApp.Tracer

defmodule MyApp.Tracer do
  @moduledoc false
  use Spandex.Tracer, otp_app: :my_app

  def customize_metadata(%{body_params: %{"query" => query}} = conn) when is_binary(query) do
    name =
      query
      |> String.split("{")
      |> Enum.at(1)
      |> String.split("(")
      |> Enum.at(0)
      |> String.replace("\n", " ")
      |> String.replace(" ", "")

    conn
    |> SpandexPhoenix.default_metadata()
    |> Keyword.update(:resource, name, fn _ -> name end)
    |> Keyword.merge(tags: [query: conn.body_params["query"], variables: conn.body_params["variables"]])
  end

  def customize_metadata(conn),
    do: SpandexPhoenix.default_metadata(conn)
end

Application

defmodule MyApp.Application do
  ...
  SpandexPhoenix.Telemetry.install(customize_metadata: &MyApp.Tracer.customize_metadata/1)
  ...
end