andreas / ocaml-graphql-server

GraphQL servers in OCaml
MIT License
624 stars 60 forks source link

Instrumentation methods #58

Open sgrove opened 7 years ago

sgrove commented 7 years ago

I'd like to have enter optional function argument to pass in on query execution that'll be called for each resolver with:

Also, an exit optional function that would get the same as above, but also the return value of the resolver

This would let use collect telemetry on each resolver in a cross-cutting manner, and would be generally very useful.

andreas commented 7 years ago

Thanks for the input! Adding middleware seems like a generally useful feature. Getting the types right might be challenge. Can you elaborate on the types you would expect for "path to the resolver", "type of the parent resolver" and "resolver arguments"?

andreas commented 7 years ago

To expand on my previous comment...

The type of the function enter (or exit for that matter), will need to work for all types of fields. Since only the context type 'ctx is identical for a query execution over a schema, this is the only part of a field which can easily be exposed to enter. Loosely typed data can easily be exposed as well e.g. exposing the value of arguments as Yojson.Basic.json. Exposing 'src or 'args is possible, but enter will not be able to make any assumptions about their nature, so it will be hard to use for anything practical. Please share your use cases though, then I can more easily understand what you need.

martijnwalraven commented 7 years ago

@andreas: For context, I'm part of the Apollo team, and we've been working on a new design for exposing trace data to Apollo Optics. Currently, this relies on an agent running in your server that collects, aggregates, and batches up data to send to the Optics backend. In the new design, trace data for an individual request is exposed under extensions as part of the GraphQL response, and a separate proxy process (provided by us, written in Go) is responsible for filtering out the trace data and performing the aggregation and batching. Our hope is that this will make it much easier to use Optics with every GraphQL server, not just with Node and Ruby (the only platforms for which we currently provide agents).

We're getting ready for early access at the moment, and we expect to make changes based on feedback, so the format is preliminary. But for an idea of what this looks like, see here. Happy to answer any questions you might have. Would be exciting to get support for instrumentation and the Apollo tracing format in OCaml!

andreas commented 7 years ago

@martijnwalraven: Thanks, that looks interesting. What does the startOffset refer to? Do you have an example of a returned extensions-value and how it's interpreted/visualized?

martijnwalraven commented 7 years ago

I plan on writing up a short spec for this soon, but startOffset refers to the start time of a resolver call in nanoseconds, relative to the startTime of the request.

One motivation for the format is to send data to Apollo Optics and visualize both individual traces and aggregated statistics there, but we would also like to add support to GraphiQL to show trace data for a single request.

To give an example of the format, for this query:

query {
  hero {
    name
    friends {
      name
    }
  }
}

The result would look like this:

{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  },
  "extensions": {
    "tracing": {
      "version": 1,
      "startTime": "2017-07-24T14:03:16.008Z",
      "endTime": "2017-07-24T14:03:16.009Z",
      "duration": 1332446,
      "execution": {
        "resolvers": [
          {
            "path": [
              "hero"
            ],
            "parentType": "Query",
            "fieldName": "hero",
            "returnType": "Character",
            "startOffset": 1108856,
            "duration": 24201
          },
          {
            "path": [
              "hero",
              "name"
            ],
            "parentType": "Droid",
            "fieldName": "name",
            "returnType": "String!",
            "startOffset": 1181364,
            "duration": 7007
          },
          {
            "path": [
              "hero",
              "friends"
            ],
            "parentType": "Droid",
            "fieldName": "friends",
            "returnType": "[Character]",
            "startOffset": 1196580,
            "duration": 116086
          },
          {
            "path": [
              "hero",
              "friends",
              0,
              "name"
            ],
            "parentType": "Human",
            "fieldName": "name",
            "returnType": "String!",
            "startOffset": 1284820,
            "duration": 2516
          },
          {
            "path": [
              "hero",
              "friends",
              1,
              "name"
            ],
            "parentType": "Human",
            "fieldName": "name",
            "returnType": "String!",
            "startOffset": 1297121,
            "duration": 1373
          },
          {
            "path": [
              "hero",
              "friends",
              2,
              "name"
            ],
            "parentType": "Human",
            "fieldName": "name",
            "returnType": "String!",
            "startOffset": 1305854,
            "duration": 1286
          }
        ]
      }
    }
  }
}

As you can see, the overhead of the trace data is pretty significant, but it compresses quite well, so the recommendation is to enable gzip.

martijnwalraven commented 7 years ago

@andreas: Just a heads up that I just published a description of the proposed Apollo Tracing format. Would be great to get your feedback and see how we can add support for this to the OCaml server!