Skip to content

GraphQL Instrumenting/Metrics #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"graphql": "0.4.4",
"graphql-relay": "0.3.6",
"handlebars": "4.0.3",
"keen-js": "3.4.0-rc2",
"lodash": "3.10.1",
"node-fetch": "1.3.2",
"qs": "5.1.0",
Expand All @@ -37,6 +38,6 @@
},
"engines": {
"node": "4.1.0",
"npm": "2.14.3"
"npm": "3.5.3"
}
}
4 changes: 0 additions & 4 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
<script src="{{ CONSTANTS.reactPath }}"></script>
<script src="{{ CONSTANTS.fetchPath }}"></script>
<script src="/graphiql.js"></script>
<script type="text/javascript">
window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var n=t.forceSSL||"https:"===document.location.protocol,a=document.createElement("script");a.type="text/javascript",a.async=!0,a.src=(n?"https:":"http:")+"//cdn.heapanalytics.com/js/heap-"+e+".js";var o=document.getElementsByTagName("script")[0];o.parentNode.insertBefore(a,o);for(var r=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["clearEventProperties","identify","setEventProperties","track","unsetEventProperty"],c=0;c<p.length;c++)heap[p[c]]=r(p[c])};
heap.load("{{ CONSTANTS.heapId }}");
</script>
</head>
<body style='padding: 30px; font-family: sans-serif;'>
<h1>GraphQLHub APIs:</h1>
Expand Down
4 changes: 0 additions & 4 deletions public/playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
<script src="{{ CONSTANTS.reactPath }}"></script>
<script src="{{ CONSTANTS.fetchPath }}"></script>
<script src="/graphiql.js"></script>
<script type="text/javascript">
window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var n=t.forceSSL||"https:"===document.location.protocol,a=document.createElement("script");a.type="text/javascript",a.async=!0,a.src=(n?"https:":"http:")+"//cdn.heapanalytics.com/js/heap-"+e+".js";var o=document.getElementsByTagName("script")[0];o.parentNode.insertBefore(a,o);for(var r=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["clearEventProperties","identify","setEventProperties","track","unsetEventProperty"],c=0;c<p.length;c++)heap[p[c]]=r(p[c])};
heap.load("{{ CONSTANTS.heapId }}");
</script>
</head>
<body>
Loading...
Expand Down
8 changes: 6 additions & 2 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ import fs from 'fs';
import Handlebars from 'handlebars';

import { Schema } from './schemas/graphqlhub';
import instrumentationMiddleware from './src/graphQLInstrumentation';

import path from 'path';

import timingCallback from './src/timingCallback';

let IS_PROD = (process.env.NODE_ENV === 'production');
let CDN = {
react : 'https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.min.js',
fetch : 'https://cdnjs.cloudflare.com/ajax/libs/fetch/0.9.0/fetch.min.js',
};

let CONSTANTS = {
heapId : process.env.HEAP_ID,
reactPath : (IS_PROD ? CDN.react : '/react.js'),
fetchPath : (IS_PROD ? CDN.fetch : '/fetch.js')
};
Expand All @@ -37,7 +39,9 @@ app.get('/', function(req, res) {
res.send(INDEX);
});
app.use(express.static(path.join(__dirname, 'public')));
app.use('/graphql', cors(), graphqlHTTP({ schema: Schema }));
app.use('/graphql', cors(), instrumentationMiddleware(Schema, timingCallback, { addToResponse : false }), graphqlHTTP((req, res) => {
return { schema: Schema, rootValue : req.rootValue }
}));

