import fs from 'fs-extra' ; import _ from 'lodash' ; import path from 'path' ; import { LambdaPermissionBuilder , SubscriptionFilterBuilder , TemplateBuilder } from './utils' ; const iamLambdaTemplate = require ( '../templates/iam/lambda-role.json' ) ; const withXrayTracingPermissions = require ( '../templates/iam/withXrayTracingPermissions.js' ) ; class ServerlessEsLogsPlugin { public hooks : { [ name ] : ( ) => void ; } ; private provider : any ; private serverless : any ; private options : { [ name ] : any ; } ; private logProcesserDir : string = '_es-logs' ; private logProcesserName : string = 'esLogsProcesser' ; private logProcesserLogicalId : string ; private defaultLambdaFilterPattern : string = '[timestamp=*Z, request_id="*-*", event]' ; private defaultApiGWFilterPattern : string = '[event]' ; private defaultIndexDateSeparator : string = '.' ; constructor ( serverless , options ) { this . serverless = serverless ; this . provider = serverless . getProvider ( 'aws' ) ; this . options = options ; const normalizedName = this . provider . naming . getNormalizedFunctionName ( this . logProcesserName ) ; this . logProcesserLogicalId = `${ normalizedName }LambdaFunction` ; this . hooks = { 'after:package:initialize' : this . afterPackageInitialize . bind ( this ) , 'after:package:createDeploymentArtifacts' : this . afterPackageCreateDeploymentArtifacts . bind ( this ) , 'aws:package:finalize:mergeCustomProviderResources' : this . mergeCustomProviderResources . bind ( this ) , } ; } private custom ( ) { return this . serverless . service . custom || { } ; } private afterPackageCreateDeploymentArtifacts ( ) { this . serverless . cli . log ( 'ServerlessEsLogsPlugin.afterPackageCreateDeploymentArtifacts()' ) ; this . cleanupFiles ( ) ; } private afterPackageInitialize ( ) { this . serverless . cli . log ( 'ServerlessEsLogsPlugin.afterPackageInitialize()' ) ; this . formatCommandLineOpts ( ) ; this . validatePluginOptions ( ) ; this . addLogProcesser ( ) ; } private mergeCustomProviderResources ( ) { this . serverless . cli . log ( 'ServerlessEsLogsPlugin.mergeCustomProviderResources()' ) ; const { includeApiGWLogs , retentionInDays , useDefaultRole , xrayTracingPermissions } = this . custom ( ) . esLogs ; const template = this . serverless . service . provider . compiledCloudFormationTemplate ; this . addLambdaCloudwatchSubscriptions ( ) ; if ( retentionInDays !== undefined ) { this . configureLogRetention ( retentionInDays ) ; } if ( xrayTracingPermissions === true ) { const statement = iamLambdaTemplate . ServerlessEsLogsLambdaIAMRole . Properties . Policies [ 0 ] . PolicyDocument . Statement ; statement . push ( withXrayTracingPermissions ) ; } if ( this . serverless . service . provider . role && ! useDefaultRole ) { _ . merge ( template . Resources , iamLambdaTemplate ) ; this . patchLogProcesserRole ( ) ; } else if ( ! this . serverless . service . provider . role ) { const updatedPolicies = template . Resources . IamRoleLambdaExecution . Properties . Policies . concat ( iamLambdaTemplate . ServerlessEsLogsLambdaIAMRole . Properties . Policies ) ; template . Resources . IamRoleLambdaExecution . Properties . Policies = updatedPolicies ; } if ( includeApiGWLogs === true ) { this . addApiGwCloudwatchSubscription ( ) ; } } private formatCommandLineOpts ( ) { this . options . stage = this . options . stage || this . serverless . service . provider . stage || ( this . serverless . service . defaults && this . serverless . service . defaults . stage ) || 'dev' ; this . options . region = this . options . region || this . serverless . service . provider . region || ( this . serverless . service . defaults && this . serverless . service . defaults . region ) || 'us-east-1' ; } private validatePluginOptions ( ) { const { esLogs } = this . custom ( ) ; if ( ! esLogs ) { throw new this . serverless . classes . Error ( `ERROR: No configuration provided for serverless-es-logs!` ) ; } const { endpoint , index , indexDateSeparator , tags } = esLogs ; if ( ! endpoint ) { throw new this . serverless . classes . Error ( `ERROR: Must define an endpoint for serverless-es-logs!` ) ; } if ( ! index ) { throw new this . serverless . classes . Error ( `ERROR: Must define an index for serverless-es-logs!` ) ; } if ( tags && ! _ . isPlainObject ( tags ) ) { throw new this . serverless . classes . Error ( `ERROR: Tags must be an object! You provided '${ tags }'.` ) ; } if ( indexDateSeparator && ! _ . isString ( indexDateSeparator ) ) { throw new this . serverless . classes . Error ( `ERROR: indexDateSeparator must be a string! You provided '${ indexDateSeparator }'.` ) ; } } private addApiGwCloudwatchSubscription ( ) { const { esLogs } = this . custom ( ) ; const filterPattern = esLogs . apiGWFilterPattern || this . defaultApiGWFilterPattern ; const apiGwLogGroupLogicalId = 'ApiGatewayLogGroup' ; const template = this . serverless . service . provider . compiledCloudFormationTemplate ; if ( template && template . Resources [ apiGwLogGroupLogicalId ] ) { const { LogGroupName } = template . Resources [ apiGwLogGroupLogicalId ] . Properties ; const subscriptionLogicalId = `${ apiGwLogGroupLogicalId }SubscriptionFilter` ; const permissionLogicalId = `${ apiGwLogGroupLogicalId }CWPermission` ; const processorFunctionName = template . Resources [ this . logProcesserLogicalId ] . Properties . FunctionName ; const permission = new LambdaPermissionBuilder ( ) . withFunctionName ( processorFunctionName ) . withPrincipal ( { 'Fn::Sub' : 'logs.${AWS::Region}.amazonaws.com' , } ) . withSourceArn ( { 'Fn::Join' : [ '' , [ 'arn:aws:logs:' , { Ref : 'AWS::Region' , } , ':' , { Ref : 'AWS::AccountId' , } , ':log-group:' , LogGroupName , '*' , ] , ] , } ) . withDependsOn ( [ this . logProcesserLogicalId , apiGwLogGroupLogicalId ] ) . build ( ) ; const subscriptionFilter = new SubscriptionFilterBuilder ( ) . withDestinationArn ( { 'Fn::GetAtt' : [ this . logProcesserLogicalId , 'Arn' , ] , } ) . withFilterPattern ( filterPattern ) . withLogGroupName ( LogGroupName ) . withDependsOn ( [ this . logProcesserLogicalId , permissionLogicalId ] ) . build ( ) ; const subscriptionTemplate = new TemplateBuilder ( ) . withResource ( permissionLogicalId , permission ) . withResource ( subscriptionLogicalId , subscriptionFilter ) . build ( ) ; _ . merge ( template , subscriptionTemplate ) ; } } private addLambdaCloudwatchSubscriptions ( ) { const { esLogs } = this . custom ( ) ; const filterPattern = esLogs . filterPattern || this . defaultLambdaFilterPattern ; const template = this . serverless . service . provider . compiledCloudFormationTemplate ; const functions = this . serverless . service . getAllFunctions ( ) ; functions . forEach ( ( name ) => { if ( name === this . logProcesserName ) { return ; } const normalizedFunctionName = this . provider . naming . getNormalizedFunctionName ( name ) ; const subscriptionLogicalId = `${ normalizedFunctionName }SubscriptionFilter` ; const permissionLogicalId = `${ normalizedFunctionName }CWPermission` ; const logGroupLogicalId = `${ normalizedFunctionName }LogGroup` ; const logGroupName = template . Resources [ logGroupLogicalId ] . Properties . LogGroupName ; const permission = new LambdaPermissionBuilder ( ) . withFunctionName ( { 'Fn::GetAtt' : [ this . logProcesserLogicalId , 'Arn' , ] , } ) . withPrincipal ( { 'Fn::Sub' : 'logs.${AWS::Region}.amazonaws.com' , } ) . withSourceArn ( { 'Fn::GetAtt' : [ logGroupLogicalId , 'Arn' , ] , } ) . withDependsOn ( [ this . logProcesserLogicalId , logGroupLogicalId ] ) . build ( ) ; const subscriptionFilter = new SubscriptionFilterBuilder ( ) . withDestinationArn ( { 'Fn::GetAtt' : [ this . logProcesserLogicalId , 'Arn' , ] , } ) . withFilterPattern ( filterPattern ) . withLogGroupName ( logGroupName ) . withDependsOn ( [ this . logProcesserLogicalId , permissionLogicalId ] ) . build ( ) ; const subscriptionTemplate = new TemplateBuilder ( ) . withResource ( permissionLogicalId , permission ) . withResource ( subscriptionLogicalId , subscriptionFilter ) . build ( ) ; _ . merge ( template , subscriptionTemplate ) ; } ) ; } private configureLogRetention ( retentionInDays ) { const template = this . serverless . service . provider . compiledCloudFormationTemplate ; Object . keys ( template . Resources ) . forEach ( ( key ) => { if ( template . Resources [ key ] . Type === 'AWS::Logs::LogGroup' ) { template . Resources [ key ] . Properties . RetentionInDays = retentionInDays ; } } ) ; } private addLogProcesser ( ) { const { index , indexDateSeparator , endpoint , tags , vpc } = this . custom ( ) . esLogs ; const tagsStringified = tags ? JSON . stringify ( tags ) : '' ; const dirPath = path . join ( this . serverless . config . servicePath , this . logProcesserDir ) ; const filePath = path . join ( dirPath , 'index.js' ) ; const handler = `${ this . logProcesserDir }/index.handler` ; const name = `${ this . serverless . service . service }-${ this . options . stage }-es-logs-plugin` ; fs . ensureDirSync ( dirPath ) ; fs . copySync ( path . resolve ( __dirname , '../templates/code/logsToEs.js' ) , filePath ) ; this . serverless . service . functions [ this . logProcesserName ] = { description : 'Serverless ES Logs Plugin' , environment : { ES_ENDPOINT : endpoint , ES_INDEX_PREFIX : index , ES_INDEX_DATE_SEPARATOR : indexDateSeparator || this . defaultIndexDateSeparator , ES_TAGS : tagsStringified , } , vpc , events : [ ] , handler , memorySize : 512 , name , package : { exclude : [ '**' ] , include : [ `${ this . logProcesserDir }/**` ] , individually : true , } , runtime : 'nodejs12.x' , timeout : 60 , tracing : false , } ; } private patchLogProcesserRole ( ) { const template = this . serverless . service . provider . compiledCloudFormationTemplate ; template . Resources [ this . logProcesserLogicalId ] . DependsOn . push ( 'ServerlessEsLogsLambdaIAMRole' ) ; template . Resources [ this . logProcesserLogicalId ] . Properties . Role = { 'Fn::GetAtt' : [ 'ServerlessEsLogsLambdaIAMRole' , 'Arn' , ] , } ; } private cleanupFiles ( ) { const dirPath = path . join ( this . serverless . config . servicePath , this . logProcesserDir ) ; fs . removeSync ( dirPath ) ; } } export = ServerlessEsLogsPlugin ;