Skip to content

Commit

Permalink
Implement experimental AWS ECS resource attributes
Browse files Browse the repository at this point in the history
Implement the experimental AWS ECS resource attributes [1] using
the Metadata v4 Endpoint.

[1] https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud_provider/aws/ecs/
  • Loading branch information
Michele Mancioppi committed Jul 6, 2022
1 parent e265723 commit e94077f
Show file tree
Hide file tree
Showing 4 changed files with 433 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ import {
CloudPlatformValues,
SemanticResourceAttributes,
} from '@opentelemetry/semantic-conventions';
import * as http from 'http';
import * as util from 'util';
import * as fs from 'fs';
import * as os from 'os';
import { getEnv } from '@opentelemetry/core';

const HTTP_TIMEOUT_IN_MS = 1000;

/**
* The AwsEcsDetector can be used to detect if a process is running in AWS
* ECS and return a {@link Resource} populated with data about the ECS
Expand All @@ -38,27 +41,65 @@ import { getEnv } from '@opentelemetry/core';
export class AwsEcsDetector implements Detector {
readonly CONTAINER_ID_LENGTH = 64;
readonly DEFAULT_CGROUP_PATH = '/proc/self/cgroup';

private static readFileAsync = util.promisify(fs.readFile);

private static async getUrlAsJson(url: string): Promise<any> {
return new Promise<string>((resolve, reject) => {
const request = http.get(url, (response: any) => {
if (response.statusCode >= 400) {
reject(`Request to '${url}' failed with status ${response.statusCode}`);
}
/*
* Concatenate the response out of chunks:
* https://nodejs.org/api/stream.html#stream_event_data
*/
let responseBody = '';
response.on('data', (chunk: Buffer) => (responseBody += chunk.toString()));
// All the data has been read, resolve the Promise
response.on('end', () => resolve(responseBody));
});

// Set an aggressive timeout to prevent lock-ups
request.setTimeout(HTTP_TIMEOUT_IN_MS, () => {
request.destroy();
});
// Connection error, disconnection, etc.
request.on('error', reject);
request.end();
})
.then(responseBodyRaw => JSON.parse(responseBodyRaw));
}

async detect(_config?: ResourceDetectionConfig): Promise<Resource> {
const env = getEnv();
if (!env.ECS_CONTAINER_METADATA_URI_V4 && !env.ECS_CONTAINER_METADATA_URI) {
diag.debug('AwsEcsDetector failed: Process is not on ECS');
return Resource.empty();
}

const hostName = os.hostname();
const containerId = await this._getContainerId();

return !hostName && !containerId
? Resource.empty()
: new Resource({
[SemanticResourceAttributes.CLOUD_PROVIDER]: CloudProviderValues.AWS,
[SemanticResourceAttributes.CLOUD_PLATFORM]:
CloudPlatformValues.AWS_ECS,
[SemanticResourceAttributes.CONTAINER_NAME]: hostName || '',
[SemanticResourceAttributes.CONTAINER_ID]: containerId || '',
});
const [containerAndHostnameResource, metadatav4Resource] =
await Promise.all([
this._getContainerIdAndHostnameResource(),
this._getMetadataV4Resource()
]);

const metadataResource = containerAndHostnameResource.merge(metadatav4Resource);

if (!metadataResource.attributes) {
return Resource.empty();
}

/*
* We return the Cloud Provider and Platform only when some other more detailed
* attributes are available
*/
return new Resource({
[SemanticResourceAttributes.CLOUD_PROVIDER]: CloudProviderValues.AWS,
[SemanticResourceAttributes.CLOUD_PLATFORM]:
CloudPlatformValues.AWS_ECS,
})
.merge(metadataResource);
}

/**
Expand All @@ -68,7 +109,10 @@ export class AwsEcsDetector implements Detector {
* we do not throw an error but throw warning message
* and then return null string
*/
private async _getContainerId(): Promise<string | undefined> {
private async _getContainerIdAndHostnameResource(): Promise<Resource> {
const hostName = os.hostname();

var containerId;
try {
const rawData = await AwsEcsDetector.readFileAsync(
this.DEFAULT_CGROUP_PATH,
Expand All @@ -77,14 +121,55 @@ export class AwsEcsDetector implements Detector {
const splitData = rawData.trim().split('\n');
for (const str of splitData) {
if (str.length > this.CONTAINER_ID_LENGTH) {
return str.substring(str.length - this.CONTAINER_ID_LENGTH);
containerId = str.substring(str.length - this.CONTAINER_ID_LENGTH);
break;
}
}
} catch (e) {
diag.warn(`AwsEcsDetector failed to read container ID: ${e.message}`);
}
return undefined;

if (hostName || containerId) {
return new Resource({
[SemanticResourceAttributes.CONTAINER_NAME]: hostName || '',
[SemanticResourceAttributes.CONTAINER_ID]: containerId || '',
});
}

return Resource.empty();
}

private async _getMetadataV4Resource(): Promise<Resource> {
const metadataUrl = getEnv().ECS_CONTAINER_METADATA_URI_V4;

if (!metadataUrl) {
return Resource.empty();
}

const [responseContainer, responseTask] = await Promise.all([
AwsEcsDetector.getUrlAsJson(metadataUrl),
AwsEcsDetector.getUrlAsJson(`${metadataUrl}/task`),
]);

const taskArn: string = responseTask['TaskARN'];

const baseArn: string = taskArn.substring(0, taskArn.lastIndexOf(':'));
const cluster: string = responseTask['Cluster'];

const clusterArn = cluster.indexOf('arn:') == 0 ? cluster : `${baseArn}:cluster/${cluster}`;

const containerArn: string = responseContainer['ContainerARN'];

// https://github.com/open-telemetry/opentelemetry-specification/blob/main/semantic_conventions/resource/cloud_provider/aws/ecs.yaml
return new Resource({
[SemanticResourceAttributes.AWS_ECS_CONTAINER_ARN]: containerArn,
[SemanticResourceAttributes.AWS_ECS_CLUSTER_ARN]: clusterArn,
[SemanticResourceAttributes.AWS_ECS_LAUNCHTYPE]: responseTask['LaunchType'],
[SemanticResourceAttributes.AWS_ECS_TASK_ARN]: taskArn,
[SemanticResourceAttributes.AWS_ECS_TASK_FAMILY]: responseTask['Family'],
[SemanticResourceAttributes.AWS_ECS_TASK_REVISION]: responseTask['Revision'],
});
}
}

export const awsEcsDetector = new AwsEcsDetector();
export const awsEcsDetector = new AwsEcsDetector();
Loading

0 comments on commit e94077f

Please sign in to comment.