Make your own GraphQL metrics dashboard - Part 2

Hi and welcome back to make your own graphQL performance dashboard! If you missed part 1 you should go there to catch up first.

Let’s create our awesome proxy now! As explained in part 1, the proxy will take requests and pipe them to the graphql server, parse the response and send the client a stripped down response. The client won’t need the extra data in the tracing body.

import { Agent } from 'http';
import * as express from 'express';
import * as request from 'request';

const app = express();

// create a default request to the original graphQL server
const origin = request.defaults({
  baseUrl: 'http://localhost:5000/',
  agent: new Agent({ keepAlive: true })
});

// create a default request to metrics api server
const api = request.defaults({
  baseUrl: 'http://localhost:8000/api/',
  agent: new Agent({ keepAlive: true })
});

// catch all /graphql requests with json body parser
app.use('/graphql', express.json(), (req, res, next) => {
  // we'll save the query string and (optional) operationName
  // build a similar request to the incoming request
  let query, operationName;

  if (req.method === 'POST') {
    query = req.body.query;
    operationName = req.body.operationName;
  } else if (req.method === 'GET') {
    query = req.query.query;
    operationName = req.query.operationName;
  }

  const options = {
    uri: req.originalUrl,
    method: req.method,
    headers: req.headers,
    json: req.body
  };
  origin(options, (err, queryRes, body) => {
    // relay status code to client
    res.status(queryRes.statusCode);

    if (queryRes.statusCode > 400) {
      // relay error body/message to client
      res.send(body).end();
    } else {
      // now we have the GraphQL response with the extensions metrics
      const { data, errors, extensions } = body;

      // send back response to client with data and/or errors
      res.json({ data, errors });

      if (query) collectMetrics(query, operationName, extensions, errors);
    }
  });
});

// pipe all other requests (like graphiql) to original
app.all('*', (req, res) => {
  const options = { uri: req.path, qs: req.query };
  req
    .pipe(origin(options))
    .pipe(res);
});

app.listen(4000, () => {
  console.log('Proxy started http://localhost:4000/graphiql');
});

/**
 * Collect query metadata and post to metrics api.
 *
 * @param query GraphQL query string.
 * @param operationName (optional)
 * @param extensions object with tracing and/or caching metadata
 * @param errors array of Graphql errors
 */
function collectMetrics(query, operationName, extensions, errors) {
  // clean up query to remove sensitive data from parameters
  query = query.replace(/(\:.*?)(?=\,)|(\:.*?)(?=\))/g, '');
  // remove redundant whitespace
  query = query.replace(/\s+/g, ' ').trim();

  console.log(query, operationName, extensions, errors);
}

Now if we start the server using yarn start and navigate to the proxy’s graphiQL instead of the mock server, we should be able to make a query and not get any of the extensions object in the response! Meanwhile, in the console you should see the query, operation name, extensions object and, if any, errors. Very cool! Join us next time in part 3 when we take all that data and put it into a postgres database using jsonb and makes some queries!

Make your own GraphQL metrics dashboard - Part 1

Hello and welcome to my multi-part tutorial series on how to build your own GraphQL metrics dashboard for fun! In this series will leverage the Apollo Tracing interface to collected and parse metric data from your GraphQL API. This series is inspired by the incredibly cool Apollo Engine, which uses the tracing extension we’ll use to get that awesome performance data. GraphQL has a unique execution pattern that allows us to do some really cool stuff that isn’t possible with plain REST out of the box, like query usage on a per field basis. Due to GraphQL’s ability to resolve data from multiple sources asynchronously and piece it together, we can see which areas of our queries are slowing down the overall response and build timelines for each query.

Let’s start with a mocked GraphQL server, feel free to use an existing GraphQL server if you want. It’ll allow us to generate and inspect that trace data we need for our dashboard later. Here’s what it’ll look like when you make a request:

   Client        Proxy        Mock (or real API)
     |             |             |
 1.  | --- req --> |             |
 2.  |             | --- req --> |
 3.  |             | <-- res --- |
 4.  | <-- res --  |             |
     |             |             |
  1. A client makes a GraphQL request to the proxy.
  2. The proxy pipes the request to the GraphQL server.
  3. The proxy takes the response and collects the metric data.
  4. The proxy returns the data response to the client.

Later the proxy will send the metric data to our metrics database. Let’s jump right in. I use TypeScript but feel free to use Babel or any other ES6 tools, I’m mostly going to use ES6 features in this tutorial series anyways.

$ mkdir graphql-metrics
$ cd graphql-metrics
$ yarn init -y
$ yarn add apollo-server-express express request graphql graphql-tools
$ yarn add -D typescript ts-node nodemon

Here’s the start script you want to add to package.json:

"start": "nodemon -e ts -x 'ts-node mock-server.ts'"

Let’s start that mock-server.ts file:

