diff --git a/integration_tests/snapshots/correct-lambda-function-arm-stack-snapshot.json b/integration_tests/snapshots/correct-lambda-function-arm-stack-snapshot.json index 78ca68d9..f3fed048 100644 --- a/integration_tests/snapshots/correct-lambda-function-arm-stack-snapshot.json +++ b/integration_tests/snapshots/correct-lambda-function-arm-stack-snapshot.json @@ -46,16 +46,16 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "lambdaFunction.handler", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "DD_MERGE_XRAY_TRACES": "false", "DD_LOGS_INJECTION": "false", "DD_SERVERLESS_LOGS_ENABLED": "true", "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", - "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234" + "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "" } }, "Handler": "/opt/nodejs/node_modules/datadog-lambda-js/handler.handler", diff --git a/integration_tests/snapshots/correct-lambda-function-stack-snapshot.json b/integration_tests/snapshots/correct-lambda-function-stack-snapshot.json index 8b383808..30edcf43 100644 --- a/integration_tests/snapshots/correct-lambda-function-stack-snapshot.json +++ b/integration_tests/snapshots/correct-lambda-function-stack-snapshot.json @@ -43,16 +43,16 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "lambdaFunction.handler", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "DD_MERGE_XRAY_TRACES": "false", "DD_LOGS_INJECTION": "false", "DD_SERVERLESS_LOGS_ENABLED": "true", "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", - "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234" + "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "" } }, "Handler": "/opt/nodejs/node_modules/datadog-lambda-js/handler.handler", diff --git a/integration_tests/snapshots/correct-lambda-java-function-stack-snapshot.json b/integration_tests/snapshots/correct-lambda-java-function-stack-snapshot.json index ddf8d527..b362c917 100644 --- a/integration_tests/snapshots/correct-lambda-java-function-stack-snapshot.json +++ b/integration_tests/snapshots/correct-lambda-java-function-stack-snapshot.json @@ -44,16 +44,16 @@ "Environment": { "Variables": { "AWS_LAMBDA_EXEC_WRAPPER": "/opt/datadog_wrapper", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "DD_MERGE_XRAY_TRACES": "false", "DD_LOGS_INJECTION": "false", "DD_SERVERLESS_LOGS_ENABLED": "true", "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", - "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234" + "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "" } }, "Handler": "handleRequest", diff --git a/integration_tests/snapshots/correct-lambda-java-stack-snapshot.json b/integration_tests/snapshots/correct-lambda-java-stack-snapshot.json index 8d829577..a9ebcfd0 100644 --- a/integration_tests/snapshots/correct-lambda-java-stack-snapshot.json +++ b/integration_tests/snapshots/correct-lambda-java-stack-snapshot.json @@ -44,6 +44,9 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "hello.handler", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "DD_MERGE_XRAY_TRACES": "true", "DD_LOGS_INJECTION": "false", @@ -52,9 +55,6 @@ "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", "DD_LOG_LEVEL": "debug", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234", "DD_TAGS": "git.commit.sha:XXXXXXXX,git.repository_url:github.com/DataDog/datadog-cdk-constructs" } }, diff --git a/integration_tests/snapshots/correct-lambda-nodejs-function-stack-snapshot.json b/integration_tests/snapshots/correct-lambda-nodejs-function-stack-snapshot.json index 624cb717..99e1447e 100644 --- a/integration_tests/snapshots/correct-lambda-nodejs-function-stack-snapshot.json +++ b/integration_tests/snapshots/correct-lambda-nodejs-function-stack-snapshot.json @@ -44,16 +44,16 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "index.handler", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "DD_MERGE_XRAY_TRACES": "false", "DD_LOGS_INJECTION": "false", "DD_SERVERLESS_LOGS_ENABLED": "true", "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", - "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234" + "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "" } }, "Handler": "/opt/nodejs/node_modules/datadog-lambda-js/handler.handler", diff --git a/integration_tests/snapshots/correct-lambda-provided-arm-stack-snapshot.json b/integration_tests/snapshots/correct-lambda-provided-arm-stack-snapshot.json index 61b557b8..e201e180 100644 --- a/integration_tests/snapshots/correct-lambda-provided-arm-stack-snapshot.json +++ b/integration_tests/snapshots/correct-lambda-provided-arm-stack-snapshot.json @@ -121,16 +121,16 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "handler.handler", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "DD_MERGE_XRAY_TRACES": "false", "DD_LOGS_INJECTION": "false", "DD_SERVERLESS_LOGS_ENABLED": "true", "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", - "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234" + "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "" } }, "Handler": "handler.handler", diff --git a/integration_tests/snapshots/correct-lambda-provided-stack-snapshot.json b/integration_tests/snapshots/correct-lambda-provided-stack-snapshot.json index d86fbaec..3d4de53f 100644 --- a/integration_tests/snapshots/correct-lambda-provided-stack-snapshot.json +++ b/integration_tests/snapshots/correct-lambda-provided-stack-snapshot.json @@ -118,16 +118,16 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "handler.handler", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "DD_MERGE_XRAY_TRACES": "false", "DD_LOGS_INJECTION": "false", "DD_SERVERLESS_LOGS_ENABLED": "true", "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", - "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234" + "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "" } }, "Handler": "handler.handler", diff --git a/integration_tests/snapshots/correct-lambda-python-function-stack-snapshot.json b/integration_tests/snapshots/correct-lambda-python-function-stack-snapshot.json index 151e9d1c..72cfdec3 100644 --- a/integration_tests/snapshots/correct-lambda-python-function-stack-snapshot.json +++ b/integration_tests/snapshots/correct-lambda-python-function-stack-snapshot.json @@ -44,16 +44,16 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "example-python.handler", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "DD_MERGE_XRAY_TRACES": "false", "DD_LOGS_INJECTION": "false", "DD_SERVERLESS_LOGS_ENABLED": "true", "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", - "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234" + "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "" } }, "Handler": "datadog_lambda.handler.handler", diff --git a/integration_tests/snapshots/correct-lambda-singleton-function-stack-snapshot.json b/integration_tests/snapshots/correct-lambda-singleton-function-stack-snapshot.json index 80f4c2bd..28f66f46 100644 --- a/integration_tests/snapshots/correct-lambda-singleton-function-stack-snapshot.json +++ b/integration_tests/snapshots/correct-lambda-singleton-function-stack-snapshot.json @@ -43,16 +43,16 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "lambdaFunction.handler", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "DD_MERGE_XRAY_TRACES": "false", "DD_LOGS_INJECTION": "false", "DD_SERVERLESS_LOGS_ENABLED": "true", "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", - "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234" + "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "" } }, "Handler": "/opt/nodejs/node_modules/datadog-lambda-js/handler.handler", diff --git a/integration_tests/snapshots/correct-lambda_python_stack-snapshot.json b/integration_tests/snapshots/correct-lambda_python_stack-snapshot.json index 810a0952..24273406 100644 --- a/integration_tests/snapshots/correct-lambda_python_stack-snapshot.json +++ b/integration_tests/snapshots/correct-lambda_python_stack-snapshot.json @@ -46,6 +46,9 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "hello.lambda_handler", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "AWS_LAMBDA_EXEC_WRAPPER": "/opt/datadog_wrapper", "DD_SERVERLESS_APPSEC_ENABLED": "true", @@ -55,9 +58,6 @@ "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234", "DD_TAGS": "git.commit.sha:XXXXXXXX,git.repository_url:github.com/DataDog/datadog-cdk-constructs" } }, @@ -160,6 +160,9 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "hello.lambda_handler", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "AWS_LAMBDA_EXEC_WRAPPER": "/opt/datadog_wrapper", "DD_SERVERLESS_APPSEC_ENABLED": "true", @@ -169,9 +172,6 @@ "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234", "DD_TAGS": "git.commit.sha:XXXXXXXX,git.repository_url:github.com/DataDog/datadog-cdk-constructs" } }, @@ -274,6 +274,9 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "bootstrap", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "AWS_LAMBDA_EXEC_WRAPPER": "/opt/datadog_wrapper", "DD_SERVERLESS_APPSEC_ENABLED": "true", @@ -283,9 +286,6 @@ "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234", "DD_TAGS": "git.commit.sha:XXXXXXXX,git.repository_url:github.com/DataDog/datadog-cdk-constructs" } }, @@ -375,6 +375,9 @@ "Environment": { "Variables": { "AWS_LAMBDA_EXEC_WRAPPER": "/opt/datadog_wrapper", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "DD_SERVERLESS_APPSEC_ENABLED": "true", "DD_MERGE_XRAY_TRACES": "false", @@ -383,9 +386,6 @@ "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234", "DD_TAGS": "git.commit.sha:XXXXXXXX,git.repository_url:github.com/DataDog/datadog-cdk-constructs" } }, diff --git a/integration_tests/snapshots/correct-step-function-stack-snapshot.json b/integration_tests/snapshots/correct-step-function-stack-snapshot.json index 643a1a5f..7be17645 100644 --- a/integration_tests/snapshots/correct-step-function-stack-snapshot.json +++ b/integration_tests/snapshots/correct-step-function-stack-snapshot.json @@ -250,6 +250,12 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "index.handler", + "DD_ENV": "dev", + "DD_SERVICE": "cdk-test-service", + "DD_VERSION": "1.0.0", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "AWS_LAMBDA_EXEC_WRAPPER": "/opt/datadog_wrapper", "DD_SERVERLESS_APPSEC_ENABLED": "true", @@ -259,12 +265,6 @@ "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_ENV": "dev", - "DD_SERVICE": "cdk-test-service", - "DD_VERSION": "1.0.0", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234", "DD_TAGS": "git.commit.sha:XXXXXXXX,git.repository_url:github.com/DataDog/datadog-cdk-constructs" } }, diff --git a/integration_tests/snapshots/correct-step_functions_python_stack-snapshot.json b/integration_tests/snapshots/correct-step_functions_python_stack-snapshot.json index f3d09e88..88179048 100644 --- a/integration_tests/snapshots/correct-step_functions_python_stack-snapshot.json +++ b/integration_tests/snapshots/correct-step_functions_python_stack-snapshot.json @@ -253,6 +253,12 @@ "Environment": { "Variables": { "DD_LAMBDA_HANDLER": "hello.lambda_handler", + "DD_ENV": "dev", + "DD_SERVICE": "cdk-test-service", + "DD_VERSION": "1.0.0", + "DD_FLUSH_TO_LOG": "false", + "DD_SITE": "datadoghq.com", + "DD_API_KEY": "1234", "DD_TRACE_ENABLED": "true", "AWS_LAMBDA_EXEC_WRAPPER": "/opt/datadog_wrapper", "DD_SERVERLESS_APPSEC_ENABLED": "true", @@ -262,12 +268,6 @@ "DD_CAPTURE_LAMBDA_PAYLOAD": "false", "DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING": "", "DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING": "", - "DD_ENV": "dev", - "DD_SERVICE": "cdk-test-service", - "DD_VERSION": "1.0.0", - "DD_FLUSH_TO_LOG": "false", - "DD_SITE": "datadoghq.com", - "DD_API_KEY": "1234", "DD_TAGS": "git.commit.sha:XXXXXXXX,git.repository_url:github.com/DataDog/datadog-cdk-constructs" } }, diff --git a/src/datadog-lambda.ts b/src/datadog-lambda.ts index 2da27487..5286e40a 100644 --- a/src/datadog-lambda.ts +++ b/src/datadog-lambda.ts @@ -6,7 +6,7 @@ * Copyright 2021 Datadog, Inc. */ -import { Tags, Stack } from "aws-cdk-lib"; +import { Tags, Stack, Aspects } from "aws-cdk-lib"; import * as iam from "aws-cdk-lib/aws-iam"; import * as lambda from "aws-cdk-lib/aws-lambda"; import * as logs from "aws-cdk-lib/aws-logs"; @@ -18,19 +18,17 @@ import { redirectHandlers, addForwarder, addForwarderToLogGroups, - applyEnvVariables, TagKeys, DatadogLambdaStrictProps, - setGitEnvironmentVariables, setDDEnvVariables, DatadogLambdaDefaultProps, DatadogLambdaProps, Transport, applyExtensionLayer, - DD_TAGS, siteList, invalidSiteError, } from "./index"; +import { DatadogLambdaEnvAspect } from "./env-aspect"; import { DatadogAppSecMode, LambdaFunction } from "./interfaces"; import { setTags } from "./tag"; @@ -44,6 +42,10 @@ export class DatadogLambda extends Construct { gitRepoUrlOverride: string | undefined; lambdas: LambdaFunction[]; contextGitShaOverrideKey: string = "datadog-lambda.git-commit-sha-override"; + // Functions whose Datadog env vars are applied by the synth-time aspect, and the set of + // stacks the aspect has already been registered on (registered once per stack). + private readonly envAspectTargets: Set = new Set(); + private readonly envAspectRegisteredStacks: Set = new Set(); constructor(scope: Construct, id: string, props: DatadogLambdaProps) { if (process.env.DD_CONSTRUCT_DEBUG_LOGS?.toLowerCase() === "true") { @@ -168,48 +170,51 @@ export class DatadogLambda extends Construct { } addCdkConstructVersionTag(lambdaFunction); - applyEnvVariables(lambdaFunction, baseProps); + // Unconditional, prop-driven writes happen eagerly. The "set default only if unset" and + // source code integration logic both need to read existing env vars, so they run in a + // synth-time aspect (see registerEnvAspect) after all user addEnvironment calls. setDDEnvVariables(lambdaFunction, this.props); setTagsForFunction(lambdaFunction, this.props); this.transport.applyEnvVars(lambdaFunction); - if (baseProps.sourceCodeIntegration) { - this.addGitCommitMetadata([lambdaFunction]); - } + this.registerEnvAspect(lambdaFunction, baseProps); this.lambdas.push(lambdaFunction); } } + // Adds a function to the synth-time env aspect, registering the aspect on the function's stack + // the first time that stack is seen. + private registerEnvAspect(lambdaFunction: lambda.Function, baseProps: DatadogLambdaStrictProps): void { + this.envAspectTargets.add(lambdaFunction); + + const stack = Stack.of(lambdaFunction); + if (this.envAspectRegisteredStacks.has(stack)) { + return; + } + this.envAspectRegisteredStacks.add(stack); + + Aspects.of(stack).add( + new DatadogLambdaEnvAspect({ + targets: this.envAspectTargets, + baseProps, + sourceCodeIntegration: baseProps.sourceCodeIntegration ?? true, + getGitCommitShaOverride: () => this.gitCommitShaOverride, + getGitRepoUrlOverride: () => this.gitRepoUrlOverride, + }), + ); + } + public overrideGitMetadata(gitCommitSha: string, gitRepoUrl?: string): void { + // Only record the overrides. The synth-time aspect reads these (via getter closures) when it + // builds the DD_TAGS source code integration value, so overrides set before or after + // addLambdaFunctions both take effect -- no need to rewrite already-added functions here. if (gitCommitSha) { this.gitCommitShaOverride = gitCommitSha; } if (gitRepoUrl) { this.gitRepoUrlOverride = gitRepoUrl; } - - // If any lambdas have already been added, override the commit sha and url - if (this.lambdas) { - this.lambdas.forEach((lambdaFunction: any) => { - const existingTags = lambdaFunction.environment.map.get(DD_TAGS); - if (existingTags === undefined) { - return; - } - const tags = existingTags.value.split(","); - if (gitCommitSha) { - const index = tags.findIndex((val: string) => val.split(":")[0] === "git.commit.sha"); - tags[index] = `git.commit.sha:${gitCommitSha}`; - } - - if (gitRepoUrl) { - const index = tags.findIndex((val: string) => val.split(":")[0] === "git.repository_url"); - tags[index] = `git.repository_url:${gitRepoUrl}`; - } - - lambdaFunction.addEnvironment(DD_TAGS, tags.join(",")); - }); - } } // unused parameters gitCommitSha and gitRepoUrl are kept for backwards compatibility @@ -222,8 +227,11 @@ export class DatadogLambda extends Construct { // @ts-ignore gitRepoUrl?: string, ): void { + // Ensure the given functions are instrumented by the synth-time aspect, which applies source + // code integration tags using the recorded git overrides. + const baseProps = handleSettingPropDefaults(this.props); const extractedLambdaFunctions = extractSingletonFunctions(lambdaFunctions); - setGitEnvironmentVariables(extractedLambdaFunctions, this.gitCommitShaOverride, this.gitRepoUrlOverride); + extractedLambdaFunctions.forEach((lambdaFunction) => this.registerEnvAspect(lambdaFunction, baseProps)); } public addForwarderToNonLambdaLogGroups(logGroups: logs.ILogGroup[]) { diff --git a/src/env-aspect.ts b/src/env-aspect.ts new file mode 100644 index 00000000..869bc7ca --- /dev/null +++ b/src/env-aspect.ts @@ -0,0 +1,65 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed + * under the Apache License Version 2.0. + * + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2021 Datadog, Inc. + */ + +import { IAspect } from "aws-cdk-lib"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import { IConstruct } from "constructs"; +import log from "loglevel"; +import { applyEnvVariables, applySourceCodeIntegration, readResolvedFunctionEnv } from "./env"; +import { DatadogLambdaStrictProps } from "./interfaces"; + +/** @internal */ +export interface DatadogLambdaEnvAspectOptions { + /** The functions this aspect should instrument. Other functions in the tree are ignored. */ + readonly targets: Set; + readonly baseProps: DatadogLambdaStrictProps; + readonly sourceCodeIntegration: boolean; + /** Read lazily at synth time so values set via overrideGitMetadata after registration apply. */ + readonly getGitCommitShaOverride: () => string | undefined; + readonly getGitRepoUrlOverride: () => string | undefined; +} + +/** + * CDK Aspect that applies Datadog environment variables to instrumented Lambda functions at + * synth time rather than eagerly during `addLambdaFunctions`. + * + * Why an Aspect: aws-cdk-lib exposes no public API to read a function's env vars eagerly, and + * reaching into the private `Function.environment` field is what broke `cdk synth` across a + * routine aws-cdk-lib minor. Aspects run during synthesis, after all user `addEnvironment` + * calls -- whether made before or after `addLambdaFunctions` -- have executed. At that point we + * read the function's resolved env vars through the public token-resolution API and only fill + * in Datadog defaults the user has not already set. This preserves user-set values regardless + * of ordering, with no dependency on aws-cdk-lib internals. + */ +/** @internal */ +export class DatadogLambdaEnvAspect implements IAspect { + constructor(private readonly options: DatadogLambdaEnvAspectOptions) {} + + public visit(node: IConstruct): void { + if (!(node instanceof lambda.Function)) { + return; + } + if (!this.options.targets.has(node)) { + return; + } + + log.debug(`Applying Datadog env vars to ${node.node.path} at synth time`); + const existingEnv = readResolvedFunctionEnv(node); + + applyEnvVariables(node, this.options.baseProps, existingEnv); + + if (this.options.sourceCodeIntegration) { + applySourceCodeIntegration( + node, + existingEnv, + this.options.getGitCommitShaOverride(), + this.options.getGitRepoUrlOverride(), + ); + } + } +} diff --git a/src/env.ts b/src/env.ts index 0fec9a0b..6e0eeaa8 100644 --- a/src/env.ts +++ b/src/env.ts @@ -6,6 +6,7 @@ * Copyright 2021 Datadog, Inc. */ +import { Stack } from "aws-cdk-lib"; import * as lambda from "aws-cdk-lib/aws-lambda"; import log from "loglevel"; import { runtimeLookup, RuntimeType } from "./constants"; @@ -43,12 +44,28 @@ const execSync = require("child_process").execSync; const URL = require("url").URL; -export function setGitEnvironmentVariables( - lambdas: any[], +/** + * Reads a Lambda function's currently-configured environment variables through the public + * token-resolution API instead of reaching into aws-cdk-lib's private `Function.environment` + * field. Returns a plain `{ KEY: value }` map (empty if no env vars are set). + * + * Resolving the L1 `CfnFunction.environment` is only meaningful once all `addEnvironment` + * calls have run -- i.e. at synth time, from inside a CDK Aspect (see DatadogLambdaEnvAspect). + */ +export function readResolvedFunctionEnv(lam: lambda.Function): Record { + const cfnFunction = lam.node.defaultChild as lambda.CfnFunction; + const resolved = Stack.of(lam).resolve(cfnFunction.environment) as { variables?: Record } | undefined; + return resolved?.variables ?? {}; +} + +/** + * Computes the source code integration `DD_TAGS` value (`git.commit.sha:...,git.repository_url:...`) + * from local git data, applying any overrides. Returns undefined when git data is unavailable. + */ +export function getSourceCodeIntegrationTags( gitCommitShaOverride?: string | undefined, gitRepoUrlOverride?: string | undefined, -): void { - log.debug("Adding source code integration..."); +): string | undefined { let { hash, gitRepoUrl } = getGitData(); if (gitCommitShaOverride) { log.debug(`Using git SHA override. Will be ${gitCommitShaOverride} instead of ${hash}`); @@ -59,15 +76,28 @@ export function setGitEnvironmentVariables( gitRepoUrl = gitRepoUrlOverride; } - if (hash == "" || gitRepoUrl == "") return; + if (hash == "" || gitRepoUrl == "") return undefined; + + return `git.commit.sha:${hash},git.repository_url:${gitRepoUrl}`; +} + +/** + * Appends source code integration git tags to a function's existing `DD_TAGS`, using the + * supplied resolved env map to read the current value (no private field access). + */ +export function applySourceCodeIntegration( + lam: lambda.Function, + existingEnv: Record, + gitCommitShaOverride?: string | undefined, + gitRepoUrlOverride?: string | undefined, +): void { + log.debug("Adding source code integration..."); + const tagsValue = getSourceCodeIntegrationTags(gitCommitShaOverride, gitRepoUrlOverride); + if (tagsValue === undefined) return; - // We're using an any type here because AWS does not expose the `environment` field in their type - lambdas.forEach((lam) => { - const tagsValue = `git.commit.sha:${hash},git.repository_url:${gitRepoUrl}`; - const existingTagValue = lam.environment.map.get(DD_TAGS)?.value; - const finalTagValue = existingTagValue ? `${existingTagValue},${tagsValue}` : tagsValue; - lam.addEnvironment(DD_TAGS, finalTagValue); - }); + const existingTagValue = existingEnv[DD_TAGS]; + const finalTagValue = existingTagValue ? `${existingTagValue},${tagsValue}` : tagsValue; + lam.addEnvironment(DD_TAGS, finalTagValue); } function getGitData(): { hash: string; gitRepoUrl: string } { @@ -117,11 +147,16 @@ function filterSensitiveInfoFromRepository(repositoryUrl: string): string { } } -export function applyEnvVariables(lam: lambda.Function, baseProps: DatadogLambdaStrictProps): void { +export function applyEnvVariables( + lam: lambda.Function, + baseProps: DatadogLambdaStrictProps, + existingEnv: Record, +): void { log.debug(`Setting environment variables...`); - const lam_with_env_vars: any = lam; //cast to any to access the private environment fields like in setGitEnvironmentVariables + // `existingEnv` is the function's resolved env map (read via the public API, not the private + // `Function.environment` field). Only set a default when the user hasn't already set the key. const setEnvIfUndefined = (envVar: string, value: string | boolean) => { - if (lam_with_env_vars.environment.map.get(envVar) === undefined) { + if (existingEnv[envVar] === undefined) { lam.addEnvironment(envVar, value.toString().toLowerCase()); } }; diff --git a/test/datadog-lambda.spec.ts b/test/datadog-lambda.spec.ts index 0b9a4bd2..a3aab40c 100644 --- a/test/datadog-lambda.spec.ts +++ b/test/datadog-lambda.spec.ts @@ -1,5 +1,5 @@ import { App, Fn, Stack, Token } from "aws-cdk-lib"; -import { Template } from "aws-cdk-lib/assertions"; +import { Match, Template } from "aws-cdk-lib/assertions"; import { Key } from "aws-cdk-lib/aws-kms"; import * as lambda from "aws-cdk-lib/aws-lambda"; import { LogGroup } from "aws-cdk-lib/aws-logs"; @@ -854,12 +854,12 @@ describe("overrideGitMetadata", () => { }); datadogLambda.overrideGitMetadata("fake-sha", "fake-url"); datadogLambda.addLambdaFunctions([hello], stack); - expect((hello).environment.map.get(DD_TAGS).value.split(",")).toEqual( - expect.arrayContaining(["git.commit.sha:fake-sha"]), - ); - expect((hello).environment.map.get(DD_TAGS).value.split(",")).toEqual( - expect.arrayContaining(["git.repository_url:fake-url"]), - ); + Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp("git\\.commit\\.sha:fake-sha") } }, + }); + Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp("git\\.repository_url:fake-url") } }, + }); }); it("overrides existing lambda functions", () => { @@ -880,12 +880,12 @@ describe("overrideGitMetadata", () => { }); datadogLambda.addLambdaFunctions([hello], stack); datadogLambda.overrideGitMetadata("fake-sha", "fake-url"); - expect((hello).environment.map.get(DD_TAGS).value.split(",")).toEqual( - expect.arrayContaining(["git.commit.sha:fake-sha"]), - ); - expect((hello).environment.map.get(DD_TAGS).value.split(",")).toEqual( - expect.arrayContaining(["git.repository_url:fake-url"]), - ); + Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp("git\\.commit\\.sha:fake-sha") } }, + }); + Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp("git\\.repository_url:fake-url") } }, + }); }); it("overrides both existing and new lambda functions", () => { const app = new App(); @@ -912,13 +912,12 @@ describe("overrideGitMetadata", () => { datadogLambda.overrideGitMetadata("fake-sha", "fake-url"); datadogLambda.addLambdaFunctions([goodbye], stack); - [hello, goodbye].forEach((f) => { - expect((f).environment.map.get(DD_TAGS).value.split(",")).toEqual( - expect.arrayContaining(["git.commit.sha:fake-sha"]), - ); - expect((f).environment.map.get(DD_TAGS).value.split(",")).toEqual( - expect.arrayContaining(["git.repository_url:fake-url"]), - ); + Template.fromStack(stack).resourceCountIs("AWS::Lambda::Function", 2); + Template.fromStack(stack).allResourcesProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp("git\\.commit\\.sha:fake-sha") } }, + }); + Template.fromStack(stack).allResourcesProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp("git\\.repository_url:fake-url") } }, }); }); @@ -948,11 +947,12 @@ describe("overrideGitMetadata", () => { datadogLambda.overrideGitMetadata("fake-sha"); datadogLambda.addLambdaFunctions([goodbye], stack); - [hello, goodbye].forEach((f) => { - expect((f).environment.map.get(DD_TAGS).value.split(",")).toEqual( - expect.arrayContaining(["git.commit.sha:fake-sha"]), - ); - expect((f).environment.map.get(DD_TAGS).value.split(",")).toEqual(expect.arrayContaining(["testVar:xyz"])); + Template.fromStack(stack).resourceCountIs("AWS::Lambda::Function", 2); + Template.fromStack(stack).allResourcesProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp("git\\.commit\\.sha:fake-sha") } }, + }); + Template.fromStack(stack).allResourcesProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp("testVar:xyz") } }, }); }); @@ -981,10 +981,12 @@ describe("overrideGitMetadata", () => { datadogLambda.overrideGitMetadata("fake-sha"); datadogLambda.addLambdaFunctions([goodbye], stack); - [hello, goodbye].forEach((f) => { - expect((f).environment.map.get(DD_TAGS).value.split(",")).toEqual( - expect.arrayContaining([expect.stringContaining("git.commit.sha:fake-sha"), expect.stringMatching(REPO_REGEX)]), - ); + Template.fromStack(stack).resourceCountIs("AWS::Lambda::Function", 2); + Template.fromStack(stack).allResourcesProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp("git\\.commit\\.sha:fake-sha") } }, + }); + Template.fromStack(stack).allResourcesProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp(REPO_REGEX.source) } }, }); }); @@ -1006,15 +1008,12 @@ describe("overrideGitMetadata", () => { }); datadogLambda.addLambdaFunctions([hello], stack); - expect( - (hello).environment.map - .get(DD_TAGS) - .value.split(",") - .some((item: string) => item.includes("git.commit.sha")), - ).toEqual(true); - expect((hello).environment.map.get(DD_TAGS).value.split(",")).toEqual( - expect.arrayContaining([expect.stringMatching(REPO_REGEX)]), - ); + Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp("git\\.commit\\.sha:") } }, + }); + Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp(REPO_REGEX.source) } }, + }); }); it("overrides using context", () => { @@ -1039,8 +1038,8 @@ describe("overrideGitMetadata", () => { }); datadogLambda.addLambdaFunctions([hello], stack); - expect((hello).environment.map.get(DD_TAGS).value.split(",")).toEqual( - expect.arrayContaining(["git.commit.sha:fake-sha"]), - ); + Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { + Environment: { Variables: { [DD_TAGS]: Match.stringLikeRegexp("git\\.commit\\.sha:fake-sha") } }, + }); }); });