diff --git a/README.md b/README.md index 5a9ba251..374687f6 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,10 @@ 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. + +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 2da27487..8941d104 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, @@ -181,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; @@ -191,23 +206,21 @@ 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}`; + 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); } - lambdaFunction.addEnvironment(DD_TAGS, tags.join(",")); + setTrackedEnv(lambdaFunction, DD_TAGS, tags.join(",")); }); } } @@ -305,6 +318,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 0fec9a0b..13ce03d7 100644 --- a/src/env.ts +++ b/src/env.ts @@ -6,10 +6,10 @@ * 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 { setTrackedEnv, getTrackedEnv, hasTrackedEnv } from "./env-tracker"; +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"; @@ -44,7 +44,7 @@ const execSync = require("child_process").execSync; const URL = require("url").URL; export function setGitEnvironmentVariables( - lambdas: any[], + lambdas: LambdaFunction[], gitCommitShaOverride?: string | undefined, gitRepoUrlOverride?: string | undefined, ): void { @@ -61,12 +61,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 +116,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 +142,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..2fab7d80 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,83 @@ 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"), + }, + }, + }); + }); +}); + +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", + }, + }, + }); }); }); diff --git a/test/env.spec.ts b/test/env.spec.ts index fb590159..1f300919 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",