/apollo-studio-data-fetcher

This project is aimed to provide actual query duration statistics from apollo-studio.

Primary LanguageTypeScript

Apollo Studio Data Fetcher

What is this repo for?

This project is aimed to provide actual query duration statistics from apollo-studio.

These timing metrics (AKA timing-hints) can then be utilized in a custom graphql complexity estimator and plugged into the graphql-query-complexity library designed to "... protect your GraphQL servers against resource exhaustion and DoS attacks".

Usage

  • create a schema.graphql containing the AST version of your graphql schema inside the data folder
  • create a .env file, based on .env.example
  • build with yarn
  • run with yarn run

What it does

  1. logins into apollo-studio using puppeteer and intercepts your auth cookies, and the timing metrics persistent query hash
  2. reads your schema from schema.graphql, and runs a graphql timing metric query, via apollo-studio graphql api (using cookies and persistent query hash from [1])
  3. arranges the data into an object of the following structure:
{
   "Address.city": 0.003780965612170304,
   "Address.country": 0.0036712738793939926,
   "Address.entrance": 0.0037941881736387904,
   ...
}
  1. stores this data in an aws s3 bucket

Note: you can (and it makes sense to) run the above logic periodically.

Example for a possible implementation of complexity-estimator using the timing metrics

In your apollo-server implementation, add a plugin:

import { getComplexity, simpleEstimator } from 'graphql-query-complexity';

...

class ComplexityValidator implements ApolloServerPlugin<any> {
  
  ...
   
   requestDidStart(requestContext: GraphQLRequestContext<any>): GraphQLRequestListener<any> | void {
      return {
         didResolveOperation({ request, document }) {
            const query = request.operationName
                    ? separateOperations(document)[request.operationName]
                    : document;
            const complexity = getComplexity({
               schema,
               query,
               variables: request.variables,
               estimators: [
                  // where timingMetrics is the data generated by this (apollo-studio-data-fetcher) project
                  timingBasedEstimator(timingMetrics), 
                  simpleEstimator({ defaultComplexity: 100 })
               ],
            });
            validateComplexity(...);
         }
      };
   }
}

while the timingBasedEstimator implementation can be something along these lines:

import { ComplexityEstimatorArgs } from 'graphql-query-complexity';
import { get } from 'lodash';

/**
 * Complexity estimator based on actual query timing metrics fetched from apollo-studio.
 * The return unit is milliseconds-equivalent.
 *
 * @param timingMetrics
 */
export function timingBasedEstimator(timingMetrics) {
  return (options: ComplexityEstimatorArgs) => {
    const timingMetricKey = `${options.type.name}.${options.field.name}`;
    const timingMetricValue = timingMetrics[timingMetricKey];
    if (timingMetricValue) {
      // Your actual calculation might be different, using such a multiplier, is just an examle.
      const multiplier = Math.max(get(options, 'args.first', 1), get(options, 'args.last', 1));
      return (timingMetricValue + options.childComplexity) * multiplier;
    }
    return undefined; // Fallback to simpleEstimator.
  };
}

TODO:

  • add some tests
  • support mutation / interfaces / unions etc.