let SHORTCUTS = {
reddit : '/playground?query=%23%20Hit%20the%20Play%20button%20above!%0A%23%20Hit%20"Docs"%20on%20the%20right%20to%20explore%20the%20API%0A%0A%7B%0A%20%20graphQLHub%0A%20%09reddit%20%7B%0A%20%20%20%20user(username%3A%20"kn0thing")%20%7B%0A%20%20%20%20%20%20username%0A%20%20%20%20%20%20commentKarma%0A%20%20%20%20%20%20createdISO%0A%20%20%20%20%7D%0A%20%20%20%20subreddit(name%3A%20"movies")%7B%0A%20%20%20%20%20%20newListings(limit%3A%202)%20%7B%0A%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20comments%20%7B%0A%20%20%20%20%20%20%20%20%20%20body%0A%20%20%20%20%20%20%20%20%20%20author%20%7B%20%0A%20%20%20%20%20%20%20%20%20%20%20%20username%0A%20%20%20%20%20%20%20%20%20%20%09commentKarma%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D',
Expand Down
88 changes: 88 additions & 0 deletions src/graphQLInstrumentation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// adapted from https://gist.github.com/eyston/7ec48ceb30213e7fbb36
// http://hueypetersen.com/posts/2015/11/06/instrumenting-graphql-js/
// https://github.com/graphql/graphql-js/issues/109

import { GraphQLObjectType, GraphQLScalarType } from 'graphql/type/definition';

function defaultResolveFn(source, args, { fieldName }) {
var property = source[fieldName];
return typeof property === 'function' ? property.call(source) : property;
};

const wrapPromise = (next) => {
return (obj, args, info) => {
try {
return Promise.resolve(next(obj, args, info));
} catch (e) {
return Promise.reject(e);
}
}
};

const withTiming = (next) => {
return (obj, args, info) => {
const start = new Date().getTime();
return Promise.resolve(next(obj, args, info)).then(res => {
info.rootValue.response.timing.fields.push({
type: info.parentType.name,
field: info.fieldName,
args,
duration: (new Date().getTime() - start) / 1000
});
return res;
});
}
}

const schemaFieldsForEach = (schema, fn) => {
Object.keys(schema.getTypeMap())
.filter(typeName => typeName.indexOf('__') !== 0) // remove schema fields...
.map(typeName => schema.getType(typeName))
.filter(type => type instanceof GraphQLObjectType) // make sure its an object
.forEach(type => {
let fields = type.getFields();
Object.keys(fields).forEach(fieldName => {
let field = fields[fieldName]
fn(field, type);
});
});
};

export default function graphQLInstrumentation(schema, loggingCallback, { addToResponse } = {}) {
schemaFieldsForEach(schema, (field, type) => {
field.resolve = withTiming(wrapPromise(field.resolve || defaultResolveFn));
});
return (req, res, next) => {
const start = new Date().getTime();
let _send = res.send;
res.send = function() {
const end = new Date().getTime();
const duration = (end - start) / 1000;
req.rootValue.response.timing.duration = duration;
res.send = _send;
process.nextTick(() => {
loggingCallback(req.rootValue.response.timing);
});
if (addToResponse) {
// NOTE this is tied to what express-graphql does; changes in future version
if (res.get('Content-Type') === 'text/json; charset=utf-8') {
let jsonString = arguments[0];
let obj = JSON.parse(jsonString);
obj.extensions = { instrumentation : req.rootValue.response };
return _send.apply(res, [JSON.stringify(obj)]);
}
}
return _send.apply(res, arguments);
};
// NOTE server.js explicitly uses `req.rootValue`
req.rootValue = {
response : {
timing : {
duration : undefined,
fields : []
}
}
};
next();
};
};
8 changes: 8 additions & 0 deletions src/keen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Keen from 'keen-js';

const client = new Keen({
projectId : process.env.KEEN_PROJECT_ID,
writeKey : process.env.KEEN_WRITE_KEY,
});

export { client as default };
11 changes: 11 additions & 0 deletions src/timingCallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import keen from './keen';

export default (timings) => {
keen.addEvent('totalDurations', { duration : timings.duration });
timings.fields.forEach(({ type, field, args, duration }) => {
// filter out really fast things (probably just obj.property calls)
if (duration > 0.005) {
keen.addEvent('fieldDurations', { type, field, duration });
}
});
}