Skip to content

Commit

Permalink
feat: Added utilization info for ECS (#2565)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsumners-nr authored Sep 12, 2024
1 parent 619f23c commit 6f92073
Show file tree
Hide file tree
Showing 13 changed files with 476 additions and 289 deletions.
7 changes: 4 additions & 3 deletions lib/metrics/names.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
80 changes: 8 additions & 72 deletions lib/utilization/docker-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -17,12 +16,17 @@ 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,
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)
Expand All @@ -37,76 +41,8 @@ module.exports.getBootId = 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
}

/**
Expand Down
112 changes: 112 additions & 0 deletions lib/utilization/ecs-info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'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 = _getEcsContainerId,
hasAwsContainerApi = _hasAwsContainerApi,
recordIdError = _recordIdError
} = {}
) {
// 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)
}

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)
}
if (dockerId === null) {
// Some error happened where we could not find the id. Skipping.
return callback(null, null)
}
return callback(null, { ecsDockerId: dockerId })
}
})
}

/**
* 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()
}
7 changes: 4 additions & 3 deletions lib/utilization/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
5 changes: 4 additions & 1 deletion test/integration/api/shutdown.tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions test/integration/infinite-tracing-connection.tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
3 changes: 3 additions & 0 deletions test/integration/newrelic-harvest-limits.tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
})
Expand Down
3 changes: 3 additions & 0 deletions test/integration/newrelic-response-handling.tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ function createStatusCodeTest(testCase) {
transaction_tracer: {
record_sql: 'obfuscated',
explain_threshold: Number.MIN_VALUE // force SQL traces
},
utilization: {
detect_aws: false
}
})

Expand Down
7 changes: 7 additions & 0 deletions test/integration/utilization/system-info.tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand All @@ -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
Expand All @@ -48,9 +53,11 @@ 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')
t.ok(ecsScope.isDone())
fetchSystemInfo(agent, function checkCache(err, cachedInfo) {
t.same(cachedInfo.vendors.aws, {
instanceType: 'test.type',
Expand Down
Loading

0 comments on commit 6f92073

Please sign in to comment.