Skip to content

Optionally capture Lambda request and response payloads #222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Oct 1, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"printWidth": 120,
"trailingComma": "all",
"arrowParens": "always"
"arrowParens": "always",
"semi": true
}
14 changes: 13 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import {
setLogger,
promisifiedHandler,
logDebug,
tagObject,
} from "./utils";
export { TraceHeaders } from "./trace";
import { extractHTTPStatusCodeTag } from "./trace/trigger";

export const apiKeyEnvVar = "DD_API_KEY";
export const apiKeyKMSEnvVar = "DD_KMS_API_KEY";
export const capturePayloadEnvVar = "DD_CAPTURE_PAYLOAD";
export const siteURLEnvVar = "DD_SITE";
export const logLevelEnvVar = "DD_LOG_LEVEL";
export const logForwardingEnvVar = "DD_FLUSH_TO_LOG";
Expand Down Expand Up @@ -60,6 +62,7 @@ export const defaultConfig: Config = {
apiKey: "",
apiKeyKMS: "",
autoPatchHTTP: true,
capturePayload: false,
debugLogging: false,
enhancedMetrics: true,
forceWrap: false,
Expand Down Expand Up @@ -139,6 +142,10 @@ export function datadog<TEvent, TResult>(
// Store the status tag in the listener to send to Xray on invocation completion
traceListener.triggerTags["http.status_code"] = statusCode;
if (traceListener.currentSpan) {
if (finalConfig.capturePayload) {
tagObject(traceListener.currentSpan, "function.request", localEvent);
tagObject(traceListener.currentSpan, "function.response", localResult);
}
traceListener.currentSpan.setTag("http.status_code", statusCode);
}
}
Expand Down Expand Up @@ -176,7 +183,7 @@ export function datadog<TEvent, TResult>(
* Sends a Distribution metric asynchronously to the Datadog API.
* @param name The name of the metric to send.
* @param value The value of the metric
* @param metricTime The timesamp associated with this metric data point.
* @param metricTime The timestamp associated with this metric data point.
* @param tags The tags associated with the metric. Should be of the format "tag:value".
*/
export function sendDistributionMetricWithDate(name: string, value: number, metricTime: Date, ...tags: string[]) {
Expand Down Expand Up @@ -260,6 +267,11 @@ function getConfig(userConfig?: Partial<Config>): Config {
config.mergeDatadogXrayTraces = result === "true";
}

if (userConfig === undefined || userConfig.capturePayload === undefined) {
const result = getEnvValue(capturePayloadEnvVar, "false").toLocaleLowerCase();
config.capturePayload = result === "true";
}

return config;
}

Expand Down
7 changes: 6 additions & 1 deletion src/trace/listener.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ jest.mock("./trace-context-service", () => {
});

describe("TraceListener", () => {
const defaultConfig = { autoPatchHTTP: true, mergeDatadogXrayTraces: false, injectLogContext: false };
const defaultConfig = {
autoPatchHTTP: true,
capturePayload: false,
mergeDatadogXrayTraces: false,
injectLogContext: false,
};
const context = {
invokedFunctionArn: "arn:aws:lambda:us-east-1:123456789101:function:my-lambda",
awsRequestId: "1234",
Expand Down
4 changes: 4 additions & 0 deletions src/trace/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export interface TraceConfig {
* @default true.
*/
autoPatchHTTP: boolean;
/**
* Whether to capture the lambda payload and response in Datadog.
*/
capturePayload: boolean;
/**
* Whether to automatically patch console.log with Datadog's tracing ids.
*/
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { wrap, promisifiedHandler } from "./handler";
export { Timer } from "./timer";
export { logError, logDebug, Logger, setLogLevel, setLogger, LogLevel } from "./log";
export { get, post } from "./request";
export { tagObject } from "./tagObject";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we typically name files like this tag-object.ts

60 changes: 60 additions & 0 deletions src/utils/tagObject.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { tagObject } from "./tagObject";

describe("tagObject", () => {
const setTag = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});
it("tags something simple", () => {
const span = {
setTag,
};
tagObject(span, "lambda_payload", { request: { myKey: "myValue" } });
expect(setTag).toBeCalledWith("lambda_payload.request.myKey", "myValue");
});
it("tags complex objects", () => {
const span = {
setTag,
};
tagObject(span, "lambda_payload", {
request: {
keyOne: "foobar",
myObject: {
anotherKey: ["array", "of", "values"],
},
val: null,
number: 1,
},
});
expect(setTag.mock.calls).toEqual([
["lambda_payload.request.keyOne", "foobar"],
["lambda_payload.request.myObject.anotherKey.0", "array"],
["lambda_payload.request.myObject.anotherKey.1", "of"],
["lambda_payload.request.myObject.anotherKey.2", "values"],
["lambda_payload.request.val", null],
["lambda_payload.request.number", 1],
]);
});
it("tags arrays of objects", () => {
const span = {
setTag,
};
tagObject(span, "lambda_payload", {
request: {
vals: [{ thingOne: 1 }, { thingTwo: 2 }],
},
});
expect(setTag.mock.calls).toEqual([
["lambda_payload.request.vals.0.thingOne", 1],
["lambda_payload.request.vals.1.thingTwo", 2],
]);
});
it("redacts common secret keys", () => {
const span = {
setTag,
};
tagObject(span, "lambda_payload", { request: { headers: { authorization: "myValue" } } });
expect(setTag).toBeCalledWith("lambda_payload.request.headers.authorization", "redacted");
});
});
34 changes: 34 additions & 0 deletions src/utils/tagObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const redactableKeys = ["authorization", "x-authorization", "password", "token"];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could be customizable by the customer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good question - we actually already allow the customer to define obfuscation rules via datadog.yaml https://docs.datadoghq.com/tracing/setup_overview/configure_data_security/?tab=datadogyaml#replace-rules-for-tag-filtering

This is just a hardcoded list to prevent non-configured payload capture from indexing basic/common secrets. It's not something we want to automatically index. If a user wants to capture these, they'll have to manually set a tag.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I see, so maybe we should make a link/note about this config option when we'll release this new env var?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup! And it'll be in the blog post :)


export function tagObject(currentSpan: any, key: string, obj: any): any {
if (obj === null) {
return currentSpan.setTag(key, obj);
}
if (typeof obj === "string") {
let parsed: string;
try {
parsed = JSON.parse(obj);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a string, does it need to be parsed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. This is needed to deeply tag nested JSON into separate items, otherwise it just tags an object (ala: setTag('request.body', { foo: { blah: 'ahhh' }}), which doesn't display nicely in the app.

} catch (e) {
const redacted = redactVal(key, obj.substring(0, 5000));
return currentSpan.setTag(key, redacted);
}
return tagObject(currentSpan, key, parsed);
}
if (typeof obj === "number") {
return currentSpan.setTag(key, obj);
}
if (typeof obj === "object") {
for (const [k, v] of Object.entries(obj)) {
tagObject(currentSpan, `${key}.${k}`, v);
}
return;
}
}

function redactVal(k: string, v: string): string {
const splitKey = k.split(".").pop() || k;
if (redactableKeys.includes(splitKey)) {
return "redacted";
}
return v;
}