Lazy Loading Angular modules from a remote server

Today I’m going to share a technique for lazy loading Angular modules from another server. This is very useful for projects which have grown too big to be monorepo or if monorepos aren’t used in your company. It allows lazy loaded modules to be developed and deployed independently.

Step 1 - Build a remote module

Create (or migrate an existing module to) a new Angular CLI repo (ng new $NAME --routing). Then install ng-packagr and http-server (yarn add -D ng-packagr http-server). Now create an entry point for the module called EntryModule and add routing (ng g m entry --routing). Add an EntryComponent and something to distinguish this module and component like <p>Module $NAME - entry works!</p>. Change the build script to ng-packagr -p package.json and add the followig to package.json:

"ngPackage": {
  "lib": {
    "entryFile": "src/public_api.ts"
  }
}

Now create that file touch src/public_api.ts and add the following:

/**
 * Entry point for the remote module
 */
export * from './app/entry/entry.module';

I used the Entry naming convension to clearly define an entry point of the module otherwise you could have a bunch of feature modules and it’s not clear which one is the root unlike normal Angular apps which always have AppModule.

Add a root path for EntryComponent in EntryRoutingModule (ie. routes = [{ path: '', component: EntryComponent }]).

In AppRoutingModule do the following:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', loadChildren: './entry/entry.module#EntryModule' }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Now make sure you’re importing RoutingModule.forRoot([]) and AppRoutingModule in AppModule. Ensure that EntryModule is not imported in AppModule or AppRoutingModule. This setup will emulate the actual app that will serve this module. We’ll call yarn build to start ng-packagr and while that runs we’ll move onto step 2

Step 2 - Build the core app

The core app will be deployed and use SystemJs to load the remote module. So create a new app (ng new core --routing) called core (or whatever you want really). Install systemsj (yarn add systemjs) and http-server with cpy-cli (yarn add -D cpy-cli http-server). In your angular.json/.angular-cli.json add "node_modules/systemjs/dist/system.js", to “scripts”.

Now open main.ts and add the setup for SystemJs like so. Add any modules your remote module imports like ngrx, clarity, rxjs, etc.

// ...
import * as common from '@angular/common';
import * as commonHttp from '@angular/common/http';
import * as core from '@angular/core';
import * as router from '@angular/router';
import * as rxjs from 'rxjs';
import * as rxjsOperators from 'rxjs/operators';

declare const SystemJS;
SystemJS.set('@angular/core', SystemJS.newModule(core));
SystemJS.set('@angular/common', SystemJS.newModule(common));
SystemJS.set('@angular/common/http', SystemJS.newModule(commonHttp));
SystemJS.set('@angular/router', SystemJS.newModule(router));
SystemJS.set('rxjs', SystemJS.newModule(rxjs));
SystemJS.set('rxjs/operators', SystemJS.newModule(rxjsOperators));
// ...

Now here’s the magic part, open AppRoutingModule and add the following function and routes:

/**
 * Lazy load remote bundle (AOT compatible!)
 * @param bundleName
 */
export const loadRemoteChildren = bundleName =>
  SystemJS.import(`http://127.0.0.1:8888/${bundleName}.umd.min.js`)
    .then(module => module.EntryModule)
    .catch(err => console.error(err));

const routes: Routes = [
  {
    path: '',
    children: [
      {
        path: 'lazy',
        loadChildren: () => loadRemoteChildren('NAME') // your module name from earlier
      }
    ]
  }
]

Step 3 - Serving the bundle and app

By now the ng-packagr build should be done and you’ll have a dist folder with a bunch of stuff. The only file(s) we care about are in the bundles folder. These are SystemJs compatible UMD bundles. You can use cpy-cli to move them or just serve them from there using http-server dist/bundles -p 8888.

Now start the core dev server using yarn start and go to http://localhost:4200. When we inspect the network tab we’ll see an initial main.js load, then a BUNDLE.umd.min.js request and your entry component should appear on the page! Neat!

If you’re looking for a complete example repo with lazy loaded modules and a lazy loaded non-routing module (eg. a navbar) click here!.

Thanks for reading!

Make your own GraphQL metrics dashboard - Part 5

Hey everyone and welcome back to Make your own GraphQL metrics dashboard. Here’s just a reminder to catch up part 1, part 2, part 3, part 4 if you’re just joining us.

