Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added utilization info for ECS #2565

Merged
merged 6 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i know you have unit tests but this integration test should be assertion the ecs vendor:

    t.same(systemInfo.vendors.ecs, {
      ecsDockerId: 'ecs-container-1'
    })

fetchSystemInfo(agent, function checkCache(err, cachedInfo) {
t.same(cachedInfo.vendors.aws, {
instanceType: 'test.type',
Expand Down
Loading
Loading