UNPKG

apollo-tracer

Version:

Trace collection library for Node.js GraphQL servers

199 lines (182 loc) 5.57 kB
import now from 'performance-now'; import uuid from 'node-uuid'; import request from 'request'; import { forEachField } from 'graphql-tools'; const TRACER_INGRESS_URL = 'https://nim-test-ingress.appspot.com'; class Tracer { // TODO make sure Tracer can NEVER crash the server. // maybe wrap everything in try/catch, but need to test that. constructor({ TRACER_APP_KEY, sendReports = true, reportFilterFn, proxy }) { if (!TRACER_APP_KEY || TRACER_APP_KEY.length < 36) { throw new Error('Tracer requires a well-formatted TRACER_APP_KEY'); } // TODO check that sendReports is a boolean // TODO check that report filter fn is a function (if defined) this.TRACER_APP_KEY = TRACER_APP_KEY; this.startTime = (new Date()).getTime(); this.startHrTime = now(); this.sendReports = sendReports; this.reportFilterFn = reportFilterFn; this.proxy = proxy; } sendReport(report) { let filteredEvents = report.events; if (this.reportFilterFn) { filteredEvents = report.events.filter(this.reportFilterFn); } const options = { url: TRACER_INGRESS_URL, proxy: this.proxy, method: 'PUT', headers: { 'user-agent': `apollo tracer v${report.tracerApiVersion}`, }, json: { ...report, events: filteredEvents, }, }; request(options, (err) => { if (err) { console.error('Error trying to report to tracer backend:', err.message); return; } // console.log('status', response.statusCode); }); } newLoggerInstance() { const queryId = uuid.v4(); const events = []; let idCounter = 0; const startTime = (new Date()).getTime(); const startHrTime = now(); const log = (type, data = null) => { const id = idCounter++; const timestamp = now(); // const timestamp = (new Date()).getTime(); // console.log(timestamp, type, id, data); events.push({ id, timestamp, type, data }); return id; }; const report = () => { return { TRACER_APP_KEY: this.TRACER_APP_KEY, tracerApiVersion: '0.1.0', queryId, startTime, startHrTime, events, }; }; const submit = () => { if (this.sendReports) { this.sendReport(report()); } }; return { log, report, submit, }; } /* log(type, data = null) { // TODO ensure props is a valid props thingy // TODO ensure info is a valid info thingy // TODO ensure type is a valid type thingy const id = this.idCounter++; const timestamp = now(); // const timestamp = (new Date()).getTime(); console.log(timestamp, type, id, data); this.events.push({ id, timestamp, type, data }); return id; } report() { return { queryId: this.queryId, startTime: this.startTime, startHrTime: this.startHrTime, events: this.events, }; } */ } function decorateWithTracer(fn, info) { return (p, a, ctx, i) => { const startEventId = ctx.tracer.log('resolver.start', info); let result; try { result = fn(p, a, ctx, i); } catch (e) { // console.log('yeah, it errored directly'); ctx.tracer.log('resolver.end', { ...info, resolverError: { message: e.message, stack: e.stack, }, startEventId, }); throw e; } try { if (result === null) { ctx.tracer.log('resolver.end', { ...info, returnedNull: true, startEventId }); return result; } if (typeof result === 'undefined') { ctx.tracer.log('resolver.end', { ...info, returnedUndefined: true, startEventId }); return result; } if (typeof result.then === 'function') { result.then((res) => { ctx.tracer.log('resolver.end', { ...info, startEventId }); return res; }) .catch((err) => { // console.log('whoa, it threw an error!'); ctx.tracer.log('resolver.end', { ...info, startEventId }); throw err; }); } else { // console.log('did not return a promise. logging now'); ctx.tracer.log('resolver.end', { ...info, startEventId }); } return result; } catch (e) { // XXX this should basically never happen // if it does happen, we want to be able to collect these events. ctx.tracer.log('tracer.error', { ...info, result, tracerError: { message: e.message, stack: e.stack, }, startEventId, }); ctx.tracer.log('resolver.end', { ...info, startEventId }); return result; } }; } // This function modifies the schema in place to add tracing around all resolve functions function addTracingToResolvers(schema) { // XXX this is a hacky way of making sure that the schema only gets decorated // with tracer once. if (schema._apolloTracerApplied) { // console.log('Tracing already added to resolve functions. Not adding again.'); return; } // eslint-disable-next-line no-param-reassign schema._apolloTracerApplied = true; forEachField(schema, (field, typeName, fieldName) => { const functionName = `${typeName}.${fieldName}`; if (field.resolve) { // eslint-disable-next-line no-param-reassign field.resolve = decorateWithTracer( field.resolve, { type: 'resolve', functionName }, ); } }); } export { Tracer, decorateWithTracer, addTracingToResolvers };