From 92a5421be5509d104ae05a0043c38d09d3bd7562 Mon Sep 17 00:00:00 2001 From: Yiming Luo Date: Tue, 7 Jan 2025 17:34:03 -0500 Subject: [PATCH] [Step Function] 3.2 Extract class ConfigLoader and LambdaConfigLoader --- serverless/src/index.ts | 4 +- serverless/src/lambda/env.ts | 306 +++++++++++++++------------- serverless/test/lambda/env.spec.ts | 22 +- serverless/test/lambda/tags.spec.ts | 13 +- 4 files changed, 181 insertions(+), 164 deletions(-) diff --git a/serverless/src/index.ts b/serverless/src/index.ts index ccb45a7..4d69939 100644 --- a/serverless/src/index.ts +++ b/serverless/src/index.ts @@ -1,4 +1,4 @@ -import { validateParameters as validateLambdaParameters, getConfig as getLambdaConfig } from "./lambda/env"; +import { validateParameters as validateLambdaParameters, LambdaConfigLoader } from "./lambda/env"; import { instrumentLambdas } from "./lambda/lambda"; import { InputEvent, OutputEvent, SUCCESS, FAILURE } from "./types"; import { instrumentStateMachines } from "./step_function/step_function"; @@ -11,7 +11,7 @@ export const handler = async (event: InputEvent, _: any): Promise = const fragment = event.fragment; - const lambdaConfig = getLambdaConfig(event); + const lambdaConfig = new LambdaConfigLoader().getConfig(event); const errors = validateLambdaParameters(lambdaConfig); if (errors.length > 0) { return { diff --git a/serverless/src/lambda/env.ts b/serverless/src/lambda/env.ts index f44cc13..25aed98 100644 --- a/serverless/src/lambda/env.ts +++ b/serverless/src/lambda/env.ts @@ -86,6 +86,168 @@ export interface Configuration { apmFlushDeadline?: string; } +abstract class ConfigLoader { + abstract readonly defaultConfiguration: TConfig; + /** + * Returns the default configuration with any values overwritten by environment variables. + */ + abstract getConfigFromEnvVars(): TConfig; + + /** + * Returns the configuration. + * If DatadogServerless transform params are set, then the priority order is: + * 1. CloudFormation Macro params + * 2. Environment variables + * 3. Default configuration + * Otherwise, if CloudFormation Mappings for Datadog are set, then the priority order is: + * 1. CloudFormation Mappings params + * 2. Environment variables + * 3. Default configuration + * Otherwise, the priority order is: + * 1. Environment variables + * 2. Default configuration + */ + public getConfig(event: InputEvent): TConfig { + let config: TConfig; + // Use the parameters given for this specific transform/macro if it exists + const transformParams = event.params ?? {}; + if (Object.keys(transformParams).length > 0) { + log.debug("Parsing config from CloudFormation transform/macro parameters"); + config = this.getConfigFromCfnParams(transformParams); + } else { + // If not, check the Mappings section for Datadog config parameters as well + log.debug("Parsing config from CloudFormation template mappings"); + config = this.getConfigFromCfnMappings(event.fragment.Mappings); + } + return config; + } + + /** + * Parses the Mappings section for Datadog config parameters. + * Assumes that the parameters live under the Mappings section in this format: + * + * Mappings: + * Datadog: + * Parameters: + * addLayers: true + * ... + */ + public getConfigFromCfnMappings(mappings: any): TConfig { + if (mappings === undefined || mappings[DATADOG] === undefined) { + log.debug("No Datadog mappings found in the CloudFormation template, using the default config"); + return this.getConfigFromEnvVars(); + } + return this.getConfigFromCfnParams(mappings[DATADOG][PARAMETERS]); + } + + /** + * Takes a set of parameters from the CloudFormation template. This could come from either + * the Mappings section of the template, or directly from the Parameters under the transform/macro + * as the 'params' property under the original InputEvent to the handler in src/index.ts + * + * Uses these parameters as the Datadog configuration, and for values that are required in the + * configuration but not provided in the parameters, uses the default values from + * the defaultConfiguration above. + */ + public getConfigFromCfnParams(params: CfnParams) { + let datadogConfig = params as Partial | undefined; + if (datadogConfig === undefined) { + log.debug("No Datadog config found, using the default config"); + datadogConfig = {}; + } + return { + ...this.getConfigFromEnvVars(), + ...datadogConfig, + }; + } +} + +export class LambdaConfigLoader extends ConfigLoader { + readonly defaultConfiguration: Configuration = { + addLayers: true, + addExtension: false, + exclude: [], + flushMetricsToLogs: true, + logLevel: undefined, + site: "datadoghq.com", + enableXrayTracing: false, + enableDDTracing: true, + enableDDLogs: true, + enableEnhancedMetrics: true, + captureLambdaPayload: false, + }; + + public getConfigFromEnvVars(): Configuration { + const config: Configuration = { + ...this.defaultConfiguration, + }; + + if (apiKeyEnvVar in process.env) { + config.apiKey = process.env[apiKeyEnvVar]; + } + if (apiKeySecretArnEnvVar in process.env) { + config.apiKeySecretArn = process.env[apiKeySecretArnEnvVar]; + } + if (apiKeyKMSEnvVar in process.env) { + config.apiKMSKey = process.env[apiKeyKMSEnvVar]; + } + if (siteURLEnvVar in process.env && process.env[siteURLEnvVar] !== undefined) { + // Fall back to default site for type safety + config.site = process.env[siteURLEnvVar] ?? this.defaultConfiguration.site; + } + if (logLevelEnvVar in process.env) { + config.logLevel = process.env[logLevelEnvVar]; + } + if (logForwardingEnvVar in process.env) { + config.flushMetricsToLogs = process.env[logForwardingEnvVar] === "true"; + } + if (enhancedMetricsEnvVar in process.env) { + config.enableEnhancedMetrics = process.env[enhancedMetricsEnvVar] === "true"; + } + if (enableDDLogsEnvVar in process.env) { + config.enableDDLogs = process.env[enableDDLogsEnvVar] === "true"; + } + if (captureLambdaPayloadEnvVar in process.env) { + config.captureLambdaPayload = process.env[captureLambdaPayloadEnvVar] === "true"; + } + if (serviceEnvVar in process.env) { + config.service = process.env[serviceEnvVar]; + } + if (envEnvVar in process.env) { + config.env = process.env[envEnvVar]; + } + if (versionEnvVar in process.env) { + config.version = process.env[versionEnvVar]; + } + if (tagsEnvVar in process.env) { + config.tags = process.env[tagsEnvVar]; + } + if (ddColdStartTracingEnabledEnvVar in process.env) { + config.enableColdStartTracing = process.env[ddColdStartTracingEnabledEnvVar] === "true"; + } + if (ddMinColdStartDurationEnvVar in process.env) { + config.minColdStartTraceDuration = process.env[ddMinColdStartDurationEnvVar]; + } + if (ddColdStartTracingSkipLibsEnvVar in process.env) { + config.coldStartTraceSkipLibs = process.env[ddColdStartTracingSkipLibsEnvVar]; + } + if (ddProfilingEnabledEnvVar in process.env) { + config.enableProfiling = process.env[ddProfilingEnabledEnvVar] === "true"; + } + if (ddEncodeAuthorizerContextEnvVar in process.env) { + config.encodeAuthorizerContext = process.env[ddEncodeAuthorizerContextEnvVar] === "true"; + } + if (ddDecodeAuthorizerContextEnvVar in process.env) { + config.decodeAuthorizerContext = process.env[ddDecodeAuthorizerContextEnvVar] === "true"; + } + if (ddApmFlushDeadlineMillisecondsEnvVar in process.env) { + config.apmFlushDeadline = process.env[ddApmFlushDeadlineMillisecondsEnvVar]; + } + + return config; + } +} + // Same interface as Configuration above, except all parameters are optional, since user does // not have to provide the values (in which case we will use the default configuration below). interface CfnParams extends Partial {} @@ -113,150 +275,6 @@ const ddEncodeAuthorizerContextEnvVar = "DD_ENCODE_AUTHORIZER_CONTEXT"; const ddDecodeAuthorizerContextEnvVar = "DD_DECODE_AUTHORIZER_CONTEXT"; const ddApmFlushDeadlineMillisecondsEnvVar = "DD_APM_FLUSH_DEADLINE_MILLISECONDS"; -export const defaultConfiguration: Configuration = { - addLayers: true, - addExtension: false, - exclude: [], - flushMetricsToLogs: true, - logLevel: undefined, - site: "datadoghq.com", - enableXrayTracing: false, - enableDDTracing: true, - enableDDLogs: true, - enableEnhancedMetrics: true, - captureLambdaPayload: false, -}; - -/** - * Returns the default configuration with any values overwritten by environment variables. - */ -export function getConfig(event: InputEvent): Configuration { - let config: Configuration; - // Use the parameters given for this specific transform/macro if it exists - const transformParams = event.params ?? {}; - if (Object.keys(transformParams).length > 0) { - log.debug("Parsing config from CloudFormation transform/macro parameters"); - config = getConfigFromCfnParams(transformParams); - } else { - // If not, check the Mappings section for Datadog config parameters as well - log.debug("Parsing config from CloudFormation template mappings"); - config = getConfigFromCfnMappings(event.fragment.Mappings); - } - return config; -} - -/** - * Returns the default configuration with any values overwritten by environment variables. - */ -export function getConfigFromEnvVars(): Configuration { - const config: Configuration = { - ...defaultConfiguration, - }; - - if (apiKeyEnvVar in process.env) { - config.apiKey = process.env[apiKeyEnvVar]; - } - if (apiKeySecretArnEnvVar in process.env) { - config.apiKeySecretArn = process.env[apiKeySecretArnEnvVar]; - } - if (apiKeyKMSEnvVar in process.env) { - config.apiKMSKey = process.env[apiKeyKMSEnvVar]; - } - if (siteURLEnvVar in process.env && process.env[siteURLEnvVar] !== undefined) { - // Fall back to default site for type safety - config.site = process.env[siteURLEnvVar] ?? defaultConfiguration.site; - } - if (logLevelEnvVar in process.env) { - config.logLevel = process.env[logLevelEnvVar]; - } - if (logForwardingEnvVar in process.env) { - config.flushMetricsToLogs = process.env[logForwardingEnvVar] === "true"; - } - if (enhancedMetricsEnvVar in process.env) { - config.enableEnhancedMetrics = process.env[enhancedMetricsEnvVar] === "true"; - } - if (enableDDLogsEnvVar in process.env) { - config.enableDDLogs = process.env[enableDDLogsEnvVar] === "true"; - } - if (captureLambdaPayloadEnvVar in process.env) { - config.captureLambdaPayload = process.env[captureLambdaPayloadEnvVar] === "true"; - } - if (serviceEnvVar in process.env) { - config.service = process.env[serviceEnvVar]; - } - if (envEnvVar in process.env) { - config.env = process.env[envEnvVar]; - } - if (versionEnvVar in process.env) { - config.version = process.env[versionEnvVar]; - } - if (tagsEnvVar in process.env) { - config.tags = process.env[tagsEnvVar]; - } - if (ddColdStartTracingEnabledEnvVar in process.env) { - config.enableColdStartTracing = process.env[ddColdStartTracingEnabledEnvVar] === "true"; - } - if (ddMinColdStartDurationEnvVar in process.env) { - config.minColdStartTraceDuration = process.env[ddMinColdStartDurationEnvVar]; - } - if (ddColdStartTracingSkipLibsEnvVar in process.env) { - config.coldStartTraceSkipLibs = process.env[ddColdStartTracingSkipLibsEnvVar]; - } - if (ddProfilingEnabledEnvVar in process.env) { - config.enableProfiling = process.env[ddProfilingEnabledEnvVar] === "true"; - } - if (ddEncodeAuthorizerContextEnvVar in process.env) { - config.encodeAuthorizerContext = process.env[ddEncodeAuthorizerContextEnvVar] === "true"; - } - if (ddDecodeAuthorizerContextEnvVar in process.env) { - config.decodeAuthorizerContext = process.env[ddDecodeAuthorizerContextEnvVar] === "true"; - } - if (ddApmFlushDeadlineMillisecondsEnvVar in process.env) { - config.apmFlushDeadline = process.env[ddApmFlushDeadlineMillisecondsEnvVar]; - } - - return config; -} - -/** - * Parses the Mappings section for Datadog config parameters. - * Assumes that the parameters live under the Mappings section in this format: - * - * Mappings: - * Datadog: - * Parameters: - * addLayers: true - * ... - */ -export function getConfigFromCfnMappings(mappings: any): Configuration { - if (mappings === undefined || mappings[DATADOG] === undefined) { - log.debug("No Datadog mappings found in the CloudFormation template, using the default config"); - return getConfigFromEnvVars(); - } - return getConfigFromCfnParams(mappings[DATADOG][PARAMETERS]); -} - -/** - * Takes a set of parameters from the CloudFormation template. This could come from either - * the Mappings section of the template, or directly from the Parameters under the transform/macro - * as the 'params' property under the original InputEvent to the handler in src/index.ts - * - * Uses these parameters as the Datadog configuration, and for values that are required in the - * configuration but not provided in the parameters, uses the default values from - * the defaultConfiguration above. - */ -export function getConfigFromCfnParams(params: CfnParams) { - let datadogConfig = params as Partial | undefined; - if (datadogConfig === undefined) { - log.debug("No Datadog config found, using the default config"); - datadogConfig = {}; - } - return { - ...getConfigFromEnvVars(), - ...datadogConfig, - }; -} - export function validateParameters(config: Configuration): string[] { log.debug("Validating parameters..."); const errors: string[] = []; diff --git a/serverless/test/lambda/env.spec.ts b/serverless/test/lambda/env.spec.ts index 6a16864..93a4881 100644 --- a/serverless/test/lambda/env.spec.ts +++ b/serverless/test/lambda/env.spec.ts @@ -1,14 +1,12 @@ import { - getConfigFromEnvVars, - getConfigFromCfnMappings, - getConfigFromCfnParams, - defaultConfiguration, + LambdaConfigLoader, setEnvConfiguration, validateParameters, checkForMultipleApiKeys, } from "../../src/lambda/env"; import { ArchitectureType, LambdaFunction, RuntimeType } from "../../src/lambda/layer"; +const loader = new LambdaConfigLoader(); describe("getConfig", () => { it("correctly parses parameters from Mappings", () => { const params = { @@ -16,13 +14,13 @@ describe("getConfig", () => { logLevel: "error", }; const mappings = { Datadog: { Parameters: params } }; - const config = getConfigFromCfnMappings(mappings); + const config = loader.getConfigFromCfnMappings(mappings); expect(config).toMatchObject(params); }); it("gets default configuration when no parameters are specified", () => { - const config = getConfigFromCfnParams({}); - expect(config).toEqual(expect.objectContaining(defaultConfiguration)); + const config = loader.getConfigFromCfnParams({}); + expect(config).toEqual(expect.objectContaining(loader.defaultConfiguration)); }); it("gets a mixed a configuration when some values are present", () => { @@ -30,7 +28,7 @@ describe("getConfig", () => { site: "my-site", enableXrayTracing: false, }; - const config = getConfigFromCfnParams(params); + const config = loader.getConfigFromCfnParams(params); expect(config).toEqual( expect.objectContaining({ addLayers: true, @@ -63,7 +61,7 @@ describe("getConfig", () => { process.env["DD_API_KEY_SECRET_ARN"] = "arn:aws:secretsmanager:my-region-1:123456789012:secret:DdApiKeySecret-abcd1234"; process.env["DD_FLUSH_TO_LOG"] = "false"; - const config = getConfigFromEnvVars(); + const config = loader.getConfigFromEnvVars(); expect(config).toEqual( expect.objectContaining({ addLayers: true, @@ -94,7 +92,7 @@ describe("getConfig", () => { enableEnhancedMetrics: true, captureLambdaPayload: false, }; - const config = getConfigFromCfnParams(params); + const config = loader.getConfigFromCfnParams(params); expect(config).toEqual( expect.objectContaining({ addLayers: true, @@ -138,7 +136,7 @@ describe("setEnvConfiguration", () => { version: "1", tags: "team:avengers,project:marvel", }; - setEnvConfiguration({ ...defaultConfiguration, ...config }, [lambda]); + setEnvConfiguration({ ...loader.defaultConfiguration, ...config }, [lambda]); expect(lambda.properties.Environment).toEqual({ Variables: { @@ -178,7 +176,7 @@ describe("setEnvConfiguration", () => { version: "1", tags: "team:avengers,project:marvel", }; - setEnvConfiguration({ ...defaultConfiguration, ...config }, [lambda]); + setEnvConfiguration({ ...loader.defaultConfiguration, ...config }, [lambda]); expect(lambda.properties.Environment).toEqual({ Variables: { diff --git a/serverless/test/lambda/tags.spec.ts b/serverless/test/lambda/tags.spec.ts index 7cc1a48..988f836 100644 --- a/serverless/test/lambda/tags.spec.ts +++ b/serverless/test/lambda/tags.spec.ts @@ -1,4 +1,4 @@ -import { defaultConfiguration, getConfigFromEnvVars } from "../../src/lambda/env"; +import { LambdaConfigLoader } from "../../src/lambda/env"; import { RuntimeType, LambdaFunction } from "../../src/lambda/layer"; import { addDDTags, addMacroTag, addCDKTag, addSAMTag } from "../../src/lambda/tags"; @@ -16,10 +16,11 @@ function mockLambdaFunction(tags: { Key: string; Value: string }[]) { } as LambdaFunction; } +const configLoader = new LambdaConfigLoader(); describe("addDDTags", () => { it("does not override existing tags on function", () => { const config = { - ...defaultConfiguration, + ...configLoader.defaultConfiguration, service: "my-other-service", env: "test", version: "1", @@ -39,7 +40,7 @@ describe("addDDTags", () => { it("does not add tags if provided config doesn't have tags", () => { const lambda = mockLambdaFunction([]); - addDDTags([lambda], defaultConfiguration); + addDDTags([lambda], configLoader.defaultConfiguration); expect(lambda.properties.Tags).toEqual([]); }); @@ -59,7 +60,7 @@ describe("addDDTags", () => { it("does add tags from the environment", () => { process.env["DD_TAGS"] = "strongest_avenger:hulk"; const config = { - ...getConfigFromEnvVars(), + ...configLoader.getConfigFromEnvVars(), service: "my-service", }; const lambda = mockLambdaFunction([]); @@ -74,7 +75,7 @@ describe("addDDTags", () => { it("creates tags property if needed", () => { const config = { - ...defaultConfiguration, + ...configLoader.defaultConfiguration, service: "my-service", env: "test", version: "1", @@ -94,7 +95,7 @@ describe("addDDTags", () => { it("adds to existing tags property if needed", () => { const config = { - ...defaultConfiguration, + ...configLoader.defaultConfiguration, service: "my-service", version: "1", tags: "team:avengers",