In this post we’re going to try and build a list of GraphQL Types from all the traces then do some math to figure out the usage percentage of each of the fields. This is not possible to do with your standard REST so it’s a really cool idea. It allows you to make decisions later about your API like “This field is only used 0.1% of queries, maybe we should just drop it” or “This field is being used in almost all the queries (and possibly in WHERE clauses), let’s index it or optimize it’s resolution”. We’ll also look at adding simple routing to the UI to change views between the Operations and Schema tables based on the URL.

So let’s create a few new models in the api repo:

// api/src/models/resolvers.ts
import { db } from '../connectors';

export class Resolvers {
  static init() {
    db.query(`
      create or replace temp view resolver as
      select trace.id as trace_id, operation_id, x.*
      from
        trace,
        jsonb_array_elements(resolvers) as resolver,
        jsonb_to_record(resolver) as x(
          path text[],
          duration int,
          "fieldName" text,
          "parentType" text,
          "returnType" text,
          "startOffset" int
        )`);
  }
}

So this is our first view. If you’ve never used a sql view before it’s basically a shortcut query, every time we select ... from resolver it will run the query above first. Is it the best for performance? Probably not in the long run but rememeber, this is just for fun and learning. I will most likely be posting updates and optimizations as we go along. views make our lives easier and I’ll take that option first! This view will be crucial to building more complex queries and views for metrics! It takes all the traces inserted and splits each resolver in resolvers into rows based on the schema of x(). This means we can be a little more flexible if the Apollo Tracing format changes. We need to use double quotes whenever fields are mixed cased in postgres, slightly annoying but less so since it will map nicely to Javascript.

We’ll create another model that will list all the unique graphQL fields we have:

// api/src/models/types.ts
import { db } from '../connectors';

export class Types {
  static allTypes() {
    return db
      .query(
        `select md5(row("parentType", "fieldName")::text) as key,
                "parentType",
                "fieldName",
                "returnType"
          from resolver
          group by 2, 3, 4;`
      )
      .then(res => res.rows);
  }
}

So you might be wondering why I created a md5 hash, well in react we all know that it likes to have a key prop when mapping components so this is a cheap way to have something to use as key. I won’t call the column id because it isn’t a primary key and we won’t be doing lookups with it. Add it to our api schema and resolvers:

# api/src/schema.graphql

type Query {
  allOperations: [Operation]
  operation(id: ID!): Operation
  trace(id: ID!): Trace
  allTypes: [Type]
}

type Type {
  key: ID
  parentType: String
  fieldName: String
  returnType: String
  startOffset: Int
}
// api/src/resolvers.ts

// ...
  Query: {
    allOperations: () => Operations.allOperations(),
    operation: (_, { id }) => Operations.get(id),
    trace: (_, { id }) => Traces.get(id),
    allTypes: () => Types.allTypes(), // <- newly added
  },

We’ll have to call the init script from the index as well to boot the view:

// ...
import { Operations, Traces, Resolvers } from './models';
// ...
app.listen(8000, () => {
  console.log('API started http://localhost:8000/graphiql');
  db
    .connect()
    .then(() => db.query(`create extension if not exists "uuid-ossp";`))
    .then(() => Operations.init())
    .then(() => Traces.init())
    .then(() => Resolvers.init()) // <- newly added
    .catch(err => {
      console.error('pg error:', err);
      process.exit(1);
    });
});

Adding simple routing to UI

Now if we go back to the UI, we’ll realize that we could do simple component switch but I want to render components based off the URL. So I like to use this method for routing. We’ll do ours a little differently. If you prefer using react-router instead of history you can scroll down to the Adding a Schema table section.

Start by installing history with yarn add history, the create a new file ui/src/routes.js:

// ui/src/routes.js
import Operations from './components/operations/Operations';
import Schema from './components/schema/Schema';

const routes = {
  '/': Operations,
  '/schema': Schema
};

export default routes;

This object will map URLs into components. Then we’ll create a little 404 page component:

// ui/src/pages/NotFound.js
import React from 'react';

const NotFound = () => <p>404 - Not Found</p>;

export default NotFound;

Now let’s create a history module to handle browswer history, routing and change detection.

// ui/src/history.js
import createHistory from 'history/createBrowserHistory';

// we might use universal rendering so check if browser or server
const isBrowser = typeof window !== 'undefined';

