lib/instrumentation/modules/@hapi/hapi.js (156 lines of code) (raw):

/* * Copyright Elasticsearch B.V. and other contributors where applicable. * Licensed under the BSD 2-Clause License; you may not use this file except in * compliance with the BSD 2-Clause License. */ 'use strict'; var semver = require('semver'); var shimmer = require('../../shimmer'); var onPreAuthSym = Symbol('ElasticAPMOnPreAuth'); // Collect simple data a Hapi `event.data` object, typically from a Hapi // 'log' or 'request' server event (https://hapi.dev/api/#server.events). This // limits to including simple property values (bool, string, number, Date) to // limit the possibility of accidentally capturing huge data in `captureError` // below. // // This implementation is based on lib/errors.js#attributesFromErr. function simpleDataFromEventData(agent, eventData) { try { let simpleRepr = simpleReprFromVal(eventData); if (simpleRepr !== undefined) { return simpleRepr; } let n = 0; const attrs = {}; const keys = Object.keys(eventData); for (let i = 0; i < keys.length; i++) { const key = keys[i]; let val = eventData[key]; simpleRepr = simpleReprFromVal(val); if (simpleRepr) { attrs[key] = simpleRepr; n++; } } return n ? attrs : undefined; } catch (err) { agent.logger.trace( 'hapi: could not gather simple attrs from event data: ' + err.message, ); } } // If `val` is a "simple" type (bool, string, number, Date), then return a // reasonable value to represent it in a JSON serialization. Otherwise, return // undefined. function simpleReprFromVal(val) { switch (typeof val) { case 'boolean': case 'string': case 'number': break; case 'object': // Ignore all objects except Dates. if ( val === null || typeof val.toISOString !== 'function' || typeof val.getTime !== 'function' ) { return; } else if (Number.isNaN(val.getTime())) { val = 'Invalid Date'; // calling toISOString() on invalid dates throws } else { val = val.toISOString(); } break; default: return; } return val; } module.exports = function (hapi, agent, { version, enabled }) { if (!enabled) { return hapi; } if (!semver.satisfies(version, '>=17.9.0 <22.0.0')) { agent.logger.debug('@hapi/hapi@%s not supported, skipping', version); return hapi; } agent.setFramework({ name: 'hapi', version, overwrite: false }); agent.logger.debug('shimming hapi.Server, hapi.server'); shimmer.massWrap(hapi, ['Server', 'server'], function (orig) { return function (options) { var res = orig.apply(this, arguments); patchServer(res); return res; }; }); function patchServer(server) { // Hooks that are always allowed if (typeof server.on === 'function') { attachEvents(server); } else if (typeof server.events.on === 'function') { attachEvents(server.events); } else { agent.logger.debug('unable to enable hapi error tracking'); } server.ext('onPreAuth', onPreAuth); server.ext('onPreResponse', onPreResponse); if (agent._conf.captureBody !== 'off') { server.ext('onPostAuth', onPostAuth); } } function attachEvents(emitter) { emitter.on('log', function (event, tags) { captureError('log', null, event, tags); }); emitter.on('request', function (req, event, tags) { captureError('request', req, event, tags); }); } function captureError(type, req, event, tags) { if (!event || !tags.error || event.channel === 'internal') { return; } // Hapi 'log' and 'request' events (https://hapi.dev/api/#server.events) // have `event.error`, `event.data`, or neither. // `agent.captureError` requires an Error instance or string for its first // arg: bias to getting that, then any other data add to `opts.custom.data`. const info = event.error || event.data; let errOrStr, data; if (info instanceof Error || typeof info === 'string') { errOrStr = info; } else if (info) { data = simpleDataFromEventData(agent, info); } if (!errOrStr) { errOrStr = 'hapi server emitted a "' + type + '" event tagged "error"'; } agent.captureError(errOrStr, { custom: { tags: event.tags, data, }, request: req && req.raw && req.raw.req, }); } function onPreAuth(request, reply) { agent.logger.debug('received hapi onPreAuth event'); // Record the fact that the preAuth extension have been called. This // info is useful later to know if this is a CORS preflight request // that is automatically handled by hapi (as those will not trigger // the onPreAuth extention) request[onPreAuthSym] = true; if (request.route) { // fingerprint was introduced in hapi 11 and is a little more // stable in case the param names change // - path example: /foo/{bar*2} // - fingerprint example: /foo/?/? var fingerprint = request.route.fingerprint || request.route.path; if (fingerprint) { var name = (request.raw && request.raw.req && request.raw.req.method) || (request.route.method && request.route.method.toUpperCase()); if (typeof name === 'string') { name = name + ' ' + fingerprint; } else { name = fingerprint; } agent._instrumentation.setDefaultTransactionName(name); } } return reply.continue; } function onPostAuth(request, reply) { if (request.payload && request.raw && request.raw.req) { // Save the parsed req body to be picked up by getContextFromRequest(). request.raw.req.payload = request.payload; } return reply.continue; } function onPreResponse(request, reply) { agent.logger.debug('received hapi onPreResponse event'); // Detection of CORS preflight requests: // There is no easy way in hapi to get the matched route for a // CORS preflight request that matches any of the autogenerated // routes created by hapi when `cors: true`. The best solution is to // detect the request "fingerprint" using the magic if-sentence below // and group all those requests into on type of transaction if ( !request[onPreAuthSym] && request.route && request.route.path === '/{p*}' && request.raw && request.raw.req && request.raw.req.method === 'OPTIONS' && request.raw.req.headers['access-control-request-method'] ) { agent._instrumentation.setDefaultTransactionName('CORS preflight'); } return reply.continue; } return hapi; };