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

Allow overriding environment and provide LocalEnvironment #19

Merged
merged 6 commits into from
Feb 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions src/config/EnvironmentConfigurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

import { IConfiguration } from './IConfiguration';
import Environments from "../environment/Environments";

const ENV_VAR_PREFIX = 'AWS_EMF';

Expand All @@ -24,6 +25,7 @@ enum ConfigKeys {
SERVICE_NAME = 'SERVICE_NAME',
SERVICE_TYPE = 'SERVICE_TYPE',
AGENT_ENDPOINT = 'AGENT_ENDPOINT',
ENVIRONMENT_OVERRIDE = 'ENVIRONMENT'
}

export class EnvironmentConfigurationProvider {
Expand All @@ -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()
};
}

Expand All @@ -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;
}
}
12 changes: 12 additions & 0 deletions src/config/IConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* limitations under the License.
*/

import Environments from "../environment/Environments";

export interface IConfiguration {
/**
* Whether or not internal logging should be enabled.
Expand Down Expand Up @@ -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;
}
36 changes: 36 additions & 0 deletions src/config/__tests__/EnvironmentConfigurationProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as faker from 'faker';
import Environments from '../../environment/Environments';

beforeEach(() => {
jest.resetModules();
Expand Down Expand Up @@ -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);
});
75 changes: 56 additions & 19 deletions src/environment/EnvironmentDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IEnvironment>;

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<IEnvironment> => {
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<IEnvironment> => {
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<IEnvironment> => {
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<IEnvironment> => {
return environmentPromise;
};

const cleanResolveEnvironment = async (): Promise<IEnvironment> => {
environment = undefined;
return await _resolveEnvironment();
};

export { EnvironmentProvider, resolveEnvironment, resetEnvironment };
export { EnvironmentProvider, resolveEnvironment, cleanResolveEnvironment };
9 changes: 9 additions & 0 deletions src/environment/Environments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
enum Environments {
Local = "Local",
Lambda = "Lambda",
Agent = "Agent",
EC2 = "EC2",
Unknown = ""
};

export default Environments;
4 changes: 2 additions & 2 deletions src/environment/LambdaEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}
Expand Down
62 changes: 62 additions & 0 deletions src/environment/LocalEnvironment.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
// 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;
}
}
Loading