lib/instrumentation/modules/elasticsearch.js (155 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'; const { URL, URLSearchParams } = require('url'); var shimmer = require('../shimmer'); var { getDBDestination } = require('../context'); const { getElasticsearchDbStatement } = require('../elasticsearch-shared'); const startsWithProtocolRE = /^([a-z]+:)?\/\//i; const DEFAULT_PORT = 9200; const DEFAULT_PORT_FROM_PROTO = { 'http:': 80, 'https:': 443, }; // This is an imperfect equivalent of the handling in the `Transport` // constructor and internal `Host` parsing function in the ES client. // This returns a `URL` object or null. function getTargetUrlFromTransportConfig(config) { const transportHosts = config ? config.host || config.hosts : null; if (!transportHosts) { return null; } let firstTransportHost = Array.isArray(transportHosts) ? transportHosts[0] : transportHosts; if (!firstTransportHost) { return null; } if (typeof firstTransportHost === 'string') { // "example.com:42" or "someprotocol://example.com:42" or // "someprotocol://example.com". if (!startsWithProtocolRE.test(firstTransportHost)) { firstTransportHost = 'http://' + firstTransportHost; } try { return new URL(firstTransportHost); } catch (_err) { return null; } } else if (typeof firstTransportHost === 'object') { let proto = firstTransportHost.protocol || 'http:'; if (!proto.endsWith(':')) { proto += ':'; } const hostname = firstTransportHost.hostname || firstTransportHost.host; const port = firstTransportHost.port || DEFAULT_PORT; try { return new URL(proto + '//' + hostname + ':' + port); } catch (_ignoredErr) { return null; } } return null; } module.exports = function (elasticsearch, agent, { enabled }) { if (!enabled) return elasticsearch; const ins = agent._instrumentation; const elasticsearchCaptureBodyUrlsRegExp = agent._conf.elasticsearchCaptureBodyUrlsRegExp; agent.logger.debug('shimming elasticsearch.Transport.prototype.request'); shimmer.wrap( elasticsearch.Transport && elasticsearch.Transport.prototype, 'request', wrapRequest, ); return elasticsearch; function wrapRequest(original) { return function wrappedRequest(params, cb) { var span = ins.createSpan(null, 'db', 'elasticsearch', 'request', { exitSpan: true, }); var id = span && span.transaction.id; var method = params && params.method; var path = params && params.path; agent.logger.debug( 'intercepted call to elasticsearch.Transport.prototype.request %o', { id, method, path }, ); if (span && method && path) { span.name = `Elasticsearch: ${method} ${path}`; // Set DB context. const dbContext = { type: 'elasticsearch', }; const statement = getElasticsearchDbStatement( path, params && params.body, elasticsearchCaptureBodyUrlsRegExp, ); if (statement) { dbContext.statement = statement; } span.setDbContext(dbContext); // Get the remote host information from elasticsearch Transport options. const targetUrl = getTargetUrlFromTransportConfig(this._config); let port = targetUrl && targetUrl.port; if (!port && targetUrl) { port = DEFAULT_PORT_FROM_PROTO[targetUrl.protocol]; } span._setDestinationContext( getDBDestination(targetUrl && targetUrl.hostname, port), ); if (targetUrl) { targetUrl.pathname = path; targetUrl.search = new URLSearchParams(params.query).toString(); span.setHttpContext({ url: targetUrl.toString(), }); } const parentRunContext = ins.currRunContext(); const spanRunContext = parentRunContext.enterSpan(span); if (typeof cb === 'function') { var args = Array.prototype.slice.call(arguments); args[1] = function () { span.end(); ins.withRunContext(parentRunContext, cb, this, ...arguments); }; return ins.withRunContext(spanRunContext, original, this, ...args); } else { const originalPromise = ins.withRunContext( spanRunContext, original, this, ...arguments, ); const descriptors = Object.getOwnPropertyDescriptors(originalPromise); delete descriptors.domain; const inspectedPromise = originalPromise.then( function (value) { span.end(); return value; }, function (err) { span.end(); throw err; }, ); Object.defineProperties(inspectedPromise, descriptors); // we have to properly end the span when user aborts the request shimmer.wrap( inspectedPromise, 'abort', function wrapAbort(originalAbort) { return function wrappedAbort() { if (span.ended) return; agent.logger.debug( 'intercepted call to elasticsearch.Transport.request.abort %o', { id, method, path }, ); const originalReturn = originalAbort.apply(this, args); span.end(); return originalReturn; }; }, ); return inspectedPromise; } } else { agent.logger.debug('could not instrument elasticsearch request %o', { id, }); return original.apply(this, arguments); } }; } };