diff --git a/README.md b/README.md index ce7876c..6e9ea7d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Generate CloudWatch Metrics embedded within structured log events. The embedded * [Installation](#installation) * [Usage](#usage) * [API](#api) +* [Configuration](#configuration) * [Examples](#examples) * [Development](#development) @@ -193,7 +194,7 @@ setNamespace("MyApplication"); Flushes the current MetricsContext to the configured sink and resets all properties, dimensions and metric values. The namespace and default dimensions will be preserved across flushes. -### Configuration +## Configuration All configuration values can be set using environment variables with the prefix (`AWS_EMF_`). Configuration should be performed as close to application start up as possible. @@ -268,6 +269,52 @@ Configuration.logStreamName = "LogStreamName"; AWS_EMF_LOG_STREAM_NAME=LogStreamName ``` +**AgentEndpoint**: For agent-based platforms, you may optionally configure the endpoint to reach the agent on. + +Example: + +```js +// in process +const { Configuration } = require("aws-embedded-metrics"); +Configuration.agentEndpoint = "udp://127.0.0.1:1000"; + +// environment +AWS_EMF_AGENT_ENDPOINT="udp://127.0.0.1:1000" +``` + +**EnvironmentOverride**: Short circuit auto-environment detection by explicitly defining how events should be sent. + +Valid values include: + +- Local: no decoration and sends over stdout +- Lambda: decorates logs with Lambda metadata and sends over stdout +- Agent: no decoration and sends over TCP +- EC2: decorates logs with EC2 metadata and sends over TCP + +Example: + +```js +// in process +const { Configuration } = require("aws-embedded-metrics"); +Configuration.environmentOverride = "Local"; + +// environment +AWS_EMF_AGENT_ENDPOINT=Local +``` + +**EnableDebugLogging**: Enable debug logging for the library. If the library is not behaving as expected, you can set this to true to log to console. + +Example: + +```js +// in process +const { Configuration } = require("aws-embedded-metrics"); +Configuration.debuggingLoggingEnabled = true; + +// environment +AWS_EMF_ENABLE_DEBUG_LOGGING=true +``` + ## Examples Check out the [examples](https://github.com/awslabs/aws-embedded-metrics-node/tree/master/examples) directory to get started. diff --git a/src/config/EnvironmentConfigurationProvider.ts b/src/config/EnvironmentConfigurationProvider.ts index f3b0f43..3e8e6ca 100644 --- a/src/config/EnvironmentConfigurationProvider.ts +++ b/src/config/EnvironmentConfigurationProvider.ts @@ -14,6 +14,7 @@ */ import { IConfiguration } from './IConfiguration'; +import Environments from "../environment/Environments"; const ENV_VAR_PREFIX = 'AWS_EMF'; @@ -24,6 +25,7 @@ enum ConfigKeys { SERVICE_NAME = 'SERVICE_NAME', SERVICE_TYPE = 'SERVICE_TYPE', AGENT_ENDPOINT = 'AGENT_ENDPOINT', + ENVIRONMENT_OVERRIDE = 'ENVIRONMENT' } export class EnvironmentConfigurationProvider { @@ -37,6 +39,7 @@ export class EnvironmentConfigurationProvider { this.getEnvVariable(ConfigKeys.SERVICE_NAME) || this.getEnvVariableWithoutPrefix(ConfigKeys.SERVICE_NAME), serviceType: this.getEnvVariable(ConfigKeys.SERVICE_TYPE) || this.getEnvVariableWithoutPrefix(ConfigKeys.SERVICE_TYPE), + environmentOverride: this.getEnvironmentOverride() }; } @@ -52,4 +55,13 @@ export class EnvironmentConfigurationProvider { const configValue = this.getEnvVariable(configKey); return !configValue ? fallback : configValue.toLowerCase() === 'true'; } + + getEnvironmentOverride(): Environments { + const overrideValue = this.getEnvVariable(ConfigKeys.ENVIRONMENT_OVERRIDE); + const environment = Environments[overrideValue as keyof typeof Environments]; + if (environment === undefined) { + return Environments.Unknown; + } + return environment; + } } diff --git a/src/config/IConfiguration.ts b/src/config/IConfiguration.ts index adc8abd..a8083af 100644 --- a/src/config/IConfiguration.ts +++ b/src/config/IConfiguration.ts @@ -13,6 +13,8 @@ * limitations under the License. */ +import Environments from "../environment/Environments"; + export interface IConfiguration { /** * Whether or not internal logging should be enabled. @@ -45,4 +47,14 @@ export interface IConfiguration { * The endpoint to use to connect to the CloudWatch Agent */ agentEndpoint: string | undefined; + + /** + * Environment override. This will short circuit auto-environment detection. + * Valid values include: + * - Local: no decoration and sends over stdout + * - Lambda: decorates logs with Lambda metadata and sends over stdout + * - Agent: no decoration and sends over TCP + * - EC2: decorates logs with EC2 metadata and sends over TCP + */ + environmentOverride: Environments | undefined; } diff --git a/src/config/__tests__/EnvironmentConfigurationProvider.test.ts b/src/config/__tests__/EnvironmentConfigurationProvider.test.ts index 461c24d..8882abc 100644 --- a/src/config/__tests__/EnvironmentConfigurationProvider.test.ts +++ b/src/config/__tests__/EnvironmentConfigurationProvider.test.ts @@ -1,4 +1,5 @@ import * as faker from 'faker'; +import Environments from '../../environment/Environments'; beforeEach(() => { jest.resetModules(); @@ -171,3 +172,38 @@ test('can set agent endpoint from environment', () => { const result = config.agentEndpoint; expect(result).toBe(expectedValue); }); + +test('can set environment override from environment', () => { + // arrange + const expectedValue = "Local" + process.env.AWS_EMF_ENVIRONMENT = expectedValue; + + // act + const config = getConfig(); + + // assert + const result = config.environmentOverride; + expect(result).toBe(Environments.Local); +}); + +test('if environment override is not set, default to unknown', () => { + // arrange + process.env.AWS_EMF_ENVIRONMENT = ""; + // act + const config = getConfig(); + + // assert + const result = config.environmentOverride; + expect(result).toBe(Environments.Unknown); +}); + +test('if environment override cannot be parsed, default to unknown', () => { + // arrange + process.env.AWS_EMF_ENVIRONMENT = faker.random.alphaNumeric(); + // act + const config = getConfig(); + + // assert + const result = config.environmentOverride; + expect(result).toBe(Environments.Unknown); +}); diff --git a/src/environment/EnvironmentDetector.ts b/src/environment/EnvironmentDetector.ts index dd3ac3f..769030d 100644 --- a/src/environment/EnvironmentDetector.ts +++ b/src/environment/EnvironmentDetector.ts @@ -18,49 +18,86 @@ import { DefaultEnvironment } from './DefaultEnvironment'; import { EC2Environment } from './EC2Environment'; import { IEnvironment } from './IEnvironment'; import { LambdaEnvironment } from './LambdaEnvironment'; +import config from '../config/Configuration'; +import Environments from './Environments'; +import { LocalEnvironment } from './LocalEnvironment'; type EnvironmentProvider = () => Promise; -const environments = [new LambdaEnvironment(), new EC2Environment()]; +const lambdaEnvironment = new LambdaEnvironment(); +const ec2Environment = new EC2Environment(); const defaultEnvironment = new DefaultEnvironment(); +const environments = [lambdaEnvironment, ec2Environment]; -let environment: IEnvironment | undefined; -const resolveEnvironment: EnvironmentProvider = async (): Promise => { - if (environment) { - return environment; +let environment : IEnvironment | undefined = defaultEnvironment; + +const getEnvironmentFromOverride = (): IEnvironment | undefined => { + // short-circuit environment detection and use override + switch (config.environmentOverride) { + case Environments.Agent: + return defaultEnvironment; + case Environments.EC2: + return ec2Environment; + case Environments.Lambda: + return lambdaEnvironment; + case Environments.Local: + return new LocalEnvironment(); + case Environments.Unknown: + default: + return undefined; } +} +const discoverEnvironment = async (): Promise => { for (const envUnderTest of environments) { LOG(`Testing: ${envUnderTest.constructor.name}`); - let isEnvironment = false; try { - isEnvironment = await envUnderTest.probe(); + if (await envUnderTest.probe()) { + return envUnderTest; + } } catch (e) { LOG(`Failed probe: ${envUnderTest.constructor.name}`); } - - if (isEnvironment) { - environment = envUnderTest; - break; - } } + return defaultEnvironment; +} - if (!environment) { - environment = defaultEnvironment; +const _resolveEnvironment: EnvironmentProvider = async (): Promise => { + if (environment) { + return environment; } - LOG(`Using Environment: ${environment.constructor.name}`); - + if (config.environmentOverride) { + LOG("Environment override supplied", config.environmentOverride); + // this will be falsy if an invalid configuration value is provided + environment = getEnvironmentFromOverride() + if (environment) { + return environment; + } + else { + LOG('Invalid environment provided. Falling back to auto-discovery.', config.environmentOverride); + } + } + + environment = await discoverEnvironment(); // eslint-disable-line require-atomic-updates return environment; }; -const resetEnvironment = (): void => (environment = undefined); + // pro-actively begin resolving the environment // this will allow us to kick off any async tasks // at module load time to reduce any blocking that // may occur on the initial flush() -resolveEnvironment(); +const environmentPromise = _resolveEnvironment(); +const resolveEnvironment: EnvironmentProvider = async (): Promise => { + return environmentPromise; +}; + +const cleanResolveEnvironment = async (): Promise => { + environment = undefined; + return await _resolveEnvironment(); +}; -export { EnvironmentProvider, resolveEnvironment, resetEnvironment }; +export { EnvironmentProvider, resolveEnvironment, cleanResolveEnvironment }; diff --git a/src/environment/Environments.ts b/src/environment/Environments.ts new file mode 100644 index 0000000..5462782 --- /dev/null +++ b/src/environment/Environments.ts @@ -0,0 +1,9 @@ +enum Environments { + Local = "Local", + Lambda = "Lambda", + Agent = "Agent", + EC2 = "EC2", + Unknown = "" +}; + +export default Environments; \ No newline at end of file diff --git a/src/environment/LambdaEnvironment.ts b/src/environment/LambdaEnvironment.ts index ca87c2c..e53e98f 100644 --- a/src/environment/LambdaEnvironment.ts +++ b/src/environment/LambdaEnvironment.ts @@ -14,7 +14,7 @@ */ import { MetricsContext } from '../logger/MetricsContext'; -import { LambdaSink } from '../sinks/LambdaSink'; +import { ConsoleSink } from '../sinks/ConsoleSink'; import { ISink } from '../sinks/Sink'; import { IEnvironment } from './IEnvironment'; @@ -51,7 +51,7 @@ export class LambdaEnvironment implements IEnvironment { public getSink(): ISink { if (!this.sink) { - this.sink = new LambdaSink(); + this.sink = new ConsoleSink(); } return this.sink; } diff --git a/src/environment/LocalEnvironment.ts b/src/environment/LocalEnvironment.ts new file mode 100644 index 0000000..cc66f56 --- /dev/null +++ b/src/environment/LocalEnvironment.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. + * 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 + * + * http://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 config from '../config/Configuration'; +import { ISink } from '../sinks/Sink'; +import { LOG } from '../utils/Logger'; +import { IEnvironment } from './IEnvironment'; +import { ConsoleSink } from '../sinks/ConsoleSink'; + +export class LocalEnvironment implements IEnvironment { + private sink: ISink | undefined; + + public probe(): Promise { + // probe is not intended to be used in the LocalEnvironment + // To use the local environment you should set the environment + // override + return Promise.resolve(false); + } + + public getName(): string { + if (!config.serviceName) { + LOG('Unknown ServiceName.'); + return 'Unknown'; + } + return config.serviceName; + } + + public getType(): string { + if (!config.serviceType) { + LOG('Unknown ServiceType.'); + return 'Unknown'; + } + return config.serviceType; + } + + public getLogGroupName(): string { + return config.logGroupName ? config.logGroupName : `${this.getName()}-metrics`; + } + + public configureContext(): void { + // no-op + } + + public getSink(): ISink { + if (!this.sink) { + this.sink = new ConsoleSink(); + } + return this.sink; + } +} diff --git a/src/environment/__tests__/EnvironmentDetector.test.ts b/src/environment/__tests__/EnvironmentDetector.test.ts index f6af77e..d68fb93 100644 --- a/src/environment/__tests__/EnvironmentDetector.test.ts +++ b/src/environment/__tests__/EnvironmentDetector.test.ts @@ -1,10 +1,10 @@ import * as faker from 'faker'; -import { resetEnvironment, resolveEnvironment } from '../EnvironmentDetector'; +import { cleanResolveEnvironment } from '../EnvironmentDetector'; +import config from '../../config/Configuration'; +import Environments from '../Environments'; beforeEach(() => { process.env = {}; - resetEnvironment(); - jest.resetModules(); }); test('resolveEnvironment() returns LambdaEnvironment if AWS_LAMBDA_FUNCTION_NAME specified', async () => { @@ -12,7 +12,7 @@ test('resolveEnvironment() returns LambdaEnvironment if AWS_LAMBDA_FUNCTION_NAME process.env.AWS_LAMBDA_FUNCTION_NAME = faker.random.word(); // act - const result = await resolveEnvironment(); + const result = await cleanResolveEnvironment(); // assert expect(result.constructor.name).toBe('LambdaEnvironment'); @@ -21,8 +21,32 @@ test('resolveEnvironment() returns LambdaEnvironment if AWS_LAMBDA_FUNCTION_NAME test('resolveEnvironment() returns DefaultEnvironment if nothing else was detected', async () => { // arrange // act - const result = await resolveEnvironment(); + const result = await cleanResolveEnvironment(); // assert expect(result.constructor.name).toBe('DefaultEnvironment'); }, 10000); + +test('resolveEnvironment() honors configured override', async () => { + // arrange + config.environmentOverride = Environments.Local; + + // act + const result = await cleanResolveEnvironment(); + + // assert + expect(result.constructor.name).toBe('LocalEnvironment'); +}); + +test('resolveEnvironment() ignores invalid override and falls back to discovery', async () => { + // arrange + // @ts-ignore + config.environmentOverride = "Invalid"; + process.env.AWS_LAMBDA_FUNCTION_NAME = faker.random.word(); + + // act + const result = await cleanResolveEnvironment(); + + // assert + expect(result.constructor.name).toBe('LambdaEnvironment'); +}); \ No newline at end of file diff --git a/src/environment/__tests__/LambdaEnvironment.test.ts b/src/environment/__tests__/LambdaEnvironment.test.ts index 2c3e2c4..f9e6dff 100644 --- a/src/environment/__tests__/LambdaEnvironment.test.ts +++ b/src/environment/__tests__/LambdaEnvironment.test.ts @@ -51,9 +51,9 @@ test('getLogGroupName() returns function name', () => { expect(result).toBe(expectedName); }); -test('createSink() creates a LambdaSink', () => { +test('createSink() creates a ConsoleSink', () => { // arrange - const expectedSink = 'LambdaSink'; + const expectedSink = 'ConsoleSink'; const env = new LambdaEnvironment(); // act diff --git a/src/environment/__tests__/LocalEnvironment.test.ts b/src/environment/__tests__/LocalEnvironment.test.ts new file mode 100644 index 0000000..09410e1 --- /dev/null +++ b/src/environment/__tests__/LocalEnvironment.test.ts @@ -0,0 +1,101 @@ +import * as faker from 'faker'; +import config from '../../config/Configuration'; +import { LocalEnvironment } from '../LocalEnvironment'; + +test('probe() always returns false', async () => { + // arrange + const env = new LocalEnvironment(); + + // act + const result = await env.probe(); + + // assert + expect(result).toBe(false); +}); + +test('getName() returns "Unknown" if not specified', () => { + // arrange + const env = new LocalEnvironment(); + + // act + const result = env.getName(); + + // assert + expect(result).toBe('Unknown'); +}); + +test('getType() returns "Unknown" if not specified', () => { + // arrange + const env = new LocalEnvironment(); + + // act + const result = env.getType(); + + // assert + expect(result).toBe('Unknown'); +}); + +test('getName() returns name if configured', () => { + // arrange + const expectedName = faker.random.word(); + config.serviceName = expectedName; + const env = new LocalEnvironment(); + + // act + const result = env.getName(); + + // assert + expect(result).toBe(expectedName); +}); + +test('getType() returns type if configured', () => { + // arrange + const expectedType = faker.random.word(); + config.serviceType = expectedType; + const env = new LocalEnvironment(); + + // act + const result = env.getType(); + + // assert + expect(result).toBe(expectedType); +}); + +test('getLogGroupName() returns logGroup if configured', () => { + // arrange + const name = faker.random.word(); + config.logGroupName = name; + const env = new LocalEnvironment(); + + // act + const result = env.getLogGroupName(); + + // assert + expect(result).toBe(name); +}); + +test('getLogGroupName() returns -metrics if not configured', () => { + // arrange + const serviceName = faker.random.word(); + config.logGroupName = undefined; + config.serviceName = serviceName; + const env = new LocalEnvironment(); + + // act + const result = env.getLogGroupName(); + + // assert + expect(result).toBe(`${serviceName}-metrics`); +}); + +test('getSink() creates a ConsoleSink', () => { + // arrange + const expectedSink = 'ConsoleSink'; + const env = new LocalEnvironment(); + + // act + const sink = env.getSink(); + + // assert + expect(sink.name).toBe(expectedSink); +}); diff --git a/src/index.ts b/src/index.ts index d6f0bb9..883c260 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ */ export { MetricsLogger } from './logger/MetricsLogger'; -export { LambdaSink } from './sinks/LambdaSink'; +export { ConsoleSink as LocalSink } from './sinks/ConsoleSink'; export { AgentSink } from './sinks/AgentSink'; export { metricScope } from './logger/MetricScope'; export { createMetricsLogger } from './logger/MetricsLoggerFactory'; diff --git a/src/sinks/LambdaSink.ts b/src/sinks/ConsoleSink.ts similarity index 93% rename from src/sinks/LambdaSink.ts rename to src/sinks/ConsoleSink.ts index ff981bc..59468de 100644 --- a/src/sinks/LambdaSink.ts +++ b/src/sinks/ConsoleSink.ts @@ -22,8 +22,8 @@ import { ISink } from './Sink'; * A sink that flushes log data to stdout. * This is the preferred sink for Lambda functions. */ -export class LambdaSink implements ISink { - public readonly name: string = 'LambdaSink'; +export class ConsoleSink implements ISink { + public readonly name: string = 'ConsoleSink'; private serializer: ISerializer; diff --git a/src/sinks/__tests__/LambdaSink.test.ts b/src/sinks/__tests__/ConsoleSink.test.ts similarity index 83% rename from src/sinks/__tests__/LambdaSink.test.ts rename to src/sinks/__tests__/ConsoleSink.test.ts index 33f8350..00a3454 100644 --- a/src/sinks/__tests__/LambdaSink.test.ts +++ b/src/sinks/__tests__/ConsoleSink.test.ts @@ -1,6 +1,6 @@ import * as faker from 'faker'; import { MetricsContext } from '../../logger/MetricsContext'; -import { LambdaSink } from '../LambdaSink'; +import { ConsoleSink } from '../ConsoleSink'; beforeEach(() => { console.log = jest.fn(); @@ -13,7 +13,7 @@ test('accept serializes and writes result to stdout', () => { serialize: jest.fn(() => expected), }; - const sink = new LambdaSink(serializer); + const sink = new ConsoleSink(serializer); // act sink.accept(MetricsContext.empty());