apollo-tracer
Version:
Trace collection library for Node.js GraphQL servers
235 lines (203 loc) • 8.15 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.addTracingToResolvers = exports.decorateWithTracer = exports.Tracer = undefined;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _performanceNow = require('performance-now');
var _performanceNow2 = _interopRequireDefault(_performanceNow);
var _nodeUuid = require('node-uuid');
var _nodeUuid2 = _interopRequireDefault(_nodeUuid);
var _request = require('request');
var _request2 = _interopRequireDefault(_request);
var _graphqlTools = require('graphql-tools');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var TRACER_INGRESS_URL = 'https://nim-test-ingress.appspot.com';
var Tracer = function () {
// TODO make sure Tracer can NEVER crash the server.
// maybe wrap everything in try/catch, but need to test that.
function Tracer(_ref) {
var TRACER_APP_KEY = _ref.TRACER_APP_KEY;
var _ref$sendReports = _ref.sendReports;
var sendReports = _ref$sendReports === undefined ? true : _ref$sendReports;
var reportFilterFn = _ref.reportFilterFn;
var proxy = _ref.proxy;
_classCallCheck(this, Tracer);
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 = (0, _performanceNow2.default)();
this.sendReports = sendReports;
this.reportFilterFn = reportFilterFn;
this.proxy = proxy;
}
_createClass(Tracer, [{
key: 'sendReport',
value: function sendReport(report) {
var filteredEvents = report.events;
if (this.reportFilterFn) {
filteredEvents = report.events.filter(this.reportFilterFn);
}
var options = {
url: TRACER_INGRESS_URL,
proxy: this.proxy,
method: 'PUT',
headers: {
'user-agent': 'apollo tracer v' + report.tracerApiVersion
},
json: _extends({}, report, {
events: filteredEvents
})
};
(0, _request2.default)(options, function (err) {
if (err) {
console.error('Error trying to report to tracer backend:', err.message);
return;
}
// console.log('status', response.statusCode);
});
}
}, {
key: 'newLoggerInstance',
value: function newLoggerInstance() {
var _this = this;
var queryId = _nodeUuid2.default.v4();
var events = [];
var idCounter = 0;
var startTime = new Date().getTime();
var startHrTime = (0, _performanceNow2.default)();
var log = function log(type) {
var data = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1];
var id = idCounter++;
var timestamp = (0, _performanceNow2.default)();
// const timestamp = (new Date()).getTime();
// console.log(timestamp, type, id, data);
events.push({ id: id, timestamp: timestamp, type: type, data: data });
return id;
};
var report = function report() {
return {
TRACER_APP_KEY: _this.TRACER_APP_KEY,
tracerApiVersion: '0.1.0',
queryId: queryId,
startTime: startTime,
startHrTime: startHrTime,
events: events
};
};
var submit = function submit() {
if (_this.sendReports) {
_this.sendReport(report());
}
};
return {
log: log,
report: report,
submit: 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,
};
} */
}]);
return Tracer;
}();
function decorateWithTracer(fn, info) {
return function (p, a, ctx, i) {
var startEventId = ctx.tracer.log('resolver.start', info);
var result = void 0;
try {
result = fn(p, a, ctx, i);
} catch (e) {
// console.log('yeah, it errored directly');
ctx.tracer.log('resolver.end', _extends({}, info, {
resolverError: {
message: e.message,
stack: e.stack
},
startEventId: startEventId
}));
throw e;
}
try {
if (result === null) {
ctx.tracer.log('resolver.end', _extends({}, info, { returnedNull: true, startEventId: startEventId }));
return result;
}
if (typeof result === 'undefined') {
ctx.tracer.log('resolver.end', _extends({}, info, { returnedUndefined: true, startEventId: startEventId }));
return result;
}
if (typeof result.then === 'function') {
result.then(function (res) {
ctx.tracer.log('resolver.end', _extends({}, info, { startEventId: startEventId }));
return res;
}).catch(function (err) {
// console.log('whoa, it threw an error!');
ctx.tracer.log('resolver.end', _extends({}, info, { startEventId: startEventId }));
throw err;
});
} else {
// console.log('did not return a promise. logging now');
ctx.tracer.log('resolver.end', _extends({}, info, { startEventId: 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', _extends({}, info, {
result: result,
tracerError: {
message: e.message,
stack: e.stack
},
startEventId: startEventId
}));
ctx.tracer.log('resolver.end', _extends({}, info, { startEventId: 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;
(0, _graphqlTools.forEachField)(schema, function (field, typeName, fieldName) {
var functionName = typeName + '.' + fieldName;
if (field.resolve) {
// eslint-disable-next-line no-param-reassign
field.resolve = decorateWithTracer(field.resolve, { type: 'resolve', functionName: functionName });
}
});
}
exports.Tracer = Tracer;
exports.decorateWithTracer = decorateWithTracer;
exports.addTracingToResolvers = addTracingToResolvers;