const history = isBrowser ? createHistory() : {};

const { location } = history;

const onChangeListeners = [];

function push(pathname) {
  // push a new path into history
  window.history.pushState({}, '', pathname);
  // call all the callbacks
  onChangeListeners.forEach(cb => cb(pathname));
}

function onChange(cb) {
  // add a callback
  onChangeListeners.push(cb);
}

export default { history, location, push, onChange };

Now we’ll have to refactor our App component into a class and add the router:

// ui/src/components/app/App.js
import './App.css';

import PropTypes from 'prop-types';
import React from 'react';

import history from '../../history';
import routes from '../../routes';
import NotFound from '../pages/NotFound';
import Sidebar from '../sidebar/Sidebar';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      pathname: props.pathname
    };
  }

  // use prop-types to validate pathname is one of the routes in routes
  static propTypes = {
    pathname: PropTypes.oneOf(Object.keys(routes)).isRequired
  };

  componentDidMount() {
    // update App state when path changes
    history.onChange(pathname => this.setState({ pathname }));
  }

  render() {
    // get the component from routes or display 404 page
    const { pathname } = this.state;
    const Outlet = routes[pathname] || NotFound;

    return (
      <div className="App">
        <header className="App__header">
          <h1 className="App__title">GraphQL Metrics</h1>
        </header>
        <Sidebar />
        <Outlet />
      </div>
    );
  }
}

export default App;

Now we’ll create a component that’s a glorified anchor to push new pathnames into history:

// ui/src/components/link/Link.js
import './Link.css';

import React from 'react';

import history from '../../history';

class Link extends React.Component {
  constructor(props) {
    super(props);
    // we'll use this to style the active route anchor
    this.state = { active: history.location.pathname === props.href };
  }

  // update the active state when path changes
  componentDidMount() {
    history.onChange(pathname =>
      this.setState({ active: pathname === this.props.href })
    );
  }

  // self explanatory
  onClick = e => {
    const { href } = this.props;
    const aNewTab = e.metaKey || e.ctrlKey;
    const anExternalLink = href.startsWith('http');

    if (!aNewTab && !anExternalLink) {
      e.preventDefault();
      history.push(href);
    }
  };

  // react people is this the best way to do className like this?
  // Leave a comment on GitHub I'd like to know
  render() {
    const { href, children } = this.props;
    const className = 'Link' + (this.state.active ? ' active' : '');
    return (
      <a className="Link {className}" href={href} onClick={this.onClick}>
        {children}
      </a>
    );
  }
}

export default Link;
/* Link.css */
a.Link {
  color: #fffbfc;
}
a.Link.active {
  text-decoration: none;
  color: #ef6461;
}

Ok so that was a bit of code to get router going but compared to react-router, I feel like we’re doing pretty good for a few lines of code:

  • Change view based on path
  • Subscribe to path changes and get current path easily
  • Change styles based on current path

Adding a table for schema types

We’re now ready to keep going with our dashboard and make a Schema component to display the list of allTypes we created earlier.

// ui/src/components/schema/Schema.js
import './Schema.css';

import gql from 'graphql-tag';
import React from 'react';
import { Query } from 'react-apollo';

import { groupBy } from '../../utils';

const allTypes = gql`
  {
    allTypes {
      key
      parentType
      fieldName
      returnType
    }
  }
`;

const Schema = () => (
  <table className="Schema">
    <thead>
      <tr>
        <th>Type</th>
        <th>Field</th>
        <th>Return Type</th>
      </tr>
    </thead>
    <SchemaTypes />
  </table>
);

const SchemaTypes = () => (
  <Query query={allTypes}>
    {({ loading, error, data }) => {
      if (loading) return RowSpan('Loading...');
      if (error) return RowSpan(error.message);

      const types = groupBy(data.allTypes, t => t.parentType);
      return Object.entries(types).map(([parentType, fields], i) => {
        return (
          <tbody key={i}>
            <tr>
              <td>{parentType}</td>
            </tr>
            {fields.map(FieldRow)}
          </tbody>
        );
      });
    }}
  </Query>
);

const FieldRow = ({ key, fieldName, returnType }) => (
  <tr key={key}>
    <td />
    <td>{fieldName}</td>
    <td>{returnType}</td>
  </tr>
);

