Skip to content

Commit

Permalink
feat: Added support for getting container ids from ECS metadata API
Browse files Browse the repository at this point in the history
  • Loading branch information
jsumners-nr committed Jun 25, 2024
1 parent 7b3c8d1 commit 52fafcd
Show file tree
Hide file tree
Showing 4 changed files with 371 additions and 22 deletions.
145 changes: 124 additions & 21 deletions lib/utilization/docker-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,162 @@

'use strict'

const logger = require('../logger').child({ component: 'docker-info' })
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')
const os = require('os')
let vendorInfo = null

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() {
vendorInfo = null
}

module.exports.getBootId = function getBootId(agent, callback) {
module.exports.getBootId = 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)
}

common.readProc('/proc/sys/kernel/random/boot_id', function readProcBootId(err, data) {
if (!data) {
bootIdError()
return callback(null, null)
fs.access(BOOT_ID_PROC_FILE, fs.constants.F_OK, (err) => {
if (err == null) {
// The boot id proc file exists, so use it to get the container id.
return common.readProc(BOOT_ID_PROC_FILE, (_, data, cbAgent = agent) => {
readProcBootId({ data, agent: cbAgent, callback })
})
}

data = data.trim()
const asciiData = Buffer.from(data, 'ascii').toString()
logger.debug('Container boot id is not available in cgroups info')

if (data !== asciiData) {
bootIdError()
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)
}

if (data.length !== 36) {
bootIdError()
if (data.length > 128) {
data = data.substring(0, 128)
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)
}
}
})
})

return callback(null, data)
req.on('error', () => {
logger.debug('Failed to query ECS endpoint, omitting boot info')
recordBootIdError(agent)
callback(null, null)
})

function bootIdError() {
agent.metrics.getOrCreateMetric(NAMES.UTILIZATION.BOOT_ID_ERROR).incrementCallCount()
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
}

/**
* Increments a supportability metric to indicate that there was an error
* while trying to read the boot id from the system.
*
* @param {object} agent Newrelic agent instance.
*/
function recordBootIdError(agent) {
agent.metrics.getOrCreateMetric(NAMES.UTILIZATION.BOOT_ID_ERROR).incrementCallCount()
}

/**
* Utility function to parse a Docker boot id from a cgroup proc file.
*
* @param {Buffer} data The information from the proc file.
* @param {object} agent Newrelic agent instance.
* @param {Function} callback Typical error first callback. Second parameter
* is the boot id as a string.
*
* @returns {*}
*/
function readProcBootId({ data, agent, callback }) {
if (!data) {
recordBootIdError(agent)
return callback(null, null)
}

data = data.trim()
const asciiData = Buffer.from(data, 'ascii').toString()

if (data !== asciiData) {
recordBootIdError(agent)
return callback(null, null)
}

if (data.length !== 36) {
recordBootIdError(agent)
if (data.length > 128) {
data = data.substring(0, 128)
}
}

return callback(null, data)
}

