Skip to content

Commit 0ba7bca

Browse files
authored
fix: stop reading private aws-cdk-lib Function.environment field (#621)
* [SVLS-9359] stop reading private aws-cdk-lib Function.environment field * [SVLS-9359] add setEnvironment method to seed construct-tracked env vars
1 parent d523307 commit 0ba7bca

6 files changed

Lines changed: 466 additions & 146 deletions

File tree

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,60 @@ _Note_: The descriptions use the npm package parameters, but they also apply to
280280
| `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` |
281281
| `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`. |
282282

283+
#### Setting `DD_*` environment variables
284+
285+
To configure Datadog variables for every instrumented function, set the matching field on `DatadogLambdaProps` (for example, `enableDatadogTracing`, `logLevel`, `env`, or `tags`).
286+
287+
To override a value on a single function, use one of:
288+
289+
- `datadogLambda.setEnvironment(func, key, value)` before `datadogLambda.addLambdaFunctions()`, to override a construct default while letting the construct finish instrumenting the function.
290+
- `func.addEnvironment(key, value)` after `datadogLambda.addLambdaFunctions()`, to override the value set during instrumentation.
291+
292+
When more than one source sets the same key, the following order applies (highest precedence first):
293+
294+
1. `func.addEnvironment(key, value)` called after `datadogLambda.addLambdaFunctions()`.
295+
2. `DatadogLambdaProps` fields dedicated to that key. These fields overwrite a value for the same key set through `datadogLambda.setEnvironment()`:
296+
- Unified service tagging: `env`, `service`, `version`
297+
- Cold-start tracing: `enableColdStartTracing`, `minColdStartTraceDuration`, `coldStartTraceSkipLibs`
298+
- Other tracer settings: `enableProfiling`, `encodeAuthorizerContext`, `decodeAuthorizerContext`, `apmFlushDeadline`
299+
- LLM Observability: `llmObsEnabled`, `llmObsMlApp`, `llmObsAgentlessEnabled`
300+
- Transport: `site`, `apiKey`, `apiKeySecretArn`, `apiKeySsmArn`, `apiKmsKey`, `flushMetricsToLogs`
301+
3. `datadogLambda.setEnvironment(func, key, value)` called before `datadogLambda.addLambdaFunctions()`.
302+
4. Construct defaults, which apply only when nothing else set the key: `enableDatadogTracing`, `datadogAppSecMode`, `enableMergeXrayTraces`, `injectLogContext`, `enableDatadogLogs`, `captureLambdaPayload`, `captureCloudServicePayload`, `logLevel`
303+
304+
`datadogLambda.addLambdaFunctions` merges the `DD_TAGS` environment variable from three sources, in order:
305+
306+
1. `DatadogLambdaProps.tags`.
307+
2. Per-function tags from `datadogLambda.setEnvironment(func, 'DD_TAGS', ...)`.
308+
3. `git.commit.sha` and `git.repository_url` from source code integration.
309+
310+
If the same tag key appears in more than one source, the later source wins.
311+
312+
The following example shows these rules in practice:
313+
314+
```typescript
315+
const myFunction = new lambda.Function(this, 'MyFunction', {
316+
// ...
317+
});
318+
319+
const datadogLambda = new DatadogLambda(this, 'DatadogLambda', {
320+
// ...
321+
tags: 'env:prod,team:platform',
322+
});
323+
324+
datadogLambda.setEnvironment(myFunction, 'DD_TRACE_ENABLED', 'false');
325+
datadogLambda.setEnvironment(myFunction, 'DD_TAGS', 'service:worker,team:payments');
326+
datadogLambda.addLambdaFunctions([myFunction]);
327+
328+
myFunction.addEnvironment('DD_LOG_LEVEL', 'debug');
329+
```
330+
331+
Final values on `myFunction`:
332+
333+
- `DD_TRACE_ENABLED=false`, from `datadogLambda.setEnvironment`, overriding the default.
334+
- `DD_LOG_LEVEL=debug`, from `myFunction.addEnvironment`, overriding the construct.
335+
- `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.
336+
283337
#### Default layer versions
284338

285339
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:

src/datadog-lambda.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as logs from "aws-cdk-lib/aws-logs";
1313
import { ISecret, Secret } from "aws-cdk-lib/aws-secretsmanager";
1414
import { Construct } from "constructs";
1515
import log from "loglevel";
16+
import { hasTrackedEnv, mergeTrackedGitTags, setTrackedEnv } from "./env-tracker";
1617
import {
1718
applyLayers,
1819
redirectHandlers,
@@ -181,6 +182,25 @@ export class DatadogLambda extends Construct {
181182
}
182183
}
183184

185+
/**
186+
* Pre-set a Datadog environment variable on `lambdaFunction`. Call before
187+
* `addLambdaFunctions([lambdaFunction])`.
188+
*
189+
* Precedence, highest first:
190+
* 1. `func.addEnvironment()` called after `addLambdaFunctions()`.
191+
* 2. `DatadogLambdaProps` fields dedicated to `key` (for example, `env` for `DD_ENV`).
192+
* 3. This method.
193+
* 4. Construct defaults (for example, `enableDatadogTracing` for `DD_TRACE_ENABLED`).
194+
*
195+
* `addLambdaFunctions` merges `DD_TAGS` from `DatadogLambdaProps.tags`, per-function
196+
* tags from this method, and git tags from source code integration, in that order.
197+
* On duplicate tag keys, the later source wins.
198+
*/
199+
public setEnvironment(lambdaFunction: LambdaFunction, key: string, value: string): void {
200+
const [extractedLambdaFunction] = extractSingletonFunctions([lambdaFunction]);
201+
setTrackedEnv(extractedLambdaFunction, key, value);
202+
}
203+
184204
public overrideGitMetadata(gitCommitSha: string, gitRepoUrl?: string): void {
185205
if (gitCommitSha) {
186206
this.gitCommitShaOverride = gitCommitSha;
@@ -191,23 +211,18 @@ export class DatadogLambda extends Construct {
191211

192212
// If any lambdas have already been added, override the commit sha and url
193213
if (this.lambdas) {
194-
this.lambdas.forEach((lambdaFunction: any) => {
195-
const existingTags = lambdaFunction.environment.map.get(DD_TAGS);
196-
if (existingTags === undefined) {
214+
this.lambdas.forEach((lambdaFunction: LambdaFunction) => {
215+
if (!hasTrackedEnv(lambdaFunction, DD_TAGS)) {
197216
return;
198217
}
199-
const tags = existingTags.value.split(",");
200-
if (gitCommitSha) {
201-
const index = tags.findIndex((val: string) => val.split(":")[0] === "git.commit.sha");
202-
tags[index] = `git.commit.sha:${gitCommitSha}`;
203-
}
218+
const gitTags = [
219+
gitCommitSha ? `git.commit.sha:${gitCommitSha}` : undefined,
220+
gitRepoUrl ? `git.repository_url:${gitRepoUrl}` : undefined,
221+
].filter((tag): tag is string => tag !== undefined);
204222

205-
if (gitRepoUrl) {
206-
const index = tags.findIndex((val: string) => val.split(":")[0] === "git.repository_url");
207-
tags[index] = `git.repository_url:${gitRepoUrl}`;
223+
if (gitTags.length > 0) {
224+
mergeTrackedGitTags(lambdaFunction, gitTags.join(","));
208225
}
209-
210-
lambdaFunction.addEnvironment(DD_TAGS, tags.join(","));
211226
});
212227
}
213228
}

src/env-tracker.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed
3+
* under the Apache License Version 2.0.
4+
*
5+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
6+
* Copyright 2021 Datadog, Inc.
7+
*/
8+
9+
import { LambdaFunction } from "./interfaces";
10+
11+
const DD_TAGS = "DD_TAGS";
12+
13+
type Tags = Map<string, string>;
14+
15+
interface TrackedEnvironment {
16+
readonly values: Map<string, string>;
17+
readonly propTags: Tags;
18+
readonly functionTags: Tags;
19+
readonly gitTags: Tags;
20+
tagsSet: boolean;
21+
}
22+
23+
// Bookkeeping for env vars this library writes to Lambda functions. aws-cdk-lib does not
24+
// expose Function.environment publicly, so we mirror our own writes here and read from
25+
// this map instead of the private field.
26+
//
27+
// Not exported from index.ts -- internal to the package.
28+
//
29+
// WeakMap so functions can be garbage-collected when their stack goes out of scope (for
30+
// example, between test cases).
31+
//
32+
// Env vars set via func.addEnvironment() outside this library are invisible here and will
33+
// be overwritten if the library writes the same key. Configure DD_* vars via
34+
// DatadogLambdaProps or datadogLambda.setEnvironment(), or call func.addEnvironment()
35+
// after datadogLambda.addLambdaFunctions().
36+
const ddEnvTracker: WeakMap<LambdaFunction, TrackedEnvironment> = new WeakMap();
37+
38+
export function setTrackedEnv(lam: LambdaFunction, key: string, value: string): void {
39+
if (key === DD_TAGS) {
40+
const tracked = getOrCreateTrackedEnvironment(lam);
41+
replaceTags(tracked.functionTags, value);
42+
writeTags(lam, tracked);
43+
return;
44+
}
45+
46+
getOrCreateTrackedEnvironment(lam).values.set(key, value);
47+
lam.addEnvironment(key, value);
48+
}
49+
50+
export function setTrackedPropTags(lam: LambdaFunction, value: string): void {
51+
const tracked = getOrCreateTrackedEnvironment(lam);
52+
replaceTags(tracked.propTags, value);
53+
writeTags(lam, tracked);
54+
}
55+
56+
export function mergeTrackedGitTags(lam: LambdaFunction, value: string): void {
57+
const tracked = getOrCreateTrackedEnvironment(lam);
58+
mergeTags(tracked.gitTags, value);
59+
writeTags(lam, tracked);
60+
}
61+
62+
export function hasTrackedEnv(lam: LambdaFunction, key: string): boolean {
63+
const tracked = ddEnvTracker.get(lam);
64+
return key === DD_TAGS ? (tracked?.tagsSet ?? false) : (tracked?.values.has(key) ?? false);
65+
}
66+
67+
function getOrCreateTrackedEnvironment(lam: LambdaFunction): TrackedEnvironment {
68+
let tracked = ddEnvTracker.get(lam);
69+
if (!tracked) {
70+
tracked = {
71+
values: new Map(),
72+
propTags: new Map(),
73+
functionTags: new Map(),
74+
gitTags: new Map(),
75+
tagsSet: false,
76+
};
77+
ddEnvTracker.set(lam, tracked);
78+
}
79+
return tracked;
80+
}
81+
82+
function replaceTags(tags: Tags, value: string): void {
83+
tags.clear();
84+
mergeTags(tags, value);
85+
}
86+
87+
function mergeTags(tags: Tags, value: string): void {
88+
for (const tag of value.split(",")) {
89+
// Split only the key because tag values can contain colons.
90+
const separator = tag.indexOf(":");
91+
const key = separator > 0 ? tag.slice(0, separator) : tag;
92+
tags.delete(key);
93+
tags.set(key, tag);
94+
}
95+
}
96+
97+
function writeTags(lam: LambdaFunction, tracked: TrackedEnvironment): void {
98+
tracked.tagsSet = true;
99+
lam.addEnvironment(DD_TAGS, serializeTags(tracked));
100+
}
101+
102+
function serializeTags(tracked: TrackedEnvironment): string {
103+
const tags: Tags = new Map();
104+
for (const source of [tracked.propTags, tracked.functionTags, tracked.gitTags]) {
105+
for (const [key, tag] of source) {
106+
// Move replaced tags to the position of the higher-precedence source.
107+
tags.delete(key);
108+
tags.set(key, tag);
109+
}
110+
}
111+
return [...tags.values()].join(",");
112+
}

0 commit comments

Comments
 (0)