From eb73c9aba3604d55d988b58e82aca263274e5020 Mon Sep 17 00:00:00 2001 From: James Sumners Date: Thu, 12 Sep 2024 10:55:08 -0400 Subject: [PATCH 1/6] feat: Added utilization info for ECS --- lib/utilization/docker-info.js | 12 +++- lib/utilization/ecs-info.js | 32 ++++++++++ lib/utilization/index.js | 7 ++- test/unit/utilization/ecs-info.test.js | 86 ++++++++++++++++++++++++++ test/unit/utilization/main.test.js | 6 ++ 5 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 lib/utilization/ecs-info.js create mode 100644 test/unit/utilization/ecs-info.test.js diff --git a/lib/utilization/docker-info.js b/lib/utilization/docker-info.js index e9c39d1da8..cff587d716 100644 --- a/lib/utilization/docker-info.js +++ b/lib/utilization/docker-info.js @@ -17,12 +17,18 @@ const CGROUPS_V1_PATH = '/proc/self/cgroup' const CGROUPS_V2_PATH = '/proc/self/mountinfo' const BOOT_ID_PROC_FILE = '/proc/sys/kernel/random/boot_id' -module.exports.getVendorInfo = fetchDockerVendorInfo -module.exports.clearVendorCache = function clearDockerVendorCache() { +module.exports = { + clearVendorCache: clearDockerVendorCache, + getBootId, + getEcsContainerId, + getVendorInfo: fetchDockerVendorInfo +} + +function clearDockerVendorCache() { vendorInfo = null } -module.exports.getBootId = function getBootId(agent, callback, logger = log) { +function getBootId(agent, callback, logger = log) { if (!/linux/i.test(os.platform())) { logger.debug('Platform is not a flavor of linux, omitting boot info') return setImmediate(callback, null, null) diff --git a/lib/utilization/ecs-info.js b/lib/utilization/ecs-info.js new file mode 100644 index 0000000000..b247fe052e --- /dev/null +++ b/lib/utilization/ecs-info.js @@ -0,0 +1,32 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = function fetchEcsInfo( + agent, + callback, + { + logger = require('../logger').child({ component: 'ecs-info' }), + getEcsContainerId = require('./docker-info').getEcsContainerId + } = {} +) { + // Per spec, we do not have a `detect_ecs` key. Since ECS is a service of AWS, + // we rely on the `detect_aws` setting. + if (!agent.config.utilization || !agent.config.utilization.detect_aws) { + return setImmediate(callback, null) + } + + getEcsContainerId({ + agent, + logger, + callback: (error, dockerId) => { + if (error) { + return callback(error, null) + } + return callback(null, { ecsDockerId: dockerId }) + } + }) +} diff --git a/lib/utilization/index.js b/lib/utilization/index.js index 887bdeb954..b2971a5b6f 100644 --- a/lib/utilization/index.js +++ b/lib/utilization/index.js @@ -9,11 +9,12 @@ const logger = require('../logger').child({ component: 'utilization' }) const VENDOR_METHODS = { aws: require('./aws-info'), - pcf: require('./pcf-info'), azure: require('./azure-info'), - gcp: require('./gcp-info'), docker: require('./docker-info').getVendorInfo, - kubernetes: require('./kubernetes-info') + ecs: require('./ecs-info'), + gcp: require('./gcp-info'), + kubernetes: require('./kubernetes-info'), + pcf: require('./pcf-info') } const VENDOR_NAMES = Object.keys(VENDOR_METHODS) diff --git a/test/unit/utilization/ecs-info.test.js b/test/unit/utilization/ecs-info.test.js new file mode 100644 index 0000000000..5e32372aee --- /dev/null +++ b/test/unit/utilization/ecs-info.test.js @@ -0,0 +1,86 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const helper = require('../../lib/agent_helper') +const fetchEcsInfo = require('../../../lib/utilization/ecs-info') + +test('returns null if utilization is disabled', (t, end) => { + const agent = { + config: { + utilization: false + } + } + fetchEcsInfo(agent, (error, data) => { + assert.equal(error, null) + assert.equal(data, null) + end() + }) +}) + +test('returns null if detect_aws is disabled', (t, end) => { + const agent = { + config: { + utilization: { + detect_aws: false + } + } + } + fetchEcsInfo(agent, (error, data) => { + assert.equal(error, null) + assert.equal(data, null) + end() + }) +}) + +test('returns null if error encountered', (t, end) => { + const agent = helper.loadMockedAgent({ + utilization: { + detect_aws: true + } + }) + t.after(() => helper.unloadAgent(agent)) + + fetchEcsInfo( + agent, + (error, data) => { + assert.equal(error.message, 'boom') + assert.equal(data, null) + end() + }, + { getEcsContainerId } + ) + + function getEcsContainerId({ callback }) { + callback(Error('boom')) + } +}) + +test('returns container id', (t, end) => { + const agent = helper.loadMockedAgent({ + utilization: { + detect_aws: true + } + }) + t.after(() => helper.unloadAgent(agent)) + + fetchEcsInfo( + agent, + (error, data) => { + assert.equal(error, null) + assert.deepStrictEqual(data, { ecsDockerId: 'ecs-container-1' }) + end() + }, + { getEcsContainerId } + ) + + function getEcsContainerId({ callback }) { + callback(null, 'ecs-container-1') + } +}) diff --git a/test/unit/utilization/main.test.js b/test/unit/utilization/main.test.js index acca4122fe..0a0a2ecad4 100644 --- a/test/unit/utilization/main.test.js +++ b/test/unit/utilization/main.test.js @@ -33,6 +33,7 @@ test('getVendors', async function (t) { let azureCalled = false let gcpCalled = false let dockerCalled = false + let ecsCalled = false let kubernetesCalled = false let pcfCalled = false @@ -55,6 +56,10 @@ test('getVendors', async function (t) { cb() } }, + './ecs-info': function (agentArg, cb) { + ecsCalled = true + cb() + }, './kubernetes-info': (agentArg, cb) => { kubernetesCalled = true cb() @@ -71,6 +76,7 @@ test('getVendors', async function (t) { assert.ok(azureCalled) assert.ok(gcpCalled) assert.ok(dockerCalled) + assert.ok(ecsCalled) assert.ok(kubernetesCalled) assert.ok(pcfCalled) end() From a5b1e78b3df4f1ec1650d7d67246418bf01aae45 Mon Sep 17 00:00:00 2001 From: James Sumners Date: Thu, 12 Sep 2024 12:24:38 -0400 Subject: [PATCH 2/6] fixes --- lib/utilization/ecs-info.js | 4 ++ test/unit/facts.test.js | 67 ++++++++++++-------------- test/unit/utilization/ecs-info.test.js | 23 +++++++++ 3 files changed, 57 insertions(+), 37 deletions(-) diff --git a/lib/utilization/ecs-info.js b/lib/utilization/ecs-info.js index b247fe052e..9289811e1a 100644 --- a/lib/utilization/ecs-info.js +++ b/lib/utilization/ecs-info.js @@ -26,6 +26,10 @@ module.exports = function fetchEcsInfo( if (error) { return callback(error, null) } + if (dockerId === null) { + // Some error happened where we could not find the id. Skipping. + return callback(null, null) + } return callback(null, { ecsDockerId: dockerId }) } }) diff --git a/test/unit/facts.test.js b/test/unit/facts.test.js index fe96f02a74..aa79dea051 100644 --- a/test/unit/facts.test.js +++ b/test/unit/facts.test.js @@ -439,11 +439,6 @@ tap.test('utilization', (t) => { // We don't collect full hostnames delete expected.full_hostname - // Stub out docker container id query to make this consistent on all OSes. - sysInfo._getDockerContainerId = (_agent, callback) => { - return callback(null) - } - agent = helper.loadMockedAgent(config) if (mockHostname) { agent.config.getHostnameSafe = mockHostname @@ -470,33 +465,36 @@ tap.test('utilization', (t) => { function makeMockCommonRequest(t, test, type) { return (opts, _agent, cb) => { t.equal(_agent, agent) - setImmediate( - cb, - null, - JSON.stringify( - type === 'aws' - ? { - instanceId: test.input_aws_id, - instanceType: test.input_aws_type, - availabilityZone: test.input_aws_zone - } - : type === 'azure' - ? { - location: test.input_azure_location, - name: test.input_azure_name, - vmId: test.input_azure_id, - vmSize: test.input_azure_size - } - : type === 'gcp' - ? { - id: test.input_gcp_id, - machineType: test.input_gcp_type, - name: test.input_gcp_name, - zone: test.input_gcp_zone - } - : null - ) - ) + let payload = null + switch (type) { + case 'aws': { + payload = { + instanceId: test.input_aws_id, + instanceType: test.input_aws_type, + availabilityZone: test.input_aws_zone + } + break + } + case 'azure': { + payload = { + location: test.input_azure_location, + name: test.input_azure_name, + vmId: test.input_azure_id, + vmSize: test.input_azure_size + } + break + } + case 'gcp': { + payload = { + id: test.input_gcp_id, + machineType: test.input_gcp_type, + name: test.input_gcp_name, + zone: test.input_gcp_zone + } + break + } + } + setImmediate(cb, null, JSON.stringify(payload)) } } }) @@ -584,11 +582,6 @@ tap.test('boot_id', (t) => { const expected = test.expected_output_json - // Stub out docker container id query to make this consistent on all OSes. - sysInfo._getDockerContainerId = (_agent, callback) => { - return callback(null) - } - agent = helper.loadMockedAgent(DISABLE_ALL_DETECTIONS) if (mockHostname) { agent.config.getHostnameSafe = mockHostname diff --git a/test/unit/utilization/ecs-info.test.js b/test/unit/utilization/ecs-info.test.js index 5e32372aee..20a4f71a9f 100644 --- a/test/unit/utilization/ecs-info.test.js +++ b/test/unit/utilization/ecs-info.test.js @@ -62,6 +62,29 @@ test('returns null if error encountered', (t, end) => { } }) +test('returns null if got null', (t, end) => { + const agent = helper.loadMockedAgent({ + utilization: { + detect_aws: true + } + }) + t.after(() => helper.unloadAgent(agent)) + + fetchEcsInfo( + agent, + (error, data) => { + assert.equal(error, null) + assert.equal(data, null) + end() + }, + { getEcsContainerId } + ) + + function getEcsContainerId({ callback }) { + callback(null, null) + } +}) + test('returns container id', (t, end) => { const agent = helper.loadMockedAgent({ utilization: { From 636ff5cddd173d7d90716c9189f334f825c7f5e9 Mon Sep 17 00:00:00 2001 From: James Sumners Date: Thu, 12 Sep 2024 12:42:05 -0400 Subject: [PATCH 3/6] fixes --- test/integration/infinite-tracing-connection.tap.js | 3 +++ test/integration/newrelic-harvest-limits.tap.js | 3 +++ test/integration/newrelic-response-handling.tap.js | 3 +++ test/integration/utilization/system-info.tap.js | 6 ++++++ 4 files changed, 15 insertions(+) diff --git a/test/integration/infinite-tracing-connection.tap.js b/test/integration/infinite-tracing-connection.tap.js index 8a5823e701..77eb348de9 100644 --- a/test/integration/infinite-tracing-connection.tap.js +++ b/test/integration/infinite-tracing-connection.tap.js @@ -290,6 +290,9 @@ const infiniteTracingService = grpc.loadPackageDefinition(packageDefinition).com record_sql: 'obfuscated', explain_threshold: Number.MIN_VALUE // force SQL traces }, + utilization: { + detect_aws: false + }, infinite_tracing: { ...config, span_events: { diff --git a/test/integration/newrelic-harvest-limits.tap.js b/test/integration/newrelic-harvest-limits.tap.js index 9964b5b46a..b5adda9001 100644 --- a/test/integration/newrelic-harvest-limits.tap.js +++ b/test/integration/newrelic-harvest-limits.tap.js @@ -54,6 +54,9 @@ tap.test('Connect calls re-generate harvest limits from original config values', host: TEST_DOMAIN, application_logging: { enabled: true + }, + utilization: { + detect_aws: false } }) }) diff --git a/test/integration/newrelic-response-handling.tap.js b/test/integration/newrelic-response-handling.tap.js index fdb5a0ee60..2dbc37bc36 100644 --- a/test/integration/newrelic-response-handling.tap.js +++ b/test/integration/newrelic-response-handling.tap.js @@ -90,6 +90,9 @@ function createStatusCodeTest(testCase) { transaction_tracer: { record_sql: 'obfuscated', explain_threshold: Number.MIN_VALUE // force SQL traces + }, + utilization: { + detect_aws: false } }) diff --git a/test/integration/utilization/system-info.tap.js b/test/integration/utilization/system-info.tap.js index 46cdd2bc04..522b28f087 100644 --- a/test/integration/utilization/system-info.tap.js +++ b/test/integration/utilization/system-info.tap.js @@ -13,6 +13,10 @@ const fetchSystemInfo = require('../../../lib/system-info') test('pricing system-info aws', function (t) { const awsHost = 'http://169.254.169.254' + process.env.ECS_CONTAINER_METADATA_URI_V4 = awsHost + '/docker' + t.teardown(() => { + delete process.env.ECS_CONTAINER_METADATA_URI_V4 + }) const awsResponses = { 'dynamic/instance-identity/document': { @@ -22,6 +26,7 @@ test('pricing system-info aws', function (t) { } } + const ecsScope = nock(awsHost).get('/docker').reply(200, { DockerId: 'ecs-container-1' }) const awsRedirect = nock(awsHost) awsRedirect.put('/latest/api/token').reply(200, 'awsToken') // eslint-disable-next-line guard-for-in @@ -51,6 +56,7 @@ test('pricing system-info aws', function (t) { // This will throw an error if the sys info isn't being cached properly t.ok(awsRedirect.isDone(), 'should exhaust nock endpoints') + t.ok(ecsScope.isDone()) fetchSystemInfo(agent, function checkCache(err, cachedInfo) { t.same(cachedInfo.vendors.aws, { instanceType: 'test.type', From 5febab4b93c0f466f3a16558dda2eb19c55e26ea Mon Sep 17 00:00:00 2001 From: James Sumners Date: Thu, 12 Sep 2024 12:50:30 -0400 Subject: [PATCH 4/6] fixes --- test/integration/api/shutdown.tap.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/integration/api/shutdown.tap.js b/test/integration/api/shutdown.tap.js index 0805965ffe..86f1e34286 100644 --- a/test/integration/api/shutdown.tap.js +++ b/test/integration/api/shutdown.tap.js @@ -28,7 +28,10 @@ tap.test('#shutdown', (t) => { agent = helper.loadMockedAgent({ license_key: EXPECTED_LICENSE_KEY, - host: TEST_DOMAIN + host: TEST_DOMAIN, + utilization: { + detect_aws: false + } }) agent.config.no_immediate_harvest = true From 7187bde1d23f47eccde5fd41665df49b3715a7d9 Mon Sep 17 00:00:00 2001 From: James Sumners Date: Thu, 12 Sep 2024 15:14:49 -0400 Subject: [PATCH 5/6] refactor --- lib/metrics/names.js | 7 +- lib/utilization/docker-info.js | 70 ----- lib/utilization/ecs-info.js | 78 +++++- .../utilization/system-info.tap.js | 1 + test/unit/utilization/docker-info.test.js | 256 ++++++------------ test/unit/utilization/ecs-info.test.js | 211 +++++++++++---- 6 files changed, 320 insertions(+), 303 deletions(-) diff --git a/lib/metrics/names.js b/lib/metrics/names.js index b4b94e48ac..805a9d3b9b 100644 --- a/lib/metrics/names.js +++ b/lib/metrics/names.js @@ -204,11 +204,12 @@ const HAPI = { const UTILIZATION = { AWS_ERROR: SUPPORTABILITY.UTILIZATION + '/aws/error', - PCF_ERROR: SUPPORTABILITY.UTILIZATION + '/pcf/error', AZURE_ERROR: SUPPORTABILITY.UTILIZATION + '/azure/error', - GCP_ERROR: SUPPORTABILITY.UTILIZATION + '/gcp/error', + BOOT_ID_ERROR: SUPPORTABILITY.UTILIZATION + '/boot_id/error', DOCKER_ERROR: SUPPORTABILITY.UTILIZATION + '/docker/error', - BOOT_ID_ERROR: SUPPORTABILITY.UTILIZATION + '/boot_id/error' + ECS_CONTAINER_ERROR: SUPPORTABILITY.UTILIZATION + '/ecs/container_id/error', + GCP_ERROR: SUPPORTABILITY.UTILIZATION + '/gcp/error', + PCF_ERROR: SUPPORTABILITY.UTILIZATION + '/pcf/error' } const CUSTOM_EVENTS = { diff --git a/lib/utilization/docker-info.js b/lib/utilization/docker-info.js index cff587d716..d830b95998 100644 --- a/lib/utilization/docker-info.js +++ b/lib/utilization/docker-info.js @@ -6,7 +6,6 @@ 'use strict' const fs = require('node:fs') -const http = require('node:http') const log = require('../logger').child({ component: 'docker-info' }) const common = require('./common') const NAMES = require('../metrics/names') @@ -20,7 +19,6 @@ const BOOT_ID_PROC_FILE = '/proc/sys/kernel/random/boot_id' module.exports = { clearVendorCache: clearDockerVendorCache, getBootId, - getEcsContainerId, getVendorInfo: fetchDockerVendorInfo } @@ -43,76 +41,8 @@ function getBootId(agent, callback, logger = log) { } logger.debug('Container boot id is not available in cgroups info') - - if (hasAwsContainerApi() === false) { - // We don't seem to have a recognized location for getting the container - // identifier. - logger.debug('Container is not in a recognized ECS container, omitting boot info') - recordBootIdError(agent) - return callback(null, null) - } - - getEcsContainerId({ agent, callback, logger }) - }) -} - -/** - * Queries the AWS ECS metadata API to get the boot id. - * - * @param {object} params Function parameters. - * @param {object} params.agent Newrelic agent instance. - * @param {Function} params.callback Typical error first callback. The second - * parameter is the boot id as a string. - * @param {object} [params.logger] Internal logger instance. - */ -function getEcsContainerId({ agent, callback, logger }) { - const ecsApiUrl = - process.env.ECS_CONTAINER_METADATA_URI_V4 || process.env.ECS_CONTAINER_METADATA_URI - const req = http.request(ecsApiUrl, (res) => { - let body = Buffer.alloc(0) - res.on('data', (chunk) => { - body = Buffer.concat([body, chunk]) - }) - res.on('end', () => { - try { - const json = body.toString('utf8') - const data = JSON.parse(json) - if (data.DockerId == null) { - logger.debug('Failed to find DockerId in response, omitting boot info') - recordBootIdError(agent) - return callback(null, null) - } - callback(null, data.DockerId) - } catch (error) { - logger.debug('Failed to process ECS API response, omitting boot info: ' + error.message) - recordBootIdError(agent) - callback(null, null) - } - }) - }) - - req.on('error', () => { - logger.debug('Failed to query ECS endpoint, omitting boot info') - recordBootIdError(agent) callback(null, null) }) - - req.end() -} - -/** - * Inspects the running environment to determine if the AWS ECS metadata API - * is available. - * - * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ec2-metadata.html - * - * @returns {boolean} - */ -function hasAwsContainerApi() { - if (process.env.ECS_CONTAINER_METADATA_URI_V4 != null) { - return true - } - return process.env.ECS_CONTAINER_METADATA_URI != null } /** diff --git a/lib/utilization/ecs-info.js b/lib/utilization/ecs-info.js index 9289811e1a..1e6d8943f7 100644 --- a/lib/utilization/ecs-info.js +++ b/lib/utilization/ecs-info.js @@ -5,12 +5,17 @@ 'use strict' +const http = require('node:http') +const NAMES = require('../metrics/names') + module.exports = function fetchEcsInfo( agent, callback, { logger = require('../logger').child({ component: 'ecs-info' }), - getEcsContainerId = require('./docker-info').getEcsContainerId + getEcsContainerId = _getEcsContainerId, + hasAwsContainerApi = _hasAwsContainerApi, + recordIdError = _recordIdError } = {} ) { // Per spec, we do not have a `detect_ecs` key. Since ECS is a service of AWS, @@ -19,9 +24,16 @@ module.exports = function fetchEcsInfo( return setImmediate(callback, null) } + if (hasAwsContainerApi() === false) { + logger.debug('ECS API not available, omitting ECS container id info') + recordIdError(agent) + return callback(null, null) + } + getEcsContainerId({ agent, logger, + recordIdError, callback: (error, dockerId) => { if (error) { return callback(error, null) @@ -34,3 +46,67 @@ module.exports = function fetchEcsInfo( } }) } + +/** + * Queries the AWS ECS metadata API to get the boot id. + * + * @param {object} params Function parameters. + * @param {object} params.agent Newrelic agent instance. + * @param {Function} params.callback Typical error first callback. The second + * parameter is the boot id as a string. + * @param {object} params.logger Internal logger instance. + * @param {function} params.recordIdError Function to record error metric. + */ +function _getEcsContainerId({ agent, callback, logger, recordIdError }) { + const ecsApiUrl = + process.env.ECS_CONTAINER_METADATA_URI_V4 || process.env.ECS_CONTAINER_METADATA_URI + const req = http.request(ecsApiUrl, (res) => { + let body = Buffer.alloc(0) + res.on('data', (chunk) => { + body = Buffer.concat([body, chunk]) + }) + res.on('end', () => { + try { + const json = body.toString('utf8') + const data = JSON.parse(json) + if (data.DockerId == null) { + logger.debug('Failed to find DockerId in response, omitting boot info') + recordIdError(agent) + return callback(null, null) + } + callback(null, data.DockerId) + } catch (error) { + logger.debug('Failed to process ECS API response, omitting boot info: ' + error.message) + recordIdError(agent) + callback(null, null) + } + }) + }) + + req.on('error', () => { + logger.debug('Failed to query ECS endpoint, omitting boot info') + recordIdError(agent) + callback(null, null) + }) + + req.end() +} + +/** + * Inspects the running environment to determine if the AWS ECS metadata API + * is available. + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ec2-metadata.html + * + * @returns {boolean} + */ +function _hasAwsContainerApi() { + if (process.env.ECS_CONTAINER_METADATA_URI_V4 != null) { + return true + } + return process.env.ECS_CONTAINER_METADATA_URI != null +} + +function _recordIdError(agent) { + agent.metrics.getOrCreateMetric(NAMES.UTILIZATION.ECS_CONTAINER_ERROR).incrementCallCount() +} diff --git a/test/integration/utilization/system-info.tap.js b/test/integration/utilization/system-info.tap.js index 522b28f087..ae0b3e8b5d 100644 --- a/test/integration/utilization/system-info.tap.js +++ b/test/integration/utilization/system-info.tap.js @@ -53,6 +53,7 @@ test('pricing system-info aws', function (t) { instanceId: 'test.id', availabilityZone: 'us-west-2b' }) + t.same(systemInfo.vendors.ecs, { ecsDockerId: 'ecs-container-1' }) // This will throw an error if the sys info isn't being cached properly t.ok(awsRedirect.isDone(), 'should exhaust nock endpoints') diff --git a/test/unit/utilization/docker-info.test.js b/test/unit/utilization/docker-info.test.js index cad8ad0bc0..8eac18a3f0 100644 --- a/test/unit/utilization/docker-info.test.js +++ b/test/unit/utilization/docker-info.test.js @@ -7,186 +7,96 @@ const test = require('node:test') const assert = require('node:assert') -const fs = require('node:fs') -const http = require('node:http') -const os = require('node:os') const helper = require('../../lib/agent_helper') -const standardResponse = require('./aws-ecs-api-response.json') -const { getBootId } = require('../../../lib/utilization/docker-info') - -async function getServer() { - const server = http.createServer((req, res) => { - res.writeHead(200, { 'content-type': 'application/json' }) - - switch (req.url) { - case '/json-error': { - res.end(`{"invalid":"json"`) - break - } - - case '/no-id': { - res.end(`{}`) - break - } - - default: { - res.end(JSON.stringify(standardResponse)) - } +const { removeModules, removeMatchedModules } = require('../../lib/cache-buster') + +test.beforeEach(async (ctx) => { + ctx.nr = {} + + const fs = require('fs') + const os = require('os') + ctx.nr.orig = { + fs_access: fs.access, + fs_readFile: fs.readFile, + os_platform: os.platform + } + fs.access = (file, mode, cb) => { + cb(Error('no proc file')) + } + os.platform = () => 'linux' + ctx.nr.fs = fs + ctx.nr.os = os + + const utilCommon = require('../../../lib/utilization/common') + utilCommon.readProc = (path, cb) => { + cb(null, 'docker-1') + } + + const { getBootId } = require('../../../lib/utilization/docker-info') + ctx.nr.getBootId = getBootId + + ctx.nr.agent = helper.loadMockedAgent() + ctx.nr.agent.config.utilization = { + detect_aws: true, + detect_azure: true, + detect_gcp: true, + detect_docker: true, + detect_kubernetes: true, + detect_pcf: true + } + + ctx.nr.logs = [] + ctx.nr.logger = { + debug(msg) { + ctx.nr.logs.push(msg) } - }) - - await new Promise((resolve) => { - server.listen(0, '127.0.0.1', () => { - resolve() - }) - }) - - return server -} - -test('all tests', async (t) => { - t.beforeEach(async (ctx) => { - ctx.nr = {} - ctx.nr.orig = { - fs_access: fs.access, - os_platform: os.platform - } - fs.access = (file, mode, cb) => { - cb(Error('no proc file')) - } - os.platform = () => 'linux' - - ctx.nr.agent = helper.loadMockedAgent() - ctx.nr.agent.config.utilization = { - detect_aws: true, - detect_azure: true, - detect_gcp: true, - detect_docker: true, - detect_kubernetes: true, - detect_pcf: true - } - - ctx.nr.logs = [] - ctx.nr.logger = { - debug(msg) { - ctx.nr.logs.push(msg) - } - } - - ctx.nr.server = await getServer() - }) - - t.afterEach((ctx) => { - fs.access = ctx.nr.orig.fs_access - os.platform = ctx.nr.orig.os_platform - - ctx.nr.server.close() - - helper.unloadAgent(ctx.nr.agent) - - delete process.env.ECS_CONTAINER_METADATA_URI - delete process.env.ECS_CONTAINER_METADATA_URI_V4 - }) + } +}) - await t.test('skips if not in ecs container', (ctx, end) => { - const { agent, logs, logger } = ctx.nr +test.afterEach((ctx) => { + removeModules(['fs', 'os']) + removeMatchedModules(/docker-info/) + removeMatchedModules(/utilization\/commo/) + helper.unloadAgent(ctx.nr.agent) +}) - function callback(err, data) { - assert.ifError(err) - assert.deepEqual(logs, [ - 'Container boot id is not available in cgroups info', - 'Container is not in a recognized ECS container, omitting boot info' - ]) - assert.equal(data, null) - assert.equal( - agent.metrics._metrics.unscoped['Supportability/utilization/boot_id/error']?.callCount, - 1 - ) - end() - } +test('error if not on linux', (t, end) => { + const { agent, logger, getBootId, os } = t.nr + os.platform = () => false + getBootId(agent, callback, logger) + + function callback(error, data) { + assert.equal(error, null) + assert.equal(data, null) + assert.deepStrictEqual(t.nr.logs, ['Platform is not a flavor of linux, omitting boot info']) + end() + } +}) - getBootId(agent, callback, logger) - }) - - await t.test('records request error', (ctx, end) => { - const { agent, logs, logger, server } = ctx.nr - const info = server.address() - process.env.ECS_CONTAINER_METADATA_URI_V4 = `http://${info.address}:0` - - function callback(err, data) { - assert.ifError(err) - assert.deepEqual(logs, [ - 'Container boot id is not available in cgroups info', - `Failed to query ECS endpoint, omitting boot info` - ]) - assert.equal(data, null) - assert.equal( - agent.metrics._metrics.unscoped['Supportability/utilization/boot_id/error']?.callCount, - 1 - ) - end() - } +test('error if no proc file', (t, end) => { + const { agent, logger, getBootId } = t.nr + getBootId(agent, callback, logger) - getBootId(agent, callback, logger) - }) - - await t.test('records json parsing error', (ctx, end) => { - const { agent, logs, logger, server } = ctx.nr - const info = server.address() - process.env.ECS_CONTAINER_METADATA_URI_V4 = `http://${info.address}:${info.port}/json-error` - - function callback(err, data) { - assert.ifError(err) - assert.deepEqual(logs[0], 'Container boot id is not available in cgroups info') - assert.equal(data, null) - assert.equal( - agent.metrics._metrics.unscoped['Supportability/utilization/boot_id/error']?.callCount, - 1 - ) - end() - } + function callback(error, data) { + assert.equal(error, null) + assert.equal(data, null) + assert.deepStrictEqual(t.nr.logs, ['Container boot id is not available in cgroups info']) + end() + } +}) - getBootId(agent, callback, logger) - }) - - await t.test('records error for no id in response', (ctx, end) => { - const { agent, logs, logger, server } = ctx.nr - const info = server.address() - process.env.ECS_CONTAINER_METADATA_URI_V4 = `http://${info.address}:${info.port}/no-id` - - function callback(err, data) { - assert.ifError(err) - assert.deepEqual(logs, [ - 'Container boot id is not available in cgroups info', - 'Failed to find DockerId in response, omitting boot info' - ]) - assert.equal(data, null) - assert.equal( - agent.metrics._metrics.unscoped['Supportability/utilization/boot_id/error']?.callCount, - 1 - ) - end() - } +test('data on success', (t, end) => { + const { agent, logger, getBootId, fs } = t.nr + fs.access = (file, mode, cb) => { + cb(null) + } - getBootId(agent, callback, logger) - }) - - await t.test('records found id', (ctx, end) => { - const { agent, logs, logger, server } = ctx.nr - const info = server.address() - // Cover the non-V4 case: - process.env.ECS_CONTAINER_METADATA_URI = `http://${info.address}:${info.port}/success` - - function callback(err, data) { - assert.ifError(err) - assert.deepEqual(logs, ['Container boot id is not available in cgroups info']) - assert.equal(data, '1e1698469422439ea356071e581e8545-2769485393') - assert.ok( - !agent.metrics._metrics.unscoped['Supportability/utilization/boot_id/error']?.callCount - ) - end() - } + getBootId(agent, callback, logger) - getBootId(agent, callback, logger) - }) + function callback(error, data) { + assert.equal(error, null) + assert.equal(data, 'docker-1') + assert.deepStrictEqual(t.nr.logs, []) + end() + } }) diff --git a/test/unit/utilization/ecs-info.test.js b/test/unit/utilization/ecs-info.test.js index 20a4f71a9f..bb0d08359a 100644 --- a/test/unit/utilization/ecs-info.test.js +++ b/test/unit/utilization/ecs-info.test.js @@ -7,29 +7,72 @@ const test = require('node:test') const assert = require('node:assert') +const http = require('node:http') const helper = require('../../lib/agent_helper') +const standardResponse = require('./aws-ecs-api-response.json') const fetchEcsInfo = require('../../../lib/utilization/ecs-info') -test('returns null if utilization is disabled', (t, end) => { - const agent = { - config: { - utilization: false +async function getServer() { + const server = http.createServer((req, res) => { + res.writeHead(200, { 'content-type': 'application/json' }) + + switch (req.url) { + case '/json-error': { + res.end(`{"invalid":"json"`) + break + } + + case '/no-id': { + res.end(`{}`) + break + } + + default: { + res.end(JSON.stringify(standardResponse)) + } } - } - fetchEcsInfo(agent, (error, data) => { - assert.equal(error, null) - assert.equal(data, null) - end() }) + + await new Promise((resolve) => { + server.listen(0, '127.0.0.1', () => { + resolve() + }) + }) + + return server +} + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.loadMockedAgent({ + utilization: { + detect_aws: true + } + }) + + ctx.nr.logs = [] + ctx.nr.logger = { + debug(msg) { + ctx.nr.logs.push(msg) + } + } + + ctx.nr.server = await getServer() }) -test('returns null if detect_aws is disabled', (t, end) => { +test.afterEach((ctx) => { + ctx.nr.server.close() + helper.unloadAgent(ctx.nr.agent) + + delete process.env.ECS_CONTAINER_METADATA_URI + delete process.env.ECS_CONTAINER_METADATA_URI_V4 +}) + +test('returns null if utilization is disabled', (t, end) => { const agent = { config: { - utilization: { - detect_aws: false - } + utilization: false } } fetchEcsInfo(agent, (error, data) => { @@ -40,12 +83,7 @@ test('returns null if detect_aws is disabled', (t, end) => { }) test('returns null if error encountered', (t, end) => { - const agent = helper.loadMockedAgent({ - utilization: { - detect_aws: true - } - }) - t.after(() => helper.unloadAgent(agent)) + const { agent } = t.nr fetchEcsInfo( agent, @@ -54,7 +92,12 @@ test('returns null if error encountered', (t, end) => { assert.equal(data, null) end() }, - { getEcsContainerId } + { + getEcsContainerId, + hasAwsContainerApi() { + return true + } + } ) function getEcsContainerId({ callback }) { @@ -62,48 +105,104 @@ test('returns null if error encountered', (t, end) => { } }) -test('returns null if got null', (t, end) => { - const agent = helper.loadMockedAgent({ - utilization: { - detect_aws: true - } - }) - t.after(() => helper.unloadAgent(agent)) +test('skips if not in ecs container', (ctx, end) => { + const { agent, logs, logger } = ctx.nr - fetchEcsInfo( - agent, - (error, data) => { - assert.equal(error, null) - assert.equal(data, null) - end() - }, - { getEcsContainerId } - ) + function callback(err, data) { + assert.ifError(err) + assert.deepEqual(logs, ['ECS API not available, omitting ECS container id info']) + assert.equal(data, null) + assert.equal( + agent.metrics._metrics.unscoped['Supportability/utilization/ecs/container_id/error'] + ?.callCount, + 1 + ) + end() + } - function getEcsContainerId({ callback }) { - callback(null, null) + fetchEcsInfo(agent, callback, { logger }) +}) + +test('records request error', (ctx, end) => { + const { agent, logs, logger, server } = ctx.nr + const info = server.address() + process.env.ECS_CONTAINER_METADATA_URI_V4 = `http://${info.address}:0` + + function callback(err, data) { + assert.ifError(err) + assert.deepEqual(logs, ['Failed to query ECS endpoint, omitting boot info']) + assert.equal(data, null) + assert.equal( + agent.metrics._metrics.unscoped['Supportability/utilization/ecs/container_id/error'] + ?.callCount, + 1 + ) + end() } + + fetchEcsInfo(agent, callback, { logger }) }) -test('returns container id', (t, end) => { - const agent = helper.loadMockedAgent({ - utilization: { - detect_aws: true - } - }) - t.after(() => helper.unloadAgent(agent)) +test('records json parsing error', (ctx, end) => { + const { agent, logs, logger, server } = ctx.nr + const info = server.address() + process.env.ECS_CONTAINER_METADATA_URI_V4 = `http://${info.address}:${info.port}/json-error` - fetchEcsInfo( - agent, - (error, data) => { - assert.equal(error, null) - assert.deepStrictEqual(data, { ecsDockerId: 'ecs-container-1' }) - end() - }, - { getEcsContainerId } - ) + function callback(err, data) { + assert.ifError(err) + assert.deepEqual( + logs[0], + 'Failed to process ECS API response, omitting boot info: Unexpected end of JSON input' + ) + assert.equal(data, null) + assert.equal( + agent.metrics._metrics.unscoped['Supportability/utilization/ecs/container_id/error'] + ?.callCount, + 1 + ) + end() + } - function getEcsContainerId({ callback }) { - callback(null, 'ecs-container-1') + fetchEcsInfo(agent, callback, { logger }) +}) + +test('records error for no id in response', (ctx, end) => { + const { agent, logs, logger, server } = ctx.nr + const info = server.address() + process.env.ECS_CONTAINER_METADATA_URI_V4 = `http://${info.address}:${info.port}/no-id` + + function callback(err, data) { + assert.ifError(err) + assert.deepEqual(logs, ['Failed to find DockerId in response, omitting boot info']) + assert.equal(data, null) + assert.equal( + agent.metrics._metrics.unscoped['Supportability/utilization/ecs/container_id/error'] + ?.callCount, + 1 + ) + end() + } + + fetchEcsInfo(agent, callback, { logger }) +}) + +test('records found id', (ctx, end) => { + const { agent, logs, logger, server } = ctx.nr + const info = server.address() + // Cover the non-V4 case: + process.env.ECS_CONTAINER_METADATA_URI = `http://${info.address}:${info.port}/success` + + function callback(err, data) { + assert.ifError(err) + assert.deepEqual(logs, []) + assert.deepStrictEqual(data, { ecsDockerId: '1e1698469422439ea356071e581e8545-2769485393' }) + assert.equal( + agent.metrics._metrics.unscoped['Supportability/utilization/ecs/container_id/error'] + ?.callCount, + undefined + ) + end() } + + fetchEcsInfo(agent, callback, { logger }) }) From 3fef82290adddee96b067246c578a20620c2eaaf Mon Sep 17 00:00:00 2001 From: James Sumners Date: Thu, 12 Sep 2024 15:20:02 -0400 Subject: [PATCH 6/6] fix test --- test/unit/utilization/ecs-info.test.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/unit/utilization/ecs-info.test.js b/test/unit/utilization/ecs-info.test.js index bb0d08359a..e545626a51 100644 --- a/test/unit/utilization/ecs-info.test.js +++ b/test/unit/utilization/ecs-info.test.js @@ -150,9 +150,10 @@ test('records json parsing error', (ctx, end) => { function callback(err, data) { assert.ifError(err) - assert.deepEqual( - logs[0], - 'Failed to process ECS API response, omitting boot info: Unexpected end of JSON input' + assert.equal(logs.length, 1) + assert.equal( + logs[0].startsWith('Failed to process ECS API response, omitting boot info:'), + true ) assert.equal(data, null) assert.equal(