import * as Serverless from "serverless" ; import { FunctionDefinition } from "serverless" ; import { SimpleGit } from "simple-git" ; import { version } from "../package.json" ; import { Configuration , ddTagsEnvVar , forceExcludeDepsFromWebpack , getConfig , hasWebpackPlugin , setEnvConfiguration , } from "./env" ; import { addCloudWatchForwarderSubscriptions , addExecutionLogGroupsAndSubscriptions } from "./forwarder" ; import { newSimpleGit } from "./git" ; import { applyExtensionLayer , applyLambdaLibraryLayers , findHandlers , FunctionInfo , RuntimeType } from "./layer" ; import * as govLayers from "./layers-gov.json" ; import * as layers from "./layers.json" ; import { getCloudFormationStackId } from "./monitor-api-requests" ; import { setMonitors } from "./monitors" ; import { addOutputLinks , printOutputs } from "./output" ; import { SourceCodeIntegration } from "./source-code-integration" ; import { enableTracing , TracingMode } from "./tracing" ; import { redirectHandlers } from "./wrapper" ; export interface ExtendedFunctionDefinition extends FunctionDefinition { tags ? : { [ key ] : string ; } ; environment ? : { [ key ] : string ; } ; } enum TagKeys { Service = "service" , Env = "env" , Plugin = "dd_sls_plugin" } module . exports = class ServerlessPlugin { public hooks = { "after:datadog:clean:init" : this . afterPackageFunction . bind ( this ) , "after:datadog:generate:init" : this . beforePackageFunction . bind ( this ) , "after:deploy:function:packageFunction" : this . afterPackageFunction . bind ( this ) , "after:package:createDeploymentArtifacts" : this . afterPackageFunction . bind ( this ) , "after:package:initialize" : this . beforePackageFunction . bind ( this ) , "before:deploy:function:packageFunction" : this . beforePackageFunction . bind ( this ) , "before:offline:start:init" : this . beforePackageFunction . bind ( this ) , "before:step-functions-offline:start" : this . beforePackageFunction . bind ( this ) , "after:deploy:deploy" : this . afterDeploy . bind ( this ) , "before:package:finalize" : this . afterPackageFunction . bind ( this ) , } ; public commands = { datadog : { commands : { clean : { lifecycleEvents : [ "init" ] , usage : "Cleans up wrapper handler functions for DataDog, not necessary in most cases" , } , generate : { lifecycleEvents : [ "init" ] , usage : "Generates wrapper handler functions for DataDog, not necessary in most cases" , } , } , lifecycleEvents : [ "clean" , "generate" ] , usage : "Automatically instruments your lambdas with DataDog" , } , } ; constructor ( private serverless , _ ) { } private async beforePackageFunction ( ) { const config = getConfig ( this . serverless . service ) ; if ( config . enabled === false ) return ; this . serverless . cli . log ( "Auto instrumenting functions with Datadog" ) ; configHasOldProperties ( config ) ; if ( config . monitorsApiKey !== undefined || config . monitorsAppKey !== undefined ) { this . serverless . cli . log ( "Warning: `monitorsApiKey` and `monitorsAppKey` have been deprecated. Please set DATADOG_API_KEY and DATADOG_APP_KEY in your environment instead." ) ; } validateConfiguration ( config ) ; const defaultRuntime = this . serverless . service . provider . runtime ; const handlers = findHandlers ( this . serverless . service , config . exclude , defaultRuntime ) ; setEnvConfiguration ( config , handlers ) ; const allLayers = { regions : { ... layers . regions , ... govLayers . regions } } ; if ( config . addLayers ) { this . serverless . cli . log ( "Adding Lambda Library Layers to functions" ) ; this . debugLogHandlers ( handlers ) ; applyLambdaLibraryLayers ( this . serverless . service , handlers , allLayers ) ; if ( hasWebpackPlugin ( this . serverless . service ) ) { forceExcludeDepsFromWebpack ( this . serverless . service ) ; } } else { this . serverless . cli . log ( "Skipping adding Lambda Library Layers, make sure you are packaging them yourself" ) ; } if ( config . addExtension ) { this . serverless . cli . log ( "Adding Datadog Lambda Extension Layer to functions" ) ; this . debugLogHandlers ( handlers ) ; applyExtensionLayer ( this . serverless . service , handlers , allLayers ) ; } else { this . serverless . cli . log ( "Skipping adding Lambda Extension Layer" ) ; } let tracingMode = TracingMode . NONE ; if ( config . enableXrayTracing && config . enableDDTracing ) { tracingMode = TracingMode . HYBRID ; } else if ( config . enableDDTracing ) { tracingMode = TracingMode . DD_TRACE ; } else if ( config . enableXrayTracing ) { tracingMode = TracingMode . XRAY ; } enableTracing ( this . serverless . service , tracingMode , handlers ) ; } private async afterPackageFunction ( ) { const config = getConfig ( this . serverless . service ) ; if ( config . enabled === false ) return ; const forwarderConfigs = { AddExtension : config . addExtension , IntegrationTesting : config . integrationTesting , SubToAccessLogGroups : config . subscribeToAccessLogs , SubToExecutionLogGroups : config . subscribeToExecutionLogs , } ; const defaultRuntime = this . serverless . service . provider . runtime ; const handlers = findHandlers ( this . serverless . service , config . exclude , defaultRuntime ) ; let datadogForwarderArn ; datadogForwarderArn = setDatadogForwarder ( config ) ; this . serverless . cli . log ( "Setting Datadog Forwarder" ) ; if ( datadogForwarderArn ) { const aws = this . serverless . getProvider ( "aws" ) ; const errors = await addCloudWatchForwarderSubscriptions ( this . serverless . service , aws , datadogForwarderArn , forwarderConfigs , handlers ) ; if ( config . subscribeToExecutionLogs ) { await addExecutionLogGroupsAndSubscriptions ( this . serverless . service , aws , datadogForwarderArn ) ; } for ( const error of errors ) { this . serverless . cli . log ( error ) ; } } this . addTags ( handlers , config . enableTags ) ; const simpleGit = await newSimpleGit ( ) ; if ( config . enableSourceCodeIntegration && ( process . env . DATADOG_API_KEY ?? config . apiKey ) !== undefined && ( await simpleGit . checkIsRepo ( ) ) ) { try { await this . addSourceCodeIntegration ( handlers , simpleGit , ( process . env . DATADOG_API_KEY ?? config . apiKey ) ! , config . site ) ; } catch ( err ) { this . serverless . cli . log ( `Error occurred when adding source code integration: ${ err }` ) ; return ; } } redirectHandlers ( handlers , config . addLayers , config . customHandler ) ; if ( config . integrationTesting === false ) { await addOutputLinks ( this . serverless , config . site , handlers ) ; } else { this . serverless . cli . log ( "Skipped adding output links because 'integrationTesting' is set true" ) ; } } private async afterDeploy ( ) { const config = getConfig ( this . serverless . service ) ; const service = this . serverless . service . getServiceName ( ) ; const env = this . serverless . getProvider ( "aws" ) . getStage ( ) ; if ( config . enabled === false ) return ; if ( config . monitors && ( config . apiKey ?? process . env . DATADOG_API_KEY ) && ( config . appKey ?? process . env . DATADOG_APP_KEY ) ) { const cloudFormationStackId = await getCloudFormationStackId ( this . serverless ) ; try { const logStatements = await setMonitors ( config . site , config . monitors , ( config . apiKey ?? process . env . DATADOG_API_KEY ) ! , ( config . appKey ?? process . env . DATADOG_APP_KEY ) ! , cloudFormationStackId , service , env ) ; for ( const logStatement of logStatements ) { this . serverless . cli . log ( logStatement ) ; } } catch ( err ) { if ( err instanceof Error ) { this . serverless . cli . log ( `Error occurred when configuring monitors: ${ err . message }` ) ; } } } return printOutputs ( this . serverless , config . site ) ; } private debugLogHandlers ( handlers ) { for ( const handler of handlers ) { if ( handler . type === RuntimeType . UNSUPPORTED ) { if ( handler . runtime === undefined ) { this . serverless . cli . log ( `Unable to determine runtime for function ${ handler . name }` ) ; } else { this . serverless . cli . log ( `Unable to add Lambda Layers to function ${ handler . name } with runtime ${ handler . runtime }` ) ; } } } } /**
     * Check for service and env tags on provider level (under tags and stackTags),
     * as well as function level. Automatically create tags for service and env with
     * properties from deployment configurations if needed; does not override any existing values.
     */ private addTags ( handlers , enableTags ) { const provider = this . serverless . service . provider as any ; this . serverless . cli . log ( `Adding Plugin Version ${ version } tag` ) ; if ( enableTags ) { this . serverless . cli . log ( `Adding service and environment tags` ) ; } handlers . forEach ( ( { handler } ) => { handler . tags ??= { } ; handler . tags [ TagKeys . Plugin ] = `v${ version }` ; if ( enableTags ) { if ( ! provider . tags ?. [ TagKeys . Service ] && ! provider . stackTags ?. [ TagKeys . Service ] ) { handler . tags [ TagKeys . Service ] ??= this . serverless . service . getServiceName ( ) ; } if ( ! provider . tags ?. [ TagKeys . Env ] && ! provider . stackTags ?. [ TagKeys . Env ] ) { handler . tags [ TagKeys . Env ] ??= this . serverless . getProvider ( "aws" ) . getStage ( ) ; } } } ) ; } /**
     * Uploads git metadata for the current directory to Datadog and goes through
     * each function defined in serverless and attaches the git.commit.sha to DD_TAGS.
     */ private async addSourceCodeIntegration ( handlers , simpleGit , apiKey , datadogSite ) { const sourceCodeIntegration = new SourceCodeIntegration ( apiKey , datadogSite , simpleGit ) ; const gitCommitHash = await sourceCodeIntegration . uploadGitMetadata ( ) ; handlers . forEach ( ( { handler } ) => { handler . environment ??= { } ; handler . environment [ ddTagsEnvVar ] = "git.commit.sha:" + gitCommitHash ; } ) ; } } ; function configHasOldProperties ( obj ) { let hasOldProperties = false ; let message = "The following configuration options have been removed:" ; if ( obj . subscribeToApiGatewayLogs ) { message += " subscribeToApiGatewayLogs" ; hasOldProperties = true ; } if ( obj . subscribeToHttpApiLogs ) { message += " subscribeToHttpApiLogs" ; hasOldProperties = true ; } if ( obj . subscribeToWebsocketLogs ) { message += " subscribeToWebsocketLogs" ; hasOldProperties = true ; } if ( hasOldProperties ) { throw new Error ( message + ". Please use the subscribeToAccessLogs or subscribeToExecutionLogs options instead." ) ; } } function validateConfiguration ( config ) { checkForMultipleApiKeys ( config ) ; const siteList = [ "datadoghq.com" , "datadoghq.eu" , "us3.datadoghq.com" , "us5.datadoghq.com" , "ddog-gov.com" , ] ; if ( config . site !== undefined && ! siteList . includes ( config . site . toLowerCase ( ) ) ) { throw new Error ( "Warning: Invalid site URL. Must be either datadoghq.com, datadoghq.eu, us3.datadoghq.com, us5.datadoghq.com, or ddog-gov.com." ) ; } if ( config . addExtension ) { if ( config . apiKey === undefined && process . env . DATADOG_API_KEY === undefined && config . apiKMSKey === undefined && config . apiKeySecretArn === undefined ) { throw new Error ( "When `addExtension` is true, the environment variable `DATADOG_API_KEY` or configuration variable `apiKMSKey` or `apiKeySecretArn` must be set." ) ; } } if ( config . monitors ) { if ( ( process . env . DATADOG_API_KEY === undefined || process . env . DATADOG_APP_KEY === undefined ) && ( config . apiKey === undefined || config . appKey === undefined ) ) { throw new Error ( "When `monitors` is enabled, `DATADOG_API_KEY` and `DATADOG_APP_KEY` environment variables must be set." ) ; } } } function checkForMultipleApiKeys ( config ) { let multipleApiKeysMessage ; if ( config . apiKey !== undefined && config . apiKMSKey !== undefined && config . apiKeySecretArn !== undefined ) { multipleApiKeysMessage = "`apiKey`, `apiKMSKey`, and `apiKeySecretArn`" ; } else if ( config . apiKey !== undefined && config . apiKMSKey !== undefined ) { multipleApiKeysMessage = "`apiKey` and `apiKMSKey`" ; } else if ( config . apiKey !== undefined && config . apiKeySecretArn !== undefined ) { multipleApiKeysMessage = "`apiKey` and `apiKeySecretArn`" ; } else if ( config . apiKMSKey !== undefined && config . apiKeySecretArn !== undefined ) { multipleApiKeysMessage = "`apiKMSKey` and `apiKeySecretArn`" ; } if ( multipleApiKeysMessage ) { throw new Error ( `${ multipleApiKeysMessage } should not be set at the same time.` ) ; } } function setDatadogForwarder ( config ) { const forwarderArn = config . forwarderArn ; const forwarder = config . forwarder ; if ( forwarderArn && forwarder ) { throw new Error ( "Both 'forwarderArn' and 'forwarder' parameters are set. Please only use the 'forwarderArn' parameter." ) ; } else if ( forwarderArn !== undefined && forwarder === undefined ) { return forwarderArn ; } else if ( forwarder !== undefined && forwarderArn === undefined ) { return forwarder ; } }