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(core): make creationStack collection for Lazy opt-in #11170

Merged
merged 4 commits into from
Oct 28, 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
16 changes: 16 additions & 0 deletions packages/@aws-cdk/core/lib/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { env } from 'process';

export const CDK_DEBUG = 'CDK_DEBUG';

export function debugModeEnabled(): boolean {
return isTruthy(env[CDK_DEBUG]);
}

const TRUTHY_VALUES = new Set(['1', 'on', 'true']);

function isTruthy(value: string | undefined): boolean {
if (!value) {
return false;
}
return TRUTHY_VALUES.has(value.toLowerCase());
}
12 changes: 10 additions & 2 deletions packages/@aws-cdk/core/lib/lazy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CDK_DEBUG, debugModeEnabled } from './debug';
import { IResolvable, IResolveContext } from './resolvable';
import { captureStackTrace } from './stack-trace';
import { Token } from './token';
Expand Down Expand Up @@ -123,10 +124,17 @@ abstract class LazyBase implements IResolvable {
public readonly creationStack: string[];

constructor() {
this.creationStack = captureStackTrace();
// Stack trace capture is conditionned to `debugModeEnabled()`, because
// lazies can be created in a fairly thrashy way, and the stack traces are
// large and slow to obtain; but are mostly useful only when debugging a
// resolution issue.
this.creationStack = debugModeEnabled()
? captureStackTrace(this.constructor)
: [`Execute again with ${CDK_DEBUG}=true to capture stack traces`];
}

public abstract resolve(context: IResolveContext): any;

public toString() {
return Token.asString(this);
}
Expand Down Expand Up @@ -188,4 +196,4 @@ class LazyAny extends LazyBase {
}
return ret;
}
}
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/core/lib/resolvable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export interface IResolvable {
* The creation stack of this resolvable which will be appended to errors
* thrown during resolution.
*
* If this returns an empty array the stack will not be attached.
* This may return an array with a single informational element indicating how
* to get this property populated, if it was skipped for performance reasons.
*/
readonly creationStack: string[];

Expand Down
36 changes: 31 additions & 5 deletions packages/@aws-cdk/core/lib/stack-trace.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
export function captureStackTrace(below?: Function): string[] {
if (process.env.CDK_DISABLE_STACK_TRACE) {
import { debugModeEnabled } from './debug';

/**
* Captures the current process' stack trace.
*
* Stack traces are often invaluable tools to help diagnose problems, however
* their capture is a rather expensive operation, and the stack traces can be
* large. Consequently, users are stronly advised to condition capturing stack
* traces to specific user opt-in.
*
* If the `CDK_DISABLE_STACK_TRACE` environment variable is set (to any value,
* except for an empty string), no stack traces will be captured, and instead
* the literal value `['stack traces disabled']` will be returned instead. This
* is only true if the `CDK_DEBUG` environment variable is not set to `'true'`
* or '1', in which case stack traces are *always* captured.
*
* @param below an optional function starting from which stack frames will be
* ignored. Defaults to the `captureStackTrace` function itself.
* @param limit and optional upper bound to the number of stack frames to be
* captured. If not provided, this defaults to
* `Number.MAX_SAFE_INTEGER`, effectively meaning "no limit".
*
* @returns the captured stack trace, as an array of stack frames.
*/
export function captureStackTrace(
below: Function = captureStackTrace,
limit = Number.MAX_SAFE_INTEGER,
): string[] {
if (process.env.CDK_DISABLE_STACK_TRACE && !debugModeEnabled()) {
return ['stack traces disabled'];
}

below = below || captureStackTrace; // hide myself if nothing else
const object = { stack: '' };
const object: { stack?: string } = {};
const previousLimit = Error.stackTraceLimit;
try {
Error.stackTraceLimit = Number.MAX_SAFE_INTEGER;
Error.stackTraceLimit = limit;
Error.captureStackTrace(object, below);
} finally {
Error.stackTraceLimit = previousLimit;
Expand Down
29 changes: 26 additions & 3 deletions packages/@aws-cdk/core/test/tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,26 +589,49 @@ nodeunitShim({
return tests;
})(),

'creation stack is attached to errors emitted during resolve'(test: Test) {
'creation stack is attached to errors emitted during resolve with CDK_DEBUG=true'(test: Test) {
function showMeInTheStackTrace() {
return Lazy.stringValue({ produce: () => { throw new Error('fooError'); } });
}

const previousValue = reEnableStackTraceCollection();
const previousValue = process.env.CDK_DEBUG;
process.env.CDK_DEBUG = 'true';
const x = showMeInTheStackTrace();
let message;
try {
resolve(x);
} catch (e) {
message = e.message;
} finally {
restoreStackTraceColection(previousValue);
process.env.CDK_DEBUG = previousValue;
}

test.ok(message && message.includes('showMeInTheStackTrace'));
test.done();
},

'creation stack is omitted without CDK_DEBUG=true'(test: Test) {
function showMeInTheStackTrace() {
return Lazy.stringValue({ produce: () => { throw new Error('fooError'); } });
}

const previousValue = process.env.CDK_DEBUG;
delete process.env.CDK_DEBUG;

const x = showMeInTheStackTrace();
let message;
try {
resolve(x);
} catch (e) {
message = e.message;
} finally {
process.env.CDK_DEBUG = previousValue;
}

test.ok(message && message.includes('Execute again with CDK_DEBUG=true'));
test.done();
},

'stringifyNumber': {
'converts number to string'(test: Test) {
test.equal(Tokenization.stringifyNumber(100), '100');
Expand Down
7 changes: 5 additions & 2 deletions packages/@aws-cdk/core/test/util.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { Stack } from '../lib';
import { CDK_DEBUG } from '../lib/debug';
import { synthesize } from '../lib/private/synthesis';

export function toCloudFormation(stack: Stack): any {
return synthesize(stack, { skipValidation: true }).getStackByName(stack.stackName).template;
}

export function reEnableStackTraceCollection(): any {
export function reEnableStackTraceCollection(): string | undefined {
const previousValue = process.env.CDK_DISABLE_STACK_TRACE;
process.env.CDK_DISABLE_STACK_TRACE = '';
process.env[CDK_DEBUG] = 'true';
return previousValue;
}

export function restoreStackTraceColection(previousValue: any): void {
export function restoreStackTraceColection(previousValue: string | undefined): void {
process.env.CDK_DISABLE_STACK_TRACE = previousValue;
delete process.env[CDK_DEBUG];
}
1 change: 1 addition & 0 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ async function parseCommandLineArguments() {
.option('json', { type: 'boolean', alias: 'j', desc: 'Use JSON output instead of YAML when templates are printed to STDOUT', default: false })
.option('verbose', { type: 'boolean', alias: 'v', desc: 'Show debug logs (specify multiple times to increase verbosity)', default: false })
.count('verbose')
.option('debug', { type: 'boolean', desc: 'Enable emission of additional debugging information, such as creation stack traces of tokens', default: false })
.option('profile', { type: 'string', desc: 'Use the indicated AWS profile as the default environment', requiresArg: true })
.option('proxy', { type: 'string', desc: 'Use the indicated proxy. Will read from HTTPS_PROXY environment variable if not specified', requiresArg: true })
.option('ca-bundle-path', { type: 'string', desc: 'Path to CA certificate to use when validating HTTPS requests. Will read from AWS_CA_BUNDLE environment variable if not specified', requiresArg: true })
Expand Down
5 changes: 5 additions & 0 deletions packages/aws-cdk/lib/api/cxapp/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
const context = config.context.all;
await populateDefaultEnvironmentIfNeeded(aws, env);

const debugMode: boolean = config.settings.get(['debug']) ?? true;
if (debugMode) {
env.CDK_DEBUG = 'true';
}

const pathMetadata: boolean = config.settings.get(['pathMetadata']) ?? true;
if (pathMetadata) {
context[cxapi.PATH_METADATA_ENABLE_CONTEXT] = true;
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export class Settings {
app: argv.app,
browser: argv.browser,
context,
debug: argv.debug,
tags,
language: argv.language,
pathMetadata: argv.pathMetadata,
Expand Down