diff --git a/packages/opentelemetry-resource-detector-aws/src/detectors/AwsBeanstalkDetector.ts b/packages/opentelemetry-resource-detector-aws/src/detectors/AwsBeanstalkDetector.ts new file mode 100644 index 000000000..f50225857 --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/src/detectors/AwsBeanstalkDetector.ts @@ -0,0 +1,79 @@ +/* + * 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, + SERVICE_RESOURCE, + ResourceDetectionConfigWithLogger, +} from '@opentelemetry/resources'; +import * as fs from 'fs'; +import * as util from 'util'; + +/** + * The AwsBeanstalkDetector can be used to detect if a process is running in AWS Elastic + * Beanstalk and return a {@link Resource} populated with data about the beanstalk + * plugins of AWS X-Ray. Returns an empty Resource if detection fails. + * + * See https://docs.amazonaws.cn/en_us/xray/latest/devguide/xray-guide.pdf + * for more details about detecting information of Elastic Beanstalk plugins + */ + +const DEFAULT_BEANSTALK_CONF_PATH = + '/var/elasticbeanstalk/xray/environment.conf'; +const WIN_OS_BEANSTALK_CONF_PATH = + 'C:\\Program Files\\Amazon\\XRay\\environment.conf'; + +export class AwsBeanstalkDetector implements Detector { + BEANSTALK_CONF_PATH: string; + private static readFileAsync = util.promisify(fs.readFile); + private static fileAccessAsync = util.promisify(fs.access); + + constructor() { + if (process.platform === 'win32') { + this.BEANSTALK_CONF_PATH = WIN_OS_BEANSTALK_CONF_PATH; + } else { + this.BEANSTALK_CONF_PATH = DEFAULT_BEANSTALK_CONF_PATH; + } + } + + async detect(config: ResourceDetectionConfigWithLogger): Promise { + try { + await AwsBeanstalkDetector.fileAccessAsync( + this.BEANSTALK_CONF_PATH, + fs.constants.R_OK + ); + + const rawData = await AwsBeanstalkDetector.readFileAsync( + this.BEANSTALK_CONF_PATH, + 'utf8' + ); + const parsedData = JSON.parse(rawData); + + return new Resource({ + [SERVICE_RESOURCE.NAME]: 'elastic_beanstalk', + [SERVICE_RESOURCE.NAMESPACE]: parsedData.environment_name, + [SERVICE_RESOURCE.VERSION]: parsedData.version_label, + [SERVICE_RESOURCE.INSTANCE_ID]: parsedData.deployment_id, + }); + } catch (e) { + config.logger.debug(`AwsBeanstalkDetector failed: ${e.message}`); + return Resource.empty(); + } + } +} + +export const awsBeanstalkDetector = new AwsBeanstalkDetector(); diff --git a/packages/opentelemetry-resource-detector-aws/src/detectors/index.ts b/packages/opentelemetry-resource-detector-aws/src/detectors/index.ts index 215622744..4bd440dd0 100644 --- a/packages/opentelemetry-resource-detector-aws/src/detectors/index.ts +++ b/packages/opentelemetry-resource-detector-aws/src/detectors/index.ts @@ -15,3 +15,4 @@ */ export * from './AwsEc2Detector'; +export * from './AwsBeanstalkDetector'; diff --git a/packages/opentelemetry-resource-detector-aws/test/detectors/AwsBeanstalkDetector.test.ts b/packages/opentelemetry-resource-detector-aws/test/detectors/AwsBeanstalkDetector.test.ts new file mode 100644 index 000000000..9ccebd706 --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/test/detectors/AwsBeanstalkDetector.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { awsBeanstalkDetector, AwsBeanstalkDetector } from '../../src'; +import { + assertEmptyResource, + assertServiceResource, +} from '@opentelemetry/resources/test/util/resource-assertions'; +import { NoopLogger } from '@opentelemetry/core'; + +describe('BeanstalkResourceDetector', () => { + const err = new Error('failed to read config file'); + const data = { + version_label: 'app-5a56-170119_190650-stage-170119_190650', + deployment_id: '32', + environment_name: 'scorekeep', + }; + const noisyData = { + noise: 'noise', + version_label: 'app-5a56-170119_190650-stage-170119_190650', + deployment_id: '32', + environment_name: 'scorekeep', + }; + + let readStub, fileStub; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should successfully return resource data', async () => { + fileStub = sandbox + .stub(AwsBeanstalkDetector, 'fileAccessAsync' as any) + .resolves(); + readStub = sandbox + .stub(AwsBeanstalkDetector, 'readFileAsync' as any) + .resolves(JSON.stringify(data)); + sandbox.stub(JSON, 'parse').returns(data); + + const resource = await awsBeanstalkDetector.detect({ + logger: new NoopLogger(), + }); + + sandbox.assert.calledOnce(fileStub); + sandbox.assert.calledOnce(readStub); + assert.ok(resource); + assertServiceResource(resource, { + name: 'elastic_beanstalk', + namespace: 'scorekeep', + version: 'app-5a56-170119_190650-stage-170119_190650', + instanceId: '32', + }); + }); + + it('should successfully return resource data with noise', async () => { + fileStub = sandbox + .stub(AwsBeanstalkDetector, 'fileAccessAsync' as any) + .resolves(); + readStub = sandbox + .stub(AwsBeanstalkDetector, 'readFileAsync' as any) + .resolves(JSON.stringify(noisyData)); + sandbox.stub(JSON, 'parse').returns(noisyData); + + const resource = await awsBeanstalkDetector.detect({ + logger: new NoopLogger(), + }); + + sandbox.assert.calledOnce(fileStub); + sandbox.assert.calledOnce(readStub); + assert.ok(resource); + assertServiceResource(resource, { + name: 'elastic_beanstalk', + namespace: 'scorekeep', + version: 'app-5a56-170119_190650-stage-170119_190650', + instanceId: '32', + }); + }); + + it('should return empty resource when failing to read file', async () => { + fileStub = sandbox + .stub(AwsBeanstalkDetector, 'fileAccessAsync' as any) + .resolves(); + readStub = sandbox + .stub(AwsBeanstalkDetector, 'readFileAsync' as any) + .rejects(err); + + const resource = await awsBeanstalkDetector.detect({ + logger: new NoopLogger(), + }); + + sandbox.assert.calledOnce(fileStub); + sandbox.assert.calledOnce(readStub); + assert.ok(resource); + assertEmptyResource(resource); + }); + + it('should return empty resource when config file does not exist', async () => { + fileStub = sandbox + .stub(AwsBeanstalkDetector, 'fileAccessAsync' as any) + .rejects(err); + readStub = sandbox + .stub(AwsBeanstalkDetector, 'readFileAsync' as any) + .resolves(JSON.stringify(data)); + + const resource = await awsBeanstalkDetector.detect({ + logger: new NoopLogger(), + }); + + sandbox.assert.calledOnce(fileStub); + sandbox.assert.notCalled(readStub); + assert.ok(resource); + assertEmptyResource(resource); + }); +});