const RowSpan = content => (
  <tbody>
    <tr>
      <td colSpan="3">{content}</td>
    </tr>
  </tbody>
);

export default Schema;

Awesome work everyone! If we checkout the page (hopefully we’ve been checking on it now and again for issues) We should now see something like this:

Schema table view

Metrics

Now for the juicy stuff! I promised you usage statistics in the intro so here we are finally! Let’s calculate the usage percentage of a field inside a type. We will need the count of queries made with each type and the count of queries containing each field. We’ll have to add on to the allTypes query like so:

// api/src/models/types.ts
import { db } from '../connectors';

export class Types {
  static allTypes() {
    return db
      .query(
        `with p as (
          select "parentType",
                  count(*) as parent_count
          from resolver
          group by 1
        ), f as (
          select "parentType",
                 "fieldName",
                 count(*) as field_count
          from resolver
          group by 1, 2
        )
        select md5(row(f."parentType", f."fieldName")::text) as key,
               f."parentType",
               f."fieldName",
               round((field_count * 100)::numeric / parent_count, 1) as usage
        from p
        join f on p."parentType" = f."parentType";`
      )
      .then(res => res.rows);
  }
}

Let’s deconstruct this query again to explain what is happening:

select "parentType",
        count(*) as parent_count
from resolver
group by 1;

 parentType | parent_count
------------+--------------
 User       |          226
 Query      |          166

The with as syntax allows us to create a temporary table essentially to make it easier to build a complex query. So we see here we have the total number of queries with all the different types.

 select "parentType",
        "fieldName",
        count(*) as field_count
from resolver
group by 1, 2;

 parentType | fieldName | field_count
------------+-----------+-------------
 User       | name      |          73
 User       | parent    |           2
 User       | codes     |          34
 User       | email     |          51
 User       | age       |          66
 Query      | fn        |          58
 Query      | me        |         108

The second with query gets us the total number of queries with a given type and field! Now the third with query is there for a lack of a better understanding. I’m sure there is a more efficient way to get the returnType but I couldn’t crack it. Perhaps a SQL wizard reading this can help me out. Now all we need to do is join these queries and add some math to get the percentage!

with p as (
  select "parentType",
          count(*) as parent_count
  from resolver
  group by 1
), f as (
  select "parentType",
          "fieldName",
          count(*) as field_count
  from resolver
  group by 1, 2
), r as (
  select "parentType",
          "fieldName",
          "returnType"
  from resolver
  group by 1, 2, 3
)
select md5(row(f."parentType", f."fieldName")::text) as key,
        f."parentType",
        f."fieldName",
        r."returnType",
        round((field_count * 100)::numeric / parent_count, 1) as usage
from p
join f on p."parentType" = f."parentType"
join r on f."parentType" = r."parentType" and f."fieldName" = r."fieldName";

               key                | parentType | fieldName | returnType | usage
----------------------------------+------------+-----------+------------+-------
 36c67cfe4fd186c2fa98ca431069cb56 | User       | name      | String     |  32.3
 888a5fba1f296404df873c2f0549c7b5 | User       | parent    | User       |   0.9
 ebc6ce5cdc9722dbe998ec219eeae919 | User       | codes     | [Int]      |  15.0
 95331ca259e940d1692a20b50b1fe746 | User       | email     | String     |  22.6
 ff428b6b6708d3fcdbb453d6117be2a3 | User       | age       | Int        |  29.2
 7a8bf89734f62dee77063596fe8caac1 | Query      | fn        | User       |  34.9
 09cc45d11bdd6877bfddb0465fcb3d12 | Query      | me        | User       |  65.1

Super cool! Now we add usage to the type Type:

# api/src/schema.graphql

# ...
type Type {
  key: ID
  parentType: String
  fieldName: String
  returnType: String
  usage: Float
}
# ...

Nice! Now if we go back to the ui Schema.js:

// ...
const allTypes = gql`
  {
    allTypes {
      key
      parentType
      fieldName
      returnType
      usage
    }
  }
`;

const Schema = () => (
  <table className="Schema">
    <thead>
      <tr>
        <th>Type</th>
        <th>Field</th>
        <th>Return Type</th>
        <th>Usage %</th>
      </tr>
    </thead>
    <SchemaTypes />
  </table>
);

// ...

