Skip to content

Commit 1ef5527

Browse files
authored
Optionally capture Lambda request and response payloads (#222)
* feat: Allow users to capture and tag request and response payloads. * fix: remove commented console * feat: switch to funciton.request and function.response * feat: Max depth 5 for tagging payloads * feat: Update env var name after product feedback. Update filename to be consistent * fix: Vscode will update imports but not save the file if it's open, nice * fix: Fix renamed import * feat: set maxDepth to 10
1 parent fefe509 commit 1ef5527

File tree

7 files changed

+126
-3
lines changed

7 files changed

+126
-3
lines changed

.prettierrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"printWidth": 120,
33
"trailingComma": "all",
4-
"arrowParens": "always"
4+
"arrowParens": "always",
5+
"semi": true
56
}

src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ import {
1717
setLogger,
1818
promisifiedHandler,
1919
logDebug,
20+
tagObject,
2021
} from "./utils";
2122
export { TraceHeaders } from "./trace";
2223
import { extractHTTPStatusCodeTag } from "./trace/trigger";
2324

2425
export const apiKeyEnvVar = "DD_API_KEY";
2526
export const apiKeyKMSEnvVar = "DD_KMS_API_KEY";
27+
export const captureLambdaPayloadEnvVar = "DD_CAPTURE_LAMBDA_PAYLOAD";
2628
export const siteURLEnvVar = "DD_SITE";
2729
export const logLevelEnvVar = "DD_LOG_LEVEL";
2830
export const logForwardingEnvVar = "DD_FLUSH_TO_LOG";
@@ -60,6 +62,7 @@ export const defaultConfig: Config = {
6062
apiKey: "",
6163
apiKeyKMS: "",
6264
autoPatchHTTP: true,
65+
captureLambdaPayload: false,
6366
debugLogging: false,
6467
enhancedMetrics: true,
6568
forceWrap: false,
@@ -139,6 +142,10 @@ export function datadog<TEvent, TResult>(
139142
// Store the status tag in the listener to send to Xray on invocation completion
140143
traceListener.triggerTags["http.status_code"] = statusCode;
141144
if (traceListener.currentSpan) {
145+
if (finalConfig.captureLambdaPayload) {
146+
tagObject(traceListener.currentSpan, "function.request", localEvent);
147+
tagObject(traceListener.currentSpan, "function.response", localResult);
148+
}
142149
traceListener.currentSpan.setTag("http.status_code", statusCode);
143150
}
144151
}
@@ -176,7 +183,7 @@ export function datadog<TEvent, TResult>(
176183
* Sends a Distribution metric asynchronously to the Datadog API.
177184
* @param name The name of the metric to send.
178185
* @param value The value of the metric
179-
* @param metricTime The timesamp associated with this metric data point.
186+
* @param metricTime The timestamp associated with this metric data point.
180187
* @param tags The tags associated with the metric. Should be of the format "tag:value".
181188
*/
182189
export function sendDistributionMetricWithDate(name: string, value: number, metricTime: Date, ...tags: string[]) {
@@ -260,6 +267,11 @@ function getConfig(userConfig?: Partial<Config>): Config {
260267
config.mergeDatadogXrayTraces = result === "true";
261268
}
262269

270+
if (userConfig === undefined || userConfig.captureLambdaPayload === undefined) {
271+
const result = getEnvValue(captureLambdaPayloadEnvVar, "false").toLocaleLowerCase();
272+
config.captureLambdaPayload = result === "true";
273+
}
274+
263275
return config;
264276
}
265277

src/trace/listener.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ jest.mock("./trace-context-service", () => {
4545
});
4646

4747
describe("TraceListener", () => {
48-
const defaultConfig = { autoPatchHTTP: true, mergeDatadogXrayTraces: false, injectLogContext: false };
48+
const defaultConfig = {
49+
autoPatchHTTP: true,
50+
captureLambdaPayload: false,
51+
mergeDatadogXrayTraces: false,
52+
injectLogContext: false,
53+
};
4954
const context = {
5055
invokedFunctionArn: "arn:aws:lambda:us-east-1:123456789101:function:my-lambda",
5156
awsRequestId: "1234",

src/trace/listener.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export interface TraceConfig {
2626
* @default true.
2727
*/
2828
autoPatchHTTP: boolean;
29+
/**
30+
* Whether to capture the lambda payload and response in Datadog.
31+
*/
32+
captureLambdaPayload: boolean;
2933
/**
3034
* Whether to automatically patch console.log with Datadog's tracing ids.
3135
*/

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { wrap, promisifiedHandler } from "./handler";
33
export { Timer } from "./timer";
44
export { logError, logDebug, Logger, setLogLevel, setLogger, LogLevel } from "./log";
55
export { get, post } from "./request";
6+
export { tagObject } from "./tag-object";

src/utils/tag-object.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { tagObject } from "./tag-object";
2+
3+
describe("tagObject", () => {
4+
const setTag = jest.fn();
5+
6+
beforeEach(() => {
7+
jest.clearAllMocks();
8+
});
9+
it("tags something simple", () => {
10+
const span = {
11+
setTag,
12+
};
13+
tagObject(span, "lambda_payload", { request: { myKey: "myValue" } });
14+
expect(setTag).toBeCalledWith("lambda_payload.request.myKey", "myValue");
15+
});
16+
it("tags complex objects", () => {
17+
const span = {
18+
setTag,
19+
};
20+
tagObject(span, "lambda_payload", {
21+
request: {
22+
keyOne: "foobar",
23+
myObject: {
24+
anotherKey: ["array", "of", "values"],
25+
},
26+
val: null,
27+
number: 1,
28+
},
29+
});
30+
expect(setTag.mock.calls).toEqual([
31+
["lambda_payload.request.keyOne", "foobar"],
32+
["lambda_payload.request.myObject.anotherKey.0", "array"],
33+
["lambda_payload.request.myObject.anotherKey.1", "of"],
34+
["lambda_payload.request.myObject.anotherKey.2", "values"],
35+
["lambda_payload.request.val", null],
36+
["lambda_payload.request.number", 1],
37+
]);
38+
});
39+
it("tags arrays of objects", () => {
40+
const span = {
41+
setTag,
42+
};
43+
tagObject(span, "lambda_payload", {
44+
request: {
45+
vals: [{ thingOne: 1 }, { thingTwo: 2 }],
46+
},
47+
});
48+
expect(setTag.mock.calls).toEqual([
49+
["lambda_payload.request.vals.0.thingOne", 1],
50+
["lambda_payload.request.vals.1.thingTwo", 2],
51+
]);
52+
});
53+
it("redacts common secret keys", () => {
54+
const span = {
55+
setTag,
56+
};
57+
tagObject(span, "lambda_payload", { request: { headers: { authorization: "myValue" } } });
58+
expect(setTag).toBeCalledWith("lambda_payload.request.headers.authorization", "redacted");
59+
});
60+
});

src/utils/tag-object.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const redactableKeys = ["authorization", "x-authorization", "password", "token"];
2+
const maxDepth = 10;
3+
4+
export function tagObject(currentSpan: any, key: string, obj: any, depth = 0): any {
5+
if (depth >= maxDepth) {
6+
return;
7+
} else {
8+
depth += 1;
9+
}
10+
if (obj === null) {
11+
return currentSpan.setTag(key, obj);
12+
}
13+
if (typeof obj === "string") {
14+
let parsed: string;
15+
try {
16+
parsed = JSON.parse(obj);
17+
} catch (e) {
18+
const redacted = redactVal(key, obj.substring(0, 5000));
19+
return currentSpan.setTag(key, redacted);
20+
}
21+
return tagObject(currentSpan, key, parsed, depth);
22+
}
23+
if (typeof obj === "number") {
24+
return currentSpan.setTag(key, obj);
25+
}
26+
if (typeof obj === "object") {
27+
for (const [k, v] of Object.entries(obj)) {
28+
tagObject(currentSpan, `${key}.${k}`, v, depth);
29+
}
30+
return;
31+
}
32+
}
33+
34+
function redactVal(k: string, v: string): string {
35+
const splitKey = k.split(".").pop() || k;
36+
if (redactableKeys.includes(splitKey)) {
37+
return "redacted";
38+
}
39+
return v;
40+
}

0 commit comments

Comments
 (0)