import { graphiqlExpress, graphqlExpress } from 'apollo-server-express';
import { spawn } from 'child_process';
import * as express from 'express';
import { readFileSync } from 'fs';
import { addMockFunctionsToSchema, makeExecutableSchema } from 'graphql-tools';
import { resolve } from 'path';

const typeDefs = readFileSync(__dirname + '/schema.graphql', 'utf8');
const schema = makeExecutableSchema({ typeDefs });
// this will send dummy data responses so no need for resolvers
addMockFunctionsToSchema({ schema });

const app = express();

// Start proxy server
const proxy = spawn('ts-node', [__dirname + '/proxy.ts']);
proxy.stdout.pipe(process.stdout);
proxy.stderr.pipe(process.stderr);
process.on('exit', () => proxy.kill());

// Setup regular apollo middleware
app.use('/graphql', express.json(), graphqlExpress({
  schema,
  // tracing enables the performance metrics we'll collect
  // with the proxy to create a dashboard
  tracing: true,
}));

app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }));

app.listen(5000, () => {
  console.log(`GraphQL started http://localhost:5000/graphiql`);
});

and a super simple schema.graphql for testing, if you don’t already have one:

type Query {
  me: User
}

type User {
  name: String
  email: String
  age: Int
}

If you were to transplant this into an existing apollo-server-express app all you need to do is spawn the proxy server and enable tracing to get started. Awesome so far, let’s run yarn start and check out the tracing data.

Tracing output in GraphiQL

That’s interesting! Now we see all this extra stuff under extensions, this is the performance trace for this query, tailored for GraphQL. Later we’ll filter this out since the client doesn’t need it but for now we’ll keep it. Now that you’ve done all that you’ve done all that finger exercising, here’s the GitHub repo for the proxy in case you want to copy my tsconfig, etc files. Please leave comments or questions in the Issues! Stay tuned for part two when we dive into the proxy server which will collect and filter this output into postgres using jsonb!

Header based pre-authentication with Django REST Framework

As the title suggests, this is a short How-To on using header based pre-authentication with Django REST Framework (DRF). I’m currently working in a system that does authentication through a gateway and forwards requests to corresponding private APIs with a custom HTTP authentication header. This scenario is quite simple to add to DRF’s pluggable authentication. You can find the entire project source code here.

Starting with a basic DRF project, change the DEFAULT_AUTHENTICATION_CLASSES to the path of your class name.

# config/settings.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'config.authentication.HeaderAuthentication'
    ]
}

In that class we can do the following, using a CustomUser object since my backend isn’t interested in using django.contrib.auth in this case.

# config/authentication.py

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed


class CustomUser:

    def __init__(self, uid):
        self.uid = uid

    def __repr__(self):
        return f'<PreAuthUser {self.uid}>'

    def __str__(self):
        return self.__repr__()


class HeaderAuthentication(BaseAuthentication):

    def authenticate(self, request):
        # META dict has custom headers as HTTP_{key} and capitalizes it.
        uid = request.META.get('HTTP_UID')

        if not uid:
            raise AuthenticationFailed('Missing authentication header')

        user = CustomUser(uid)

        return (user, None)

If we wanted to use the default Django auth user model, we could do something like so.

# config/authentication.py
from django.contrib.auth.models import User
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed


class HeaderAuthentication(BaseAuthentication):

    def authenticate(self, request):
        # META dict has custom headers as HTTP_{key} and capitalizes it.
        uid = request.META.get('HTTP_UID')

        if not uid:
            raise AuthenticationFailed('Missing authentication header')

        try:
            user = User.objects.get(pk=uid)
        except User.DoesNotExist:
            raise AuthenticationFailed('Invalid user id')

        return (user, None)

Even better, if we have a user microservice. Define a custom user class to populate the fields you wish the request.user object to have. Remove django.contrib.auth from your INSTALLED_APPS if this is the case.

# config/authentication.py
from django.contrib.auth.models import User
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed


class CustomUser:

    def __init__(self, uid, name, email, **kwargs):
        self.id = uid
        self.uid = uid
        self.name = name
        self.email = email

class HeaderAuthentication(BaseAuthentication):

    def authenticate(self, request):
        # META dict has custom headers as HTTP_{key} and capitalizes it.
        uid = request.META.get('HTTP_UID')

        if not uid:
            raise AuthenticationFailed('Missing authentication header')

        r = requests.get(f'http://users.mydomain.com/{uid}')

        try:
            r.raise_for_status()
        except requests.exceptions.RequestException
            raise AuthenticationFailed(
                f'User GET failed ({r.status_code}: {r.reason})')

        user = CustomUser(**r.json())

        return (user, None)

Obviously the approaches above a naive so please use them as a template to get started and add more robust error handling based on your requirements. Happy coding.

NOTE: I’m using PEP 498 f-string interpolation, if you aren’t using 3.6+ yet, just replace with format() or % s