Skip to content

Commit

Permalink
Resource auto detection logging (#1211)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Dyla <[email protected]>
Co-authored-by: Mayur Kale <[email protected]>
  • Loading branch information
3 people authored Jul 1, 2020
1 parent ac7dc6d commit 4d358f2
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 18 deletions.
35 changes: 35 additions & 0 deletions packages/opentelemetry-resources/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 { Logger } from '@opentelemetry/api';

/**
* ResourceDetectionConfig provides an interface for configuring resource auto-detection.
*/
export interface ResourceDetectionConfig {
/** Optional Logger. */
logger?: Logger;
}

/**
* ResourceDetectionConfigWithLogger provides an interface for interacting with
* {@link ResourceDetectionConfig} instances that must have a logger defined.
*/
export interface ResourceDetectionConfigWithLogger
extends ResourceDetectionConfig {
/** Required Logger */
logger: Logger;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,72 @@
import { Resource } from '../../Resource';
import { envDetector, awsEc2Detector, gcpDetector } from './detectors';
import { Detector } from '../../types';
import {
ResourceDetectionConfig,
ResourceDetectionConfigWithLogger,
} from '../../config';
import { Logger } from '@opentelemetry/api';
import * as util from 'util';
import { NoopLogger } from '@opentelemetry/core';

const DETECTORS: Array<Detector> = [envDetector, awsEc2Detector, gcpDetector];

/**
* Runs all resource detectors and returns the results merged into a single
* Resource.
*
* @param config Configuration for resource detection
*/
export const detectResources = async (): Promise<Resource> => {
export const detectResources = async (
config: ResourceDetectionConfig = {}
): Promise<Resource> => {
const internalConfig: ResourceDetectionConfigWithLogger = Object.assign(
{
logger: new NoopLogger(),
},
config
);

const resources: Array<Resource> = await Promise.all(
DETECTORS.map(d => {
try {
return d.detect();
return d.detect(internalConfig);
} catch {
return Resource.empty();
}
})
);
// Log Resources only if there is a user-provided logger
if (config.logger) {
logResources(config.logger, resources);
}
return resources.reduce(
(acc, resource) => acc.merge(resource),
Resource.createTelemetrySDKResource()
);
};

/**
* Writes debug information about the detected resources to the logger defined in the resource detection config, if one is provided.
*
* @param logger The {@link Logger} to write the debug information to.
* @param resources The array of {@link Resource} that should be logged. Empty entried will be ignored.
*/
const logResources = (logger: Logger, resources: Array<Resource>) => {
resources.forEach((resource, index) => {
// Print only populated resources
if (Object.keys(resource.labels).length > 0) {
const resourceDebugString = util.inspect(resource.labels, {
depth: 2,
breakLength: Infinity,
sorted: true,
compact: false,
});
const detectorName = DETECTORS[index].constructor
? DETECTORS[index].constructor.name
: 'Unknown detector';
logger.debug(`${detectorName} found resource.`);
logger.debug(resourceDebugString);
}
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as http from 'http';
import { Resource } from '../../../Resource';
import { CLOUD_RESOURCE, HOST_RESOURCE } from '../../../constants';
import { Detector } from '../../../types';
import { ResourceDetectionConfigWithLogger } from '../../../config';

/**
* The AwsEc2Detector can be used to detect if a process is running in AWS EC2
Expand All @@ -38,8 +39,10 @@ class AwsEc2Detector implements Detector {
* populated with instance metadata as labels. Returns a promise containing an
* empty {@link Resource} if the connection or parsing of the identity
* document fails.
*
* @param config The resource detection config with a required logger
*/
async detect(): Promise<Resource> {
async detect(config: ResourceDetectionConfigWithLogger): Promise<Resource> {
try {
const {
accountId,
Expand All @@ -56,7 +59,8 @@ class AwsEc2Detector implements Detector {
[HOST_RESOURCE.ID]: instanceId,
[HOST_RESOURCE.TYPE]: instanceType,
});
} catch {
} catch (e) {
config.logger.debug(`AwsEc2Detector failed: ${e.message}`);
return Resource.empty();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { Resource } from '../../../Resource';
import { Detector, ResourceLabels } from '../../../types';
import { ResourceDetectionConfigWithLogger } from '../../../config';

/**
* EnvDetector can be used to detect the presence of and create a Resource
Expand Down Expand Up @@ -45,16 +46,24 @@ class EnvDetector implements Detector {
* Returns a {@link Resource} populated with labels from the
* OTEL_RESOURCE_LABELS environment variable. Note this is an async function
* to conform to the Detector interface.
*
* @param config The resource detection config with a required logger
*/
async detect(): Promise<Resource> {
async detect(config: ResourceDetectionConfigWithLogger): Promise<Resource> {
try {
const labelString = process.env.OTEL_RESOURCE_LABELS;
if (!labelString) return Resource.empty();
if (!labelString) {
config.logger.debug(
'EnvDetector failed: Environment variable "OTEL_RESOURCE_LABELS" is missing.'
);
return Resource.empty();
}
const labels = this._parseResourceLabels(
process.env.OTEL_RESOURCE_LABELS
);
return new Resource(labels);
} catch {
} catch (e) {
config.logger.debug(`EnvDetector failed: ${e.message}`);
return Resource.empty();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,27 @@ import {
K8S_RESOURCE,
CONTAINER_RESOURCE,
} from '../../../constants';
import { ResourceDetectionConfigWithLogger } from '../../../config';

/**
* The GcpDetector can be used to detect if a process is running in the Google
* Cloud Platofrm and return a {@link Resource} populated with metadata about
* the instance. Returns an empty Resource if detection fails.
*/
class GcpDetector implements Detector {
async detect(): Promise<Resource> {
if (!(await gcpMetadata.isAvailable())) return Resource.empty();
/**
* Attempts to connect and obtain instance configuration data from the GCP metadata service.
* If the connection is succesful it returns a promise containing a {@link Resource}
* populated with instance metadata as labels. Returns a promise containing an
* empty {@link Resource} if the connection or parsing of the metadata fails.
*
* @param config The resource detection config with a required logger
*/
async detect(config: ResourceDetectionConfigWithLogger): Promise<Resource> {
if (!(await gcpMetadata.isAvailable())) {
config.logger.debug('GcpDetector failed: GCP Metadata unavailable.');
return Resource.empty();
}

const [projectId, instanceId, zoneId, clusterName] = await Promise.all([
this._getProjectId(),
Expand Down
3 changes: 2 additions & 1 deletion packages/opentelemetry-resources/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { Resource } from './Resource';
import { ResourceDetectionConfigWithLogger } from './config';

/** Interface for Resource labels */
export interface ResourceLabels {
Expand All @@ -26,5 +27,5 @@ export interface ResourceLabels {
* a detector returns a Promise containing a Resource.
*/
export interface Detector {
detect(): Promise<Resource>;
detect(config: ResourceDetectionConfigWithLogger): Promise<Resource>;
}
110 changes: 110 additions & 0 deletions packages/opentelemetry-resources/test/detect-resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import * as nock from 'nock';
import * as sinon from 'sinon';
import * as assert from 'assert';
import { URL } from 'url';
import { Resource, detectResources } from '../src';
import { awsEc2Detector } from '../src/platform/node/detectors';
Expand Down Expand Up @@ -162,4 +163,113 @@ describe('detectResources', async () => {
stub.restore();
});
});

describe('with a debug logger', () => {
// Local functions to test if a mocked method is ever called with a specific argument or regex matching for an argument.
// Needed because of race condition with parallel detectors.
const callArgsContains = (
mockedFunction: sinon.SinonSpy,
arg: any
): boolean => {
return mockedFunction.getCalls().some(call => {
return call.args.some(callarg => arg === callarg);
});
};
const callArgsMatches = (
mockedFunction: sinon.SinonSpy,
regex: RegExp
): boolean => {
return mockedFunction.getCalls().some(call => {
return regex.test(call.args.toString());
});
};

it('prints detected resources and debug messages to the logger', async () => {
// This test depends on the env detector to be functioning as intended
const mockedLoggerMethod = sinon.fake();
await detectResources({
logger: {
debug: mockedLoggerMethod,
info: sinon.fake(),
warn: sinon.fake(),
error: sinon.fake(),
},
});

// Test for AWS and GCP Detector failure
assert.ok(
callArgsContains(
mockedLoggerMethod,
'GcpDetector failed: GCP Metadata unavailable.'
)
);
assert.ok(
callArgsContains(
mockedLoggerMethod,
'AwsEc2Detector failed: Nock: Disallowed net connect for "169.254.169.254:80/latest/dynamic/instance-identity/document"'
)
);
// Test that the Env Detector successfully found its resource and populated it with the right values.
assert.ok(
callArgsContains(mockedLoggerMethod, 'EnvDetector found resource.')
);
// Regex formatting accounts for whitespace variations in util.inspect output over different node versions
assert.ok(
callArgsMatches(
mockedLoggerMethod,
/{\s+'service\.instance\.id':\s+'627cc493',\s+'service\.name':\s+'my-service',\s+'service\.namespace':\s+'default',\s+'service\.version':\s+'0\.0\.1'\s+}\s*/
)
);
});

describe('with missing environemnt variable', () => {
beforeEach(() => {
delete process.env.OTEL_RESOURCE_LABELS;
});

it('prints correct error messages when EnvDetector has no env variable', async () => {
const mockedLoggerMethod = sinon.fake();
await detectResources({
logger: {
debug: mockedLoggerMethod,
info: sinon.fake(),
warn: sinon.fake(),
error: sinon.fake(),
},
});

assert.ok(
callArgsContains(
mockedLoggerMethod,
'EnvDetector failed: Environment variable "OTEL_RESOURCE_LABELS" is missing.'
)
);
});
});

describe('with a faulty environment variable', () => {
beforeEach(() => {
process.env.OTEL_RESOURCE_LABELS = 'bad=~label';
});

it('prints correct error messages when EnvDetector has an invalid variable', async () => {
const mockedLoggerMethod = sinon.fake();
await detectResources({
logger: {
debug: mockedLoggerMethod,
info: sinon.fake(),
warn: sinon.fake(),
error: sinon.fake(),
},
});

assert.ok(
callArgsContains(
mockedLoggerMethod,
'EnvDetector failed: Label value should be a ASCII string with a length not exceed 255 characters.'
)
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
assertHostResource,
assertEmptyResource,
} from '../util/resource-assertions';
import { NoopLogger } from '@opentelemetry/core';

const { origin: AWS_HOST, pathname: AWS_PATH } = new URL(
awsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_URI
Expand Down Expand Up @@ -52,7 +53,9 @@ describe('awsEc2Detector', () => {
const scope = nock(AWS_HOST)
.get(AWS_PATH)
.reply(200, () => mockedAwsResponse);
const resource: Resource = await awsEc2Detector.detect();
const resource: Resource = await awsEc2Detector.detect({
logger: new NoopLogger(),
});
scope.done();

assert.ok(resource);
Expand All @@ -74,7 +77,9 @@ describe('awsEc2Detector', () => {
const scope = nock(AWS_HOST).get(AWS_PATH).replyWithError({
code: 'ENOTFOUND',
});
const resource: Resource = await awsEc2Detector.detect();
const resource: Resource = await awsEc2Detector.detect({
logger: new NoopLogger(),
});
scope.done();

assert.ok(resource);
Expand Down
Loading

0 comments on commit 4d358f2

Please sign in to comment.