diff --git a/.github/workflows/serverless.yml b/.github/workflows/serverless.yml index 5190c981fc9..c137d75c515 100644 --- a/.github/workflows/serverless.yml +++ b/.github/workflows/serverless.yml @@ -58,7 +58,7 @@ jobs: localstack-legacy: image: localstack/localstack:1.1.0 ports: - - "127.0.0.1:4567:4567" # Edge + - '127.0.0.1:4567:4567' # Edge env: LOCALSTACK_SERVICES: dynamodb,kinesis,s3,sqs,sns,redshift,route53,logs,serverless EXTRA_CORS_ALLOWED_HEADERS: x-amz-request-id,x-amzn-requestid,x-amz-id-2 @@ -88,31 +88,39 @@ jobs: suffix: plugins-${{ github.job }}-${{ matrix.node-version }} - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + aws-apigateway: + runs-on: ubuntu-latest + env: + PLUGINS: aws-apigateway + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: ./.github/actions/plugins/test-and-upstream + azure-functions: runs-on: ubuntu-latest services: - azurite: - image: mcr.microsoft.com/azure-storage/azurite:3.34.0 - ports: - - "127.0.0.1:10000:10000" - - "127.0.0.1:10001:10001" - - "127.0.0.1:10002:10002" - azureservicebusemulator: - image: mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2 - ports: - - "127.0.0.1:5672:5672" - - "127.0.0.1:5300:5300" - env: - ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: "Localtestpass1!" - SQL_SERVER: azuresqledge - azuresqledge: - image: mcr.microsoft.com/azure-sql-edge:1.0.7 - ports: - - "127.0.0.1:1433:1433" - env: - ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: "Localtestpass1!" + azurite: + image: mcr.microsoft.com/azure-storage/azurite:3.34.0 + ports: + - '127.0.0.1:10000:10000' + - '127.0.0.1:10001:10001' + - '127.0.0.1:10002:10002' + azureservicebusemulator: + image: mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2 + ports: + - '127.0.0.1:5672:5672' + - '127.0.0.1:5300:5300' + env: + ACCEPT_EULA: 'Y' + MSSQL_SA_PASSWORD: 'Localtestpass1!' + SQL_SERVER: azuresqledge + azuresqledge: + image: mcr.microsoft.com/azure-sql-edge:1.0.7 + ports: + - '127.0.0.1:1433:1433' + env: + ACCEPT_EULA: 'Y' + MSSQL_SA_PASSWORD: 'Localtestpass1!' env: PLUGINS: azure-functions SERVICES: azureservicebusemulator,azuresqledge @@ -127,22 +135,22 @@ jobs: azure-service-bus: runs-on: ubuntu-latest services: - azureservicebusemulator: - image: mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2 - ports: - - "127.0.0.1:5672:5672" - - "127.0.0.1:5300:5300" - env: - ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: "Localtestpass1!" - SQL_SERVER: azuresqledge - azuresqledge: - image: mcr.microsoft.com/azure-sql-edge:1.0.7 - ports: - - "127.0.0.1:1433:1433" - env: - ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: "Localtestpass1!" + azureservicebusemulator: + image: mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2 + ports: + - '127.0.0.1:5672:5672' + - '127.0.0.1:5300:5300' + env: + ACCEPT_EULA: 'Y' + MSSQL_SA_PASSWORD: 'Localtestpass1!' + SQL_SERVER: azuresqledge + azuresqledge: + image: mcr.microsoft.com/azure-sql-edge:1.0.7 + ports: + - '127.0.0.1:1433:1433' + env: + ACCEPT_EULA: 'Y' + MSSQL_SA_PASSWORD: 'Localtestpass1!' env: PLUGINS: azure-service-bus SERVICES: azureservicebusemulator,azuresqledge diff --git a/.github/workflows/system-tests.yml b/.github/workflows/system-tests.yml index f1cc093e83e..9613b063fad 100644 --- a/.github/workflows/system-tests.yml +++ b/.github/workflows/system-tests.yml @@ -38,10 +38,11 @@ jobs: id-token: write packages: write with: + ref: conti/fix-js-web-err library: nodejs binaries_artifact: system_tests_binaries - desired_execution_time: 300 # 5 minutes + desired_execution_time: 300 # 5 minutes scenarios_groups: tracer-release - excluded_scenarios: APM_TRACING_E2E_OTEL,APM_TRACING_E2E_SINGLE_SPAN # require AWS and datadog credentials + excluded_scenarios: APM_TRACING_E2E_OTEL,APM_TRACING_E2E_SINGLE_SPAN # require AWS and datadog credentials parametric_job_count: 8 skip_empty_scenarios: true diff --git a/packages/datadog-instrumentations/src/hapi.js b/packages/datadog-instrumentations/src/hapi.js index a7f04a6e470..0c229de644c 100644 --- a/packages/datadog-instrumentations/src/hapi.js +++ b/packages/datadog-instrumentations/src/hapi.js @@ -108,7 +108,7 @@ function onPreResponse (request, h) { const req = request.raw.req if (request.response instanceof Error) { - errorChannel.publish(request.response) + errorChannel.publish({ req, error: request.response }) } if (request.route) { diff --git a/packages/datadog-plugin-aws-apigateway/src/index.js b/packages/datadog-plugin-aws-apigateway/src/index.js new file mode 100644 index 00000000000..acff124ba49 --- /dev/null +++ b/packages/datadog-plugin-aws-apigateway/src/index.js @@ -0,0 +1,9 @@ +'use strict' + +const InferredProxyPlugin = require('../../datadog-plugin-inferred-proxy/src') + +class ApiGatewayPlugin extends InferredProxyPlugin { + static id = 'aws-apigateway' +} + +module.exports = ApiGatewayPlugin diff --git a/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js b/packages/datadog-plugin-aws-apigateway/test/index.spec.js similarity index 75% rename from packages/dd-trace/test/plugins/util/inferred_proxy.spec.js rename to packages/datadog-plugin-aws-apigateway/test/index.spec.js index 14d85ccb5ce..03fd78f6967 100644 --- a/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js +++ b/packages/datadog-plugin-aws-apigateway/test/index.spec.js @@ -1,8 +1,6 @@ 'use strict' -require('../../setup/tap') - -const agent = require('../agent') +const agent = require('../../dd-trace/test/plugins/agent') const { expect } = require('chai') const axios = require('axios') const { Agent } = require('http') @@ -13,62 +11,22 @@ const httpClient = axios.create({ timeout: 5000 }) -describe('Inferred Proxy Spans', function () { +describe('Plugin', function () { let http let appListener let controller let port - // tap was throwing timeout errors when trying to use hooks like `before`, so instead we just use this function - // and call before the test starts - const loadTest = async function ({ inferredProxyServicesEnabled = true } = {}) { - const options = { - inferredProxyServicesEnabled, - service: 'aws-server' - } - - await agent.load( - ['http', 'dns', 'net'], - [{ client: false }, { enabled: false }, { enabled: false }], - options - ) - - // we can't force re-init the tracer, so we have to set the config manually - const tracer = require('../../../../dd-trace').init(options) - tracer._tracer._config.inferredProxyServicesEnabled = inferredProxyServicesEnabled - - http = require('http') - - const server = new http.Server(async (req, res) => { - controller && await controller(req, res) - if (req.url === '/error') { - res.statusCode = 500 - res.end(JSON.stringify({ message: 'ERROR' })) - } else { - res.writeHead(200) - res.end(JSON.stringify({ message: 'OK' })) - } - }) - - // Force close connections when server closes - const connections = new Set() - server.on('connection', (connection) => { - connections.add(connection) - connection.on('close', () => { - connections.delete(connection) - }) - }) - - return new Promise((resolve, reject) => { - appListener = server.listen(0, '127.0.0.1', () => { - port = server.address().port - appListener._connections = connections - resolve() - }) - }) + const inferredHeaders = { + 'x-dd-proxy': 'aws-apigateway', + 'x-dd-proxy-request-time-ms': '1729780025473', + 'x-dd-proxy-path': '/test', + 'x-dd-proxy-httpmethod': 'GET', + 'x-dd-proxy-domain-name': 'example.com', + 'x-dd-proxy-stage': 'dev' } - const cleanupTest = async function () { + afterEach(async () => { controller = null if (appListener) { @@ -92,25 +50,54 @@ describe('Inferred Proxy Spans', function () { } await agent.close() - } - - const inferredHeaders = { - 'x-dd-proxy': 'aws-apigateway', - 'x-dd-proxy-request-time-ms': '1729780025473', - 'x-dd-proxy-path': '/test', - 'x-dd-proxy-httpmethod': 'GET', - 'x-dd-proxy-domain-name': 'example.com', - 'x-dd-proxy-stage': 'dev' - } - - afterEach(async () => { - await cleanupTest() }) describe('without configuration', () => { - it('should create a parent span and a child span for a 200', async () => { - await loadTest({}) + beforeEach(async () => { + const options = { + inferredProxyServicesEnabled: true, + service: 'aws-server' + } + + await agent.load( + ['http', 'dns', 'net', 'tcp', 'aws-apigateway'], + [{ client: false }, { enabled: false }, { enabled: false }, { enabled: false }, { enabled: true }], + options + ) + + require('../../dd-trace').init(options) + http = require('http') + + const server = new http.Server(async (req, res) => { + controller && await controller(req, res) + if (req.url === '/error') { + res.statusCode = 500 + res.end(JSON.stringify({ message: 'ERROR' })) + } else { + res.writeHead(200) + res.end(JSON.stringify({ message: 'OK' })) + } + }) + // Force close connections when server closes + const connections = new Set() + server.on('connection', (connection) => { + connections.add(connection) + connection.on('close', () => { + connections.delete(connection) + }) + }) + + return new Promise((resolve, reject) => { + appListener = server.listen(0, '127.0.0.1', () => { + port = server.address().port + appListener._connections = connections + resolve() + }) + }) + }) + + it('should create a parent span and a child span for a 200', async () => { await httpClient.get(`http://127.0.0.1:${port}/`, { headers: inferredHeaders }) @@ -148,8 +135,6 @@ describe('Inferred Proxy Spans', function () { }) it('should create a parent span and a child span for an error', async () => { - await loadTest({}) - await httpClient.get(`http://127.0.0.1:${port}/error`, { headers: inferredHeaders, validateStatus: function (status) { @@ -188,8 +173,6 @@ describe('Inferred Proxy Spans', function () { }) it('should not create an API Gateway span if all necessary headers are missing', async () => { - await loadTest({}) - await httpClient.get(`http://127.0.0.1:${port}/no-aws-headers`, { headers: {} }) @@ -213,8 +196,6 @@ describe('Inferred Proxy Spans', function () { }) it('should not create an API Gateway span if missing the proxy system header', async () => { - await loadTest({}) - // remove x-dd-proxy from headers const { 'x-dd-proxy': _, ...newHeaders } = inferredHeaders @@ -242,9 +223,54 @@ describe('Inferred Proxy Spans', function () { }) describe('with configuration', function () { - it('should not create a span when configured to be off', async () => { - await loadTest({ inferredProxyServicesEnabled: false }) + beforeEach(async () => { + const options = { + inferredProxyServicesEnabled: false, + service: 'aws-server' + } + // we can't force re-init the tracer, so we have to set the config manually + const tracer = require('../../dd-trace').init(options) + tracer._tracer._config.inferredProxyServicesEnabled = false + + await agent.load( + ['http', 'dns', 'net', 'aws-apigateway'], + [{ client: false }, { enabled: false }, { enabled: false }, { enabled: true }], + options + ) + + http = require('http') + + const server = new http.Server(async (req, res) => { + controller && await controller(req, res) + if (req.url === '/error') { + res.statusCode = 500 + res.end(JSON.stringify({ message: 'ERROR' })) + } else { + res.writeHead(200) + res.end(JSON.stringify({ message: 'OK' })) + } + }) + + // Force close connections when server closes + const connections = new Set() + server.on('connection', (connection) => { + connections.add(connection) + connection.on('close', () => { + connections.delete(connection) + }) + }) + + return new Promise((resolve, reject) => { + appListener = server.listen(0, '127.0.0.1', () => { + port = server.address().port + appListener._connections = connections + resolve() + }) + }) + }) + + it('should not create a span when configured to be off', async () => { await httpClient.get(`http://127.0.0.1:${port}/configured-off`, { headers: inferredHeaders }) diff --git a/packages/datadog-plugin-azure-functions/src/index.js b/packages/datadog-plugin-azure-functions/src/index.js index 23355bd11a3..c9dc911754e 100644 --- a/packages/datadog-plugin-azure-functions/src/index.js +++ b/packages/datadog-plugin-azure-functions/src/index.js @@ -1,8 +1,7 @@ 'use strict' +const web = require('../../datadog-plugin-web/src/utils') const TracingPlugin = require('../../dd-trace/src/plugins/tracing') -const serverless = require('../../dd-trace/src/plugins/util/serverless') -const web = require('../../dd-trace/src/plugins/util/web') const triggerMap = { deleteRequest: 'Http', @@ -51,13 +50,13 @@ class AzureFunctionsPlugin extends TracingPlugin { headers: Object.fromEntries(httpRequest.headers), url: path } - const context = web.patch(req) + const context = web.patch(req, this.config) context.config = this.config context.paths = [path] context.res = { statusCode: result.status } context.span = ctx.currentStore.span - serverless.finishSpan(context) + web.finishSpan(context) // Fallback for other trigger types } else { super.finish() diff --git a/packages/datadog-plugin-connect/src/index.js b/packages/datadog-plugin-connect/src/index.js index 9453fe5168e..0179be94e92 100644 --- a/packages/datadog-plugin-connect/src/index.js +++ b/packages/datadog-plugin-connect/src/index.js @@ -4,14 +4,6 @@ const RouterPlugin = require('../../datadog-plugin-router/src') class ConnectPlugin extends RouterPlugin { static id = 'connect' - - constructor (...args) { - super(...args) - - this.addSub('apm:connect:request:handle', ({ req }) => { - this.setFramework(req, 'connect', this.config) - }) - } } module.exports = ConnectPlugin diff --git a/packages/datadog-plugin-express/src/code_origin.js b/packages/datadog-plugin-express/src/code_origin.js index 19755bc1012..bb0ae12a271 100644 --- a/packages/datadog-plugin-express/src/code_origin.js +++ b/packages/datadog-plugin-express/src/code_origin.js @@ -1,10 +1,9 @@ 'use strict' const { entryTags } = require('../../datadog-code-origin') -const Plugin = require('../../dd-trace/src/plugins/plugin') -const web = require('../../dd-trace/src/plugins/util/web') +const WebPlugin = require('../../datadog-plugin-web/src') -class ExpressCodeOriginForSpansPlugin extends Plugin { +class ExpressCodeOriginForSpansPlugin extends WebPlugin { static id = 'express' constructor (...args) { @@ -15,7 +14,7 @@ class ExpressCodeOriginForSpansPlugin extends Plugin { this.addSub('apm:express:middleware:enter', ({ req, layer }) => { const tags = layerTags.get(layer) if (!tags) return - web.getContext(req).span?.addTags(tags) + this.getContext(req).span?.addTags(tags) }) this.addSub('apm:express:route:added', ({ topOfStackFunc, layer }) => { diff --git a/packages/datadog-plugin-express/src/tracing.js b/packages/datadog-plugin-express/src/tracing.js index ddedf3b3b1f..eca31fdb5f1 100644 --- a/packages/datadog-plugin-express/src/tracing.js +++ b/packages/datadog-plugin-express/src/tracing.js @@ -4,14 +4,6 @@ const RouterPlugin = require('../../datadog-plugin-router/src') class ExpressTracingPlugin extends RouterPlugin { static id = 'express' - - constructor (...args) { - super(...args) - - this.addSub('apm:express:request:handle', ({ req }) => { - this.setFramework(req, 'express', this.config) - }) - } } module.exports = ExpressTracingPlugin diff --git a/packages/datadog-plugin-fastify/src/code_origin.js b/packages/datadog-plugin-fastify/src/code_origin.js index 3b8bf1ef15d..d85187efed6 100644 --- a/packages/datadog-plugin-fastify/src/code_origin.js +++ b/packages/datadog-plugin-fastify/src/code_origin.js @@ -1,12 +1,11 @@ 'use strict' const { entryTags } = require('../../datadog-code-origin') -const Plugin = require('../../dd-trace/src/plugins/plugin') -const web = require('../../dd-trace/src/plugins/util/web') +const WebPlugin = require('../../datadog-plugin-web/src') const kCodeOriginForSpansTagsSym = Symbol('datadog.codeOriginForSpansTags') -class FastifyCodeOriginForSpansPlugin extends Plugin { +class FastifyCodeOriginForSpansPlugin extends WebPlugin { static id = 'fastify' constructor (...args) { @@ -15,7 +14,7 @@ class FastifyCodeOriginForSpansPlugin extends Plugin { this.addSub('apm:fastify:request:handle', ({ req, routeConfig }) => { const tags = routeConfig?.[kCodeOriginForSpansTagsSym] if (!tags) return - const context = web.getContext(req) + const context = this.getContext(req) context.span?.addTags(tags) }) diff --git a/packages/datadog-plugin-fastify/src/tracing.js b/packages/datadog-plugin-fastify/src/tracing.js index debe8cf61b9..afaffbf4c98 100644 --- a/packages/datadog-plugin-fastify/src/tracing.js +++ b/packages/datadog-plugin-fastify/src/tracing.js @@ -9,10 +9,6 @@ class FastifyTracingPlugin extends RouterPlugin { constructor (...args) { super(...args) - this.addSub('apm:fastify:request:handle', ({ req }) => { - this.setFramework(req, 'fastify', this.config) - }) - this.addBind('datadog:fastify:pre-parsing:start', getParentStore) this.addBind('datadog:fastify:pre-validation:start', getParentStore) diff --git a/packages/datadog-plugin-find-my-way/src/index.js b/packages/datadog-plugin-find-my-way/src/index.js index f8d152d2060..217cb9950c9 100644 --- a/packages/datadog-plugin-find-my-way/src/index.js +++ b/packages/datadog-plugin-find-my-way/src/index.js @@ -1,18 +1,9 @@ 'use strict' -const Plugin = require('../../dd-trace/src/plugins/plugin') -const web = require('../../dd-trace/src/plugins/util/web') +const WebPlugin = require('../../datadog-plugin-web/src') -class FindMyWayPlugin extends Plugin { +class FindMyWayPlugin extends WebPlugin { static id = 'find-my-way' - - constructor (...args) { - super(...args) - - this.addSub('apm:find-my-way:request:route', ({ req, route }) => { - web.setRoute(req, route) - }) - } } module.exports = FindMyWayPlugin diff --git a/packages/datadog-plugin-hapi/src/index.js b/packages/datadog-plugin-hapi/src/index.js index 2565de4e4a8..c0bbf5806d4 100644 --- a/packages/datadog-plugin-hapi/src/index.js +++ b/packages/datadog-plugin-hapi/src/index.js @@ -2,13 +2,12 @@ const { storage } = require('../../datadog-core') const RouterPlugin = require('../../datadog-plugin-router/src') -const web = require('../../dd-trace/src/plugins/util/web') class HapiPlugin extends RouterPlugin { static id = 'hapi' constructor (...args) { - super(...args) + super(...args, false) this._requestSpans = new WeakMap() @@ -16,17 +15,13 @@ class HapiPlugin extends RouterPlugin { const store = storage('legacy').getStore() const span = store && store.span - this.setFramework(req, 'hapi', this.config) + this.setFramework(req, 'hapi') this._requestSpans.set(req, span) }) - this.addSub('apm:hapi:request:route', ({ req, route }) => { - web.setRoute(req, route) - }) - - this.addSub('apm:hapi:request:error', error => { + this.addSub('apm:hapi:request:error', ({ req, error }) => { if (!error || !error.isBoom || !this.config.validateStatus(error.output.statusCode)) { - this.addError(error) + this.addError(req, error) } }) diff --git a/packages/datadog-plugin-hono/src/index.js b/packages/datadog-plugin-hono/src/index.js index cec77fdd004..5471ea91784 100644 --- a/packages/datadog-plugin-hono/src/index.js +++ b/packages/datadog-plugin-hono/src/index.js @@ -1,26 +1,9 @@ 'use strict' const RouterPlugin = require('../../datadog-plugin-router/src') -const web = require('../../dd-trace/src/plugins/util/web') class HonoPlugin extends RouterPlugin { static id = 'hono' - - constructor (...args) { - super(...args) - - this.addSub('apm:hono:request:handle', ({ req }) => { - this.setFramework(req, 'hono', this.config) - }) - - this.addSub('apm:hono:request:route', ({ req, route }) => { - web.setRoute(req, route) - }) - - this.addSub('apm:hono:request:error', ({ req, error }) => { - web.addError(req, error) - }) - } } module.exports = HonoPlugin diff --git a/packages/datadog-plugin-http/src/server.js b/packages/datadog-plugin-http/src/server.js index 7fad4f20133..91db229a9ef 100644 --- a/packages/datadog-plugin-http/src/server.js +++ b/packages/datadog-plugin-http/src/server.js @@ -1,15 +1,15 @@ 'use strict' -const ServerPlugin = require('../../dd-trace/src/plugins/server') +const WebPlugin = require('../../datadog-plugin-web/src') const { storage } = require('../../datadog-core') -const web = require('../../dd-trace/src/plugins/util/web') const { incomingHttpRequestStart, incomingHttpRequestEnd } = require('../../dd-trace/src/appsec/channels') const { COMPONENT } = require('../../dd-trace/src/constants') -class HttpServerPlugin extends ServerPlugin { +class HttpServerPlugin extends WebPlugin { static id = 'http' - static prefix = 'apm:http:server:request' + static type = 'web' + static kind = 'server' constructor (...args) { super(...args) @@ -19,12 +19,10 @@ class HttpServerPlugin extends ServerPlugin { start ({ req, res, abortController }) { const store = storage('legacy').getStore() - const span = web.startSpan( - this.tracer, - { - ...this.config, - service: this.config.service || this.serviceName(), - }, + + this.config.service = this.config.service || this.serviceName() + + const span = this.startSpan( req, res, this.operationName() @@ -35,10 +33,10 @@ class HttpServerPlugin extends ServerPlugin { this._parentStore = store this.enter(span, { ...store, req, res }) - const context = web.getContext(req) + const context = this.getContext(req) if (!context.instrumented) { - context.res.writeHead = web.wrapWriteHead(context) + context.res.writeHead = this.wrapWriteHead(context) context.instrumented = true } @@ -48,11 +46,11 @@ class HttpServerPlugin extends ServerPlugin { } error (error) { - web.addError(error) + this.addError(error) } finish ({ req }) { - const context = web.getContext(req) + const context = this.getContext(req) if (!context || !context.res) return // Not created by a http.Server instance. @@ -60,7 +58,7 @@ class HttpServerPlugin extends ServerPlugin { incomingHttpRequestEnd.publish({ req, res: context.res }) } - web.finishAll(context) + this.finishAll(context) } exit ({ req }) { @@ -70,7 +68,7 @@ class HttpServerPlugin extends ServerPlugin { } configure (config) { - return super.configure(web.normalizeConfig(config)) + return super.configure(this.normalizeConfig(config)) } } diff --git a/packages/datadog-plugin-http2/src/server.js b/packages/datadog-plugin-http2/src/server.js index c7bdb6d5274..e0de8e2c5ae 100644 --- a/packages/datadog-plugin-http2/src/server.js +++ b/packages/datadog-plugin-http2/src/server.js @@ -2,29 +2,24 @@ // Plugin temporarily disabled. See https://github.com/DataDog/dd-trace-js/issues/312 -const ServerPlugin = require('../../dd-trace/src/plugins/server') -const web = require('../../dd-trace/src/plugins/util/web') +const WebPlugin = require('../../datadog-plugin-web/src') const { COMPONENT } = require('../../dd-trace/src/constants') -class Http2ServerPlugin extends ServerPlugin { +class Http2ServerPlugin extends WebPlugin { + static id = 'http2' + static prefix = 'apm:http2:server:request' + constructor (tracer, config) { super(tracer, config) - this.addBind('apm:http2:server:response:emit', this.bindEmit) - } - - static id = 'http2' + this.addBind('apm:http2:server:response:emit', this.bindEmit.bind(this)) - static prefix = 'apm:http2:server:request' + this.configure({ service: config.service || this.serviceName() }) + } bindStart (ctx) { const { req, res } = ctx - const span = web.startSpan( - this.tracer, - { - ...this.config, - service: this.config.service || this.serviceName() - }, + const span = this.startSpan( req, res, this.operationName(), @@ -37,10 +32,10 @@ class Http2ServerPlugin extends ServerPlugin { ctx.currentStore.req = req ctx.currentStore.res = res - const context = web.getContext(req) + const context = this.getContext(req) if (!context.instrumented) { - context.res.writeHead = web.wrapWriteHead(context) + context.res.writeHead = this.wrapWriteHead(context) context.instrumented = true } @@ -52,21 +47,17 @@ class Http2ServerPlugin extends ServerPlugin { const { req } = ctx - const context = web.getContext(req) + const context = this.getContext(req) if (!context || !context.res) return // Not created by a http.Server instance. - web.finishAll(context) + this.finishAll(context) return ctx.currentStore } error (error) { - web.addError(error) - } - - configure (config) { - return super.configure(web.normalizeConfig(config)) + this.addError(error) } } diff --git a/packages/datadog-plugin-inferred-proxy/src/index.js b/packages/datadog-plugin-inferred-proxy/src/index.js new file mode 100644 index 00000000000..82124100184 --- /dev/null +++ b/packages/datadog-plugin-inferred-proxy/src/index.js @@ -0,0 +1,129 @@ +'use strict' + +const log = require('../../dd-trace/src/log') +const tags = require('../../../ext/tags') +const dc = require('dc-polyfill') +const TracingPlugin = require('../../dd-trace/src/plugins/tracing') + +const RESOURCE_NAME = tags.RESOURCE_NAME +const SPAN_TYPE = tags.SPAN_TYPE +const HTTP_URL = tags.HTTP_URL +const HTTP_METHOD = tags.HTTP_METHOD + +const PROXY_HEADER_SYSTEM = 'x-dd-proxy' +const PROXY_HEADER_START_TIME_MS = 'x-dd-proxy-request-time-ms' +const PROXY_HEADER_PATH = 'x-dd-proxy-path' +const PROXY_HEADER_HTTPMETHOD = 'x-dd-proxy-httpmethod' +const PROXY_HEADER_DOMAIN = 'x-dd-proxy-domain-name' +const PROXY_HEADER_STAGE = 'x-dd-proxy-stage' + +const loadChannel = dc.channel('dd-trace:instrumentation:load') + +class InferredProxyPlugin extends TracingPlugin { + static id = 'inferred-proxy' + static supportedProxies = { + 'aws-apigateway': { + spanName: 'aws.apigateway', + component: 'aws-apigateway' + } + } + + constructor (...args) { + super(...args) + this.addSub(`apm:${this.constructor.id}:request:handle`, this.startSpan.bind(this)) + } + + static maybeCreateInferredProxySpan (config, req, reqCtx, childOf, traceCtx) { + if (!config?.inferredProxyServicesEnabled) { + return + } + const proxyContext = InferredProxyPlugin.extractInferredProxyContext(req.headers) + if (!proxyContext) { + return + } + + const channel = dc.channel(`apm:${proxyContext.proxySystemName}:request:handle`) + + if (!channel.hasSubscribers) { + loadChannel.publish({ name: proxyContext.proxySystemName }) + } + + return channel.publish({ reqCtx, proxyContext, childOf, traceCtx }) + } + + startSpan ({ reqCtx, proxyContext, childOf, traceCtx }) { + const proxySpanInfo = InferredProxyPlugin.supportedProxies[proxyContext.proxySystemName] + + log.debug('Successfully extracted inferred span info %s for proxy:', proxyContext, proxyContext.proxySystemName) + + const span = super.startSpan(proxySpanInfo.spanName, { + childOf, + type: 'web', + startTime: proxyContext.requestTime, + integrationName: proxySpanInfo.component, + meta: { + service: proxyContext.domainName || this.config.service, + component: proxySpanInfo.component, + [SPAN_TYPE]: 'web', + [HTTP_METHOD]: proxyContext.method, + [HTTP_URL]: proxyContext.domainName + proxyContext.path, + stage: proxyContext.stage + } + }, traceCtx) + + reqCtx.inferredProxySpan = span + childOf = span + + log.debug('Successfully created inferred proxy span.') + + this.setInferredProxySpanTags(span, proxyContext) + + return childOf + } + + setInferredProxySpanTags (span, proxyContext) { + span.setTag(RESOURCE_NAME, `${proxyContext.method} ${proxyContext.path}`) + span.setTag('_dd.inferred_span', 1) + return span + } + + static extractInferredProxyContext (headers) { + if (!(PROXY_HEADER_START_TIME_MS in headers)) { + return null + } + + if (!(PROXY_HEADER_SYSTEM in headers && headers[PROXY_HEADER_SYSTEM] in InferredProxyPlugin.supportedProxies)) { + log.debug('Received headers to create inferred proxy span but headers include an unsupported proxy type', headers) + return null + } + + return { + requestTime: headers[PROXY_HEADER_START_TIME_MS] + ? Number.parseInt(headers[PROXY_HEADER_START_TIME_MS], 10) + : null, + method: headers[PROXY_HEADER_HTTPMETHOD], + path: headers[PROXY_HEADER_PATH], + stage: headers[PROXY_HEADER_STAGE], + domainName: headers[PROXY_HEADER_DOMAIN], + proxySystemName: headers[PROXY_HEADER_SYSTEM] + } + } + + static finishInferredProxySpan (context) { + const { req } = context + + if (!context.inferredProxySpan) return + + if (context.inferredProxySpanFinished && !req.stream) return + + // context.config.hooks.request(context.inferredProxySpan, req, res) # TODO: Do we need this?? + + // Only close the inferred span if one was created + if (context.inferredProxySpan) { + context.inferredProxySpan.finish() + context.inferredProxySpanFinished = true + } + } +} + +module.exports = InferredProxyPlugin diff --git a/packages/datadog-plugin-koa/src/index.js b/packages/datadog-plugin-koa/src/index.js index 594b7c648f0..d1fccc1b867 100644 --- a/packages/datadog-plugin-koa/src/index.js +++ b/packages/datadog-plugin-koa/src/index.js @@ -1,22 +1,9 @@ 'use strict' const RouterPlugin = require('../../datadog-plugin-router/src') -const web = require('../../dd-trace/src/plugins/util/web') class KoaPlugin extends RouterPlugin { static id = 'koa' - - constructor (...args) { - super(...args) - - this.addSub('apm:koa:request:handle', ({ req }) => { - this.setFramework(req, 'koa', this.config) - }) - - this.addSub('apm:koa:request:route', ({ req, route }) => { - web.setRoute(req, route) - }) - } } module.exports = KoaPlugin diff --git a/packages/datadog-plugin-microgateway-core/src/index.js b/packages/datadog-plugin-microgateway-core/src/index.js index 1160aa974ee..f6fc88fa6a8 100644 --- a/packages/datadog-plugin-microgateway-core/src/index.js +++ b/packages/datadog-plugin-microgateway-core/src/index.js @@ -1,26 +1,10 @@ 'use strict' const RouterPlugin = require('../../datadog-plugin-router/src') -const web = require('../../dd-trace/src/plugins/util/web') class MicrogatewayCorePlugin extends RouterPlugin { static id = 'microgateway-core' - - constructor (...args) { - super(...args) - - this.addSub('apm:microgateway-core:request:handle', ({ req }) => { - this.setFramework(req, 'microgateway', this.config) - }) - - this.addSub('apm:microgateway-core:request:route', ({ req, route }) => { - web.setRoute(req, route) - }) - - this.addSub('apm:microgateway-core:request:error', ({ error }) => { - this.addError(error) - }) - } + static framework = 'microgateway' } module.exports = MicrogatewayCorePlugin diff --git a/packages/datadog-plugin-next/src/index.js b/packages/datadog-plugin-next/src/index.js index d1e80f658df..d0e19b1e106 100644 --- a/packages/datadog-plugin-next/src/index.js +++ b/packages/datadog-plugin-next/src/index.js @@ -1,10 +1,10 @@ 'use strict' const ServerPlugin = require('../../dd-trace/src/plugins/server') +const web = require('../../datadog-plugin-web/src/utils') const { storage } = require('../../datadog-core') const analyticsSampler = require('../../dd-trace/src/analytics_sampler') const { COMPONENT } = require('../../dd-trace/src/constants') -const web = require('../../dd-trace/src/plugins/util/web') const errorPages = new Set(['/404', '/500', '/_error', '/_not-found', '/_not-found/page']) diff --git a/packages/datadog-plugin-restify/src/index.js b/packages/datadog-plugin-restify/src/index.js index 5f10d317936..888463f9aaa 100644 --- a/packages/datadog-plugin-restify/src/index.js +++ b/packages/datadog-plugin-restify/src/index.js @@ -1,23 +1,10 @@ 'use strict' const RouterPlugin = require('../../datadog-plugin-router/src') -const web = require('../../dd-trace/src/plugins/util/web') class RestifyPlugin extends RouterPlugin { static id = 'restify' - constructor (...args) { - super(...args) - - this.addSub('apm:restify:request:handle', ({ req }) => { - this.setFramework(req, 'restify', this.config) - }) - - this.addSub('apm:restify:request:route', ({ req, route }) => { - web.setRoute(req, route) - }) - } - configure (config) { return super.configure({ ...config, diff --git a/packages/datadog-plugin-router/src/index.js b/packages/datadog-plugin-router/src/index.js index a2a3f419046..3e70888b1ff 100644 --- a/packages/datadog-plugin-router/src/index.js +++ b/packages/datadog-plugin-router/src/index.js @@ -1,6 +1,5 @@ 'use strict' -const web = require('../../dd-trace/src/plugins/util/web') const WebPlugin = require('../../datadog-plugin-web/src') const analyticsSampler = require('../../dd-trace/src/analytics_sampler') const { storage } = require('../../datadog-core') @@ -31,8 +30,8 @@ class RouterPlugin extends WebPlugin { this._storeStack.push(store) this.enter(span, store) - web.patch(req) - web.setRoute(req, context.route) + this.patch(req) + this.setRoute(req, context.route) }) this.addSub(`apm:${this.constructor.id}:middleware:next`, ({ req }) => { @@ -58,7 +57,7 @@ class RouterPlugin extends WebPlugin { }) this.addSub(`apm:${this.constructor.id}:middleware:error`, ({ req, error }) => { - web.addError(req, error) + this.addError(req, error) if (!this.config.middleware) return diff --git a/packages/datadog-plugin-router/test/index.spec.js b/packages/datadog-plugin-router/test/index.spec.js index 4165fd88588..5939d7b0977 100644 --- a/packages/datadog-plugin-router/test/index.spec.js +++ b/packages/datadog-plugin-router/test/index.spec.js @@ -6,7 +6,6 @@ const axios = require('axios') const http = require('http') const { once } = require('events') const agent = require('../../dd-trace/test/plugins/agent') -const web = require('../../dd-trace/src/plugins/util/web') const { withVersions } = require('../../dd-trace/test/setup/mocha') const sort = spans => spans.sort((a, b) => a.start.toString() >= b.start.toString() ? 1 : -1) @@ -25,9 +24,9 @@ describe('Plugin', () => { function server (router, errorHandler = defaultErrorHandler) { return http.createServer((req, res) => { - const config = web.normalizeConfig({}) + const web = tracer._pluginManager._pluginsByName.router - web.instrument(tracer, config, req, res, 'web.request') + web.instrument(req, res, 'web.request') return router(req, res, errorHandler(req, res)) }) diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 27219be714a..0f57481001c 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -1,18 +1,361 @@ 'use strict' -const Plugin = require('../../dd-trace/src/plugins/plugin') -const web = require('../../dd-trace/src/plugins/util/web') +const analyticsSampler = require('../../dd-trace/src/analytics_sampler') +const { + ERROR_MESSAGE, + ERROR_TYPE, + ERROR_STACK +} = require('../../dd-trace/src/constants') +const TracingPlugin = require('../../dd-trace/src/plugins/tracing') +const InferredProxyPlugin = require('../../datadog-plugin-inferred-proxy/src') +const web = require('./utils') +const tags = require('../../../ext/tags') +const types = require('../../../ext/types') +const kinds = require('../../../ext/kinds') -class WebPlugin extends Plugin { - static id = 'web' +const { + contexts, + ends, + normalizeConfig, + setRoute, + patch, + root, + getContext, + addError, + finishSpan, + addAllowHeaders, + isOriginAllowed, + reactivate, +} = web + +const WEB = types.WEB +const SERVER = kinds.SERVER +const RESOURCE_NAME = tags.RESOURCE_NAME +const MANUAL_DROP = tags.MANUAL_DROP +const SERVICE_NAME = tags.SERVICE_NAME +const FORMAT_HTTP_HEADERS = 'http_headers' + +class WebPlugin extends TracingPlugin { + static id = WEB + static kind = SERVER + static type = WEB + + constructor (tracer, config, bindError = true) { + super(tracer, config) + this.addSub('apm:http:server:request:error', ({ req, error }) => { + if (error) { + this.addError(req, error) + } + }) + + this.addSub('apm:http:server:request:end', ({ req }) => { + this.finish(req) + }) + + this.addSub(`apm:${this.constructor.id}:request:handle`, ({ req }) => { + this.setFramework(req, this.constructor.framework || this.constructor.id) + }) + + this.addSub(`apm:${this.constructor.id}:request:route`, ({ req, route }) => { + this.setRoute(req, route) + }) + + if (bindError) { + this.addSub(`apm:${this.constructor.id}:request:error`, ({ req, error }) => { + this.addError(req, error) + }) + } + + this.configure(config) + } configure (config) { - return super.configure(web.normalizeConfig(config)) + return super.configure(this.normalizeConfig(config)) + } + + // Ensure the configuration has the correct structure and defaults. + normalizeConfig (config) { + return normalizeConfig(config) + } + + setFramework (req, name) { + const context = this.patch(req) + const span = context.span + + if (!span) return + + span.context()._name = `${name}.request` + span.context()._tags.component = name + span._integrationName = name + + this.setConfig(req) } - setFramework (req, name, config) { - web.setFramework(req, name, config) + setConfig (req) { + const context = contexts.get(req) + if (!context) return + + context.config = this.config + + const span = context.span + if (!span) return + + if (!context.config.filter(req.url)) { + span.setTag(MANUAL_DROP, true) + span.context()._trace.isRecording = false + } + + if (context.config.service) { + span.setTag(SERVICE_NAME, context.config.service) + } + + analyticsSampler.sample(span, context.config.measured, true) + } + + startSpan (req, res, name, traceCtx) { + const context = this.patch(req) + + let span + + if (context.span) { + context.span.context()._name = name + span = context.span + } else { + span = this.startChildSpan(name, req, traceCtx) + } + + context.tracer = this.tracer + context.span = span + context.res = res + + this.setConfig(req) + web._addRequestTags(context) + + return span + } + + wrap (req) { + const context = contexts.get(req) + if (!context.instrumented) { + this.wrapEnd(context) + context.instrumented = true + } + } + + // Start a span and activate a scope for a request. + instrument (req, res, name, callback) { + const span = this.startSpan(req, res, name) + + this.wrap(req) + + return callback && this.tracer.scope().activate(span, () => callback(span)) + } + + // Reactivate the request scope in case it was changed by a middleware. + reactivate (req, fn) { + return reactivate(req, fn) + } + + // Add a route segment that will be used for the resource name. + enterRoute (req, path) { + if (typeof path === 'string') { + contexts.get(req).paths.push(path) + } + } + + setRoute (req, path) { + setRoute(req, path) + } + + // Remove the current route segment. + exitRoute (req) { + contexts.get(req).paths.pop() + } + + // Start a new middleware span and activate a new scope with the span. + wrapMiddleware (req, middleware, name, fn) { + if (!this.active(req)) return fn() + + const context = contexts.get(req) + const tracer = context.tracer + const childOf = this.active(req) + const config = context.config + const traceCtx = context.traceCtx + + if (config.middleware === false) return this.bindAndWrapMiddlewareErrors(fn, req, tracer, childOf) + + const span = super.startSpan(name, { + childOf + }, traceCtx) + + analyticsSampler.sample(span, config.measured) + + span.addTags({ + [RESOURCE_NAME]: middleware._name || middleware.name || '' + }) + + context.middleware.push(span) + + return tracer.scope().activate(span, fn) + } + + // catch errors and apply to active span + bindAndWrapMiddlewareErrors (fn, req, tracer, activeSpan) { + try { + return tracer.scope().bind(fn, activeSpan).apply(this, arguments) + } catch (e) { + this.addError(req, e) // TODO: remove when error formatting is moved to Span + throw e + } + } + + // Finish the active middleware span. + finish (req, error) { + if (!this.active(req)) return + + const context = contexts.get(req) + const span = context.middleware.pop() + + if (span) { + if (error) { + span.addTags({ + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack + }) + } + + span.finish() + } + } + + // Register a callback to run before res.end() is called. + beforeEnd (req, callback) { + contexts.get(req).beforeEnd.push(callback) + } + + patch (req) { + return patch(req, this.config) + } + + // Return the request root span. + root (req) { + return root(req) + } + + // Return the active span. + active (req) { + const context = contexts.get(req) + + if (!context) return null + if (context.middleware.length === 0) return context.span || null + + return context.middleware.at(-1) + } + + // Extract the parent span from the headers and start a new span as its child + startChildSpan (name, req, traceCtx) { + const headers = req.headers + const reqCtx = this.getContext(req) + let childOf = this.tracer.extract(FORMAT_HTTP_HEADERS, headers) + + InferredProxyPlugin.maybeCreateInferredProxySpan(this._tracerConfig, req, reqCtx, childOf, traceCtx) + if (reqCtx.inferredProxySpan) { + childOf = reqCtx.inferredProxySpan + } + + const span = super.startSpan(name, { + childOf, + kind: this.constructor.kind, + type: this.constructor.type + }, traceCtx) + + return span + } + + // Add an error to the request + addError (req, error) { + addError(req, error) + } + + finishMiddleware (context) { + if (context.finished) return + + let span + + while ((span = context.middleware.pop())) { + span.finish() + } + } + + finishSpan (context) { + finishSpan(context) + } + + finishAll (context) { + for (const beforeEnd of context.beforeEnd) { + beforeEnd() + } + + this.finishMiddleware(context) + + this.finishSpan(context) + + InferredProxyPlugin.finishInferredProxySpan(context) + } + + wrapWriteHead (context) { + const { + req, + res + } = context + const writeHead = res.writeHead + + return function (statusCode, statusMessage, headers) { + headers = typeof statusMessage === 'string' ? headers : statusMessage + headers = Object.assign(res.getHeaders(), headers) + + if (req.method.toLowerCase() === 'options' && isOriginAllowed(req, headers)) { + addAllowHeaders(req, res, headers) + } + + return writeHead.apply(this, arguments) + } + } + + getContext (req) { + return getContext(req) + } + + wrapRes (context, req, res, end) { + return () => { + this.finishAll(context) + + return end.apply(res, arguments) + } + } + + wrapEnd (context) { + const scope = context.tracer.scope() + const req = context.req + const res = context.res + const end = res.end + + res.writeHead = this.wrapWriteHead(context) + + ends.set(res, this.wrapRes(context, req, res, end)) + + Object.defineProperty(res, 'end', { + configurable: true, + get () { + return ends.get(this) + }, + set (value) { + ends.set(this, scope.bind(value, context.span)) + } + }) } } module.exports = WebPlugin +module.exports.static = web diff --git a/packages/datadog-plugin-web/src/utils.js b/packages/datadog-plugin-web/src/utils.js new file mode 100644 index 00000000000..b9131aedebe --- /dev/null +++ b/packages/datadog-plugin-web/src/utils.js @@ -0,0 +1,377 @@ +'use strict' + +const uniq = require('../../datadog-core/src/utils/src/uniq') +const log = require('../../dd-trace/src/log') +const tags = require('../../../ext/tags') +const types = require('../../../ext/types') +const kinds = require('../../../ext/kinds') +const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter') +const { ERROR_MESSAGE } = require('../../dd-trace/src/constants') + +let extractIp + +const WEB = types.WEB +const SERVER = kinds.SERVER +const RESOURCE_NAME = tags.RESOURCE_NAME +const SPAN_TYPE = tags.SPAN_TYPE +const SPAN_KIND = tags.SPAN_KIND +const ERROR = tags.ERROR +const HTTP_METHOD = tags.HTTP_METHOD +const HTTP_URL = tags.HTTP_URL +const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE +const HTTP_ROUTE = tags.HTTP_ROUTE +const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS +const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS +const HTTP_USERAGENT = tags.HTTP_USERAGENT +const HTTP_CLIENT_IP = tags.HTTP_CLIENT_IP + +const HTTP2_HEADER_AUTHORITY = ':authority' +const HTTP2_HEADER_SCHEME = ':scheme' +const HTTP2_HEADER_PATH = ':path' + +const contexts = new WeakMap() +const ends = new WeakMap() + +function normalizeConfig (config) { + const headers = getHeadersToRecord(config) + const validateStatus = getStatusValidator(config) + const hooks = getHooks(config) + const filter = urlFilter.getFilter(config) + const middleware = getMiddlewareSetting(config) + const queryStringObfuscation = getQsObfuscator(config) + + extractIp = config.clientIpEnabled && require('../../dd-trace/src/plugins/util/ip_extractor').extractIp + + return { + ...config, + headers, + validateStatus, + hooks, + filter, + middleware, + queryStringObfuscation + } +} + +function setRoute (req, path) { + const context = contexts.get(req) + + if (!context) return + + context.paths = [path] +} + +function patch (req, config) { + let context = contexts.get(req) + + if (context) return context + + context = req.stream && contexts.get(req.stream) + + if (context) { + contexts.set(req, context) + return context + } + + context = { + req, + span: null, + paths: [], + middleware: [], + beforeEnd: [], + config + } + + contexts.set(req, context) + + return context +} + +function root (req) { + const context = contexts.get(req) + return context ? context.span : null +} + +function getContext (req) { + return contexts.get(req) +} + +function addError (req, error) { + if (error instanceof Error) { + const context = contexts.get(req) + + if (context) { + context.error = error + } + } +} + +function finishSpan (context) { + const { req, res } = context + + if (context.finished && !req.stream) return + + _addRequestTags(context) + _addResponseTags(context) + + context.config.hooks.request(context.span, req, res) + addResourceTag(context) + + context.span.finish() + context.finished = true +} + +function _obfuscateQs (url, config) { + const { queryStringObfuscation } = config + + if (queryStringObfuscation === false) return url + + const i = url.indexOf('?') + if (i === -1) return url + + const path = url.slice(0, i) + if (queryStringObfuscation === true) return path + + let qs = url.slice(i + 1) + + qs = qs.replace(queryStringObfuscation, '') + + return `${path}?${qs}` +} + +function _addRequestTags (context) { + const { req, span, inferredProxySpan } = context + const url = extractURL(req) + + span.addTags({ + [HTTP_URL]: _obfuscateQs(url, context.config), + [HTTP_METHOD]: req.method, + [SPAN_KIND]: SERVER, + [SPAN_TYPE]: WEB, + [HTTP_USERAGENT]: req.headers['user-agent'] + }) + + // if client ip has already been set by appsec, no need to run it again + if (extractIp && !span.context()._tags.hasOwnProperty(HTTP_CLIENT_IP)) { + const clientIp = extractIp(context.config, req) + + if (clientIp) { + span.setTag(HTTP_CLIENT_IP, clientIp) + inferredProxySpan?.setTag(HTTP_CLIENT_IP, clientIp) + } + } + + addHeaders(context) +} + +function _addResponseTags (context) { + const { req, res, paths, span, inferredProxySpan } = context + + const route = paths.join('') + if (route) { + span.setTag(HTTP_ROUTE, route) + } + + span.addTags({ + [HTTP_STATUS_CODE]: res.statusCode + }) + inferredProxySpan?.addTags({ + [HTTP_STATUS_CODE]: res.statusCode + }) + + addStatusError(req, res.statusCode) +} + +function addStatusError (req, statusCode) { + const context = contexts.get(req) + const { span, inferredProxySpan, error } = context + + const spanHasExistingError = span.context()._tags.error || span.context()._tags[ERROR_MESSAGE] + const inferredSpanContext = inferredProxySpan?.context() + const inferredSpanHasExistingError = inferredSpanContext?._tags.error || inferredSpanContext?._tags[ERROR_MESSAGE] + + const isValidStatusCode = context.config.validateStatus(statusCode) + + if (!spanHasExistingError && !isValidStatusCode) { + span.setTag(ERROR, error || true) + } + + if (inferredProxySpan && !inferredSpanHasExistingError && !isValidStatusCode) { + inferredProxySpan.setTag(ERROR, error || true) + } +} + +function addAllowHeaders (req, res, headers) { + const allowHeaders = splitHeader(headers['access-control-allow-headers']) + const requestHeaders = splitHeader(req.headers['access-control-request-headers']) + const contextHeaders = [ + 'x-datadog-origin', + 'x-datadog-parent-id', + 'x-datadog-sampled', // Deprecated, but still accept it in case it's sent. + 'x-datadog-sampling-priority', + 'x-datadog-trace-id', + 'x-datadog-tags' + ] + + for (const header of contextHeaders) { + if (requestHeaders.includes(header)) { + allowHeaders.push(header) + } + } + + if (allowHeaders.length > 0) { + res.setHeader('access-control-allow-headers', uniq(allowHeaders).join(',')) + } +} + +function isOriginAllowed (req, headers) { + const origin = req.headers.origin + const allowOrigin = headers['access-control-allow-origin'] + + return origin && (allowOrigin === '*' || allowOrigin === origin) +} + +function splitHeader (str) { + return typeof str === 'string' ? str.split(/\s*,\s*/) : [] +} + +function reactivate (req, fn) { + const context = contexts.get(req) + + return context + ? context.tracer.scope().activate(context.span, fn) + : fn() +} + +function addResourceTag (context) { + const { req, span } = context + const tags = span.context()._tags + + if (tags['resource.name']) return + + const resource = [req.method, tags[HTTP_ROUTE]] + .filter(Boolean) + .join(' ') + + span.setTag(RESOURCE_NAME, resource) +} + +function addHeaders (context) { + const { req, res, config, span, inferredProxySpan } = context + + config.headers.forEach(([key, tag]) => { + const reqHeader = req.headers[key] + const resHeader = res.getHeader(key) + + if (reqHeader) { + span.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) + inferredProxySpan?.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) + } + + if (resHeader) { + span.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) + inferredProxySpan?.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) + } + }) +} + +function extractURL (req) { + const headers = req.headers + + if (req.stream) { + return `${headers[HTTP2_HEADER_SCHEME]}://${headers[HTTP2_HEADER_AUTHORITY]}${headers[HTTP2_HEADER_PATH]}` + } + const protocol = getProtocol(req) + return `${protocol}://${req.headers.host}${req.originalUrl || req.url}` +} + +function getProtocol (req) { + if (req.socket && req.socket.encrypted) return 'https' + if (req.connection && req.connection.encrypted) return 'https' + + return 'http' +} + +function getHeadersToRecord (config) { + if (Array.isArray(config.headers)) { + try { + return config.headers + .map(h => h.split(':')) + .map(([key, tag]) => [key.toLowerCase(), tag]) + } catch (err) { + log.error('Web plugin error getting headers', err) + } + } else if (config.hasOwnProperty('headers')) { + log.error('Expected `headers` to be an array of strings.') + } + return [] +} + +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 < 500 +} + +const noop = () => {} + +function getHooks (config) { + const request = config.hooks?.request ?? noop + + return { request } +} + +function getMiddlewareSetting (config) { + if (config && typeof config.middleware === 'boolean') { + return config.middleware + } else if (config && config.hasOwnProperty('middleware')) { + log.error('Expected `middleware` to be a boolean.') + } + + return true +} + +function getQsObfuscator (config) { + const obfuscator = config.queryStringObfuscation + + if (typeof obfuscator === 'boolean') { + return obfuscator + } + + if (typeof obfuscator === 'string') { + if (obfuscator === '') return false // disable obfuscator + + if (obfuscator === '.*') return true // optimize full redact + + try { + return new RegExp(obfuscator, 'gi') + } catch (err) { + log.error('Web plugin error getting qs obfuscator', err) + } + } + + if (config.hasOwnProperty('queryStringObfuscation')) { + log.error('Expected `queryStringObfuscation` to be a regex string or boolean.') + } + + return true +} + +module.exports = { + normalizeConfig, + setRoute, + patch, + root, + getContext, + addError, + finishSpan, + _addRequestTags, + addAllowHeaders, + isOriginAllowed, + reactivate, + contexts, + ends, +} diff --git a/packages/dd-trace/test/plugins/util/web.spec.js b/packages/datadog-plugin-web/test/index.spec.js similarity index 76% rename from packages/dd-trace/test/plugins/util/web.spec.js rename to packages/datadog-plugin-web/test/index.spec.js index db734551ba5..00826f6a149 100644 --- a/packages/dd-trace/test/plugins/util/web.spec.js +++ b/packages/datadog-plugin-web/test/index.spec.js @@ -1,12 +1,10 @@ 'use strict' -require('../../setup/tap') - -const types = require('../../../../../ext/types') -const kinds = require('../../../../../ext/kinds') -const tags = require('../../../../../ext/tags') -const { USER_REJECT } = require('../../../../../ext/priority') -const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../../../dd-trace/src/constants') +const types = require('../../../ext/types') +const kinds = require('../../../ext/kinds') +const tags = require('../../../ext/tags') +const { USER_REJECT } = require('../../../ext/priority') +const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') const WEB = types.WEB const SERVER = kinds.SERVER @@ -24,8 +22,9 @@ const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS const HTTP_USERAGENT = tags.HTTP_USERAGENT const HTTP_CLIENT_IP = tags.HTTP_CLIENT_IP -describe('plugins/util/web', () => { +describe('Plugin', () => { let web + let webUtils let tracer let span let req @@ -57,16 +56,15 @@ describe('plugins/util/web', () => { config = { hooks: {} } tracer = require('../../..').init({ plugins: false }) - web = require('../../../src/plugins/util/web') - }) + const WebPlugin = require('../src') + web = new WebPlugin(tracer, config) - beforeEach(() => { - config = web.normalizeConfig(config) + webUtils = require('../src/utils') }) describe('normalizeConfig', () => { it('should set the correct defaults', () => { - const config = web.normalizeConfig({}) + const config = webUtils.normalizeConfig({}) expect(config).to.have.property('headers') expect(config.headers).to.be.an('array') @@ -82,7 +80,7 @@ describe('plugins/util/web', () => { }) it('should use the shared config if set', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ headers: ['test'], validateStatus: code => false, hooks: { @@ -98,7 +96,7 @@ describe('plugins/util/web', () => { describe('queryStringObfuscation', () => { it('should keep booleans as is', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ queryStringObfuscation: false }) @@ -106,7 +104,7 @@ describe('plugins/util/web', () => { }) it('should change to false when passed empty string', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ queryStringObfuscation: '' }) @@ -114,7 +112,7 @@ describe('plugins/util/web', () => { }) it('should change to true when passed ".*"', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ queryStringObfuscation: '.*' }) @@ -122,7 +120,7 @@ describe('plugins/util/web', () => { }) it('should convert to regex when passed valid string', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ queryStringObfuscation: 'a*' }) @@ -130,7 +128,7 @@ describe('plugins/util/web', () => { }) it('should default to true when passed a bad regex', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ queryStringObfuscation: '(?)' }) @@ -147,22 +145,22 @@ describe('plugins/util/web', () => { 'x-datadog-parent-id': '456' } - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { expect(span.context()._traceId.toString(10)).to.equal('123') expect(span.context()._parentId.toString(10)).to.equal('456') }) }) it('should set the service name', () => { - config.service = 'custom' + web.configure({ service: 'custom' }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { expect(span.context()._tags).to.have.property(SERVICE_NAME, 'custom') }) }) it('should activate a scope with the span', () => { - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { expect(tracer.scope().active()).to.equal(span) }) }) @@ -174,7 +172,7 @@ describe('plugins/util/web', () => { req.headers['x-forwarded-for'] = '8.8.8.8' res.statusCode = '200' - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -192,10 +190,9 @@ describe('plugins/util/web', () => { it('should add client ip tag to the span when enabled', () => { req.headers['x-forwarded-for'] = '8.8.8.8' - config.clientIpEnabled = true + web.configure({ clientIpEnabled: true }) - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -209,11 +206,9 @@ describe('plugins/util/web', () => { it('should add custom client ip tag to the span when it is configured', () => { req.headers['X-Forwad-For'] = '8.8.8.8' - config.clientIpEnabled = true - config.clientIpHeader = 'X-Forwad-For' + web.configure({ clientIpEnabled: true, clientIpHeader: 'X-Forwad-For' }) - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -227,10 +222,9 @@ describe('plugins/util/web', () => { it('should not add custom client ip tag to the span when it is not configured', () => { req.headers['X-Forwad-For'] = '8.8.8.8' - config.clientIpEnabled = true + web.configure({ clientIpEnabled: true }) - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -242,10 +236,9 @@ describe('plugins/util/web', () => { it('should not add client ip tag to the span when disabled', () => { req.headers['x-forwarded-for'] = '8.8.8.8' - config.clientIpEnabled = false + web.configure({ clientIpEnabled: false }) - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -257,9 +250,9 @@ describe('plugins/util/web', () => { it('should not replace client ip when it exists', () => { req.headers['x-forwarded-for'] = '8.8.8.8' - config.clientIpEnabled = true + web.configure({ clientIpEnabled: true }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags span.setTag(HTTP_CLIENT_IP, '1.1.1.1') @@ -273,9 +266,9 @@ describe('plugins/util/web', () => { }) it('should not add client ip tag when no candidate header is present in request', () => { - config.clientIpEnabled = true + web.configure({ clientIpEnabled: true }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -287,10 +280,10 @@ describe('plugins/util/web', () => { it('should add configured headers to the span tags', () => { req.headers.req = 'incoming' req.headers.res = 'outgoing' - config.headers = ['host', 'req:http.req', 'server', 'res:http.res'] - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.configure({ headers: ['host', 'req:http.req', 'server', 'res:http.res'] }) + + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -305,47 +298,44 @@ describe('plugins/util/web', () => { }) it('should only start one span for the entire request', () => { - web.instrument(tracer, config, req, res, 'test.request', span1 => { - web.instrument(tracer, config, req, res, 'test.request', span2 => { + web.instrument(req, res, 'test.request', span1 => { + web.instrument(req, res, 'test.request', span2 => { expect(span1).to.equal(span2) }) }) }) it('should allow overriding the span name', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { - web.instrument(tracer, config, req, res, 'test2.request', span => { + web.instrument(req, res, 'test.request', () => { + web.instrument(req, res, 'test2.request', span => { expect(span.context()._name).to.equal('test2.request') }) }) }) it('should allow overriding the span service name', () => { - web.instrument(tracer, config, req, res, 'test.request', span => { - config.service = 'test2' - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request', span => { + web.configure({ service: 'test2' }) + web.instrument(req, res, 'test.request') expect(span.context()._tags).to.have.property('service.name', 'test2') }) }) it('should only wrap res.end once', () => { - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') const end = res.end - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') expect(end).to.equal(res.end) }) it('should use the config from the last call', () => { - config.headers = ['host'] + web.configure({ headers: ['host'] }) - const override = web.normalizeConfig({ - headers: ['date'] - }) - - web.instrument(tracer, config, req, res, 'test.request', () => { - web.instrument(tracer, override, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', () => { + web.configure({ headers: ['date'] }) + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -358,15 +348,13 @@ describe('plugins/util/web', () => { }) it('should obfuscate the query string from the URL', () => { - const config = web.normalizeConfig({ - queryStringObfuscation: 'secret=.*?(&|$)' - }) + web.configure({ queryStringObfuscation: 'secret=.*?(&|$)' }) req.method = 'GET' req.url = '/user/123?secret=password&foo=bar' res.statusCode = '200' - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -395,7 +383,7 @@ describe('plugins/util/web', () => { 'access-control-allow-origin': 'http://test.com' }) - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') res.writeHead() @@ -416,7 +404,7 @@ describe('plugins/util/web', () => { 'access-control-allow-origin': 'http://test.com' }) - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') res.writeHead() @@ -430,7 +418,7 @@ describe('plugins/util/web', () => { req.headers.origin = 'http://test.com' req.headers['access-control-request-headers'] = headers - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') res.writeHead() @@ -445,7 +433,7 @@ describe('plugins/util/web', () => { 'access-control-allow-origin': 'http://test.com' }) - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') res.writeHead() @@ -458,7 +446,7 @@ describe('plugins/util/web', () => { req.headers['x-forwarded-for'] = '8.8.8.8' req.socket = { encrypted: true } - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -486,7 +474,7 @@ describe('plugins/util/web', () => { } res.statusCode = '200' - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -502,9 +490,9 @@ describe('plugins/util/web', () => { }) it('should drop filtered out requests', () => { - config.filter = () => false + web.configure({ filter: () => false }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const sampling = span.context()._sampling res.end() @@ -516,7 +504,7 @@ describe('plugins/util/web', () => { describe('on request end', () => { beforeEach(() => { - web.instrument(tracer, config, req, res, 'test.request', reqSpan => { + web.instrument(req, res, 'test.request', reqSpan => { span = reqSpan tags = span.context()._tags }) @@ -594,7 +582,7 @@ describe('plugins/util/web', () => { }) it('should set the error tag if the configured validator returns false', () => { - config.validateStatus = () => false + web.configure({ validateStatus: () => false }) res.end() @@ -614,31 +602,34 @@ describe('plugins/util/web', () => { }) it('should execute the request end hook', () => { - config.hooks.request = sinon.spy() + const hook = sinon.spy() + + web.configure({ hooks: { request: hook } }) res.end() - expect(config.hooks.request).to.have.been.calledWith(span, req, res) + expect(hook).to.have.been.calledWith(span, req, res) }) it('should execute multiple end hooks', () => { - config.hooks = { - request: sinon.spy() - } + const hook = sinon.spy() + web.configure({ hooks: { request: hook } }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { res.end() - expect(config.hooks.request).to.have.been.calledWith(span, req, res) + expect(hook).to.have.been.calledWith(span, req, res) }) }) it('should set the resource name from the http.route tag set in the hooks', () => { - config.hooks = { - request: span => span.setTag('http.route', '/custom/route') - } + web.configure({ + hooks: { + request: span => span.setTag('http.route', '/custom/route') + } + }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { res.end() expect(tags).to.have.property('resource.name', 'GET /custom/route') @@ -649,8 +640,7 @@ describe('plugins/util/web', () => { describe('enterRoute', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { span = tracer.scope().active() tags = span.context()._tags }) @@ -681,8 +671,7 @@ describe('plugins/util/web', () => { describe('exitRoute', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', reqSpan => { + web.instrument(req, res, 'test.request', reqSpan => { span = reqSpan tags = span.context()._tags }) @@ -702,8 +691,7 @@ describe('plugins/util/web', () => { describe('wrapMiddleware', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { span = tracer.scope().active() tags = span.context()._tags }) @@ -721,8 +709,7 @@ describe('plugins/util/web', () => { describe('finish', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { span = tracer.scope().active() tags = span.context()._tags }) @@ -765,7 +752,7 @@ describe('plugins/util/web', () => { describe('root', () => { it('should return the request root span', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { const span = tracer.scope().active() web.wrapMiddleware(req, () => {}, 'express.middleware', () => { @@ -781,13 +768,13 @@ describe('plugins/util/web', () => { describe('active', () => { it('should return the request span by default', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { expect(web.active(req)).to.equal(tracer.scope().active()) }) }) it('should return the active middleware span', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { const span = tracer.scope().active() web.wrapMiddleware(req, () => {}, 'express.middleware', () => { @@ -804,8 +791,7 @@ describe('plugins/util/web', () => { describe('addError', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { span = tracer.scope().active() tags = span.context()._tags }) @@ -837,8 +823,7 @@ describe('plugins/util/web', () => { describe('addStatusError', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { span = tracer.scope().active() tags = span.context()._tags }) @@ -853,7 +838,7 @@ describe('plugins/util/web', () => { }) it('should only flag requests as an error for configured status codes', () => { - config.validateStatus = () => true + web.configure({ validateStatus: () => true }) web.addStatusError(req, 500) @@ -863,68 +848,64 @@ describe('plugins/util/web', () => { describe('allowlistFilter', () => { beforeEach(() => { - config = { allowlist: ['/_okay'] } - config = web.normalizeConfig(config) + web.configure({ allowlist: ['/_okay'] }) }) it('should not filter the url', () => { - const filtered = config.filter('/_okay') + const filtered = web.config.filter('/_okay') expect(filtered).to.equal(true) }) it('should filter the url', () => { - const filtered = config.filter('/_notokay') + const filtered = web.config.filter('/_notokay') expect(filtered).to.equal(false) }) }) describe('whitelistFilter', () => { beforeEach(() => { - config = { whitelist: ['/_okay'] } - config = web.normalizeConfig(config) + web.configure({ whitelist: ['/_okay'] }) }) it('should not filter the url', () => { - const filtered = config.filter('/_okay') + const filtered = web.config.filter('/_okay') expect(filtered).to.equal(true) }) it('should filter the url', () => { - const filtered = config.filter('/_notokay') + const filtered = web.config.filter('/_notokay') expect(filtered).to.equal(false) }) }) describe('blocklistFilter', () => { beforeEach(() => { - config = { blocklist: ['/_notokay'] } - config = web.normalizeConfig(config) + web.configure({ blocklist: ['/_notokay'] }) }) it('should not filter the url', () => { - const filtered = config.filter('/_okay') + const filtered = web.config.filter('/_okay') expect(filtered).to.equal(true) }) it('should filter the url', () => { - const filtered = config.filter('/_notokay') + const filtered = web.config.filter('/_notokay') expect(filtered).to.equal(false) }) }) describe('blacklistFilter', () => { beforeEach(() => { - config = { blacklist: ['/_notokay'] } - config = web.normalizeConfig(config) + web.configure({ blacklist: ['/_notokay'] }) }) it('should not filter the url', () => { - const filtered = config.filter('/_okay') + const filtered = web.config.filter('/_okay') expect(filtered).to.equal(true) }) it('should filter the url', () => { - const filtered = config.filter('/_notokay') + const filtered = web.config.filter('/_notokay') expect(filtered).to.equal(false) }) }) @@ -933,38 +914,36 @@ describe('plugins/util/web', () => { const url = 'http://perdu.com/path/' const qs = '?data=secret' - let config - beforeEach(() => { - config = { + web.configure({ queryStringObfuscation: /secret/gi - } + }) }) it('should not obfuscate when passed false', () => { - config.queryStringObfuscation = false + web.configure({ queryStringObfuscation: false }) - const result = web.obfuscateQs(config, url + qs) + const result = web._obfuscateQs(url + qs) expect(result).to.equal(url + qs) }) it('should not obfuscate when no querystring is found', () => { - const result = web.obfuscateQs(config, url) + const result = web._obfuscateQs(url) expect(result).to.equal(url) }) it('should remove the querystring if passed true', () => { - config.queryStringObfuscation = true + web.configure({ queryStringObfuscation: true }) - const result = web.obfuscateQs(config, url + qs) + const result = web._obfuscateQs(url + qs, web.config) expect(result).to.equal(url) }) it('should obfuscate only the querystring part of the url', () => { - const result = web.obfuscateQs(config, url + 'secret/' + qs) + const result = web._obfuscateQs(url + 'secret/' + qs, web.config) expect(result).to.equal(url + 'secret/?data=') }) diff --git a/packages/dd-trace/src/appsec/api_security_sampler.js b/packages/dd-trace/src/appsec/api_security_sampler.js index bf3fc9f513d..e223a63c66b 100644 --- a/packages/dd-trace/src/appsec/api_security_sampler.js +++ b/packages/dd-trace/src/appsec/api_security_sampler.js @@ -1,7 +1,7 @@ 'use strict' const TTLCache = require('@isaacs/ttlcache') -const web = require('../plugins/util/web') +const web = require('../../../datadog-plugin-web/src/utils') const log = require('../log') const { AUTO_REJECT, USER_REJECT } = require('../../../../ext/priority') const { keepTrace } = require('../priority_sampler') diff --git a/packages/dd-trace/src/appsec/graphql.js b/packages/dd-trace/src/appsec/graphql.js index 2c59657fe2f..2c6aa6c9129 100644 --- a/packages/dd-trace/src/appsec/graphql.js +++ b/packages/dd-trace/src/appsec/graphql.js @@ -10,7 +10,7 @@ const { const log = require('../log') const waf = require('./waf') const addresses = require('./addresses') -const web = require('../plugins/util/web') +const web = require('../../../datadog-plugin-web/src/utils') const { startGraphqlResolve, graphqlMiddlewareChannel, diff --git a/packages/dd-trace/src/appsec/iast/index.js b/packages/dd-trace/src/appsec/iast/index.js index 76209f0599f..a6e255e1aba 100644 --- a/packages/dd-trace/src/appsec/iast/index.js +++ b/packages/dd-trace/src/appsec/iast/index.js @@ -2,7 +2,7 @@ const vulnerabilityReporter = require('./vulnerability-reporter') const { enableAllAnalyzers, disableAllAnalyzers } = require('./analyzers') -const web = require('../../plugins/util/web') +const web = require('../../../../datadog-plugin-web/src/utils') const { storage } = require('../../../../datadog-core') const overheadController = require('./overhead-controller') const dc = require('dc-polyfill') diff --git a/packages/dd-trace/src/appsec/iast/overhead-controller.js b/packages/dd-trace/src/appsec/iast/overhead-controller.js index ad5f005fd84..d7a0862f77a 100644 --- a/packages/dd-trace/src/appsec/iast/overhead-controller.js +++ b/packages/dd-trace/src/appsec/iast/overhead-controller.js @@ -1,7 +1,7 @@ 'use strict' const { LRUCache } = require('lru-cache') -const web = require('../../plugins/util/web') +const web = require('../../../../datadog-plugin-web/src/utils') const vulnerabilities = require('./vulnerabilities') const OVERHEAD_CONTROLLER_CONTEXT_KEY = 'oce' diff --git a/packages/dd-trace/src/appsec/index.js b/packages/dd-trace/src/appsec/index.js index cb76ab64b2b..ba982907d70 100644 --- a/packages/dd-trace/src/appsec/index.js +++ b/packages/dd-trace/src/appsec/index.js @@ -31,7 +31,7 @@ const addresses = require('./addresses') const Reporter = require('./reporter') const appsecTelemetry = require('./telemetry') const apiSecuritySampler = require('./api_security_sampler') -const web = require('../plugins/util/web') +const web = require('../../../datadog-plugin-web/src/utils') const { extractIp } = require('../plugins/util/ip_extractor') const { HTTP_CLIENT_IP } = require('../../../../ext/tags') const { isBlocked, block, setTemplates, getBlockingAction } = require('./blocking') diff --git a/packages/dd-trace/src/appsec/rasp/index.js b/packages/dd-trace/src/appsec/rasp/index.js index 6da75c91283..09bdba8faef 100644 --- a/packages/dd-trace/src/appsec/rasp/index.js +++ b/packages/dd-trace/src/appsec/rasp/index.js @@ -1,6 +1,6 @@ 'use strict' -const web = require('../../plugins/util/web') +const web = require('../../../../datadog-plugin-web/src/utils') const { setUncaughtExceptionCaptureCallbackStart, expressMiddlewareError } = require('../channels') const { block, isBlocked } = require('../blocking') const ssrf = require('./ssrf') diff --git a/packages/dd-trace/src/appsec/rasp/utils.js b/packages/dd-trace/src/appsec/rasp/utils.js index f3b3cea6ba9..c81a0465d24 100644 --- a/packages/dd-trace/src/appsec/rasp/utils.js +++ b/packages/dd-trace/src/appsec/rasp/utils.js @@ -1,6 +1,6 @@ 'use strict' -const web = require('../../plugins/util/web') +const web = require('../../../../datadog-plugin-web/src/utils') const { getCallsiteFrames, reportStackTrace, canReportStackTrace } = require('../stack_trace') const { getBlockingAction } = require('../blocking') const log = require('../../log') diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index af806041e43..a7a392adb4c 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -4,7 +4,7 @@ const dc = require('dc-polyfill') const zlib = require('zlib') const { storage } = require('../../../datadog-core') -const web = require('../plugins/util/web') +const web = require('../../../datadog-plugin-web/src/utils') const { ipHeaderList } = require('../plugins/util/ip_extractor') const { incrementWafInitMetric, diff --git a/packages/dd-trace/src/appsec/waf/index.js b/packages/dd-trace/src/appsec/waf/index.js index a2035073f73..6acd254567c 100644 --- a/packages/dd-trace/src/appsec/waf/index.js +++ b/packages/dd-trace/src/appsec/waf/index.js @@ -6,7 +6,7 @@ const Reporter = require('../reporter') const Limiter = require('../../rate_limiter') const { keepTrace } = require('../../priority_sampler') const { ASM } = require('../../standalone/product') -const web = require('../../plugins/util/web') +const web = require('../../../../datadog-plugin-web/src/utils') const { updateRateLimitedMetric } = require('../telemetry') class WafUpdateError extends Error { diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index 10e1105df04..f4f0b194f1f 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -30,6 +30,7 @@ module.exports = { get amqp10 () { return require('../../../datadog-plugin-amqp10/src') }, get amqplib () { return require('../../../datadog-plugin-amqplib/src') }, get avsc () { return require('../../../datadog-plugin-avsc/src') }, + get 'aws-apigateway' () { return require('../../../datadog-plugin-aws-apigateway/src') }, get 'aws-sdk' () { return require('../../../datadog-plugin-aws-sdk/src') }, get bunyan () { return require('../../../datadog-plugin-bunyan/src') }, get 'cassandra-driver' () { return require('../../../datadog-plugin-cassandra-driver/src') }, diff --git a/packages/dd-trace/src/plugins/util/inferred_proxy.js b/packages/dd-trace/src/plugins/util/inferred_proxy.js deleted file mode 100644 index f837608ab97..00000000000 --- a/packages/dd-trace/src/plugins/util/inferred_proxy.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict' - -const log = require('../../log') -const tags = require('../../../../../ext/tags') - -const RESOURCE_NAME = tags.RESOURCE_NAME -const SPAN_TYPE = tags.SPAN_TYPE -const HTTP_URL = tags.HTTP_URL -const HTTP_METHOD = tags.HTTP_METHOD - -const PROXY_HEADER_SYSTEM = 'x-dd-proxy' -const PROXY_HEADER_START_TIME_MS = 'x-dd-proxy-request-time-ms' -const PROXY_HEADER_PATH = 'x-dd-proxy-path' -const PROXY_HEADER_HTTPMETHOD = 'x-dd-proxy-httpmethod' -const PROXY_HEADER_DOMAIN = 'x-dd-proxy-domain-name' -const PROXY_HEADER_STAGE = 'x-dd-proxy-stage' - -const supportedProxies = { - 'aws-apigateway': { - spanName: 'aws.apigateway', - component: 'aws-apigateway' - } -} - -function createInferredProxySpan (headers, childOf, tracer, reqCtx, traceCtx, config, startSpanHelper) { - if (!headers) { - return null - } - - if (!tracer._config?.inferredProxyServicesEnabled) { - return null - } - - const proxyContext = extractInferredProxyContext(headers) - - if (!proxyContext) { - return null - } - - const proxySpanInfo = supportedProxies[proxyContext.proxySystemName] - - log.debug('Successfully extracted inferred span info %s for proxy:', proxyContext, proxyContext.proxySystemName) - - const span = startSpanHelper(tracer, proxySpanInfo.spanName, { - childOf, - type: 'web', - startTime: proxyContext.requestTime, - integrationName: proxySpanInfo.component, - meta: { - service: proxyContext.domainName || tracer._config.service, - component: proxySpanInfo.component, - [SPAN_TYPE]: 'web', - [HTTP_METHOD]: proxyContext.method, - [HTTP_URL]: proxyContext.domainName + proxyContext.path, - stage: proxyContext.stage - } - }, traceCtx, config) - - reqCtx.inferredProxySpan = span - childOf = span - - log.debug('Successfully created inferred proxy span.') - - setInferredProxySpanTags(span, proxyContext) - - return childOf -} - -function setInferredProxySpanTags (span, proxyContext) { - span.setTag(RESOURCE_NAME, `${proxyContext.method} ${proxyContext.path}`) - span.setTag('_dd.inferred_span', 1) - return span -} - -function extractInferredProxyContext (headers) { - if (!(PROXY_HEADER_START_TIME_MS in headers)) { - return null - } - - if (!(PROXY_HEADER_SYSTEM in headers && headers[PROXY_HEADER_SYSTEM] in supportedProxies)) { - log.debug('Received headers to create inferred proxy span but headers include an unsupported proxy type', headers) - return null - } - - return { - requestTime: headers[PROXY_HEADER_START_TIME_MS] - ? Number.parseInt(headers[PROXY_HEADER_START_TIME_MS], 10) - : null, - method: headers[PROXY_HEADER_HTTPMETHOD], - path: headers[PROXY_HEADER_PATH], - stage: headers[PROXY_HEADER_STAGE], - domainName: headers[PROXY_HEADER_DOMAIN], - proxySystemName: headers[PROXY_HEADER_SYSTEM] - } -} - -function finishInferredProxySpan (context) { - const { req } = context - - if (!context.inferredProxySpan) return - - if (context.inferredProxySpanFinished && !req.stream) return - - // context.config.hooks.request(context.inferredProxySpan, req, res) # TODO: Do we need this?? - - // Only close the inferred span if one was created - if (context.inferredProxySpan) { - context.inferredProxySpan.finish() - context.inferredProxySpanFinished = true - } -} - -module.exports = { - createInferredProxySpan, - finishInferredProxySpan -} diff --git a/packages/dd-trace/src/plugins/util/serverless.js b/packages/dd-trace/src/plugins/util/serverless.js deleted file mode 100644 index 43958314705..00000000000 --- a/packages/dd-trace/src/plugins/util/serverless.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -const types = require('../../../../../ext/types') -const web = require('./web') - -const serverless = { ...web } -serverless.TYPE = types.SERVERLESS - -module.exports = serverless diff --git a/packages/dd-trace/src/plugins/util/web.js b/packages/dd-trace/src/plugins/util/web.js deleted file mode 100644 index 690b5cf54f3..00000000000 --- a/packages/dd-trace/src/plugins/util/web.js +++ /dev/null @@ -1,633 +0,0 @@ -'use strict' - -const uniq = require('../../../../datadog-core/src/utils/src/uniq') -const analyticsSampler = require('../../analytics_sampler') -const FORMAT_HTTP_HEADERS = 'http_headers' -const log = require('../../log') -const tags = require('../../../../../ext/tags') -const types = require('../../../../../ext/types') -const kinds = require('../../../../../ext/kinds') -const urlFilter = require('./urlfilter') -const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../constants') -const { createInferredProxySpan, finishInferredProxySpan } = require('./inferred_proxy') -const TracingPlugin = require('../tracing') - -let extractIp - -const WEB = types.WEB -const SERVER = kinds.SERVER -const RESOURCE_NAME = tags.RESOURCE_NAME -const SERVICE_NAME = tags.SERVICE_NAME -const SPAN_TYPE = tags.SPAN_TYPE -const SPAN_KIND = tags.SPAN_KIND -const ERROR = tags.ERROR -const HTTP_METHOD = tags.HTTP_METHOD -const HTTP_URL = tags.HTTP_URL -const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE -const HTTP_ROUTE = tags.HTTP_ROUTE -const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS -const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS -const HTTP_USERAGENT = tags.HTTP_USERAGENT -const HTTP_CLIENT_IP = tags.HTTP_CLIENT_IP -const MANUAL_DROP = tags.MANUAL_DROP - -const HTTP2_HEADER_AUTHORITY = ':authority' -const HTTP2_HEADER_SCHEME = ':scheme' -const HTTP2_HEADER_PATH = ':path' - -const contexts = new WeakMap() -const ends = new WeakMap() - -// TODO: change this to no longer rely on creating a dummy plugin to be able to access startSpan -function createWebPlugin (tracer, config = {}) { - const plugin = new TracingPlugin(tracer, tracer._config) - plugin.component = 'web' - plugin.config = config - return plugin -} - -function startSpanHelper (tracer, name, options, traceCtx, config = {}) { - if (!web.plugin) { - web.plugin = createWebPlugin(tracer, config) - } - - return web.plugin.startSpan(name, { ...options, tracer, config }, traceCtx) -} - -const web = { - TYPE: WEB, - plugin: null, - - // Ensure the configuration has the correct structure and defaults. - normalizeConfig (config) { - const headers = getHeadersToRecord(config) - const validateStatus = getStatusValidator(config) - const hooks = getHooks(config) - const filter = urlFilter.getFilter(config) - const middleware = getMiddlewareSetting(config) - const queryStringObfuscation = getQsObfuscator(config) - - extractIp = config.clientIpEnabled && require('./ip_extractor').extractIp - - return { - ...config, - headers, - validateStatus, - hooks, - filter, - middleware, - queryStringObfuscation - } - }, - - setFramework (req, name, config) { - const context = this.patch(req) - const span = context.span - - if (!span) return - - span.context()._name = `${name}.request` - span.context()._tags.component = name - span._integrationName = name - - web.setConfig(req, config) - }, - - setConfig (req, config) { - const context = contexts.get(req) - const span = context.span - - context.config = config - - if (!config.filter(req.url)) { - span.setTag(MANUAL_DROP, true) - span.context()._trace.isRecording = false - } - - if (config.service) { - span.setTag(SERVICE_NAME, config.service) - } - - analyticsSampler.sample(span, config.measured, true) - }, - - startSpan (tracer, config, req, res, name, traceCtx) { - const context = this.patch(req) - - let span - - if (context.span) { - context.span.context()._name = name - span = context.span - } else { - span = web.startChildSpan(tracer, config, name, req, traceCtx) - } - - context.tracer = tracer - context.span = span - context.res = res - - this.setConfig(req, config) - addRequestTags(context, this.TYPE) - - return span - }, - wrap (req) { - const context = contexts.get(req) - if (!context.instrumented) { - this.wrapEnd(context) - context.instrumented = true - } - }, - // Start a span and activate a scope for a request. - instrument (tracer, config, req, res, name, callback) { - const span = this.startSpan(tracer, config, req, res, name) - - this.wrap(req) - - return callback && tracer.scope().activate(span, () => callback(span)) - }, - - // Reactivate the request scope in case it was changed by a middleware. - reactivate (req, fn) { - return reactivate(req, fn) - }, - - // Add a route segment that will be used for the resource name. - enterRoute (req, path) { - if (typeof path === 'string') { - contexts.get(req).paths.push(path) - } - }, - - setRoute (req, path) { - const context = contexts.get(req) - - if (!context) return - - context.paths = [path] - }, - - // Remove the current route segment. - exitRoute (req) { - contexts.get(req).paths.pop() - }, - - // Start a new middleware span and activate a new scope with the span. - wrapMiddleware (req, middleware, name, fn) { - if (!this.active(req)) return fn() - - const context = contexts.get(req) - const tracer = context.tracer - const childOf = this.active(req) - const config = context.config - const traceCtx = context.traceCtx - - if (config.middleware === false) return this.bindAndWrapMiddlewareErrors(fn, req, tracer, childOf) - - const span = startSpanHelper(tracer, name, { childOf }, traceCtx, config) - - analyticsSampler.sample(span, config.measured) - - span.addTags({ - [RESOURCE_NAME]: middleware._name || middleware.name || '' - }) - - context.middleware.push(span) - - return tracer.scope().activate(span, fn) - }, - - // catch errors and apply to active span - bindAndWrapMiddlewareErrors (fn, req, tracer, activeSpan) { - try { - return tracer.scope().bind(fn, activeSpan).apply(this, arguments) - } catch (e) { - web.addError(req, e) // TODO: remove when error formatting is moved to Span - throw e - } - }, - - // Finish the active middleware span. - finish (req, error) { - if (!this.active(req)) return - - const context = contexts.get(req) - const span = context.middleware.pop() - - if (span) { - if (error) { - span.addTags({ - [ERROR_TYPE]: error.name, - [ERROR_MESSAGE]: error.message, - [ERROR_STACK]: error.stack - }) - } - - span.finish() - } - }, - - // Register a callback to run before res.end() is called. - beforeEnd (req, callback) { - contexts.get(req).beforeEnd.push(callback) - }, - - // Prepare the request for instrumentation. - patch (req) { - let context = contexts.get(req) - - if (context) return context - - context = req.stream && contexts.get(req.stream) - - if (context) { - contexts.set(req, context) - return context - } - - context = { - req, - span: null, - paths: [], - middleware: [], - beforeEnd: [], - config: {} - } - - contexts.set(req, context) - - return context - }, - - // Return the request root span. - root (req) { - const context = contexts.get(req) - return context ? context.span : null - }, - - // Return the active span. - active (req) { - const context = contexts.get(req) - - if (!context) return null - if (context.middleware.length === 0) return context.span || null - - return context.middleware.at(-1) - }, - - // Extract the parent span from the headers and start a new span as its child - startChildSpan (tracer, config, name, req, traceCtx) { - const headers = req.headers - const reqCtx = contexts.get(req) - let childOf = tracer.extract(FORMAT_HTTP_HEADERS, headers) - - // we may have headers signaling a router proxy span should be created (such as for AWS API Gateway) - if (tracer._config?.inferredProxyServicesEnabled) { - const proxySpan = createInferredProxySpan(headers, childOf, tracer, reqCtx, traceCtx, config, startSpanHelper) - if (proxySpan) { - childOf = proxySpan - } - } - - const span = startSpanHelper(tracer, name, { childOf }, traceCtx, config) - - return span - }, - - // Validate a request's status code and then add error tags if necessary - addStatusError (req, statusCode) { - const context = contexts.get(req) - const { span, inferredProxySpan, error } = context - - const spanHasExistingError = span.context()._tags.error || span.context()._tags[ERROR_MESSAGE] - const inferredSpanContext = inferredProxySpan?.context() - const inferredSpanHasExistingError = inferredSpanContext?._tags.error || inferredSpanContext?._tags[ERROR_MESSAGE] - - const isValidStatusCode = context.config.validateStatus(statusCode) - - if (!spanHasExistingError && !isValidStatusCode) { - span.setTag(ERROR, error || true) - } - - if (inferredProxySpan && !inferredSpanHasExistingError && !isValidStatusCode) { - inferredProxySpan.setTag(ERROR, error || true) - } - }, - - // Add an error to the request - addError (req, error) { - if (error instanceof Error) { - const context = contexts.get(req) - - if (context) { - context.error = error - } - } - }, - - finishMiddleware (context) { - if (context.finished) return - - let span - - while ((span = context.middleware.pop())) { - span.finish() - } - }, - - finishSpan (context) { - const { req, res } = context - - if (context.finished && !req.stream) return - - addRequestTags(context, this.TYPE) - addResponseTags(context) - - context.config.hooks.request(context.span, req, res) - addResourceTag(context) - - context.span.finish() - context.finished = true - }, - - finishAll (context) { - for (const beforeEnd of context.beforeEnd) { - beforeEnd() - } - - web.finishMiddleware(context) - - web.finishSpan(context) - - finishInferredProxySpan(context) - }, - - obfuscateQs (config, url) { - const { queryStringObfuscation } = config - - if (queryStringObfuscation === false) return url - - const i = url.indexOf('?') - if (i === -1) return url - - const path = url.slice(0, i) - if (queryStringObfuscation === true) return path - - let qs = url.slice(i + 1) - - qs = qs.replace(queryStringObfuscation, '') - - return `${path}?${qs}` - }, - - wrapWriteHead (context) { - const { req, res } = context - const writeHead = res.writeHead - - return function (statusCode, statusMessage, headers) { - headers = typeof statusMessage === 'string' ? headers : statusMessage - headers = Object.assign(res.getHeaders(), headers) - - if (req.method.toLowerCase() === 'options' && isOriginAllowed(req, headers)) { - addAllowHeaders(req, res, headers) - } - - return writeHead.apply(this, arguments) - } - }, - getContext (req) { - return contexts.get(req) - }, - wrapRes (context, req, res, end) { - return function () { - web.finishAll(context) - - return end.apply(res, arguments) - } - }, - wrapEnd (context) { - const scope = context.tracer.scope() - const req = context.req - const res = context.res - const end = res.end - - res.writeHead = web.wrapWriteHead(context) - - ends.set(res, this.wrapRes(context, req, res, end)) - - Object.defineProperty(res, 'end', { - configurable: true, - get () { - return ends.get(this) - }, - set (value) { - ends.set(this, scope.bind(value, context.span)) - } - }) - } -} - -function addAllowHeaders (req, res, headers) { - const allowHeaders = splitHeader(headers['access-control-allow-headers']) - const requestHeaders = splitHeader(req.headers['access-control-request-headers']) - const contextHeaders = [ - 'x-datadog-origin', - 'x-datadog-parent-id', - 'x-datadog-sampled', // Deprecated, but still accept it in case it's sent. - 'x-datadog-sampling-priority', - 'x-datadog-trace-id', - 'x-datadog-tags' - ] - - for (const header of contextHeaders) { - if (requestHeaders.includes(header)) { - allowHeaders.push(header) - } - } - - if (allowHeaders.length > 0) { - res.setHeader('access-control-allow-headers', uniq(allowHeaders).join(',')) - } -} - -function isOriginAllowed (req, headers) { - const origin = req.headers.origin - const allowOrigin = headers['access-control-allow-origin'] - - return origin && (allowOrigin === '*' || allowOrigin === origin) -} - -function splitHeader (str) { - return typeof str === 'string' ? str.split(/\s*,\s*/) : [] -} - -function reactivate (req, fn) { - const context = contexts.get(req) - - return context - ? context.tracer.scope().activate(context.span, fn) - : fn() -} - -function addRequestTags (context, spanType) { - const { req, span, inferredProxySpan, config } = context - const url = extractURL(req) - - span.addTags({ - [HTTP_URL]: web.obfuscateQs(config, url), - [HTTP_METHOD]: req.method, - [SPAN_KIND]: SERVER, - [SPAN_TYPE]: spanType, - [HTTP_USERAGENT]: req.headers['user-agent'] - }) - - // if client ip has already been set by appsec, no need to run it again - if (extractIp && !span.context()._tags.hasOwnProperty(HTTP_CLIENT_IP)) { - const clientIp = extractIp(config, req) - - if (clientIp) { - span.setTag(HTTP_CLIENT_IP, clientIp) - inferredProxySpan?.setTag(HTTP_CLIENT_IP, clientIp) - } - } - - addHeaders(context) -} - -function addResponseTags (context) { - const { req, res, paths, span, inferredProxySpan } = context - - const route = paths.join('') - if (route) { - span.setTag(HTTP_ROUTE, route) - } - - span.addTags({ - [HTTP_STATUS_CODE]: res.statusCode - }) - inferredProxySpan?.addTags({ - [HTTP_STATUS_CODE]: res.statusCode - }) - - web.addStatusError(req, res.statusCode) -} - -function addResourceTag (context) { - const { req, span } = context - const tags = span.context()._tags - - if (tags['resource.name']) return - - const resource = [req.method, tags[HTTP_ROUTE]] - .filter(Boolean) - .join(' ') - - span.setTag(RESOURCE_NAME, resource) -} - -function addHeaders (context) { - const { req, res, config, span, inferredProxySpan } = context - - config.headers.forEach(([key, tag]) => { - const reqHeader = req.headers[key] - const resHeader = res.getHeader(key) - - if (reqHeader) { - span.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) - inferredProxySpan?.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) - } - - if (resHeader) { - span.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) - inferredProxySpan?.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) - } - }) -} - -function extractURL (req) { - const headers = req.headers - - if (req.stream) { - return `${headers[HTTP2_HEADER_SCHEME]}://${headers[HTTP2_HEADER_AUTHORITY]}${headers[HTTP2_HEADER_PATH]}` - } - const protocol = getProtocol(req) - return `${protocol}://${req.headers.host}${req.originalUrl || req.url}` -} - -function getProtocol (req) { - if (req.socket && req.socket.encrypted) return 'https' - if (req.connection && req.connection.encrypted) return 'https' - - return 'http' -} - -function getHeadersToRecord (config) { - if (Array.isArray(config.headers)) { - try { - return config.headers - .map(h => h.split(':')) - .map(([key, tag]) => [key.toLowerCase(), tag]) - } catch (err) { - log.error('Web plugin error getting headers', err) - } - } else if (config.hasOwnProperty('headers')) { - log.error('Expected `headers` to be an array of strings.') - } - return [] -} - -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 < 500 -} - -const noop = () => {} - -function getHooks (config) { - const request = config.hooks?.request ?? noop - - return { request } -} - -function getMiddlewareSetting (config) { - if (config && typeof config.middleware === 'boolean') { - return config.middleware - } else if (config && config.hasOwnProperty('middleware')) { - log.error('Expected `middleware` to be a boolean.') - } - - return true -} - -function getQsObfuscator (config) { - const obfuscator = config.queryStringObfuscation - - if (typeof obfuscator === 'boolean') { - return obfuscator - } - - if (typeof obfuscator === 'string') { - if (obfuscator === '') return false // disable obfuscator - - if (obfuscator === '.*') return true // optimize full redact - - try { - return new RegExp(obfuscator, 'gi') - } catch (err) { - log.error('Web plugin error getting qs obfuscator', err) - } - } - - if (config.hasOwnProperty('queryStringObfuscation')) { - log.error('Expected `queryStringObfuscation` to be a regex string or boolean.') - } - - return true -} - -module.exports = web diff --git a/packages/dd-trace/src/supported-configurations.json b/packages/dd-trace/src/supported-configurations.json index e9ba71e91ce..a5a0653e572 100644 --- a/packages/dd-trace/src/supported-configurations.json +++ b/packages/dd-trace/src/supported-configurations.json @@ -176,6 +176,7 @@ "DD_TRACE_APOLLO_SUBGRAPH_ENABLED": ["A"], "DD_TRACE_AVSC_ENABLED": ["A"], "DD_TRACE_AWS_ADD_SPAN_POINTERS": ["A"], + "DD_TRACE_AWS_APIGATEWAY_ENABLED": ["A"], "DD_TRACE_AWS_SDK_AWS_BATCH_PROPAGATION_ENABLED": ["A"], "DD_TRACE_AWS_SDK_AWS_ENABLED": ["A"], "DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED": ["A"], diff --git a/packages/dd-trace/test/appsec/api_security_sampler.spec.js b/packages/dd-trace/test/appsec/api_security_sampler.spec.js index 064b0373201..b4d2f67c219 100644 --- a/packages/dd-trace/test/appsec/api_security_sampler.spec.js +++ b/packages/dd-trace/test/appsec/api_security_sampler.spec.js @@ -16,14 +16,11 @@ describe('API Security Sampler', () => { webStub = { root: sinon.stub(), - getContext: sinon.stub(), - _prioritySampler: { - isSampled: sinon.stub() - } + getContext: sinon.stub() } apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { - '../plugins/util/web': webStub + '../../../datadog-plugin-web/src/utils': webStub }) span = { @@ -204,7 +201,7 @@ describe('API Security Sampler', () => { beforeEach(() => { keepTraceStub = sinon.stub() apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { - '../plugins/util/web': webStub, + '../../../datadog-plugin-web/src/utils': webStub, '../priority_sampler': { keepTrace: keepTraceStub }, diff --git a/packages/dd-trace/test/appsec/graphql.spec.js b/packages/dd-trace/test/appsec/graphql.spec.js index 401ad5f2b74..e067d2848f5 100644 --- a/packages/dd-trace/test/appsec/graphql.spec.js +++ b/packages/dd-trace/test/appsec/graphql.spec.js @@ -2,7 +2,7 @@ const proxyquire = require('proxyquire') const waf = require('../../src/appsec/waf') -const web = require('../../src/plugins/util/web') +const web = require('../../../datadog-plugin-web/src/utils') const { storage } = require('../../../datadog-core') const addresses = require('../../src/appsec/addresses') diff --git a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js index a0d41a53a0a..66083c762db 100644 --- a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js +++ b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js @@ -23,7 +23,7 @@ describe('Overhead controller', () => { } overheadController = proxyquire('../../../src/appsec/iast/overhead-controller', { - '../../plugins/util/web': web + '../../../../datadog-plugin-web/src/utils': web }) oceContextKey = overheadController.OVERHEAD_CONTROLLER_CONTEXT_KEY diff --git a/packages/dd-trace/test/appsec/index.spec.js b/packages/dd-trace/test/appsec/index.spec.js index 2d314ad31c6..8252d8bae12 100644 --- a/packages/dd-trace/test/appsec/index.spec.js +++ b/packages/dd-trace/test/appsec/index.spec.js @@ -91,10 +91,7 @@ describe('AppSec Index', function () { web = { root: sinon.stub(), - getContext: sinon.stub(), - _prioritySampler: { - isSampled: sinon.stub() - } + getContext: sinon.stub() } blocking = { @@ -124,7 +121,7 @@ describe('AppSec Index', function () { } apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { - '../plugins/util/web': web + '../../../datadog-plugin-web/src/utils': web }) sinon.spy(apiSecuritySampler, 'sampleRequest') @@ -145,7 +142,7 @@ describe('AppSec Index', function () { AppSec = proxyquire('../../src/appsec', { '../log': log, - '../plugins/util/web': web, + '../../../datadog-plugin-web/src/utils': web, './blocking': blocking, './user_tracking': UserTracking, './telemetry': appsecTelemetry, @@ -416,7 +413,7 @@ describe('AppSec Index', function () { statusCode: 201 } - web.patch(req) + web.patch(req, config) sinon.stub(Reporter, 'finishRequest') @@ -454,7 +451,7 @@ describe('AppSec Index', function () { 'content-length': '15' } - web.patch(req) + web.patch(req, config) sinon.stub(Reporter, 'finishRequest') sinon.stub(waf, 'disposeContext') @@ -499,7 +496,7 @@ describe('AppSec Index', function () { statusCode: 201 } - web.patch(req) + web.patch(req, config) sinon.stub(Reporter, 'finishRequest') @@ -545,7 +542,7 @@ describe('AppSec Index', function () { statusCode: 201 } - web.patch(req) + web.patch(req, config) sinon.stub(Reporter, 'finishRequest') AppSec.incomingHttpEndTranslator({ req, res }) @@ -611,7 +608,7 @@ describe('AppSec Index', function () { statusCode: 201 } - web.patch(req) + web.patch(req, config) sinon.stub(Reporter, 'finishRequest') AppSec.incomingHttpEndTranslator({ req, res }) @@ -666,7 +663,6 @@ describe('AppSec Index', function () { } web.root.returns(span) - web._prioritySampler.isSampled.returns(true) AppSec.incomingHttpEndTranslator({ req, res }) diff --git a/packages/dd-trace/test/appsec/rasp/lfi.spec.js b/packages/dd-trace/test/appsec/rasp/lfi.spec.js index b21c6473103..1622efd38a3 100644 --- a/packages/dd-trace/test/appsec/rasp/lfi.spec.js +++ b/packages/dd-trace/test/appsec/rasp/lfi.spec.js @@ -34,7 +34,7 @@ describe('RASP - lfi.js', () => { lfi = proxyquire('../../../src/appsec/rasp/lfi', { '../../../../datadog-core': { storage: () => legacyStorage }, '../waf': waf, - '../../plugins/util/web': web, + '../../../../datadog-plugin-web/src/utils': web, '../blocking': blocking, './fs-plugin': appsecFsPlugin }) diff --git a/packages/dd-trace/test/appsec/rasp/utils.spec.js b/packages/dd-trace/test/appsec/rasp/utils.spec.js index 14b39417bd0..3e4f8c2a193 100644 --- a/packages/dd-trace/test/appsec/rasp/utils.spec.js +++ b/packages/dd-trace/test/appsec/rasp/utils.spec.js @@ -24,7 +24,7 @@ describe('RASP - utils.js', () => { } utils = proxyquire('../../../src/appsec/rasp/utils', { - '../../plugins/util/web': web, + '../../../../datadog-plugin-web/src/utils': web, '../stack_trace': stackTrace, '../telemetry': telemetry, '../blocking': { diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index e0e8b089073..ff55a4db00d 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -59,7 +59,7 @@ describe('reporter', () => { } Reporter = proxyquire('../../src/appsec/reporter', { - '../plugins/util/web': web, + '../../../datadog-plugin-web/src/utils': web, './telemetry': telemetry }) }) diff --git a/packages/dd-trace/test/appsec/waf/index.spec.js b/packages/dd-trace/test/appsec/waf/index.spec.js index 9719cb7f1fb..8362cbc5a75 100644 --- a/packages/dd-trace/test/appsec/waf/index.spec.js +++ b/packages/dd-trace/test/appsec/waf/index.spec.js @@ -59,7 +59,7 @@ describe('WAF Manager', () => { '../../rate_limiter': function () { return limiterStub }, '../../priority_sampler': { keepTrace }, '../../standalone/product': { ASM: 'ASM' }, - '../../plugins/util/web': webMock, + '../../../../datadog-plugin-web/src/utils': webMock, '../telemetry': { updateRateLimitedMetric } }) waf.destroy() diff --git a/packages/dd-trace/test/plugins/plugin-structure.spec.js b/packages/dd-trace/test/plugins/plugin-structure.spec.js index 4f0ec779ec1..65ea4539f9b 100644 --- a/packages/dd-trace/test/plugins/plugin-structure.spec.js +++ b/packages/dd-trace/test/plugins/plugin-structure.spec.js @@ -10,7 +10,9 @@ const path = require('path') const hooks = require('../../../datadog-instrumentations/src/helpers/hooks') const abstractPlugins = [ - 'web' // web is an abstract plugin, and will not have an instrumentation file + 'web', // web is an abstract plugin, and will not have an instrumentation file + 'aws-apigateway', // aws-apigateway is an inferred proxy plugin, and will not have an instrumentation file + 'inferred-proxy' // inferred-proxy is an inferred proxy plugin, and will not have an instrumentation file ] // we have some plugin directories that we don't actually have a tracing plugin for, but exist for special cases @@ -18,7 +20,6 @@ const abstractPlugins = [ const missingPlugins = [ 'datadog-plugin-axios', // we test axios to ensure our functionality works with axios, see: https://github.com/DataDog/dd-trace-js/pull/1469 'datadog-plugin-limitd-client', // limitd-client instrumentation handles trace context propagation, no tracing is done - 'datadog-plugin-mongoose' // mongoose tracing is done through mongodb-core instrumentation ] // instrumentations that do not have a hook, but are still instrumented