From a08446302940d3b36593994efb207588747d9f30 Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Tue, 23 Jun 2026 11:10:59 -0400 Subject: [PATCH 1/5] [SVLS-9359] stop reading private aws-cdk-lib Function.environment field --- src/datadog-lambda.ts | 12 ++-- src/env.ts | 106 +++++++++++++++++----------- test/datadog-lambda.spec.ts | 136 +++++++++++++++++++++++++----------- test/env.spec.ts | 68 +++++------------- 4 files changed, 185 insertions(+), 137 deletions(-) diff --git a/src/datadog-lambda.ts b/src/datadog-lambda.ts index 2da27487..f27932de 100644 --- a/src/datadog-lambda.ts +++ b/src/datadog-lambda.ts @@ -30,6 +30,8 @@ import { DD_TAGS, siteList, invalidSiteError, + getTrackedEnv, + setTrackedEnv, } from "./index"; import { DatadogAppSecMode, LambdaFunction } from "./interfaces"; import { setTags } from "./tag"; @@ -191,12 +193,12 @@ export class DatadogLambda extends Construct { // 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) { + this.lambdas.forEach((lambdaFunction: LambdaFunction) => { + const existingTagValue = getTrackedEnv(lambdaFunction, DD_TAGS); + if (existingTagValue === undefined) { return; } - const tags = existingTags.value.split(","); + const tags = existingTagValue.split(","); if (gitCommitSha) { const index = tags.findIndex((val: string) => val.split(":")[0] === "git.commit.sha"); tags[index] = `git.commit.sha:${gitCommitSha}`; @@ -207,7 +209,7 @@ export class DatadogLambda extends Construct { tags[index] = `git.repository_url:${gitRepoUrl}`; } - lambdaFunction.addEnvironment(DD_TAGS, tags.join(",")); + setTrackedEnv(lambdaFunction, DD_TAGS, tags.join(",")); }); } } diff --git a/src/env.ts b/src/env.ts index 0fec9a0b..e9633de7 100644 --- a/src/env.ts +++ b/src/env.ts @@ -6,10 +6,9 @@ * Copyright 2021 Datadog, Inc. */ -import * as lambda from "aws-cdk-lib/aws-lambda"; import log from "loglevel"; import { runtimeLookup, RuntimeType } from "./constants"; -import { DatadogLambdaProps, DatadogLambdaStrictProps } from "./interfaces"; +import { DatadogLambdaProps, DatadogLambdaStrictProps, LambdaFunction } from "./interfaces"; export const AWS_LAMBDA_EXEC_WRAPPER_KEY = "AWS_LAMBDA_EXEC_WRAPPER"; export const AWS_LAMBDA_EXEC_WRAPPER_VAL = "/opt/datadog_wrapper"; @@ -43,8 +42,36 @@ const execSync = require("child_process").execSync; const URL = require("url").URL; +// Tracks env vars written by this library per Function, so we can read them back without +// touching aws-cdk-lib's private Function.environment field. aws-cdk-lib has no public +// read accessor for env vars — the WeakMap is the library's own bookkeeping only. +// +// Note: env vars set on a Function via func.addEnvironment() outside of this library +// are not tracked here and will be overridden if the library sets the same key. +// Configure DD_* vars via DatadogLambdaProps, or call func.addEnvironment() after +// calling addLambdaFunctions(). +const ddEnvTracker: WeakMap> = new WeakMap(); + +export function setTrackedEnv(lam: LambdaFunction, key: string, value: string): void { + let envMap = ddEnvTracker.get(lam); + if (!envMap) { + envMap = new Map(); + ddEnvTracker.set(lam, envMap); + } + envMap.set(key, value); + lam.addEnvironment(key, value); +} + +export function getTrackedEnv(lam: LambdaFunction, key: string): string | undefined { + return ddEnvTracker.get(lam)?.get(key); +} + +export function hasTrackedEnv(lam: LambdaFunction, key: string): boolean { + return ddEnvTracker.get(lam)?.has(key) ?? false; +} + export function setGitEnvironmentVariables( - lambdas: any[], + lambdas: LambdaFunction[], gitCommitShaOverride?: string | undefined, gitRepoUrlOverride?: string | undefined, ): void { @@ -61,12 +88,11 @@ export function setGitEnvironmentVariables( if (hash == "" || gitRepoUrl == "") return; - // We're using an any type here because AWS does not expose the `environment` field in their type + const tagsValue = `git.commit.sha:${hash},git.repository_url:${gitRepoUrl}`; lambdas.forEach((lam) => { - const tagsValue = `git.commit.sha:${hash},git.repository_url:${gitRepoUrl}`; - const existingTagValue = lam.environment.map.get(DD_TAGS)?.value; + const existingTagValue = getTrackedEnv(lam, DD_TAGS); const finalTagValue = existingTagValue ? `${existingTagValue},${tagsValue}` : tagsValue; - lam.addEnvironment(DD_TAGS, finalTagValue); + setTrackedEnv(lam, DD_TAGS, finalTagValue); }); } @@ -117,17 +143,16 @@ function filterSensitiveInfoFromRepository(repositoryUrl: string): string { } } -export function applyEnvVariables(lam: lambda.Function, baseProps: DatadogLambdaStrictProps): void { +export function applyEnvVariables(lam: LambdaFunction, baseProps: DatadogLambdaStrictProps): 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 - const setEnvIfUndefined = (envVar: string, value: string | boolean) => { - if (lam_with_env_vars.environment.map.get(envVar) === undefined) { - lam.addEnvironment(envVar, value.toString().toLowerCase()); + + const setEnvIfUnset = (envVar: string, value: string | boolean) => { + if (!hasTrackedEnv(lam, envVar)) { + setTrackedEnv(lam, envVar, value.toString().toLowerCase()); } }; - //for each env variable, only set to default if it is NOT already set by user - setEnvIfUndefined(ENABLE_DD_TRACING_ENV_VAR, baseProps.enableDatadogTracing); + setEnvIfUnset(ENABLE_DD_TRACING_ENV_VAR, baseProps.enableDatadogTracing); const runtimeType = runtimeLookup[lam.runtime.name]; @@ -144,75 +169,74 @@ export function applyEnvVariables(lam: lambda.Function, baseProps: DatadogLambda baseProps.pythonLayerVersion >= 114) || baseProps.datadogAppSecMode === "tracer" ) { - setEnvIfUndefined(ENABLE_DD_APPSEC_ENV_VAR, "true"); + setEnvIfUnset(ENABLE_DD_APPSEC_ENV_VAR, "true"); } else if (baseProps.datadogAppSecMode === "on" || baseProps.datadogAppSecMode === "extension") { - setEnvIfUndefined(AWS_LAMBDA_EXEC_WRAPPER_KEY, AWS_LAMBDA_EXEC_WRAPPER_VAL); - setEnvIfUndefined(ENABLE_DD_SERVERLESS_APPSEC_ENV_VAR, "true"); + setEnvIfUnset(AWS_LAMBDA_EXEC_WRAPPER_KEY, AWS_LAMBDA_EXEC_WRAPPER_VAL); + setEnvIfUnset(ENABLE_DD_SERVERLESS_APPSEC_ENV_VAR, "true"); } - setEnvIfUndefined(ENABLE_XRAY_TRACE_MERGING_ENV_VAR, baseProps.enableMergeXrayTraces); + setEnvIfUnset(ENABLE_XRAY_TRACE_MERGING_ENV_VAR, baseProps.enableMergeXrayTraces); if (baseProps.extensionLayerVersion || baseProps.extensionLayerArn) { - setEnvIfUndefined(INJECT_LOG_CONTEXT_ENV_VAR, "false"); + setEnvIfUnset(INJECT_LOG_CONTEXT_ENV_VAR, "false"); } else { - setEnvIfUndefined(INJECT_LOG_CONTEXT_ENV_VAR, baseProps.injectLogContext); + setEnvIfUnset(INJECT_LOG_CONTEXT_ENV_VAR, baseProps.injectLogContext); } - setEnvIfUndefined(ENABLE_DD_LOGS_ENV_VAR, baseProps.enableDatadogLogs); - setEnvIfUndefined(CAPTURE_LAMBDA_PAYLOAD_ENV_VAR, baseProps.captureLambdaPayload); + setEnvIfUnset(ENABLE_DD_LOGS_ENV_VAR, baseProps.enableDatadogLogs); + setEnvIfUnset(CAPTURE_LAMBDA_PAYLOAD_ENV_VAR, baseProps.captureLambdaPayload); - //Cloud Payload Tagging - set to "all" when enabled, empty string when disabled const CLOUD_PAYLOAD_TAGGING_VALUE = baseProps.captureCloudServicePayload ? "all" : ""; - setEnvIfUndefined(DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING, CLOUD_PAYLOAD_TAGGING_VALUE); - setEnvIfUndefined(DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING, CLOUD_PAYLOAD_TAGGING_VALUE); + setEnvIfUnset(DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING, CLOUD_PAYLOAD_TAGGING_VALUE); + setEnvIfUnset(DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING, CLOUD_PAYLOAD_TAGGING_VALUE); if (baseProps.logLevel) { - setEnvIfUndefined(LOG_LEVEL_ENV_VAR, baseProps.logLevel); + setEnvIfUnset(LOG_LEVEL_ENV_VAR, baseProps.logLevel); } } -export function setDDEnvVariables(lam: lambda.Function, props: DatadogLambdaProps): void { +export function setDDEnvVariables(lam: LambdaFunction, props: DatadogLambdaProps): void { if (props.env) { - lam.addEnvironment(DD_ENV_ENV_VAR, props.env); + setTrackedEnv(lam, DD_ENV_ENV_VAR, props.env); } if (props.service) { - lam.addEnvironment(DD_SERVICE_ENV_VAR, props.service); + setTrackedEnv(lam, DD_SERVICE_ENV_VAR, props.service); } if (props.version) { - lam.addEnvironment(DD_VERSION_ENV_VAR, props.version); + setTrackedEnv(lam, DD_VERSION_ENV_VAR, props.version); } if (props.tags) { - lam.addEnvironment(DD_TAGS, props.tags); + setTrackedEnv(lam, DD_TAGS, props.tags); } if (props.enableColdStartTracing !== undefined) { - lam.addEnvironment(DD_COLD_START_TRACING, props.enableColdStartTracing.toString().toLowerCase()); + setTrackedEnv(lam, DD_COLD_START_TRACING, props.enableColdStartTracing.toString().toLowerCase()); } if (props.minColdStartTraceDuration !== undefined) { - lam.addEnvironment(DD_MIN_COLD_START_DURATION, props.minColdStartTraceDuration.toString().toLowerCase()); + setTrackedEnv(lam, DD_MIN_COLD_START_DURATION, props.minColdStartTraceDuration.toString().toLowerCase()); } if (props.coldStartTraceSkipLibs !== undefined) { - lam.addEnvironment(DD_COLD_START_TRACE_SKIP_LIB, props.coldStartTraceSkipLibs); + setTrackedEnv(lam, DD_COLD_START_TRACE_SKIP_LIB, props.coldStartTraceSkipLibs); } if (props.enableProfiling !== undefined) { - lam.addEnvironment(DD_PROFILING_ENABLED, props.enableProfiling.toString().toLowerCase()); + setTrackedEnv(lam, DD_PROFILING_ENABLED, props.enableProfiling.toString().toLowerCase()); } if (props.encodeAuthorizerContext !== undefined) { - lam.addEnvironment(DD_ENCODE_AUTHORIZER_CONTEXT, props.encodeAuthorizerContext.toString().toLowerCase()); + setTrackedEnv(lam, DD_ENCODE_AUTHORIZER_CONTEXT, props.encodeAuthorizerContext.toString().toLowerCase()); } if (props.decodeAuthorizerContext !== undefined) { - lam.addEnvironment(DD_DECODE_AUTHORIZER_CONTEXT, props.decodeAuthorizerContext.toString().toLowerCase()); + setTrackedEnv(lam, DD_DECODE_AUTHORIZER_CONTEXT, props.decodeAuthorizerContext.toString().toLowerCase()); } if (props.apmFlushDeadline !== undefined) { - lam.addEnvironment(DD_APM_FLUSH_DEADLINE_MILLISECONDS, props.apmFlushDeadline.toString().toLowerCase()); + setTrackedEnv(lam, DD_APM_FLUSH_DEADLINE_MILLISECONDS, props.apmFlushDeadline.toString().toLowerCase()); } if (props.llmObsEnabled !== undefined) { - lam.addEnvironment(DD_LLMOBS_ENABLED, props.llmObsEnabled.toString().toLowerCase()); + setTrackedEnv(lam, DD_LLMOBS_ENABLED, props.llmObsEnabled.toString().toLowerCase()); } if (props.llmObsMlApp !== undefined) { - lam.addEnvironment(DD_LLMOBS_ML_APP, props.llmObsMlApp); + setTrackedEnv(lam, DD_LLMOBS_ML_APP, props.llmObsMlApp); } if (props.llmObsAgentlessEnabled !== undefined) { - lam.addEnvironment(DD_LLMOBS_AGENTLESS_ENABLED, props.llmObsAgentlessEnabled.toString().toLowerCase()); + setTrackedEnv(lam, DD_LLMOBS_AGENTLESS_ENABLED, props.llmObsAgentlessEnabled.toString().toLowerCase()); } } diff --git a/test/datadog-lambda.spec.ts b/test/datadog-lambda.spec.ts index 0b9a4bd2..9afe6bc6 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"; @@ -17,7 +17,6 @@ const versionJson = require("../version.json"); const EXTENSION_LAYER_VERSION = 5; const CUSTOM_EXTENSION_LAYER_ARN = "arn:aws:lambda:us-east-1:123456789:layer:Datadog-Extension-custom:1"; const NODE_LAYER_VERSION = 91; -const REPO_REGEX = /git\.repository_url:.*\/[^/]+\/datadog-cdk-constructs(\.git)?/; describe("validateProps", () => { it("throws an error when the site is set to an invalid site URL", () => { @@ -854,12 +853,20 @@ 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,13 +887,22 @@ 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(); const stack = new Stack(app, "stack"); @@ -912,13 +928,21 @@ 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"]), - ); + // Both functions should have the overridden values + 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 +972,20 @@ 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 +1014,20 @@ 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("git\\.repository_url:"), + }, + }, }); }); @@ -1006,15 +1049,20 @@ 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("git\\.repository_url:"), + }, + }, + }); }); it("overrides using context", () => { @@ -1039,8 +1087,12 @@ 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"), + }, + }, + }); }); }); diff --git a/test/env.spec.ts b/test/env.spec.ts index fb590159..a077826d 100644 --- a/test/env.spec.ts +++ b/test/env.spec.ts @@ -70,7 +70,11 @@ describe("applyEnvVariables", () => { }); }); - it("does not override user-defined env variables before datadogLambda.addLambdaFunctions", () => { + it("overrides env variables set via func.addEnvironment() before datadogLambda.addLambdaFunctions", () => { + // The library cannot read aws-cdk-lib's private Function.environment field, so it has no way + // to detect env vars set on the function before addLambdaFunctions is called. + // To customize DD_* env vars, use DatadogLambdaProps or call func.addEnvironment() AFTER + // addLambdaFunctions(). const app = new App(); const stack = new Stack(app, "stack", { env: { @@ -83,36 +87,25 @@ describe("applyEnvVariables", () => { handler: "hello.handler", }); hello.addEnvironment(ENABLE_DD_TRACING_ENV_VAR, "False"); - hello.addEnvironment(ENABLE_DD_SERVERLESS_APPSEC_ENV_VAR, "True"); - hello.addEnvironment(AWS_LAMBDA_EXEC_WRAPPER_KEY, AWS_LAMBDA_EXEC_WRAPPER_VAL); hello.addEnvironment(ENABLE_XRAY_TRACE_MERGING_ENV_VAR, "True"); hello.addEnvironment(INJECT_LOG_CONTEXT_ENV_VAR, "False"); hello.addEnvironment(ENABLE_DD_LOGS_ENV_VAR, "False"); hello.addEnvironment(CAPTURE_LAMBDA_PAYLOAD_ENV_VAR, "True"); - hello.addEnvironment(DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING, "all"); - hello.addEnvironment(DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING, "all"); - hello.addEnvironment(LOG_LEVEL_ENV_VAR, "debug"); const datadogLambda = new DatadogLambda(stack, "Datadog", { forwarderArn: "arn:test:forwarder:sa-east-1:12345678:1", nodeLayerVersion: NODE_LAYER_VERSION, }); datadogLambda.addLambdaFunctions([hello]); + // Library defaults win because pre-set values are not visible to the library. Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { Environment: { Variables: { - [DD_HANDLER_ENV_VAR]: "hello.handler", - [FLUSH_METRICS_TO_LOGS_ENV_VAR]: "true", - [ENABLE_DD_TRACING_ENV_VAR]: "False", - [ENABLE_DD_SERVERLESS_APPSEC_ENV_VAR]: "True", - [AWS_LAMBDA_EXEC_WRAPPER_KEY]: AWS_LAMBDA_EXEC_WRAPPER_VAL, - [ENABLE_XRAY_TRACE_MERGING_ENV_VAR]: "True", - [INJECT_LOG_CONTEXT_ENV_VAR]: "False", - [ENABLE_DD_LOGS_ENV_VAR]: "False", - [CAPTURE_LAMBDA_PAYLOAD_ENV_VAR]: "True", - [DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING]: "all", - [DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING]: "all", - [LOG_LEVEL_ENV_VAR]: "debug", + [ENABLE_DD_TRACING_ENV_VAR]: "true", + [ENABLE_XRAY_TRACE_MERGING_ENV_VAR]: "false", + [INJECT_LOG_CONTEXT_ENV_VAR]: "true", + [ENABLE_DD_LOGS_ENV_VAR]: "true", + [CAPTURE_LAMBDA_PAYLOAD_ENV_VAR]: "false", }, }, }); @@ -167,16 +160,16 @@ describe("applyEnvVariables", () => { }); }); - it("does not override custom user-defined env variables per lambda when different values are set in the DatadogLambda constructor", () => { + it("applies constructor-configured values to all lambdas, regardless of pre-set env vars", () => { + // env vars set via func.addEnvironment() BEFORE addLambdaFunctions() are overridden by the library. + // To customize DD_* vars per-function, call func.addEnvironment() AFTER addLambdaFunctions(). const app = new App(); const stack = new Stack(app, "stack", { env: { region: "us-west-2", }, }); - //constructor sets to opposite of default - // hello1 sets nothing, should default to constructor values - // hello2 sets some values, should override constructor values + // hello1 sets nothing; hello2 sets env vars before the construct — both should get constructor values const hello1 = new lambda.Function(stack, "HelloHandler1", { runtime: lambda.Runtime.NODEJS_18_X, code: lambda.Code.fromInline("test"), @@ -196,7 +189,6 @@ describe("applyEnvVariables", () => { enableDatadogASM: true, enableDatadogLogs: true, captureLambdaPayload: true, - // testing injectLogContext, enableMergeXrayTraces are handled accordingly by applyEnvVariables logLevel: "constructor-set", captureCloudServicePayload: true, }); @@ -208,42 +200,20 @@ describe("applyEnvVariables", () => { hello2.addEnvironment(INJECT_LOG_CONTEXT_ENV_VAR, "False"); hello2.addEnvironment(ENABLE_DD_LOGS_ENV_VAR, "False"); hello2.addEnvironment(CAPTURE_LAMBDA_PAYLOAD_ENV_VAR, "False"); - hello2.addEnvironment(DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING, "$.*"); - hello2.addEnvironment(DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING, "$.*"); hello2.addEnvironment(LOG_LEVEL_ENV_VAR, "debug"); datadogLambda.addLambdaFunctions([hello1, hello2]); - Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { - //match hello2 - Environment: { - Variables: { - [DD_HANDLER_ENV_VAR]: "hello.handler", - [FLUSH_METRICS_TO_LOGS_ENV_VAR]: "false", //extension layer is set - [ENABLE_DD_TRACING_ENV_VAR]: "True", - [ENABLE_DD_SERVERLESS_APPSEC_ENV_VAR]: "False", - [AWS_LAMBDA_EXEC_WRAPPER_KEY]: "user-set value", - [ENABLE_XRAY_TRACE_MERGING_ENV_VAR]: "True", - [INJECT_LOG_CONTEXT_ENV_VAR]: "False", - [ENABLE_DD_LOGS_ENV_VAR]: "False", - [CAPTURE_LAMBDA_PAYLOAD_ENV_VAR]: "False", - [DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING]: "$.*", - [DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING]: "$.*", - [LOG_LEVEL_ENV_VAR]: "debug", - }, - }, - }); - Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { - //first hello1, constructor + applyEnvVariables default values + // Both functions get the same constructor-configured values; hello2's pre-sets are overridden + Template.fromStack(stack).allResourcesProperties("AWS::Lambda::Function", { Environment: { Variables: { - [DD_HANDLER_ENV_VAR]: "hello.handler", - [FLUSH_METRICS_TO_LOGS_ENV_VAR]: "false", //extension layer is set + [FLUSH_METRICS_TO_LOGS_ENV_VAR]: "false", [ENABLE_DD_TRACING_ENV_VAR]: "true", [ENABLE_DD_SERVERLESS_APPSEC_ENV_VAR]: "true", [AWS_LAMBDA_EXEC_WRAPPER_KEY]: AWS_LAMBDA_EXEC_WRAPPER_VAL, [ENABLE_XRAY_TRACE_MERGING_ENV_VAR]: "false", - [INJECT_LOG_CONTEXT_ENV_VAR]: "false", //extension layer is set (an applyenvVariables branch statement) + [INJECT_LOG_CONTEXT_ENV_VAR]: "false", [ENABLE_DD_LOGS_ENV_VAR]: "true", [CAPTURE_LAMBDA_PAYLOAD_ENV_VAR]: "true", [DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING]: "all", From b63bfb03135a1904b4b6764048138d47af9566f0 Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Tue, 23 Jun 2026 11:14:01 -0400 Subject: [PATCH 2/5] [SVLS-9359] document DD_* env var ordering constraint in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5a9ba251..7e5a19ad 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,8 @@ When `addLambdaFunctions` is called, the Datadog CDK construct grants your Lambd The DatadogLambda construct takes in a list of lambda functions and installs the Datadog Lambda Library by attaching the Lambda Layers for [.NET][19], [Java][15], [Node.js][2], and [Python][1] to your functions. It redirects to a replacement handler that initializes the Lambda Library without any required code changes. Additional configurations added to the Datadog CDK construct will also translate into their respective environment variables under each lambda function (if applicable / required). +**Configuring `DD_*` environment variables:** Set Datadog environment variables through `DatadogLambdaProps` (e.g. `enableDatadogTracing`, `logLevel`, `tags`). If you need to set a `DD_*` variable directly on a function, call `func.addEnvironment()` **after** `addLambdaFunctions()` — values set before will be overridden by the construct. + While Lambda function based log groups are handled by the `addLambdaFunctions` method automatically, the construct has an additional function `addForwarderToNonLambdaLogGroups` which subscribes the forwarder to any additional log groups of your choosing. ## Step Functions From 111e4dc3c1e29b8795e0be888c8357f93ffde6df Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Tue, 23 Jun 2026 17:24:27 -0400 Subject: [PATCH 3/5] [SVLS-9359] add WeakMap GC rationale to comment --- src/env.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/env.ts b/src/env.ts index e9633de7..9b11fb52 100644 --- a/src/env.ts +++ b/src/env.ts @@ -46,6 +46,9 @@ const URL = require("url").URL; // touching aws-cdk-lib's private Function.environment field. aws-cdk-lib has no public // read accessor for env vars — the WeakMap is the library's own bookkeeping only. // +// WeakMap (vs Map) so Function objects can be garbage-collected when a stack goes out of +// scope (e.g. between test cases) without this module holding a lingering reference. +// // Note: env vars set on a Function via func.addEnvironment() outside of this library // are not tracked here and will be overridden if the library sets the same key. // Configure DD_* vars via DatadogLambdaProps, or call func.addEnvironment() after From 062d08f736abff8fbd554241e51c051554491ed2 Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Wed, 24 Jun 2026 15:06:25 -0400 Subject: [PATCH 4/5] [SVLS-9359] move env trackers to internal module and harden git tag upsert --- README.md | 2 +- src/datadog-lambda.ts | 20 ++++++++++++++------ src/env-tracker.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ src/env.ts | 32 +------------------------------- test/env.spec.ts | 2 +- 5 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 src/env-tracker.ts diff --git a/README.md b/README.md index 7e5a19ad..4229ea8e 100644 --- a/README.md +++ b/README.md @@ -409,7 +409,7 @@ When `addLambdaFunctions` is called, the Datadog CDK construct grants your Lambd The DatadogLambda construct takes in a list of lambda functions and installs the Datadog Lambda Library by attaching the Lambda Layers for [.NET][19], [Java][15], [Node.js][2], and [Python][1] to your functions. It redirects to a replacement handler that initializes the Lambda Library without any required code changes. Additional configurations added to the Datadog CDK construct will also translate into their respective environment variables under each lambda function (if applicable / required). -**Configuring `DD_*` environment variables:** Set Datadog environment variables through `DatadogLambdaProps` (e.g. `enableDatadogTracing`, `logLevel`, `tags`). If you need to set a `DD_*` variable directly on a function, call `func.addEnvironment()` **after** `addLambdaFunctions()` — values set before will be overridden by the construct. +**Configuring `DD_*` environment variables:** Set Datadog environment variables through `DatadogLambdaProps` (e.g. `enableDatadogTracing`, `logLevel`, `tags`). If you need to set a `DD_*` variable directly on a function, call `func.addEnvironment()` **after** `addLambdaFunctions()` -- values set before will be overridden by the construct. While Lambda function based log groups are handled by the `addLambdaFunctions` method automatically, the construct has an additional function `addForwarderToNonLambdaLogGroups` which subscribes the forwarder to any additional log groups of your choosing. diff --git a/src/datadog-lambda.ts b/src/datadog-lambda.ts index f27932de..cdeaa425 100644 --- a/src/datadog-lambda.ts +++ b/src/datadog-lambda.ts @@ -13,6 +13,7 @@ import * as logs from "aws-cdk-lib/aws-logs"; import { ISecret, Secret } from "aws-cdk-lib/aws-secretsmanager"; import { Construct } from "constructs"; import log from "loglevel"; +import { getTrackedEnv, setTrackedEnv } from "./env-tracker"; import { applyLayers, redirectHandlers, @@ -30,8 +31,6 @@ import { DD_TAGS, siteList, invalidSiteError, - getTrackedEnv, - setTrackedEnv, } from "./index"; import { DatadogAppSecMode, LambdaFunction } from "./interfaces"; import { setTags } from "./tag"; @@ -200,13 +199,11 @@ export class DatadogLambda extends Construct { } const tags = existingTagValue.split(","); if (gitCommitSha) { - const index = tags.findIndex((val: string) => val.split(":")[0] === "git.commit.sha"); - tags[index] = `git.commit.sha:${gitCommitSha}`; + upsertTag(tags, "git.commit.sha", gitCommitSha); } if (gitRepoUrl) { - const index = tags.findIndex((val: string) => val.split(":")[0] === "git.repository_url"); - tags[index] = `git.repository_url:${gitRepoUrl}`; + upsertTag(tags, "git.repository_url", gitRepoUrl); } setTrackedEnv(lambdaFunction, DD_TAGS, tags.join(",")); @@ -307,6 +304,17 @@ function isSingletonFunction(fn: LambdaFunction): fn is lambda.SingletonFunction return fn.hasOwnProperty("lambdaFunction"); } +// Replaces the value of an existing `key:...` tag in place, or appends it if not present. +function upsertTag(tags: string[], key: string, value: string): void { + const index = tags.findIndex((tag) => tag.split(":")[0] === key); + const entry = `${key}:${value}`; + if (index === -1) { + tags.push(entry); + } else { + tags[index] = entry; + } +} + export function validateProps(props: DatadogLambdaProps, apiKeyArnOverride = false): void { log.debug("Validating props..."); diff --git a/src/env-tracker.ts b/src/env-tracker.ts new file mode 100644 index 00000000..586d6782 --- /dev/null +++ b/src/env-tracker.ts @@ -0,0 +1,43 @@ +/* + * 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 { LambdaFunction } from "./interfaces"; + +// Tracks env vars written by this library per Function, so we can read them back without +// touching aws-cdk-lib's private Function.environment field. aws-cdk-lib has no public +// read accessor for env vars, so the WeakMap is the library's own bookkeeping only. +// +// This module is intentionally not re-exported from index.ts: the helpers are internal +// and not part of the package's public API. +// +// WeakMap (vs Map) so Function objects can be garbage-collected when a stack goes out of +// scope (e.g. between test cases) without this module holding a lingering reference. +// +// Note: env vars set on a Function via func.addEnvironment() outside of this library +// are not tracked here and will be overridden if the library sets the same key. +// Configure DD_* vars via DatadogLambdaProps, or call func.addEnvironment() after +// calling addLambdaFunctions(). +const ddEnvTracker: WeakMap> = new WeakMap(); + +export function setTrackedEnv(lam: LambdaFunction, key: string, value: string): void { + let envMap = ddEnvTracker.get(lam); + if (!envMap) { + envMap = new Map(); + ddEnvTracker.set(lam, envMap); + } + envMap.set(key, value); + lam.addEnvironment(key, value); +} + +export function getTrackedEnv(lam: LambdaFunction, key: string): string | undefined { + return ddEnvTracker.get(lam)?.get(key); +} + +export function hasTrackedEnv(lam: LambdaFunction, key: string): boolean { + return ddEnvTracker.get(lam)?.has(key) ?? false; +} diff --git a/src/env.ts b/src/env.ts index 9b11fb52..13ce03d7 100644 --- a/src/env.ts +++ b/src/env.ts @@ -8,6 +8,7 @@ import log from "loglevel"; import { runtimeLookup, RuntimeType } from "./constants"; +import { setTrackedEnv, getTrackedEnv, hasTrackedEnv } from "./env-tracker"; import { DatadogLambdaProps, DatadogLambdaStrictProps, LambdaFunction } from "./interfaces"; export const AWS_LAMBDA_EXEC_WRAPPER_KEY = "AWS_LAMBDA_EXEC_WRAPPER"; @@ -42,37 +43,6 @@ const execSync = require("child_process").execSync; const URL = require("url").URL; -// Tracks env vars written by this library per Function, so we can read them back without -// touching aws-cdk-lib's private Function.environment field. aws-cdk-lib has no public -// read accessor for env vars — the WeakMap is the library's own bookkeeping only. -// -// WeakMap (vs Map) so Function objects can be garbage-collected when a stack goes out of -// scope (e.g. between test cases) without this module holding a lingering reference. -// -// Note: env vars set on a Function via func.addEnvironment() outside of this library -// are not tracked here and will be overridden if the library sets the same key. -// Configure DD_* vars via DatadogLambdaProps, or call func.addEnvironment() after -// calling addLambdaFunctions(). -const ddEnvTracker: WeakMap> = new WeakMap(); - -export function setTrackedEnv(lam: LambdaFunction, key: string, value: string): void { - let envMap = ddEnvTracker.get(lam); - if (!envMap) { - envMap = new Map(); - ddEnvTracker.set(lam, envMap); - } - envMap.set(key, value); - lam.addEnvironment(key, value); -} - -export function getTrackedEnv(lam: LambdaFunction, key: string): string | undefined { - return ddEnvTracker.get(lam)?.get(key); -} - -export function hasTrackedEnv(lam: LambdaFunction, key: string): boolean { - return ddEnvTracker.get(lam)?.has(key) ?? false; -} - export function setGitEnvironmentVariables( lambdas: LambdaFunction[], gitCommitShaOverride?: string | undefined, diff --git a/test/env.spec.ts b/test/env.spec.ts index a077826d..1f300919 100644 --- a/test/env.spec.ts +++ b/test/env.spec.ts @@ -169,7 +169,7 @@ describe("applyEnvVariables", () => { region: "us-west-2", }, }); - // hello1 sets nothing; hello2 sets env vars before the construct — both should get constructor values + // hello1 sets nothing; hello2 sets env vars before the construct -- both should get constructor values const hello1 = new lambda.Function(stack, "HelloHandler1", { runtime: lambda.Runtime.NODEJS_18_X, code: lambda.Code.fromInline("test"), From f15ce72a559f33a0fe34871deaeb80f885194c1e Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Thu, 25 Jun 2026 17:28:06 -0400 Subject: [PATCH 5/5] [SVLS-9359] add setEnvironment method to seed construct-tracked env vars --- README.md | 2 ++ src/datadog-lambda.ts | 14 ++++++++ test/datadog-lambda.spec.ts | 71 +++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/README.md b/README.md index 4229ea8e..374687f6 100644 --- a/README.md +++ b/README.md @@ -411,6 +411,8 @@ The DatadogLambda construct takes in a list of lambda functions and installs the **Configuring `DD_*` environment variables:** Set Datadog environment variables through `DatadogLambdaProps` (e.g. `enableDatadogTracing`, `logLevel`, `tags`). If you need to set a `DD_*` variable directly on a function, call `func.addEnvironment()` **after** `addLambdaFunctions()` -- values set before will be overridden by the construct. +To set a value the construct should respect (rather than override), use `datadogLambda.setEnvironment(func, key, value)` **before** `addLambdaFunctions()`. This is useful for setting `DD_TAGS` per function: when source code integration is enabled, the construct appends git metadata (`git.commit.sha`, `git.repository_url`) to the value you set instead of overriding it. + While Lambda function based log groups are handled by the `addLambdaFunctions` method automatically, the construct has an additional function `addForwarderToNonLambdaLogGroups` which subscribes the forwarder to any additional log groups of your choosing. ## Step Functions diff --git a/src/datadog-lambda.ts b/src/datadog-lambda.ts index cdeaa425..8941d104 100644 --- a/src/datadog-lambda.ts +++ b/src/datadog-lambda.ts @@ -182,6 +182,20 @@ export class DatadogLambda extends Construct { } } + /** + * Sets an environment variable on the given Lambda function and records it in the + * construct's internal tracker, so the construct treats it as a value it manages. + * + * Call this before `addLambdaFunctions()` to seed values the construct will respect. + * The main use case is setting `DD_TAGS` per function: when source code integration is + * enabled, the construct appends git metadata (`git.commit.sha`, `git.repository_url`) + * to the tracked `DD_TAGS` rather than overriding it. + */ + public setEnvironment(lambdaFunction: LambdaFunction, key: string, value: string): void { + const [extractedLambdaFunction] = extractSingletonFunctions([lambdaFunction]); + setTrackedEnv(extractedLambdaFunction, key, value); + } + public overrideGitMetadata(gitCommitSha: string, gitRepoUrl?: string): void { if (gitCommitSha) { this.gitCommitShaOverride = gitCommitSha; diff --git a/test/datadog-lambda.spec.ts b/test/datadog-lambda.spec.ts index 9afe6bc6..2fab7d80 100644 --- a/test/datadog-lambda.spec.ts +++ b/test/datadog-lambda.spec.ts @@ -1096,3 +1096,74 @@ describe("overrideGitMetadata", () => { }); }); }); + +describe("setEnvironment", () => { + it("appends git metadata to a DD_TAGS value seeded before addLambdaFunctions", () => { + const app = new App(); + const stack = new Stack(app, "stack"); + const hello = new lambda.Function(stack, "HelloHandler", { + runtime: lambda.Runtime.NODEJS_18_X, + code: lambda.Code.fromInline("test"), + handler: "hello.handler", + }); + const datadogLambda = new DatadogLambda(stack, "Datadog", { + nodeLayerVersion: NODE_LAYER_VERSION, + extensionLayerVersion: EXTENSION_LAYER_VERSION, + apiKey: "ABC", + enableDatadogTracing: false, + flushMetricsToLogs: false, + logLevel: "debug", + }); + datadogLambda.overrideGitMetadata("fake-sha", "fake-url"); + datadogLambda.setEnvironment(hello, DD_TAGS, "team:serverless"); + datadogLambda.addLambdaFunctions([hello], stack); + + Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { + Environment: { + Variables: { + [DD_TAGS]: Match.stringLikeRegexp("team:serverless"), + }, + }, + }); + 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("does not override a value seeded before addLambdaFunctions", () => { + const app = new App(); + const stack = new Stack(app, "stack"); + const hello = new lambda.Function(stack, "HelloHandler", { + runtime: lambda.Runtime.NODEJS_18_X, + code: lambda.Code.fromInline("test"), + handler: "hello.handler", + }); + const datadogLambda = new DatadogLambda(stack, "Datadog", { + nodeLayerVersion: NODE_LAYER_VERSION, + apiKey: "ABC", + enableDatadogTracing: true, + flushMetricsToLogs: false, + }); + datadogLambda.setEnvironment(hello, "DD_TRACE_ENABLED", "false"); + datadogLambda.addLambdaFunctions([hello], stack); + + Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { + Environment: { + Variables: { + DD_TRACE_ENABLED: "false", + }, + }, + }); + }); +});