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 AWS ECS Plugins Resource Detector #1404

Merged
merged 21 commits into from
Sep 23, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7deb1f7
refactor: rebase repo and move ECS detector
EdZou Aug 19, 2020
5e39613
refactor: move Beanstalk detector to aws repo
EdZou Aug 19, 2020
9d1303b
refactor: make promisify private static member for testing
EdZou Aug 20, 2020
794b337
refactor: make promisified method to private static member
EdZou Aug 20, 2020
7a27f09
Merge remote-tracking branch 'upstream/master' into EcsDetector
EdZou Aug 20, 2020
1d28da3
refactor: re-write getContainerId and getHostName method
EdZou Aug 20, 2020
be80011
Merge remote-tracking branch 'upstream/master'
EdZou Aug 21, 2020
b12e73b
Merge remote-tracking branch 'upstream/master' into EcsDetector
EdZou Aug 21, 2020
bbd792f
refactor: refactor _getContainerId and _getHostName
EdZou Aug 21, 2020
5ab11ba
refactor: refactor hostname method
EdZou Aug 21, 2020
8ee9cc3
Merge branch 'master' into EcsDetector
EdZou Aug 24, 2020
b7ca639
refactor: solve the conflict
EdZou Aug 25, 2020
9ecd38e
refactor: try to solve the conflict
EdZou Aug 25, 2020
80b662c
Merge branch 'master' into EcsDetector
EdZou Aug 25, 2020
811677a
Merge branch 'master' into EcsDetector
obecny Aug 26, 2020
cd2a729
Merge remote-tracking branch 'upstream/master' into EcsDetector
EdZou Aug 27, 2020
2461923
chore: modified test case
EdZou Aug 27, 2020
1e3b754
Merge branch 'EcsDetector' of https://github.com/EdZou/opentelemetry-…
EdZou Aug 27, 2020
20f219d
fix: delete weird directory from nowhere
EdZou Aug 27, 2020
c8010fd
Merge branch 'master' into EcsDetector
dyladan Sep 9, 2020
0ea3f9b
Merge branch 'master' into EcsDetector
dyladan Sep 23, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,203 @@
/*
* 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 noisyCgroupData = `\n\n\n abcdefghijklmnopqrstuvwxyz \n ${correctCgroupData}`;
const multiValidCgroupData = `${correctCgroupData}\nabc${correctCgroupData}\nbcd${correctCgroupData}`;
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);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure this actually tests what it says.

each of the lines in multiValidCgroupData contain the same ID as the last 64 chars, so it doesn't necessarily prove it grabbed the first 'valid' (length > 64) one. it is minor, i can tell the code is working but perhaps a more accurate test would be to have the first line be 64 chars of a different/unexpected ID as well as having the last line be a valid, but unexpected ID to show you are grabbing the item that is expected (the middle one).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, in practice, multiple lines contain container ID in the same time. This testcase is to verify the logic here because we only grab the first valid container ID to reduce time cost.
I also think I can add an unexpected ID here. Thank you for pointing out!


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

sandbox.assert.calledOnce(hostStub);
// sandbox.assert.calledOnce(readStub);
Copy link
Contributor

Choose a reason for hiding this comment

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

stray commented-out line.

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

stray commented-out line.

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 @@ -74,6 +74,7 @@ export const assertContainerResource = (
resource: Resource,
validations: {
name?: string;
id?: string;
imageName?: string;
imageTag?: string;
}
Expand All @@ -84,6 +85,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