Skip to content

Commit

Permalink
Feat: Added AWS ECS Plugins Resource Detector (#1404)
Browse files Browse the repository at this point in the history
Co-authored-by: Bartlomiej Obecny <bobecny@gmail.com>
Co-authored-by: Daniel Dyla <dyladan@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 23, 2020
1 parent 480d1d9 commit 8b13b07
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
Detector,
Resource,
ResourceDetectionConfigWithLogger,
CONTAINER_RESOURCE,
} from '@opentelemetry/resources';
import * as util from 'util';
import * as fs from 'fs';
import * as os from 'os';

/**
* 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
* plugins of AWS X-Ray. Returns an empty Resource if detection fails.
*/
export class AwsEcsDetector implements Detector {
readonly CONTAINER_ID_LENGTH = 64;
readonly DEFAULT_CGROUP_PATH = '/proc/self/cgroup';
private static readFileAsync = util.promisify(fs.readFile);

async detect(config: ResourceDetectionConfigWithLogger): Promise<Resource> {
if (
!process.env.ECS_CONTAINER_METADATA_URI_V4 &&
!process.env.ECS_CONTAINER_METADATA_URI
) {
config.logger.debug('AwsEcsDetector failed: Process is not on ECS');
return Resource.empty();
}

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

return !hostName && !containerId
? Resource.empty()
: new Resource({
[CONTAINER_RESOURCE.NAME]: hostName || '',
[CONTAINER_RESOURCE.ID]: containerId || '',
});
}

/**
* Read container ID from cgroup file
* In ECS, even if we fail to find target file
* or target file does not contain container ID
* we do not throw an error but throw warning message
* and then return null string
*/
private async _getContainerId(
config: ResourceDetectionConfigWithLogger
): Promise<string | undefined> {
try {
const rawData = await AwsEcsDetector.readFileAsync(
this.DEFAULT_CGROUP_PATH,
'utf8'
);
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);
}
}
} catch (e) {
config.logger.warn(
`AwsEcsDetector failed to read container ID: ${e.message}`
);
}
return undefined;
}
}

export const awsEcsDetector = new AwsEcsDetector();
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@

export * from './AwsEc2Detector';
export * from './AwsBeanstalkDetector';
export * from './AwsEcsDetector';
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as assert from 'assert';
import * as sinon from 'sinon';
import {
awsEcsDetector,
AwsEcsDetector,
} from '../../src/detectors/AwsEcsDetector';
import {
assertEmptyResource,
assertContainerResource,
} from '@opentelemetry/resources/test/util/resource-assertions';
import { NoopLogger } from '@opentelemetry/core';
import * as os from 'os';