const FieldRow = ({ key, fieldName, returnType, usage }) => (
  <tr key={key}>
    <td />
    <td>{fieldName}</td>
    <td>{returnType}</td>
    <td>{usage}%</td>
  </tr>
);

// ...

Schema table view with usage percentage

Awesome! We’ve got very useful metric now that can help us make better API design decisions based on usage patterns of clients or look for optimization pathways to faster queries. This was a longer post than usual so thanks for reading! Stay tuned for more!

Some Lodash functions in ES6

There’s quite a few posts out there with Lodash functions but there’s a few that I couldn’t find so here’s a list of those ones. Check back often because I will very likely keep updating this page with more and more as I go along.

times


// lodash
_.times(n, iteratee);

// ES6
Array.from({ length: n }, iteratee);

groupBy

// lodash
_.groupBy(array, fn);

// ES6
const groupBy = (arr, fn) => {
  return arr.reduce((acc, val) => {
    (acc[fn(val)] = acc[fn(val)] || []).push(val);
    return acc;
  }, {});
}

head and tail


// lodash
const tail = _.tail(array);

// ES6
const { head, ...tail } = array;

Make your own GraphQL metrics dashboard - Part 4

Operations table UI

Hello and welome back to ‘Make your own Graphql metrics dashboard’. If you missed part 1, part 2 or part 3 head there now first.

In case you’ve never used docker-compose before and received an error last time like “unable to connect to port :5432” error you can start your postgres instance with the command docker-compose up -d which will run in the background on port 5432.

In this part, we’ll create a new queries to aggregate each Operation’s average duration (aka response time) and average requests per minute, very cool! Then we’ll add a GraphQL API so that we can start making a basic bashboard UI with react (link) and react-apollo (link). I was never good at statictics so here goes:

Open path-to-project/api and add some packages:

> $ yarn add apollo-server-express graphql-tools

I’m going to assume some basic knowledge of GraphQL since you either already have at least one GraphQL service in which you probably wanted to monitor. If you’re not sure about it then head over here to the official website for a great explanation first. Now, create a file called src/schema.ts:

import { readFileSync } from 'fs';
import { makeExecutableSchema } from 'graphql-tools';

import { resolvers } from './resolvers';

const typeDefs = readFileSync(__dirname + '/schema.graphql', 'utf8');
export const schema = makeExecutableSchema({ typeDefs, resolvers });

and also create src/schema.graphql, this will be the API our dashboard will query to get that performance data. It’ll be simple enough for now but later we’ll add more fields:

type Query {
  allOperations: [Operation]
  operation(id: ID!): Operation
  trace(id: ID!): Trace
}

type Operation {
  id: ID
  name: String
  query: String
  traces: [Trace]
}

type Trace {
  id: ID
  version: Int
  startTime: String
  endTime: String
  duration: Int
}

Let’s add a package I forgot earlier, cors: yarn add cors. Now in the api/src/index.ts:

import { graphiqlExpress, graphqlExpress } from 'apollo-server-express';
import * as express from 'express';
import * as cors from 'cors';

import { processMetric } from './api';
import { db } from './connectors';
import { Operations, Traces } from './models';
import { schema } from './schema';

const app = express();

app.use(cors()); // add cors

app.use(express.json());

app.post('/api/metrics', processMetric);

app.use('/graphql', graphqlExpress({ schema }));

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

app.listen(8000, () => {
  console.log('API started http://localhost:8000/graphiql');
  db
    .connect()
    .then(() => db.query(`create extension if not exists "uuid-ossp";`))
    .then(() => Operations.init())
    .then(() => Traces.init())
    .catch(err => {
      console.error("pg err:", err);
      process.exit(1);
    });
});

And finally we’ll wire up our resolvers to our models in src/resolvers/ts:

import { Operations, Traces } from './models';

export const resolvers = {
  Query: {
    allOperations: () => Operations.allOperations(),
    operation: (_, { id }) => Operations.get(id),
    trace: (_, { id }) => Traces.get(id),
  },
  Operation: {
    traces: ({ id }) => Traces.forOperation(id),
  },
  Trace: {
    startTime: trace => trace.start_time,
    endTime: trace => trace.end_time
  },
}

Wow! I love GraphQL, so beautiful :)

