From c9db3153a65df476e907cd652bf1780661eda28a Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Tue, 23 Jun 2026 11:10:59 -0400 Subject: [PATCH 01/10] [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 0198a5fd..1a2bc24d 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 357b5cc5..9b70570e 100644 --- a/src/env.ts +++ b/src/env.ts @@ -6,10 +6,9 @@ * Copyright 2020-2026 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 d0c43947..faad9c43 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 302b180e19f1b2a26081fb2f726cad9a03c4ef3c Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Tue, 23 Jun 2026 11:14:01 -0400 Subject: [PATCH 02/10] [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 57d9f41d..f1cbded9 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,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 a881fd4efc60b938affb5752bd16dc6219b03ff3 Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Tue, 23 Jun 2026 17:24:27 -0400 Subject: [PATCH 03/10] [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 9b70570e..0f43daf5 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 4eb6d1040e7e151d99d47f77dc28f89616dfc601 Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Wed, 24 Jun 2026 15:06:25 -0400 Subject: [PATCH 04/10] [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 f1cbded9..39aa11a9 100644 --- a/README.md +++ b/README.md @@ -402,7 +402,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 1a2bc24d..edd69378 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 0f43daf5..cbe39236 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 82cb41dd5bc908651bdbbad53ac4223373ed700b Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Thu, 25 Jun 2026 17:28:06 -0400 Subject: [PATCH 05/10] [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 39aa11a9..a39ebd4d 100644 --- a/README.md +++ b/README.md @@ -404,6 +404,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 edd69378..af18d683 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 faad9c43..ba706725 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", + }, + }, + }); + }); +}); From 9450386f4cc0bca1c9b6e154f71b46908ee2b158 Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Thu, 13 Aug 2026 17:50:47 -0400 Subject: [PATCH 06/10] [SVLS-9359] clean up internal representation --- README.md | 21 ++++++++- src/datadog-lambda.ts | 44 +++++++------------ src/env-tracker.ts | 88 +++++++++++++++++++++++++++++++++---- src/env.ts | 8 ++-- test/datadog-lambda.spec.ts | 65 ++++++++++++++++++++++++--- 5 files changed, 175 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index a39ebd4d..cee4e0a3 100644 --- a/README.md +++ b/README.md @@ -402,9 +402,26 @@ 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`). A value added with `func.addEnvironment()` before `addLambdaFunctions()` can be overwritten if the construct writes the same key. To ensure the function's value wins, add it after `addLambdaFunctions()`. -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. +For per-function environment variables that Datadog defaults should not overwrite, call `datadogLambda.setEnvironment(func, key, value)` before `addLambdaFunctions()`. `DD_TAGS` is handled specially: the construct starts with `DatadogLambdaProps.tags`, applies the per-function tags, then adds `git.commit.sha` and `git.repository_url`. When the same tag appears more than once, the later value wins. For other environment variables, `setEnvironment()` prevents a Datadog default from replacing the value. Properties that are always applied still take precedence. For example, setting `env` on `DatadogLambdaProps` determines `DD_ENV`, even if `setEnvironment()` also sets `DD_ENV`. + +```typescript +const datadogLambda = new DatadogLambda(this, 'DatadogLambda', { + ... + tags: 'env:prod,team:platform', +}); + +// Merge function-specific tags and keep tracing disabled for this function. +datadogLambda.setEnvironment(myFunction, 'DD_TAGS', 'service:worker,team:payments'); +datadogLambda.setEnvironment(myFunction, 'DD_TRACE_ENABLED', 'false'); +datadogLambda.addLambdaFunctions([myFunction]); + +// Override the construct's log level for this function. +myFunction.addEnvironment('DD_LOG_LEVEL', 'debug'); +``` + +In the example, `myFunction` keeps `DD_TRACE_ENABLED=false`. Its tags include `env:prod`, `service:worker`, and `team:payments`, with the per-function `team` replacing `team:platform`. Source code integration adds the git tags. Because `DD_LOG_LEVEL` is set after instrumentation, `debug` is the final value. 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 af18d683..69b68ab1 100644 --- a/src/datadog-lambda.ts +++ b/src/datadog-lambda.ts @@ -13,7 +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 { hasTrackedEnv, mergeTrackedGitTags, setTrackedEnv } from "./env-tracker"; import { applyLayers, redirectHandlers, @@ -183,13 +183,15 @@ 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. + * Sets a tracked environment variable before `addLambdaFunctions()` runs. * - * 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. + * For `DD_TAGS`, tags from `DatadogLambdaProps` are applied first, tags set here + * override duplicate keys, and source code integration tags take final precedence. + * For other keys, the value takes precedence when the construct would otherwise set a + * default. Construct settings and instrumentation steps that always write the key still + * take precedence. + * To guarantee final precedence for any key, call `addEnvironment()` after + * `addLambdaFunctions()` instead. */ public setEnvironment(lambdaFunction: LambdaFunction, key: string, value: string): void { const [extractedLambdaFunction] = extractSingletonFunctions([lambdaFunction]); @@ -207,20 +209,17 @@ 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: LambdaFunction) => { - const existingTagValue = getTrackedEnv(lambdaFunction, DD_TAGS); - if (existingTagValue === undefined) { + if (!hasTrackedEnv(lambdaFunction, DD_TAGS)) { return; } - const tags = existingTagValue.split(","); - if (gitCommitSha) { - upsertTag(tags, "git.commit.sha", gitCommitSha); - } + const gitTags = [ + gitCommitSha ? `git.commit.sha:${gitCommitSha}` : undefined, + gitRepoUrl ? `git.repository_url:${gitRepoUrl}` : undefined, + ].filter((tag): tag is string => tag !== undefined); - if (gitRepoUrl) { - upsertTag(tags, "git.repository_url", gitRepoUrl); + if (gitTags.length > 0) { + mergeTrackedGitTags(lambdaFunction, gitTags.join(",")); } - - setTrackedEnv(lambdaFunction, DD_TAGS, tags.join(",")); }); } } @@ -318,17 +317,6 @@ 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 index 586d6782..827faf96 100644 --- a/src/env-tracker.ts +++ b/src/env-tracker.ts @@ -8,6 +8,18 @@ import { LambdaFunction } from "./interfaces"; +const DD_TAGS = "DD_TAGS"; + +type Tags = Map; + +interface TrackedEnvironment { + readonly values: Map; + readonly propTags: Tags; + readonly functionTags: Tags; + readonly gitTags: Tags; + tagsSet: boolean; +} + // 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. @@ -22,22 +34,80 @@ import { LambdaFunction } from "./interfaces"; // 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(); +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); + if (key === DD_TAGS) { + const tracked = getOrCreateTrackedEnvironment(lam); + replaceTags(tracked.functionTags, value); + writeTags(lam, tracked); + return; } - envMap.set(key, value); + + getOrCreateTrackedEnvironment(lam).values.set(key, value); lam.addEnvironment(key, value); } -export function getTrackedEnv(lam: LambdaFunction, key: string): string | undefined { - return ddEnvTracker.get(lam)?.get(key); +export function setTrackedPropTags(lam: LambdaFunction, value: string): void { + const tracked = getOrCreateTrackedEnvironment(lam); + replaceTags(tracked.propTags, value); + writeTags(lam, tracked); +} + +export function mergeTrackedGitTags(lam: LambdaFunction, value: string): void { + const tracked = getOrCreateTrackedEnvironment(lam); + mergeTags(tracked.gitTags, value); + writeTags(lam, tracked); } export function hasTrackedEnv(lam: LambdaFunction, key: string): boolean { - return ddEnvTracker.get(lam)?.has(key) ?? false; + const tracked = ddEnvTracker.get(lam); + return key === DD_TAGS ? (tracked?.tagsSet ?? false) : (tracked?.values.has(key) ?? false); +} + +function getOrCreateTrackedEnvironment(lam: LambdaFunction): TrackedEnvironment { + let tracked = ddEnvTracker.get(lam); + if (!tracked) { + tracked = { + values: new Map(), + propTags: new Map(), + functionTags: new Map(), + gitTags: new Map(), + tagsSet: false, + }; + ddEnvTracker.set(lam, tracked); + } + return tracked; +} + +function replaceTags(tags: Tags, value: string): void { + tags.clear(); + mergeTags(tags, value); +} + +function mergeTags(tags: Tags, value: string): void { + for (const tag of value.split(",")) { + // Split only the key because tag values can contain colons. + const separator = tag.indexOf(":"); + const key = separator > 0 ? tag.slice(0, separator) : tag; + tags.delete(key); + tags.set(key, tag); + } +} + +function writeTags(lam: LambdaFunction, tracked: TrackedEnvironment): void { + tracked.tagsSet = true; + lam.addEnvironment(DD_TAGS, serializeTags(tracked)); +} + +function serializeTags(tracked: TrackedEnvironment): string { + const tags: Tags = new Map(); + for (const source of [tracked.propTags, tracked.functionTags, tracked.gitTags]) { + for (const [key, tag] of source) { + // Move replaced tags to the position of the higher-precedence source. + tags.delete(key); + tags.set(key, tag); + } + } + return [...tags.values()].join(","); } diff --git a/src/env.ts b/src/env.ts index cbe39236..d72207e7 100644 --- a/src/env.ts +++ b/src/env.ts @@ -8,7 +8,7 @@ import log from "loglevel"; import { runtimeLookup, RuntimeType } from "./constants"; -import { setTrackedEnv, getTrackedEnv, hasTrackedEnv } from "./env-tracker"; +import { setTrackedEnv, hasTrackedEnv, mergeTrackedGitTags, setTrackedPropTags } from "./env-tracker"; import { DatadogLambdaProps, DatadogLambdaStrictProps, LambdaFunction } from "./interfaces"; export const AWS_LAMBDA_EXEC_WRAPPER_KEY = "AWS_LAMBDA_EXEC_WRAPPER"; @@ -63,9 +63,7 @@ export function setGitEnvironmentVariables( const tagsValue = `git.commit.sha:${hash},git.repository_url:${gitRepoUrl}`; lambdas.forEach((lam) => { - const existingTagValue = getTrackedEnv(lam, DD_TAGS); - const finalTagValue = existingTagValue ? `${existingTagValue},${tagsValue}` : tagsValue; - setTrackedEnv(lam, DD_TAGS, finalTagValue); + mergeTrackedGitTags(lam, tagsValue); }); } @@ -179,7 +177,7 @@ export function setDDEnvVariables(lam: LambdaFunction, props: DatadogLambdaProps setTrackedEnv(lam, DD_VERSION_ENV_VAR, props.version); } if (props.tags) { - setTrackedEnv(lam, DD_TAGS, props.tags); + setTrackedPropTags(lam, props.tags); } if (props.enableColdStartTracing !== undefined) { setTrackedEnv(lam, DD_COLD_START_TRACING, props.enableColdStartTracing.toString().toLowerCase()); diff --git a/test/datadog-lambda.spec.ts b/test/datadog-lambda.spec.ts index ba706725..e4ab1289 100644 --- a/test/datadog-lambda.spec.ts +++ b/test/datadog-lambda.spec.ts @@ -1098,7 +1098,7 @@ describe("overrideGitMetadata", () => { }); describe("setEnvironment", () => { - it("appends git metadata to a DD_TAGS value seeded before addLambdaFunctions", () => { + it("merges global, per-function, and source code integration tags by key", () => { const app = new App(); const stack = new Stack(app, "stack"); const hello = new lambda.Function(stack, "HelloHandler", { @@ -1106,6 +1106,11 @@ describe("setEnvironment", () => { code: lambda.Code.fromInline("test"), handler: "hello.handler", }); + const goodbye = new lambda.Function(stack, "GoodbyeHandler", { + runtime: lambda.Runtime.NODEJS_18_X, + code: lambda.Code.fromInline("test"), + handler: "goodbye.handler", + }); const datadogLambda = new DatadogLambda(stack, "Datadog", { nodeLayerVersion: NODE_LAYER_VERSION, extensionLayerVersion: EXTENSION_LAYER_VERSION, @@ -1113,35 +1118,57 @@ describe("setEnvironment", () => { enableDatadogTracing: false, flushMetricsToLogs: false, logLevel: "debug", + tags: "env:prod,team:serverless,git.commit.sha:global", }); datadogLambda.overrideGitMetadata("fake-sha", "fake-url"); - datadogLambda.setEnvironment(hello, DD_TAGS, "team:serverless"); - datadogLambda.addLambdaFunctions([hello], stack); + datadogLambda.setEnvironment(hello, DD_TAGS, "service:hello,team:payments,git.commit.sha:function"); + datadogLambda.setEnvironment(goodbye, DD_TAGS, "service:goodbye"); + datadogLambda.addLambdaFunctions([hello, goodbye], stack); Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { + Handler: "/opt/nodejs/node_modules/datadog-lambda-js/handler.handler", Environment: { Variables: { - [DD_TAGS]: Match.stringLikeRegexp("team:serverless"), + [DD_TAGS]: "env:prod,service:hello,team:payments,git.commit.sha:fake-sha,git.repository_url:fake-url", }, }, }); Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { Environment: { Variables: { - [DD_TAGS]: Match.stringLikeRegexp("git\\.commit\\.sha:fake-sha"), + DD_LAMBDA_HANDLER: "goodbye.handler", + [DD_TAGS]: "env:prod,team:serverless,service:goodbye,git.commit.sha:fake-sha,git.repository_url:fake-url", }, }, }); + }); + + it("preserves tag values containing colons and malformed tags", () => { + 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, + sourceCodeIntegration: false, + tags: "endpoint:https://example.com,unstructured", + }); + datadogLambda.setEnvironment(hello, DD_TAGS, "team:serverless"); + datadogLambda.addLambdaFunctions([hello], stack); + Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { Environment: { Variables: { - [DD_TAGS]: Match.stringLikeRegexp("git\\.repository_url:fake-url"), + [DD_TAGS]: "endpoint:https://example.com,unstructured,team:serverless", }, }, }); }); - it("does not override a value seeded before addLambdaFunctions", () => { + it("does not override a tracked value with a construct default", () => { const app = new App(); const stack = new Stack(app, "stack"); const hello = new lambda.Function(stack, "HelloHandler", { @@ -1166,4 +1193,28 @@ describe("setEnvironment", () => { }, }); }); + + it("lets an unconditional construct setting override a tracked value", () => { + 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, + env: "global", + }); + datadogLambda.setEnvironment(hello, "DD_ENV", "function"); + datadogLambda.addLambdaFunctions([hello], stack); + + Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { + Environment: { + Variables: { + DD_ENV: "global", + }, + }, + }); + }); }); From 11f113a47496ddb9509485b9065998d96eadee51 Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Fri, 14 Aug 2026 11:36:24 -0400 Subject: [PATCH 07/10] [SVLS-9359] clean up readme --- README.md | 75 +++++++++++++++++++++++++++++++------------ src/datadog-lambda.ts | 19 ++++++----- src/env-tracker.ts | 21 ++++++------ 3 files changed, 75 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index cee4e0a3..55c45a5d 100644 --- a/README.md +++ b/README.md @@ -280,6 +280,60 @@ _Note_: The descriptions use the npm package parameters, but they also apply to | `llmObsMlApp` | `llm_obs_ml_app` | The name of your LLM application, service, or project, under which all traces and spans are grouped. This helps distinguish between different applications or experiments. See [Application naming guidelines](https://docs.datadoghq.com/llm_observability/sdk/?tab=nodejs#application-naming-guidelines) for allowed characters and other constraints. To override this value for a given root span, see [Tracing multiple applications](https://docs.datadoghq.com/llm_observability/sdk/?tab=nodejs#tracing-multiple-applications). Required if `llmObsEnabled` is `true` | | `llmObsAgentlessEnabled` | `llm_obs_agentless_enabled` | Only required if you are not using the Datadog Lambda Extension, in which case this should be set to `true`. Defaults to `false`. | +#### Setting `DD_*` environment variables + +To configure Datadog variables for every instrumented function, set the matching field on `DatadogLambdaProps` (for example, `enableDatadogTracing`, `logLevel`, `env`, `tags`). + +To override a value on a single function, use one of: + +- `datadogLambda.setEnvironment(func, key, value)` before `datadogLambda.addLambdaFunctions()`, to override a construct default while letting the construct finish instrumenting the function. +- `func.addEnvironment(key, value)` after `datadogLambda.addLambdaFunctions()`, to set the final value and bypass the construct entirely. + +When more than one source sets the same key, the following order applies (highest precedence first): + +1. `func.addEnvironment(key, value)` called after `datadogLambda.addLambdaFunctions()` +2. `DatadogLambdaProps` fields dedicated to that key. These props always overwrite the same key set through. `datadogLambda.setEnvironment`: + - Unified service tagging: `env`, `service`, `version`, `tags` + - Cold-start tracing: `enableColdStartTracing`, `minColdStartTraceDuration`, `coldStartTraceSkipLibs` + - Other tracer settings: `enableProfiling`, `encodeAuthorizerContext`, `decodeAuthorizerContext`, `apmFlushDeadline` + - LLM Observability: `llmObsEnabled`, `llmObsMlApp`, `llmObsAgentlessEnabled` + - Transport: `site`, `apiKey`, `apiKeySecretArn`, `apiKeySsmArn`, `apiKmsKey`, `flushMetricsToLogs` +3. `datadogLambda.setEnvironment(func, key, value)` called before `datadogLambda.addLambdaFunctions()`. +4. Construct defaults, which apply only when nothing else set the key: `enableDatadogTracing`, `datadogAppSecMode`, `enableMergeXrayTraces`, `injectLogContext`, `enableDatadogLogs`, `captureLambdaPayload`, `captureCloudServicePayload`, `logLevel` + +`datadogLambda.addLambdaFunctions` merges the `DD_TAGS` environment variable from three sources, in order: + +1. `DatadogLambdaProps.tags`. +2. Per-function tags from `datadogLambda.setEnvironment(func, 'DD_TAGS', ...)`. +3. `git.commit.sha` and `git.repository_url` from source code integration. + +If the same tag key appears in more than one source, the later source wins. + +The following example shows these rules in practice: + +```typescript +const myFunction = new lambda.Function(this, 'MyFunction', { + // ... +}); + +const datadogLambda = new DatadogLambda(this, 'DatadogLambda', { + // ... + tags: 'env:prod,team:platform', +}); + +datadogLambda.setEnvironment(myFunction, 'DD_TRACE_ENABLED', 'false'); +datadogLambda.setEnvironment(myFunction, 'DD_TAGS', 'service:worker,team:payments'); +datadogLambda.addLambdaFunctions([myFunction]); + +myFunction.addEnvironment('DD_LOG_LEVEL', 'debug'); +``` + +Final values on `myFunction`: + +- `DD_TRACE_ENABLED=false`, from `datadogLambda.setEnvironment`, overriding the default. +- `DD_LOG_LEVEL=debug`, from `myFunction.addEnvironment`, overriding the construct. +- `DD_TAGS=env:prod,service:worker,team:payments,git.commit.sha:...,git.repository_url:...`. `team:payments` replaces `team:platform`, and source code integration appends the git tags. + #### Default layer versions When you don't pass a `*LayerVersion` or `*LayerArn`, the construct uses a default layer version bundled with the package. These defaults track the latest released Datadog Lambda layers at the time the construct version was published, and are exposed via the `DatadogDefaultLayerVersions` class so you can reference them directly in any language: @@ -402,27 +456,6 @@ 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`). A value added with `func.addEnvironment()` before `addLambdaFunctions()` can be overwritten if the construct writes the same key. To ensure the function's value wins, add it after `addLambdaFunctions()`. - -For per-function environment variables that Datadog defaults should not overwrite, call `datadogLambda.setEnvironment(func, key, value)` before `addLambdaFunctions()`. `DD_TAGS` is handled specially: the construct starts with `DatadogLambdaProps.tags`, applies the per-function tags, then adds `git.commit.sha` and `git.repository_url`. When the same tag appears more than once, the later value wins. For other environment variables, `setEnvironment()` prevents a Datadog default from replacing the value. Properties that are always applied still take precedence. For example, setting `env` on `DatadogLambdaProps` determines `DD_ENV`, even if `setEnvironment()` also sets `DD_ENV`. - -```typescript -const datadogLambda = new DatadogLambda(this, 'DatadogLambda', { - ... - tags: 'env:prod,team:platform', -}); - -// Merge function-specific tags and keep tracing disabled for this function. -datadogLambda.setEnvironment(myFunction, 'DD_TAGS', 'service:worker,team:payments'); -datadogLambda.setEnvironment(myFunction, 'DD_TRACE_ENABLED', 'false'); -datadogLambda.addLambdaFunctions([myFunction]); - -// Override the construct's log level for this function. -myFunction.addEnvironment('DD_LOG_LEVEL', 'debug'); -``` - -In the example, `myFunction` keeps `DD_TRACE_ENABLED=false`. Its tags include `env:prod`, `service:worker`, and `team:payments`, with the per-function `team` replacing `team:platform`. Source code integration adds the git tags. Because `DD_LOG_LEVEL` is set after instrumentation, `debug` is the final value. - 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 69b68ab1..f9beb122 100644 --- a/src/datadog-lambda.ts +++ b/src/datadog-lambda.ts @@ -183,15 +183,18 @@ export class DatadogLambda extends Construct { } /** - * Sets a tracked environment variable before `addLambdaFunctions()` runs. + * Pre-set a Datadog environment variable on `lambdaFunction`. Call this before + * `addLambdaFunctions([lambdaFunction])`. * - * For `DD_TAGS`, tags from `DatadogLambdaProps` are applied first, tags set here - * override duplicate keys, and source code integration tags take final precedence. - * For other keys, the value takes precedence when the construct would otherwise set a - * default. Construct settings and instrumentation steps that always write the key still - * take precedence. - * To guarantee final precedence for any key, call `addEnvironment()` after - * `addLambdaFunctions()` instead. + * Precedence, highest wins: + * 1. `func.addEnvironment()` called after `addLambdaFunctions()`. + * 2. `DatadogLambdaProps` fields dedicated to `key` (for example, `env` for `DD_ENV`). + * 3. This method. + * 4. Construct defaults (for example, `enableDatadogTracing` for `DD_TRACE_ENABLED`). + * + * `DD_TAGS` is merged, not replaced: props tags first, then per-function tags from this + * method, then git tags from source code integration. On duplicate tag keys, the later + * source wins. */ public setEnvironment(lambdaFunction: LambdaFunction, key: string, value: string): void { const [extractedLambdaFunction] = extractSingletonFunctions([lambdaFunction]); diff --git a/src/env-tracker.ts b/src/env-tracker.ts index 827faf96..92200d4f 100644 --- a/src/env-tracker.ts +++ b/src/env-tracker.ts @@ -20,20 +20,19 @@ interface TrackedEnvironment { tagsSet: boolean; } -// 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. +// Bookkeeping for env vars this library writes to Lambda functions. aws-cdk-lib does not +// expose Function.environment publicly, so we mirror our own writes here and read from +// this map instead of the private field. // -// This module is intentionally not re-exported from index.ts: the helpers are internal -// and not part of the package's public API. +// Not exported from index.ts -- internal to the package. // -// 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. +// WeakMap so functions can be garbage-collected when their stack goes out of scope (for +// example, between test cases). // -// 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(). +// Env vars set via func.addEnvironment() outside this library are invisible here and will +// be overwritten if the library writes the same key. Configure DD_* vars via +// DatadogLambdaProps or datadogLambda.setEnvironment(), or call func.addEnvironment() +// after datadogLambda.addLambdaFunctions(). const ddEnvTracker: WeakMap = new WeakMap(); export function setTrackedEnv(lam: LambdaFunction, key: string, value: string): void { From c676e9dfb93cf2a70804f5b806272b1a47ba172f Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Fri, 14 Aug 2026 11:40:09 -0400 Subject: [PATCH 08/10] [SVLS-9359] update comment --- src/datadog-lambda.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/datadog-lambda.ts b/src/datadog-lambda.ts index f9beb122..1ea2c902 100644 --- a/src/datadog-lambda.ts +++ b/src/datadog-lambda.ts @@ -183,18 +183,18 @@ export class DatadogLambda extends Construct { } /** - * Pre-set a Datadog environment variable on `lambdaFunction`. Call this before + * Pre-set a Datadog environment variable on `lambdaFunction`. Call before * `addLambdaFunctions([lambdaFunction])`. * - * Precedence, highest wins: + * Precedence, highest first: * 1. `func.addEnvironment()` called after `addLambdaFunctions()`. * 2. `DatadogLambdaProps` fields dedicated to `key` (for example, `env` for `DD_ENV`). * 3. This method. * 4. Construct defaults (for example, `enableDatadogTracing` for `DD_TRACE_ENABLED`). * - * `DD_TAGS` is merged, not replaced: props tags first, then per-function tags from this - * method, then git tags from source code integration. On duplicate tag keys, the later - * source wins. + * `addLambdaFunctions` merges `DD_TAGS` from `DatadogLambdaProps.tags`, per-function + * tags from this method, and git tags from source code integration, in that order. + * On duplicate tag keys, the later source wins. */ public setEnvironment(lambdaFunction: LambdaFunction, key: string, value: string): void { const [extractedLambdaFunction] = extractSingletonFunctions([lambdaFunction]); From d2fb00197cc158e3290e147b58539ab8070032bf Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Fri, 14 Aug 2026 11:43:35 -0400 Subject: [PATCH 09/10] [SVLS-9359] update style --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55c45a5d..78286c5b 100644 --- a/README.md +++ b/README.md @@ -282,7 +282,7 @@ _Note_: The descriptions use the npm package parameters, but they also apply to #### Setting `DD_*` environment variables -To configure Datadog variables for every instrumented function, set the matching field on `DatadogLambdaProps` (for example, `enableDatadogTracing`, `logLevel`, `env`, `tags`). +To configure Datadog variables for every instrumented function, set the matching field on `DatadogLambdaProps` (for example, `enableDatadogTracing`, `logLevel`, `env`, or `tags`). To override a value on a single function, use one of: From 231240edcbfd3027b1781455c85cc4beba7a4697 Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Fri, 14 Aug 2026 12:44:40 -0400 Subject: [PATCH 10/10] [SVLS-9359] clean up readme pt 2 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 78286c5b..76c371ac 100644 --- a/README.md +++ b/README.md @@ -287,13 +287,13 @@ To configure Datadog variables for every instrumented function, set the matching To override a value on a single function, use one of: - `datadogLambda.setEnvironment(func, key, value)` before `datadogLambda.addLambdaFunctions()`, to override a construct default while letting the construct finish instrumenting the function. -- `func.addEnvironment(key, value)` after `datadogLambda.addLambdaFunctions()`, to set the final value and bypass the construct entirely. +- `func.addEnvironment(key, value)` after `datadogLambda.addLambdaFunctions()`, to override the value set during instrumentation. When more than one source sets the same key, the following order applies (highest precedence first): -1. `func.addEnvironment(key, value)` called after `datadogLambda.addLambdaFunctions()` -2. `DatadogLambdaProps` fields dedicated to that key. These props always overwrite the same key set through. `datadogLambda.setEnvironment`: - - Unified service tagging: `env`, `service`, `version`, `tags` +1. `func.addEnvironment(key, value)` called after `datadogLambda.addLambdaFunctions()`. +2. `DatadogLambdaProps` fields dedicated to that key. These fields overwrite a value for the same key set through `datadogLambda.setEnvironment()`: + - Unified service tagging: `env`, `service`, `version` - Cold-start tracing: `enableColdStartTracing`, `minColdStartTraceDuration`, `coldStartTraceSkipLibs` - Other tracer settings: `enableProfiling`, `encodeAuthorizerContext`, `decodeAuthorizerContext`, `apmFlushDeadline` - LLM Observability: `llmObsEnabled`, `llmObsMlApp`, `llmObsAgentlessEnabled`