/**
* Attempt to extract container id from either cgroups v1 or v2 file
*
* @param {object} agent NR instance
* @param {Function} callback function to call when done
* @param {object} [logger] internal logger instance
*/
function fetchDockerVendorInfo(agent, callback) {
function fetchDockerVendorInfo(agent, callback, logger = log) {
if (!agent.config.utilization || !agent.config.utilization.detect_docker) {
return callback(null, null)
}
Expand Down Expand Up @@ -93,8 +194,9 @@ function fetchDockerVendorInfo(agent, callback) {
*
* @param {string} data file contents
* @param {Function} callback function to call when done
* @param {object} [logger] internal logger instance
*/
function parseCGroupsV2(data, callback) {
function parseCGroupsV2(data, callback, logger = log) {
const containerLine = new RegExp('/docker/containers/([0-9a-f]{64})/')
const line = containerLine.exec(data)
if (line) {
Expand All @@ -110,8 +212,9 @@ function parseCGroupsV2(data, callback) {
* e.g. - `4:cpu:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee`
*
* @param {Function} callback function to call when done
* @param {object} [logger] internal logger instance
*/
function findCGroupsV1(callback) {
function findCGroupsV1(callback, logger = log) {
common.readProc(CGROUPS_V1_PATH, function getCGroup(err, data) {
if (!data) {
logger.debug(`${CGROUPS_V1_PATH} not found, exiting parsing containerId.`)
Expand Down
8 changes: 7 additions & 1 deletion test/unit/facts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
'use strict'

const tap = require('tap')
const fs = require('fs')
const fsAccess = fs.access
const os = require('os')
const hostname = os.hostname
const networkInterfaces = os.networkInterfaces
Expand Down Expand Up @@ -518,6 +520,7 @@ tap.test('boot_id', (t) => {
startingOsPlatform = os.platform

os.platform = () => 'linux'
fs.access = (file, mode, cb) => cb(null)
})

t.afterEach(() => {
Expand All @@ -530,6 +533,7 @@ tap.test('boot_id', (t) => {
sysInfo._getDockerContainerId = startingDockerInfo
common.readProc = startingCommonReadProc
os.platform = startingOsPlatform
fs.access = fsAccess

startingGetMemory = null
startingGetProcessor = null
Expand Down Expand Up @@ -562,7 +566,9 @@ tap.test('boot_id', (t) => {
break

case 'input_boot_id':
mockReadProc = (file, cb) => cb(null, testValue)
mockReadProc = (file, cb) => {
cb(null, testValue, agent)
}
break

// Ignore these keys.
Expand Down
50 changes: 50 additions & 0 deletions test/unit/utilization/aws-ecs-api-response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"DockerId": "1e1698469422439ea356071e581e8545-2769485393",
"Name": "fargateapp",
"DockerName": "fargateapp",
"Image": "123456789012.dkr.ecr.us-west-2.amazonaws.com/fargatetest:latest",
"ImageID": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd",
"Labels": {
"com.amazonaws.ecs.cluster": "arn:aws:ecs:us-west-2:123456789012:cluster/testcluster",
"com.amazonaws.ecs.container-name": "fargateapp",
"com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:123456789012:task/testcluster/1e1698469422439ea356071e581e8545",
"com.amazonaws.ecs.task-definition-family": "fargatetestapp",
"com.amazonaws.ecs.task-definition-version": "7"
},
"DesiredStatus": "RUNNING",
"KnownStatus": "RUNNING",
"Limits": {
"CPU": 2
},
"CreatedAt": "2024-04-25T17:38:31.073208914Z",
"StartedAt": "2024-04-25T17:38:31.073208914Z",
"Type": "NORMAL",
"LogDriver": "awslogs",
"LogOptions": {
"awslogs-create-group": "true",
"awslogs-group": "/ecs/fargatetestapp",
"awslogs-region": "us-west-2",
"awslogs-stream": "ecs/fargateapp/1e1698469422439ea356071e581e8545"
},
"ContainerARN": "arn:aws:ecs:us-west-2:123456789012:container/testcluster/1e1698469422439ea356071e581e8545/050256a5-a7f3-461c-a16f-aca4eae37b01",
"Networks": [
{
"NetworkMode": "awsvpc",
"IPv4Addresses": [
"10.10.10.10"
],
"AttachmentIndex": 0,
"MACAddress": "06:d7:3f:49:1d:a7",
"IPv4SubnetCIDRBlock": "10.10.10.0/20",
"DomainNameServers": [
"10.10.10.2"
],
"DomainNameSearchList": [
"us-west-2.compute.internal"
],
"PrivateDNSName": "ip-10-10-10-10.us-west-2.compute.internal",
"SubnetGatewayIpv4Address": "10.10.10.1/20"
}
],
"Snapshotter": "overlayfs"
}
Loading

0 comments on commit 52fafcd

Please sign in to comment.