diff --git a/__tests__/server/utils/logging/__snapshots__/attachRequestSpies.spec.js.snap b/__tests__/server/utils/logging/__snapshots__/attachRequestSpies.spec.js.snap new file mode 100644 index 000000000..cc95d7561 --- /dev/null +++ b/__tests__/server/utils/logging/__snapshots__/attachRequestSpies.spec.js.snap @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`attachRequestSpies requestSpy is called with object options 1`] = ` +Url { + "auth": "user:password", + "hash": "#so-blue", + "host": "example.tld:8080", + "hostname": "example.tld", + "href": "https://user:password@example.tld:8080/somewhere?over=rainbow#so-blue", + "path": "/somewhere?over=rainbow", + "pathname": "/somewhere", + "port": "8080", + "protocol": "https:", + "query": "over=rainbow", + "search": "?over=rainbow", + "slashes": true, +} +`; + +exports[`attachRequestSpies requestSpy is called with object options 2`] = ` +Url { + "auth": "user:password", + "hash": "#so-blue", + "host": "example.tld:8080", + "hostname": "example.tld", + "href": "http://user:password@example.tld:8080/somewhere?over=rainbow#so-blue", + "path": "/somewhere?over=rainbow", + "pathname": "/somewhere", + "port": "8080", + "protocol": "http:", + "query": "over=rainbow", + "search": "?over=rainbow", + "slashes": true, +} +`; + +exports[`attachRequestSpies requestSpy is called with parsed options 1`] = ` +Url { + "auth": "user:password", + "hash": "#so-blue", + "host": "example.tld:8080", + "hostname": "example.tld", + "href": "http://user:password@example.tld:8080/somewhere?over=rainbow#so-blue", + "path": "/somewhere?over=rainbow", + "pathname": "/somewhere", + "port": "8080", + "protocol": "http:", + "query": "over=rainbow", + "search": "?over=rainbow", + "slashes": true, +} +`; + +exports[`attachRequestSpies requestSpy is called with sparse object options 1`] = ` +Url { + "auth": null, + "hash": null, + "host": "localhost", + "hostname": "localhost", + "href": "https://localhost/", + "path": "/", + "pathname": "/", + "port": null, + "protocol": "https:", + "query": null, + "search": null, + "slashes": true, +} +`; + +exports[`attachRequestSpies requestSpy is called with sparse object options 2`] = ` +Url { + "auth": null, + "hash": null, + "host": "localhost", + "hostname": "localhost", + "href": "http://localhost/", + "path": "/", + "pathname": "/", + "port": null, + "protocol": "http:", + "query": null, + "search": null, + "slashes": true, +} +`; + +exports[`attachRequestSpies socketCloseSpy is called when the request socket closes 1`] = ` +Url { + "auth": null, + "hash": null, + "host": "example.tld", + "hostname": "example.tld", + "href": "http://example.tld/", + "path": "/", + "pathname": "/", + "port": null, + "protocol": "http:", + "query": null, + "search": null, + "slashes": true, +} +`; + +exports[`attachRequestSpies throws if requestSpy is not a function 1`] = `"requestSpy must be a function (was "undefined")"`; + +exports[`attachRequestSpies throws if socketCloseSpy is provided but is not a function 1`] = `"socketCloseSpy must be function if provided (was "string")"`; diff --git a/__tests__/server/utils/logging/__snapshots__/attachSpy.spec.js.snap b/__tests__/server/utils/logging/__snapshots__/attachSpy.spec.js.snap new file mode 100644 index 000000000..4212324da --- /dev/null +++ b/__tests__/server/utils/logging/__snapshots__/attachSpy.spec.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`attachSpy throws if the method name is not a function on the object 1`] = `"method is not a function"`; + +exports[`attachSpy throws if the spy is not a function 1`] = `"spy must be a function (was "string")"`; diff --git a/__tests__/server/utils/logging/attachRequestSpies.spec.js b/__tests__/server/utils/logging/attachRequestSpies.spec.js new file mode 100644 index 000000000..2b1802b48 --- /dev/null +++ b/__tests__/server/utils/logging/attachRequestSpies.spec.js @@ -0,0 +1,131 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,either express + * or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ + +import http from 'node:http'; +import https from 'node:https'; +import onFinished from 'on-finished'; + +import attachRequestSpies from '../../../../src/server/utils/logging/attachRequestSpies'; + +jest.mock('node:http', () => ({ request: jest.fn() })); +jest.mock('node:https', () => ({ request: jest.fn() })); +jest.mock('on-finished'); + +describe('attachRequestSpies', () => { + it('throws if requestSpy is not a function', () => { + expect(attachRequestSpies).toThrowErrorMatchingSnapshot(); + }); + + it('does not throw if socketCloseSpy is not provided', () => { + expect(() => attachRequestSpies(() => {})).not.toThrow(); + }); + + it('throws if socketCloseSpy is provided but is not a function', () => { + expect(() => attachRequestSpies(() => {}, 'apples')).toThrowErrorMatchingSnapshot(); + }); + + it('attaches http and https spies', () => { + const requestSpy = jest.fn(); + const originalHttpRequest = http.request; + const originalHttpsRequest = https.request; + attachRequestSpies(requestSpy); + expect(http.request).not.toEqual(originalHttpRequest); + expect(https.request).not.toEqual(originalHttpsRequest); + http.request('http://example.com'); + https.request('https://example.com'); + expect(requestSpy).toHaveBeenCalledTimes(2); + }); + + describe('requestSpy', () => { + it('is called with clientRequest', () => { + const fakeOriginalHttpRequest = jest.fn(); + const fakeOriginalHttpsRequest = jest.fn(); + http.request = fakeOriginalHttpRequest; + https.request = fakeOriginalHttpsRequest; + + const requestSpy = jest.fn(); + attachRequestSpies(requestSpy); + http.request('http://example.tld'); + https.request('https://example.tld'); + + expect(requestSpy).toHaveBeenCalledTimes(2); + expect(fakeOriginalHttpRequest).toHaveBeenCalledWith('http://example.tld'); + expect(fakeOriginalHttpsRequest).toHaveBeenCalledWith('https://example.tld'); + }); + + it('is called with object options', () => { + const requestSpy = jest.fn(); + attachRequestSpies(requestSpy); + + https.request({ + protocol: 'https', + hostname: 'example.tld', + port: 8080, + method: 'GET', + path: '/somewhere?over=rainbow#so-blue', + auth: 'user:password', + }); + + http.request({ + protocol: 'http', + hostname: 'example.tld', + port: 8080, + method: 'GET', + path: '/somewhere?over=rainbow#so-blue', + auth: 'user:password', + }); + + expect(requestSpy).toHaveBeenCalledTimes(2); + expect(requestSpy.mock.calls[0][1]).toMatchSnapshot(); + expect(requestSpy.mock.calls[1][1]).toMatchSnapshot(); + }); + + it('is called with sparse object options', () => { + const requestSpy = jest.fn(); + attachRequestSpies(requestSpy); + + https.request({ method: 'GET' }); + http.request({ method: 'GET' }); + + expect(requestSpy).toHaveBeenCalledTimes(2); + expect(requestSpy.mock.calls[0][1]).toMatchSnapshot(); + expect(requestSpy.mock.calls[1][1]).toMatchSnapshot(); + }); + + it('is called with parsed options', () => { + const requestSpy = jest.fn(); + attachRequestSpies(requestSpy); + http.request('http://user:password@example.tld:8080/somewhere?over=rainbow#so-blue'); + expect(requestSpy).toHaveBeenCalledTimes(1); + expect(requestSpy.mock.calls[0][1]).toMatchSnapshot(); + }); + }); + + describe('socketCloseSpy', () => { + it('is called when the request socket closes', () => { + const fakeOriginalRequest = jest.fn(); + http.request = fakeOriginalRequest; + onFinished.mockClear(); + const socketCloseSpy = jest.fn(); + attachRequestSpies(jest.fn(), socketCloseSpy); + + http.request('http://example.tld'); + expect(onFinished).toHaveBeenCalledTimes(1); + onFinished.mock.calls[0][1](); + expect(socketCloseSpy).toHaveBeenCalledTimes(1); + expect(socketCloseSpy.mock.calls[0][1]).toMatchSnapshot(); + expect(fakeOriginalRequest).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/__tests__/server/utils/logging/attachSpy.spec.js b/__tests__/server/utils/logging/attachSpy.spec.js new file mode 100644 index 000000000..ad47854b7 --- /dev/null +++ b/__tests__/server/utils/logging/attachSpy.spec.js @@ -0,0 +1,84 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,either express + * or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ + +import attachSpy from '../../../../src/server/utils/logging/attachSpy'; + +describe('attachSpy', () => { + it('throws if the method name is not a function on the object', () => { + expect(() => attachSpy({}, 'method', () => {})).toThrowErrorMatchingSnapshot(); + }); + it('throws if the spy is not a function', () => { + expect(() => attachSpy({ method: () => {} }, 'method', 'hello')).toThrowErrorMatchingSnapshot(); + }); + describe('monkeypatched method', () => { + const originalMethod = jest.fn(() => 'some return value'); + const obj = {}; + + beforeEach(() => { + obj.method = originalMethod; + originalMethod.mockClear(); + }); + + it('invokes the spy', () => { + const spy = jest.fn(); + attachSpy(obj, 'method', spy); + expect(spy).not.toHaveBeenCalled(); + obj.method(); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('invokes the spy with args', () => { + const spy = jest.fn(); + attachSpy(obj, 'method', spy); + obj.method('g', { h: 'i' }, 9); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0][0]).toEqual(['g', { h: 'i' }, 9]); + }); + + it('invokes the spy with callOriginal', () => { + const spy = jest.fn(); + attachSpy(obj, 'method', spy); + obj.method(); + expect(spy).toHaveBeenCalledTimes(1); + expect(typeof spy.mock.calls[0][1]).toEqual('function'); + }); + + it('returns the original methods return value from callOriginal', () => { + expect.assertions(1); + attachSpy(obj, 'method', (args, callOriginal) => { + expect(callOriginal()).toEqual('some return value'); + }); + obj.method(); + }); + + it('calls the original method when the spy does not', () => { + attachSpy(obj, 'method', () => {}); + obj.method(); + expect(originalMethod).toHaveBeenCalledTimes(1); + }); + + it('does not call the original method when the spy already had', () => { + attachSpy(obj, 'method', (args, callOriginal) => { + callOriginal(); + }); + obj.method(); + expect(originalMethod).toHaveBeenCalledTimes(1); + }); + + it('returns the original methods return value to the caller of the monkeypatched method', () => { + attachSpy(obj, 'method', jest.fn()); + expect(obj.method()).toEqual('some return value'); + }); + }); +}); diff --git a/__tests__/server/utils/logging/setup.spec.js b/__tests__/server/utils/logging/setup.spec.js index fe4c7f639..29cd6581e 100644 --- a/__tests__/server/utils/logging/setup.spec.js +++ b/__tests__/server/utils/logging/setup.spec.js @@ -23,7 +23,7 @@ jest.mock('yargs', () => ({ })); describe('setup', () => { - let monkeypatches; + let attachRequestSpies; let logger; let startTimer; @@ -40,12 +40,12 @@ describe('setup', () => { function load() { jest.resetModules(); - jest.mock('@americanexpress/lumberjack'); + jest.mock('../../../../src/server/utils/logging/attachRequestSpies'); jest.mock('../../../../src/server/utils/logging/timing', () => ({ startTimer: jest.fn(), measureTime: jest.fn(() => 12), })); - ({ monkeypatches } = require('@americanexpress/lumberjack')); + attachRequestSpies = require('../../../../src/server/utils/logging/attachRequestSpies').default; ({ startTimer } = require('../../../../src/server/utils/logging/timing')); logger = require('../../../../src/server/utils/logging/logger').default; require('../../../../src/server/utils/logging/setup'); @@ -60,36 +60,9 @@ describe('setup', () => { }); }); - it('spies on HTTP requests', () => { + it('spies on requests', () => { load(); - expect(monkeypatches.attachHttpRequestSpy).toHaveBeenCalledTimes(1); - }); - - it('spies on HTTPS requests when the node major version is higher than 8', () => { - Object.defineProperty(process, 'version', { - writable: true, - value: 'v10.0.0', - }); - load(); - expect(monkeypatches.attachHttpsRequestSpy).toHaveBeenCalledTimes(1); - }); - - it('does not spy on HTTPS requests when the node major version is 8 or lower', () => { - Object.defineProperty(process, 'version', { - writable: true, - value: 'v8.0.0', - }); - load(); - expect(monkeypatches.attachHttpsRequestSpy).not.toHaveBeenCalled(); - }); - - it('does not spy on HTTPS requests when the node major version is 6 or lower', () => { - Object.defineProperty(process, 'version', { - writable: true, - value: 'v6.0.0', - }); - load(); - expect(monkeypatches.attachHttpsRequestSpy).not.toHaveBeenCalled(); + expect(attachRequestSpies).toHaveBeenCalledTimes(1); }); describe('logging outgoing requests', () => { @@ -115,8 +88,8 @@ describe('setup', () => { it('starts a timer when the request starts', () => { const { externalRequest } = createExternalRequestAndParsedUrl(); load(); - expect(monkeypatches.attachHttpRequestSpy).toHaveBeenCalledTimes(1); - const outgoingRequestSpy = monkeypatches.attachHttpRequestSpy.mock.calls[0][0]; + expect(attachRequestSpies).toHaveBeenCalledTimes(1); + const outgoingRequestSpy = attachRequestSpies.mock.calls[0][0]; outgoingRequestSpy(externalRequest); expect(startTimer).toHaveBeenCalledTimes(1); expect(startTimer).toHaveBeenCalledWith(externalRequest); @@ -125,8 +98,8 @@ describe('setup', () => { it('is level info', () => { const { externalRequest, parsedUrl } = createExternalRequestAndParsedUrl(); load(); - expect(monkeypatches.attachHttpRequestSpy).toHaveBeenCalledTimes(1); - const outgoingRequestEndSpy = monkeypatches.attachHttpRequestSpy.mock.calls[0][1]; + expect(attachRequestSpies).toHaveBeenCalledTimes(1); + const outgoingRequestEndSpy = attachRequestSpies.mock.calls[0][1]; outgoingRequestEndSpy(externalRequest, parsedUrl); expect(logger.info).toHaveBeenCalledTimes(1); expect(logger.info.mock.calls[0]).toMatchSnapshot(); @@ -140,8 +113,8 @@ describe('setup', () => { } = createExternalRequestAndParsedUrl(); externalRequestHeaders['correlation-id'] = '1234'; load(); - expect(monkeypatches.attachHttpRequestSpy).toHaveBeenCalledTimes(1); - const outgoingRequestEndSpy = monkeypatches.attachHttpRequestSpy.mock.calls[0][1]; + expect(attachRequestSpies).toHaveBeenCalledTimes(1); + const outgoingRequestEndSpy = attachRequestSpies.mock.calls[0][1]; outgoingRequestEndSpy(externalRequest, parsedUrl); expect(logger.info).toHaveBeenCalledTimes(1); const entry = logger.info.mock.calls[0][0]; @@ -157,8 +130,8 @@ describe('setup', () => { } = createExternalRequestAndParsedUrl(); delete externalRequestHeaders['correlation-id']; load(); - expect(monkeypatches.attachHttpRequestSpy).toHaveBeenCalledTimes(1); - const outgoingRequestEndSpy = monkeypatches.attachHttpRequestSpy.mock.calls[0][1]; + expect(attachRequestSpies).toHaveBeenCalledTimes(1); + const outgoingRequestEndSpy = attachRequestSpies.mock.calls[0][1]; outgoingRequestEndSpy(externalRequest, parsedUrl); const entry = logger.info.mock.calls[0][0]; expect(entry.request.metaData).toHaveProperty('correlationId', undefined); @@ -168,8 +141,8 @@ describe('setup', () => { const { externalRequest, parsedUrl } = createExternalRequestAndParsedUrl(); externalRequest.res.statusCode = 200; load(); - expect(monkeypatches.attachHttpRequestSpy).toHaveBeenCalledTimes(1); - const outgoingRequestEndSpy = monkeypatches.attachHttpRequestSpy.mock.calls[0][1]; + expect(attachRequestSpies).toHaveBeenCalledTimes(1); + const outgoingRequestEndSpy = attachRequestSpies.mock.calls[0][1]; outgoingRequestEndSpy(externalRequest, parsedUrl); const entry = logger.info.mock.calls[0][0]; expect(entry.request).toHaveProperty('statusCode', 200); @@ -179,8 +152,8 @@ describe('setup', () => { const { externalRequest, parsedUrl } = createExternalRequestAndParsedUrl(); delete externalRequest.res.statusCode; load(); - expect(monkeypatches.attachHttpRequestSpy).toHaveBeenCalledTimes(1); - const outgoingRequestEndSpy = monkeypatches.attachHttpRequestSpy.mock.calls[0][1]; + expect(attachRequestSpies).toHaveBeenCalledTimes(1); + const outgoingRequestEndSpy = attachRequestSpies.mock.calls[0][1]; outgoingRequestEndSpy(externalRequest, parsedUrl); const entry = logger.info.mock.calls[0][0]; expect(entry.request).toHaveProperty('statusCode', null); @@ -190,8 +163,8 @@ describe('setup', () => { const { externalRequest, parsedUrl } = createExternalRequestAndParsedUrl(); externalRequest.res.statusMessage = 'OK'; load(); - expect(monkeypatches.attachHttpRequestSpy).toHaveBeenCalledTimes(1); - const outgoingRequestEndSpy = monkeypatches.attachHttpRequestSpy.mock.calls[0][1]; + expect(attachRequestSpies).toHaveBeenCalledTimes(1); + const outgoingRequestEndSpy = attachRequestSpies.mock.calls[0][1]; outgoingRequestEndSpy(externalRequest, parsedUrl); const entry = logger.info.mock.calls[0][0]; expect(entry.request).toHaveProperty('statusText', 'OK'); @@ -201,8 +174,8 @@ describe('setup', () => { const { externalRequest, parsedUrl } = createExternalRequestAndParsedUrl(); delete externalRequest.res.statusMessage; load(); - expect(monkeypatches.attachHttpRequestSpy).toHaveBeenCalledTimes(1); - const outgoingRequestEndSpy = monkeypatches.attachHttpRequestSpy.mock.calls[0][1]; + expect(attachRequestSpies).toHaveBeenCalledTimes(1); + const outgoingRequestEndSpy = attachRequestSpies.mock.calls[0][1]; outgoingRequestEndSpy(externalRequest, parsedUrl); const entry = logger.info.mock.calls[0][0]; expect(entry.request).toHaveProperty('statusText', null); diff --git a/package-lock.json b/package-lock.json index 20d93f91d..d53ff6a1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "dependencies": { "@americanexpress/env-config-utils": "^2.0.2", "@americanexpress/fetch-enhancers": "^1.1.3", - "@americanexpress/lumberjack": "^1.1.4", "@americanexpress/one-app-bundler": "^6.21.1", "@americanexpress/one-app-ducks": "^4.4.2", "@americanexpress/one-app-router": "^1.2.1", @@ -49,6 +48,7 @@ "lean-intl": "^4.2.2", "matcher": "^4.0.0", "object-hash": "^3.0.0", + "on-finished": "^2.4.1", "opossum": "^8.1.3", "opossum-prometheus": "^0.3.0", "pidusage": "^3.0.2", @@ -164,14 +164,6 @@ "npm": ">=6" } }, - "node_modules/@americanexpress/lumberjack": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@americanexpress/lumberjack/-/lumberjack-1.1.4.tgz", - "integrity": "sha512-jsatpSbz4nTtrV29WYkZgydw80mGXjz9lVvfzdI8LsiaFM7uPrC0BYUDcg/JeXTFgqtnsLeBn0ZnLlg7Owm4Xg==", - "dependencies": { - "on-finished": "^2.3.0" - } - }, "node_modules/@americanexpress/one-app-bundler": { "version": "6.21.5", "resolved": "https://registry.npmjs.org/@americanexpress/one-app-bundler/-/one-app-bundler-6.21.5.tgz", diff --git a/package.json b/package.json index ba83c0526..3487c1119 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,6 @@ "dependencies": { "@americanexpress/env-config-utils": "^2.0.2", "@americanexpress/fetch-enhancers": "^1.1.3", - "@americanexpress/lumberjack": "^1.1.4", "@americanexpress/one-app-bundler": "^6.21.1", "@americanexpress/one-app-ducks": "^4.4.2", "@americanexpress/one-app-router": "^1.2.1", @@ -120,6 +119,7 @@ "lean-intl": "^4.2.2", "matcher": "^4.0.0", "object-hash": "^3.0.0", + "on-finished": "^2.4.1", "opossum": "^8.1.3", "opossum-prometheus": "^0.3.0", "pidusage": "^3.0.2", diff --git a/src/server/utils/logging/attachRequestSpies.js b/src/server/utils/logging/attachRequestSpies.js new file mode 100644 index 000000000..870bfb80c --- /dev/null +++ b/src/server/utils/logging/attachRequestSpies.js @@ -0,0 +1,89 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import https from 'node:https'; +import http from 'node:http'; +import url from 'node:url'; +import onFinished from 'on-finished'; + +import attachSpy from './attachSpy'; + +function buildUrlObject(options, defaultProtocol) { + // TODO: url.parse is deprecated, use new URL() instead + // this is not a 1:1 replacement and will require changes. + // Currently the parsed url is used to provide href to + // loggers outgoingRequestEndSpy function. + // https://github.com/americanexpress/one-app/blob/main/src/server/utils/logging/setup.js#L42 + // https://nodejs.org/api/url.html#urlparseurlstring-parsequerystring-slashesdenotehost + const parsedPath = url.parse(options.path || '/'); + const protocol = options.protocol || `${defaultProtocol}:`; + const urlObject = { + auth: options.auth, + hostname: options.hostname || options.host || 'localhost', + protocol, + port: options.port || (protocol === 'http:' ? 80 : 443), + hash: parsedPath.hash, + path: parsedPath.path, + pathname: parsedPath.pathname, + query: parsedPath.query, + search: parsedPath.search, + }; + if ( + (protocol === 'http:' && urlObject.port === 80) + || (protocol === 'https:' && urlObject.port === 443) + ) { + urlObject.port = undefined; + } + return urlObject; +} + +function parseUrl(options, defaultProtocol) { + // some data is not stored in the clientRequest, have to duplicate some parsing + // adapted from https://github.com/nodejs/node/blob/894203dae39c7f1f36fc6ba126bb5d782d79b744/lib/_http_client.js#L22 + if (typeof options === 'string') { + return url.parse(options); + } + + return url.parse(url.format(buildUrlObject(options, defaultProtocol))); +} + +function httpSpy(defaultProtocol, requestSpy, socketCloseSpy) { + return (args, callOriginal) => { + const options = args[0]; + const clientRequest = callOriginal(); + const parsedUrl = parseUrl(options, defaultProtocol); + + requestSpy(clientRequest, parsedUrl); + + if (socketCloseSpy) { + onFinished(clientRequest, () => socketCloseSpy(clientRequest, parsedUrl)); + } + }; +} + +export default function attachRequestSpies(requestSpy, socketCloseSpy) { + if (typeof requestSpy !== 'function') { + throw new TypeError(`requestSpy must be a function (was "${typeof requestSpy}")`); + } + + if (socketCloseSpy && (typeof socketCloseSpy !== 'function')) { + throw new TypeError( + `socketCloseSpy must be function if provided (was "${typeof socketCloseSpy}")`); + } + + attachSpy(https, 'request', httpSpy('https', requestSpy, socketCloseSpy)); + attachSpy(http, 'request', httpSpy('http', requestSpy, socketCloseSpy)); +} diff --git a/src/server/utils/logging/attachSpy.js b/src/server/utils/logging/attachSpy.js new file mode 100644 index 000000000..967ce325d --- /dev/null +++ b/src/server/utils/logging/attachSpy.js @@ -0,0 +1,47 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +export default function attachSpy(obj, methodName, spy) { + const origMethod = obj[methodName]; + + if (typeof origMethod !== 'function') { + throw new TypeError(`${methodName} is not a function`); + } + + if (typeof spy !== 'function') { + throw new TypeError(`spy must be a function (was "${typeof spy}")`); + } + + // we're monkeypatching, we need to reassign a property of the obj argument + // eslint-disable-next-line no-param-reassign -- see above comment + obj[methodName] = function monkeypatchedMethod(...args) { + let returnValue; + let originalCalled = false; + const callOriginal = () => { + originalCalled = true; + returnValue = origMethod.apply(obj, args); + return returnValue; + }; + + spy([...args], callOriginal); + + if (!originalCalled) { + callOriginal(); + } + + return returnValue; + }; +} diff --git a/src/server/utils/logging/setup.js b/src/server/utils/logging/setup.js index aebfe64c8..ecaa15649 100644 --- a/src/server/utils/logging/setup.js +++ b/src/server/utils/logging/setup.js @@ -14,9 +14,9 @@ * permissions and limitations under the License. */ -import { monkeypatches } from '@americanexpress/lumberjack'; import logger from './logger'; import { startTimer, measureTime } from './timing'; +import attachRequestSpies from './attachRequestSpies'; const COLON_AT_THE_END_REGEXP = /:$/; function formatProtocol(parsedUrl) { @@ -69,12 +69,4 @@ function outgoingRequestEndSpy(externalRequest, parsedUrl) { }); } -// In Node.js v8 and earlier https.request internally called http.request, but this is changed in -// later versions -// https://github.com/nodejs/node/blob/v6.x/lib/https.js#L206 -// https://github.com/nodejs/node/blob/v8.x/lib/https.js#L239 -// https://github.com/nodejs/node/blob/v10.x/lib/https.js#L271 -if (Number.parseInt(/^v(\d+)/.exec(process.version)[1], 10) > 8) { - monkeypatches.attachHttpsRequestSpy(outgoingRequestSpy, outgoingRequestEndSpy); -} -monkeypatches.attachHttpRequestSpy(outgoingRequestSpy, outgoingRequestEndSpy); +attachRequestSpies(outgoingRequestSpy, outgoingRequestEndSpy);