describe('BeanstalkResourceDetector', () => {
const errorMsg = {
fileNotFoundError: new Error('cannot find cgroup file'),
};

const correctCgroupData =
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm';
const unexpectedCgroupdata =
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
const noisyCgroupData = `\n\n\n abcdefghijklmnopqrstuvwxyz \n ${correctCgroupData}`;
const multiValidCgroupData = `${unexpectedCgroupdata}\n${correctCgroupData}\nbcd${unexpectedCgroupdata}`;
const hostNameData = 'abcd.test.testing.com';

let readStub, hostStub;
let sandbox: sinon.SinonSandbox;

beforeEach(() => {
sandbox = sinon.createSandbox();
process.env.ECS_CONTAINER_METADATA_URI_V4 = '';
process.env.ECS_CONTAINER_METADATA_URI = '';
});

afterEach(() => {
sandbox.restore();
});

it('should successfully return resource data', async () => {
process.env.ECS_CONTAINER_METADATA_URI_V4 = 'ecs_metadata_v4_uri';
hostStub = sandbox.stub(os, 'hostname').returns(hostNameData);
readStub = sandbox
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(correctCgroupData);

const resource = await awsEcsDetector.detect({
logger: new NoopLogger(),
});

sandbox.assert.calledOnce(hostStub);
sandbox.assert.calledOnce(readStub);
assert.ok(resource);
assertContainerResource(resource, {
name: 'abcd.test.testing.com',
id: 'bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm',
});
});

it('should successfully return resource data with noisy cgroup file', async () => {
process.env.ECS_CONTAINER_METADATA_URI = 'ecs_metadata_v3_uri';
hostStub = sandbox.stub(os, 'hostname').returns(hostNameData);
readStub = sandbox
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(noisyCgroupData);

const resource = await awsEcsDetector.detect({
logger: new NoopLogger(),
});

sandbox.assert.calledOnce(hostStub);
sandbox.assert.calledOnce(readStub);
assert.ok(resource);
assertContainerResource(resource, {
name: 'abcd.test.testing.com',
id: 'bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm',
});
});

it('should always return first valid line of data', async () => {
process.env.ECS_CONTAINER_METADATA_URI = 'ecs_metadata_v3_uri';
hostStub = sandbox.stub(os, 'hostname').returns(hostNameData);
readStub = sandbox
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(multiValidCgroupData);

const resource = await awsEcsDetector.detect({
logger: new NoopLogger(),
});

sandbox.assert.calledOnce(hostStub);
sandbox.assert.calledOnce(readStub);
assert.ok(resource);
assertContainerResource(resource, {
name: 'abcd.test.testing.com',
id: 'bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm',
});
});

it('should empty resource without environmental variable', async () => {
hostStub = sandbox.stub(os, 'hostname').returns(hostNameData);
readStub = sandbox
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(correctCgroupData);

const resource = await awsEcsDetector.detect({
logger: new NoopLogger(),
});

sandbox.assert.notCalled(hostStub);
sandbox.assert.notCalled(readStub);
assert.ok(resource);
assertEmptyResource(resource);
});

it('should return resource only with hostname attribute without cgroup file', async () => {
process.env.ECS_CONTAINER_METADATA_URI_V4 = 'ecs_metadata_v4_uri';
hostStub = sandbox.stub(os, 'hostname').returns(hostNameData);
readStub = sandbox
.stub(AwsEcsDetector, 'readFileAsync' as any)
.rejects(errorMsg.fileNotFoundError);

const resource = await awsEcsDetector.detect({
logger: new NoopLogger(),
});

sandbox.assert.calledOnce(hostStub);
sandbox.assert.calledOnce(readStub);
assert.ok(resource);
assertContainerResource(resource, {
name: 'abcd.test.testing.com',
});
});

it('should return resource only with hostname attribute when cgroup file does not contain valid container ID', async () => {
process.env.ECS_CONTAINER_METADATA_URI_V4 = 'ecs_metadata_v4_uri';
hostStub = sandbox.stub(os, 'hostname').returns(hostNameData);
readStub = sandbox
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves('');

const resource = await awsEcsDetector.detect({
logger: new NoopLogger(),
});

sandbox.assert.calledOnce(hostStub);
sandbox.assert.calledOnce(readStub);
assert.ok(resource);
assertContainerResource(resource, {
name: 'abcd.test.testing.com',
});
});

it('should return resource only with container ID attribute without hostname', async () => {
process.env.ECS_CONTAINER_METADATA_URI_V4 = 'ecs_metadata_v4_uri';
hostStub = sandbox.stub(os, 'hostname').returns('');
readStub = sandbox
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(correctCgroupData);

const resource = await awsEcsDetector.detect({
logger: new NoopLogger(),
});

sandbox.assert.calledOnce(hostStub);
sandbox.assert.calledOnce(readStub);
assert.ok(resource);
assertContainerResource(resource, {
id: 'bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm',
});
});

it('should return empty resource when both hostname and container ID are invalid', async () => {
process.env.ECS_CONTAINER_METADATA_URI_V4 = 'ecs_metadata_v4_uri';
hostStub = sandbox.stub(os, 'hostname').returns('');
readStub = sandbox
.stub(AwsEcsDetector, 'readFileAsync' as any)
.rejects(errorMsg.fileNotFoundError);

const resource = await awsEcsDetector.detect({
logger: new NoopLogger(),
});

sandbox.assert.calledOnce(hostStub);
sandbox.assert.calledOnce(readStub);
assert.ok(resource);
assertEmptyResource(resource);
});
});
3 changes: 3 additions & 0 deletions packages/opentelemetry-resources/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export const CONTAINER_RESOURCE = {
/** The container name. */
NAME: 'container.name',

/** The container id. */
ID: 'container.id',

/** The name of the image the container was built on. */
IMAGE_NAME: 'container.image.name',

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,13 @@ describe('assertContainerResource', () => {
it('validates optional attributes', () => {
const resource = new Resource({
[CONTAINER_RESOURCE.NAME]: 'opentelemetry-autoconf',
[CONTAINER_RESOURCE.ID]: 'abc',
[CONTAINER_RESOURCE.IMAGE_NAME]: 'gcr.io/opentelemetry/operator',
[CONTAINER_RESOURCE.IMAGE_TAG]: '0.1',
});
assertContainerResource(resource, {
name: 'opentelemetry-autoconf',
id: 'abc',
imageName: 'gcr.io/opentelemetry/operator',
imageTag: '0.1',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const assertContainerResource = (
resource: Resource,
validations: {
name?: string;
id?: string;
imageName?: string;
imageTag?: string;
}
Expand All @@ -85,6 +86,11 @@ export const assertContainerResource = (
resource.attributes[CONTAINER_RESOURCE.NAME],
validations.name
);
if (validations.id)
assert.strictEqual(
resource.attributes[CONTAINER_RESOURCE.ID],
validations.id
);
if (validations.imageName)
assert.strictEqual(
resource.attributes[CONTAINER_RESOURCE.IMAGE_NAME],
Expand Down

0 comments on commit 8b13b07

Please sign in to comment.