Now if we start our server (yarn start) we can go to (GraphiQL)[http://localhost:8000/graphiql] and try the following query:

{
  allOperations {
    id
    name
    query
    traces {
      startTime
      endTime
      duration
    }
  }
}

We should get a bunch of data, if you only get an empty array you probably haven’t generated any traces. So if that’s the case we’ll return to our proxy folder and run yarn start, then open GraphiQL and make a few dozen queries and try removing/adding fields to get a few different Operations registered and lots of Traces. Going back to the API and run the query again, you should see the Operations and their Traces now. If you get an error or something else please open an Issue.

allOperations query results

Now comes the fun part! in src/models/operations.ts:

export class Operations {

  // ...

  // let's do the easy one first
  // lets take the averge duration from all the traces for
  // a particular operation:
  static avgDuration(operationId) {
    return db
      .query(`select round(avg(duration)) from trace where operation_id = $1`, [operationId])
      .then(res => res.rows[0].round)
      .then(res => res ? res : 0);
  }

  // this one is a little more complex
  // the average request per minute
  // the inner query we truncate the date to
  // the minute and do a count over partition
  // which means count all the rows in each minute
  // now we have the rpm for each minute
  // now we avg that rpm for the average rpm
  static avgRpm(operationId) {
  return db
    .query(`select round(avg(rpm), 2) from (select distinct
            date_trunc('minute', start_time), count(*) over (
            partition by date_trunc('minute', start_time)) as rpm
            from trace where operation_id = $1) as avg`, [operationId])
    .then(res => res.rows[0].round)
    .then(res => res ? res : 0);
  }

}

Later in the series we’ll look at narrowing the time window for some queries so that we aren’t taking the average over the entire time range available, so we can do something like show me the average performance over the past hour, or day, week etc. But for now this is pretty cool I think. Let’s add those fields to the schema and resolvers:

# ...
type Operation {
  id: ID
  name: String
  query: String
  traces: [Trace]
  avgDuration: Float
  avgRpm: Float
}
# ...
// ...
  Operation: {
    traces: ({ id }) => Traces.forOperation(id),
    avgDuration: ({ id }) => Operations.avgDuration(id),
    avgRpm: ({ id }) => Operations.avgRpm(id)
  },
// ...

Let’s test that sucker out:

allOperations avgRpm and avgDuration

Very cool, so we’ve got a (very :P) basic monitoring tool in place let’s do what we all came here for: Graphs! Tables! Volume charts! Haha ok ok slow down. We’ll start simple and build up from there. Now, I’m an Angular developer by trade, all the companies I’ve worked for happen to use AngularJS and Angular but to make this more accessible I’ve learnt a bit of React. I figured most people are using React, especially the GraphQL crowed it seems. So please ignore my really noob React, hopefully it will give you the queries and ideas you need to go build something cool as well if you want!

If you don’t know React then I’d suggest following along anyways and you can map my react components into whatever you fancy, using the same queries. So let’s dive right in let’s create-react-app ui. Then we have to add the Apollo (link) client stuff: yarn add apollo-boost graphql graphql-tag react-apollo and now start the webpack server: yarn start and let’s get cracking:

// index.js
import './styles';

import ApolloClient from 'apollo-boost';
import React from 'react';
import { ApolloProvider } from 'react-apollo';
import ReactDOM from 'react-dom';

import App from './app/App';
import registerServiceWorker from './registerServiceWorker';

const client = new ApolloClient({
  uri: 'http://localhost:8000/graphql'
});

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
  , document.getElementById('root')
);

registerServiceWorker();

I think this ApolloProvider is called a higher order component?

// App.js
import './App.css';

import React from 'react';

import Operations from '../operations/Operations';
import Sidebar from '../sidebar/Sidebar';

const App = () => (
  <div className="App">
    <header className="App__header">
      <h1 className="App__title">GraphQL Metrics</h1>
    </header>
    <Sidebar />
    <Operations />
  </div>
)

export default App;

The Sidebar is optional for now since we’re not going to do any routing right now. By the way, I will include all the css files in the repo link if you want to use mine.

// Sidebar.js
import './Sidebar.css';

import React from 'react';

const Sidebar = () => (
  <nav className="Sidebar">
    <ul>
      <li>
        <a className="active">
          Operations
        </a>
      </li>
    </ul>
  </nav>
)

export default Sidebar;
// Opereations.js
import './Operations.css';

