From 7f85c87e911d117e9b0b4cb0cbc6854a3fa2a456 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 31 Jan 2022 18:13:34 -0500 Subject: [PATCH] refactor: move http client commons in util/web --- packages/datadog-plugin-http/src/client.js | 77 +----------- packages/datadog-plugin-undici/src/index.js | 133 ++------------------ packages/dd-trace/src/plugins/util/web.js | 100 +++++++++++++++ 3 files changed, 112 insertions(+), 198 deletions(-) diff --git a/packages/datadog-plugin-http/src/client.js b/packages/datadog-plugin-http/src/client.js index cc14d26bd68..f276fb9b202 100644 --- a/packages/datadog-plugin-http/src/client.js +++ b/packages/datadog-plugin-http/src/client.js @@ -8,6 +8,7 @@ const formats = require('../../../ext/formats') const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter') const analyticsSampler = require('../../dd-trace/src/analytics_sampler') const { storage } = require('../../datadog-core') +const { addErrorToSpan, getServiceName, hasAmazonSignature, client } = require('../../dd-trace/src/plugins/util/web') const HTTP_HEADERS = formats.HTTP_HEADERS const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE @@ -17,7 +18,7 @@ const SPAN_KIND = tags.SPAN_KIND const CLIENT = kinds.CLIENT function patch (http, methodName, tracer, config) { - config = normalizeConfig(tracer, config) + config = normalizeConfig(config) this.wrap(http, methodName, fn => makeRequestTrace(fn)) function makeRequestTrace (request) { @@ -84,7 +85,7 @@ function patch (http, methodName, tracer, config) { break } case 'error': - addError(span, arg) + addErrorToSpan(span, arg) case 'abort': // eslint-disable-line no-fallthrough case 'timeout': // eslint-disable-line no-fallthrough finish(req, null, span, config) @@ -119,16 +120,6 @@ function patch (http, methodName, tracer, config) { span.finish() } - function addError (span, error) { - span.addTags({ - 'error.type': error.name, - 'error.msg': error.message, - 'error.stack': error.stack - }) - - return error - } - function addRequestHeaders (req, span, config) { config.headers.forEach(key => { const value = req.getHeader(key) @@ -220,54 +211,6 @@ function patch (http, methodName, tracer, config) { } } -function getHost (options) { - if (typeof options === 'string') { - return url.parse(options).host - } - - const hostname = options.hostname || options.host || 'localhost' - const port = options.port - - return [hostname, port].filter(val => val).join(':') -} - -function getServiceName (tracer, config, options) { - if (config.splitByDomain) { - return getHost(options) - } else if (config.service) { - return config.service - } - - return `${tracer._service}-http-client` -} - -function hasAmazonSignature (options) { - if (!options) { - return false - } - - if (options.headers) { - const headers = Object.keys(options.headers) - .reduce((prev, next) => Object.assign(prev, { - [next.toLowerCase()]: options.headers[next] - }), {}) - - if (headers['x-amz-signature']) { - return true - } - - if ([].concat(headers['authorization']).some(startsWith('AWS4-HMAC-SHA256'))) { - return true - } - } - - return options.path && options.path.toLowerCase().indexOf('x-amz-signature=') !== -1 -} - -function startsWith (searchString) { - return value => String(value).startsWith(searchString) -} - function unpatch (http) { this.unwrap(http, 'request') this.unwrap(http, 'get') @@ -290,20 +233,10 @@ function getFilter (config) { return urlFilter.getFilter(config) } -function normalizeConfig (tracer, config) { +function normalizeConfig (config) { config = config.client || config - const validateStatus = getStatusValidator(config) - const propagationFilter = getFilter({ blocklist: config.propagationBlocklist }) - const headers = getHeaders(config) - const hooks = getHooks(config) - - return Object.assign({}, config, { - validateStatus, - propagationFilter, - headers, - hooks - }) + return client.normalizeConfig(config) } function getHeaders (config) { diff --git a/packages/datadog-plugin-undici/src/index.js b/packages/datadog-plugin-undici/src/index.js index d7f9e7b1627..8ca91726be4 100644 --- a/packages/datadog-plugin-undici/src/index.js +++ b/packages/datadog-plugin-undici/src/index.js @@ -8,6 +8,7 @@ const formats = require('../../../ext/formats') const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter') const analyticsSampler = require('../../dd-trace/src/analytics_sampler') const { AsyncResource, AsyncLocalStorage } = require('async_hooks') +const { addErrorToSpan, getServiceName, hasAmazonSignature, client: { normalizeConfig } } = require('../../dd-trace/src/plugins/util/web') const HTTP_HEADERS = formats.HTTP_HEADERS const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE @@ -18,15 +19,6 @@ const CLIENT = kinds.CLIENT const asyncLocalStorage = new AsyncLocalStorage() -function addError (span, error) { - span.addTags({ - 'error.type': error.name, - 'error.msg': error.message, - 'error.stack': error.stack - }) - return error -} - function parseHeaders (headers) { const pairs = headers .split('\r\n') @@ -66,7 +58,7 @@ function diagnostics (tracer, config) { ) return () => {} } - config = normalizeConfig(tracer, config) + config = normalizeConfig(config) channels.requestChannel = diagnosticsChannel.channel('undici:request:create') channels.headersChannel = diagnosticsChannel.channel( @@ -93,12 +85,14 @@ function diagnostics (tracer, config) { 'span.type': 'http', 'http.method': method, 'http.url': uri, - 'service.name': getServiceName(tracer, config, request) + 'service.name': getServiceName(tracer, config, request.origin) }) requestSpansMap.set(request, span) } - if (!(hasAmazonSignature(request) || !config.propagationFilter(uri))) { + const headers = typeof request.headers == 'string' ? parseHeaders(request.headers) : request.headers; + + if (!(hasAmazonSignature({ ...request, headers }) || !config.propagationFilter(uri))) { const injectedHeaders = {} tracer.inject(span, HTTP_HEADERS, injectedHeaders) Object.entries(injectedHeaders).forEach(([key, value]) => { @@ -111,7 +105,7 @@ function diagnostics (tracer, config) { function handleRequestError ({ request, error }) { const span = requestSpansMap.get(request) - addError(span, error) + addErrorToSpan(span, error) finish(request, null, span, config) } @@ -184,119 +178,6 @@ function finish (req, res, span, config) { span.finish() } -function getHost (options) { - return url.parse(options.origin).host -} - -function getServiceName (tracer, config, options) { - if (config.splitByDomain) { - return getHost(options) - } else if (config.service) { - return config.service - } - - return `${tracer._service}-http-client` -} - -function hasAmazonSignature (options) { - if (!options) { - return false - } - - if (options.headers) { - let headers = {} - if (typeof options.headers === 'string') { - headers = parseHeaders(options.headers) - } else { - headers = Object.keys(options.headers).reduce( - (prev, next) => - Object.assign(prev, { - [next.toLowerCase()]: options.headers[next] - }), - {} - ) - } - - if (headers['x-amz-signature']) { - return true - } - - if ( - [].concat(headers['authorization']).some(startsWith('AWS4-HMAC-SHA256')) - ) { - return true - } - } - - return ( - options.path && - options.path.toLowerCase().indexOf('x-amz-signature=') !== -1 - ) -} - -function startsWith (searchString) { - return (value) => String(value).startsWith(searchString) -} - -function getStatusValidator (config) { - if (typeof config.validateStatus === 'function') { - return config.validateStatus - } else if (config.hasOwnProperty('validateStatus')) { - log.error('Expected `validateStatus` to be a function.') - } - return (code) => code < 400 || code >= 500 -} - -function getFilter (tracer, config) { - const blocklist = tracer._url ? [getAgentFilter(tracer._url)] : [] - - config = Object.assign({}, config, { - blocklist: blocklist.concat(config.blocklist || []) - }) - - return urlFilter.getFilter(config) -} - -function getAgentFilter (url) { - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping - const agentFilter = url.href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - - return RegExp(`^${agentFilter}.*$`, 'i') -} - -function normalizeConfig (tracer, config) { - const validateStatus = getStatusValidator(config) - const filter = getFilter(tracer, config) - const propagationFilter = getFilter(tracer, { - blocklist: config.propagationBlocklist - }) - const headers = getHeaders(config) - const hooks = getHooks(config) - - return Object.assign({}, config, { - validateStatus, - filter, - propagationFilter, - headers, - hooks - }) -} - -function getHeaders (config) { - if (!Array.isArray(config.headers)) return [] - - return config.headers - .filter((key) => typeof key === 'string') - .map((key) => key.toLowerCase()) -} - -function getHooks (config) { - const noop = () => {} - const request = (config.hooks && config.hooks.request) || noop - - return { request } -} - function patch (undici, methodName, tracer, config) { this.wrap(undici, methodName, fn => makeRequestTrace(fn)) diff --git a/packages/dd-trace/src/plugins/util/web.js b/packages/dd-trace/src/plugins/util/web.js index 1a76bf039f8..6c3645a0da5 100644 --- a/packages/dd-trace/src/plugins/util/web.js +++ b/packages/dd-trace/src/plugins/util/web.js @@ -1,5 +1,6 @@ 'use strict' +const url = require('url') const uniq = require('lodash.uniq') const analyticsSampler = require('../../analytics_sampler') const FORMAT_HTTP_HEADERS = require('opentracing').FORMAT_HTTP_HEADERS @@ -29,6 +30,21 @@ const HTTP2_HEADER_AUTHORITY = ':authority' const HTTP2_HEADER_SCHEME = ':scheme' const HTTP2_HEADER_PATH = ':path' +function getHost (options) { + if (typeof options === 'string') { + return url.parse(options).host + } + + const hostname = options.hostname || options.host || 'localhost' + const port = options.port + + return [hostname, port].filter(val => val).join(':') +} + +function startsWith (searchString) { + return (value) => String(value).startsWith(searchString) +} + const web = { // Ensure the configuration has the correct structure and defaults. normalizeConfig (config) { @@ -49,6 +65,22 @@ const web = { }) }, + client: { + normalizeConfig (config) { + const validateStatus = getClientStatusValidator(config) + const propagationFilter = getFilter({ blocklist: config.propagationBlocklist }) + const headers = getHeaders(config) + const hooks = getHooks(config) + + return Object.assign({}, config, { + validateStatus, + propagationFilter, + headers, + hooks + }) + } + }, + // Start a span and activate a scope for a request. instrument (tracer, config, req, res, name, callback) { this.patch(req) @@ -144,6 +176,48 @@ const web = { } }, + getServiceName (tracer, config, options) { + if (config.splitByDomain) { + return getHost(options) + } else if (config.service) { + return config.service + } + + return `${tracer._service}-http-client` + }, + + addErrorToSpan(span, error) { + span.addTags({ + 'error.type': error.name, + 'error.msg': error.message, + 'error.stack': error.stack + }) + return error + }, + + hasAmazonSignature (options) { + if (!options) { + return false + } + + if (options.headers) { + const headers = Object.keys(options.headers) + .reduce((prev, next) => Object.assign(prev, { + [next.toLowerCase()]: options.headers[next] + }), {}) + + if (headers['x-amz-signature']) { + return true + } + + if ([].concat(headers['authorization']).some(startsWith('AWS4-HMAC-SHA256'))) { + return true + } + } + + return options.path && options.path.toLowerCase().indexOf('x-amz-signature=') !== -1 + }, + // Register a callback to run before res.end() is called. beforeEnd (req, callback) { req._datadog.beforeEnd.push(callback) @@ -449,6 +523,15 @@ function getStatusValidator (config) { return code => code < 500 } +function getClientStatusValidator (config) { + if (typeof config.validateStatus === 'function') { + return config.validateStatus + } else if (config.hasOwnProperty('validateStatus')) { + log.error('Expected `validateStatus` to be a function.') + } + return code => code < 400 || code >= 500 +} + function getHooks (config) { const noop = () => {} const request = (config.hooks && config.hooks.request) || noop @@ -466,4 +549,21 @@ function getMiddlewareSetting (config) { return true } +function getFilter (config) { + config = Object.assign({}, config, { + blocklist: config.blocklist || [] + }) + + return urlFilter.getFilter(config) +} + +function getHeaders (config) { + if (!Array.isArray(config.headers)) return [] + + return config.headers + .filter(key => typeof key === 'string') + .map(key => key.toLowerCase()) +} + + module.exports = web