import gql from 'graphql-tag';
import React from 'react';
import { Query } from 'react-apollo';

import { prettyDuration, prettyNumber } from '../utils';

const allOperations = gql`{
  allOperations {
    id name query avgDuration avgRpm
  }
}`;

const OperationRows = () => (
  <Query query={allOperations}>
    {({ loading, error, data }) => {
      if (loading) return <tr><td>Loading...</td></tr>
      if (error) return <tr><td>Error: {error}</td></tr>

      return data.allOperations.map(op => (
        <tr key={op.id}>
          <td>{op.name ? op.name : op.query}</td>
          <td>{prettyNumber(op.avgRpm)}</td>
          <td>{prettyDuration(op.avgDuration)}</td>
        </tr>
      ))
    }}
  </Query>
)

const Operations = () => (
  <div className="Operations">
    <table>
      <thead>
        <tr>
          <th>Operation</th>
          <th>Average RPM</th>
          <th>Average Duration</th>
        </tr>
      </thead>
      <tbody>
        <OperationRows />
      </tbody>
    </table>
  </div>
)

export default Operations

We’ll create a few helper functions to format our numbers a little better so prettyNumber would take 12653 and return 12.6K. prettuDuration is very similar but takes nanoseconds and rounds them up to microseconds, milliseconds, etc.

// utils.js

// 12495 => 12.4K
export function prettyNumber(value, digits = 1) {
  const units = ['K', 'M', 'B'];

  let decimal;

  for (let i = units.length - 1; i >= 0; i--) {
    decimal = Math.pow(1000, i + 1);
    if (value <= -decimal || value >= decimal) {
      return +(value / decimal).toFixed(digits) + units[i];
    }
  }
  return value.toFixed(digits);
}

// 53321424 => 53.3ms
export function prettyDuration(ns, digits = 1) {
  const units = ['μs', 'ms', 's'];

  let decimal;

  for (let i = units.length - 1; i >= 0; i--) {
    decimal = Math.pow(1000, i + 1);
    if (ns <= -decimal || ns >= decimal) {
      return +(ns / decimal).toFixed(digits) + units[i];
    }
  }
  return ns + 'ns';
}

Operations table UI

Tada! Thanks everyone for following along so far. If you have any questions or comments please go to the Issues and leave something there. I’ll also provide the full repo so you can use my stylesheets if you want or make your own. Next time in part 5 we’ll add a few more metric calculations and views centered around more fine grained details in each GraphQL resolver to pin point slow backends.

Make your own GraphQL metrics dashboard - Part 3

Hey there! Welcome back to “DIY Apollo Engine but way less nice”! We’ve already covered a lot of ground in tutorials part 1 and part 2 so I highly recommend going back to catch up before continuing, otherwise to recap: we’re building a performance monitoring bashboard specifically tailored for GraphQL using a proxy and a metrics API with postgres.

The next piece of the puzzle is our metrics API. The proxy will post tracing data for each query to the metrics API asynchronously. The metrics API will store our tracing data in postgres in a few tables and we’ll construct some views on top to aggregate performance data. After that we’ll expose this aggregate date via graphQL (obviously!) to our dashboard UI. Our architecture is growing:

   Client        Proxy        Mock (or real API)
     |             |             |
     | --- req --> |             |
     |             | --- req --> |
     |             | <-- res --- |
     | <-- res --  |
     |             |
     |             |             Metrics API      Dashboard UI
     |             |                 |                 |
     |             | --- metrics --> | --- metrics --> |
     |             |                 |                 |

So let’s get started! We’ll start by initalizing our project with all the packages we’ll need:

> $ mkdir api
> $ cd api
> $ yarn init -y
> $ yarn add -D typescript dotenv nodemon ts-node @types/{express,node,pg}
> $ yarn add express pg

I’ll be using TypeScript because it’s amazing but feel free to use any es6 tools you want. We’ll also use docker-compose to manage our postgres instance but feel free to run postgres if you know/perfer other methods.

# docker-compose.yml
version: '3'
services:
  db:
    image: postgres:latest
    ports:
      - 5432:5432
    environment:
      POSTGRES_PASSWORD: postgres

To start postgres use docker-compose up -d. Then we’ll use dotenv to manage environment variables for postgres:

# .env
PGHOST='localost'
PGUSER='postgres'
PGDATABASE='postgres'
PGPASSWORD='postgres'

In package.json we’ll add a start script:

"start": "nodemon -e ts -x 'ts-node -r dotenv/config' src/index.ts"

Let’s start hacking!

// src/index.ts
import * as express from 'express';

import { processMetric } from './api';
import { db } from './connectors';
import { Operations, Traces } from './models';

const app = express();

app.use(express.json());

// Collect metric data
app.post('/api/metrics', processMetric);

app.listen(8000, () => {
  console.log('Metrics API started http://localhost:8000/graphiql');

  // Initialize postgres or exit
  db.connect()
    .then(() => db.query(`create extension if not exists "uuid-ossp";`))
    .then(() => Operations.init())
    .then(() => Traces.init())
    .catch(err => {
      console.error("pg error:", err);
      process.exit(1);
    });
});

Ok so we’ll create our first model, the Operation, which will hold unique graphQL queries using the stipped down query string and, if you used a graphQL named operation, that as name. If we select from operation our data will looks something like this:

postgres=# select name, query from operation ;
  name   |                query
---------+----------------------------------------
 MeQuery | query MeQuery { me { name email age } }
         | { me { name } }
         | { me { age } }
         | { fn(a, b) { age } }
         | { fn(a, b) { name email } }
// src/models/operations.ts
import { db } from '../connectors';
import { Traces } from './traces';

/**
 * Operations store all unique GraphQL queries
 */
export class Operations {

  static init() {
    return db.query(`create table if not exists operation (
                      id    uuid primary key default uuid_generate_v4(),
                      query text unique not null,
                      name  text
                    );`);
  }

  static create({ query, operationName, extensions }) {
    return db
      .query(`insert into operation (query, name) values ($1, $2) returning id`)
      .then(res => res.rows[0].id)
      .then(operationId => {
        // ensure tracing data included
        if (extensions && extensions.tracing) {
          Traces.create(operationId,  extensions.tracing);
        }
      })
      .catch(err => {
        // ignore operation_query_key duplicate queries
        if (err.constraint !== 'operation_query_key') {
          throw err;
        }
      });
  }

}

Next is the Trace model which holds most of the metrics themselves like duration, startTime, etc. as well as all the resolvers as an array in jsonb! The jsonb data type is really cool because it stores json in binary format which is very fast and fully searchable and indexable! We’ll levage this to build some views to aggregate query performance!

// src/models/traces.ts

export class Traces {

  static init() {
    return db
      .query(`create table if not exists trace (
                id           uuid primary key default uuid_generate_v4(),
                operation_id uuid references operation(id),
                version      smallint not null,
                start_time   timestamp with time zone not null,
                end_time     timestamp with time zone not null,
                duration     integer not null,
                resolvers    jsonb
              );`)
      .then(() => db.query(`create index if not exists trace_operation_id_idx
                              on trace(operation_id);`))
      .then(() => db.query(`create index if not exists trace_resolvers_idx
                              on trace using gin (resolvers);`));
  }

  static create(operationId, tracing) {
    const { version, startTime, endTime, duration } = tracing;
    const resolvers = JSON.stringify(tracing.execution.resolvers);
    const values = [operationId, version, startTime, endTime, duration, resolvers];
    return db.query(`insert into trace (operation_id, version, start_time,
                     end_time, duration, resolvers) values
                     ($1, $2, $3, $4, $5, $6);`, values);
  }

}

Wow! Let’s try this puppy out! We can go to our proxy project and run yarn start and also run yarn start inside our api project. Now nagivate to the proxy here and make a few queries! Now open psql:

> $ psql -h localhost -U postgres

postgres=# select * from operation;

Tracing output in GraphiQL

postgres=# select * from trace;

Example operation select

Congrats everyone! Thanks for reading this far I really appreciate it! I hope you’re learning lots or getting some nice ideas! Maybe you’re appalled by my SQL or my TypeScript/JavaScript so please let me know on the Issues tracker or if you want to clone/inspect/fork the repo here. Feel free to ask questions you have on any of the issue trackers for the proxy or the api or email me. Stay tuned for part 4 when we start building views to aggregate performance data into stuff like how often is this field is used vs other fields on a type, how many requests per minute is my query getting and what’s the average response time for your queries. Later on we’ll get even more detailed and drill down into each resolver’s performance, since that’s something graphQL can do!