diff --git a/docs/guides/functions.md b/docs/guides/functions.md index 230cb0a36..e02a3923f 100644 --- a/docs/guides/functions.md +++ b/docs/guides/functions.md @@ -873,6 +873,33 @@ functions: ephemeralStorageSize: 1024 ``` +## Logging Configuration + +[Configuring Lambda advanced logging options](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced) + +This can be configured at the provider level (applies to all functions) or individually per function: + +```yml +# Provider-level configuration (applies to all functions) +provider: + logs: + lambda: + logFormat: JSON + applicationLogLevel: INFO + systemLogLevel: WARN + logGroup: /aws/lambda/global-log-group + +# Function-level configuration (overrides provider settings) +functions: + helloLogging: + handler: handler.handler + logs: + applicationLogLevel: DEBUG + logFormat: JSON + logGroup: helloLoggingLogGroup + systemLogLevel: DEBUG +``` + ## Lambda Hashing Algorithm migration **Note** Below migration guide is intended to be used if you are already using `v3` version of the Framework and you have `provider.lambdaHashingVersion` property set to `20200924` in your configuration file. If you are still on v2 and want to upgrade to v3, please refer to [V3 Upgrade docs](../../../guides/upgrading-v3.md#lambda-hashing-algorithm). diff --git a/docs/guides/serverless.yml.md b/docs/guides/serverless.yml.md index a5eb5e1ad..a4b9f01b3 100644 --- a/docs/guides/serverless.yml.md +++ b/docs/guides/serverless.yml.md @@ -517,6 +517,17 @@ Configure logs for the deployed resources: ```yml provider: logs: + # Optional Configuration of Lambda Logging Configuration + lambda: + # The Log Format to be used for all lambda functions (default: Text) + logFormat: JSON + # The Application Log Level to be used, This can only be set if `logFormat` is set to `JSON` + applicationLogLevel: ERROR + # The System Log Level to be used, This can only be set if `logFormat` is set to `JSON` + systemLogLevel: INFO + # The LogGroup that will be used by default. If this is set the Framework will not create LogGroups for any functions + logGroup: /aws/lambda/global-log-group + # Enable HTTP API logs # This can either be set to `httpApi: true` to use defaults, or configured via subproperties # Can only be configured if the API is created by Serverless Framework @@ -722,6 +733,12 @@ functions: maximumRetryAttempts: 1 # Maximum event age in seconds when invoking asynchronously (between 60 and 21600) maximumEventAge: 7200 + # Configuring Lambda advanced logging options + logs: + applicationLogLevel: DEBUG + logFormat: JSON + logGroup: helloLoggingLogGroup + systemLogLevel: DEBUG ``` ## Lambda events diff --git a/lib/plugins/aws/package/compile/functions.js b/lib/plugins/aws/package/compile/functions.js index 33b9fc586..e5c498749 100644 --- a/lib/plugins/aws/package/compile/functions.js +++ b/lib/plugins/aws/package/compile/functions.js @@ -632,6 +632,34 @@ class AwsCompileFunctions { } } + const logs = + functionObject.logs || + (this.serverless.service.provider.logs && this.serverless.service.provider.logs.lambda); + + if (logs) { + const loggingConfig = {}; + + if (logs.applicationLogLevel) { + loggingConfig.ApplicationLogLevel = logs.applicationLogLevel; + } + + if (logs.logFormat) { + loggingConfig.LogFormat = logs.logFormat; + } + + if (logs.logGroup) { + loggingConfig.LogGroup = logs.logGroup; + } + + if (logs.systemLogLevel) { + loggingConfig.SystemLogLevel = logs.systemLogLevel; + } + + if (Object.keys(loggingConfig).length) { + functionResource.Properties.LoggingConfig = loggingConfig; + } + } + this.compileFunctionUrl(functionName); this.compileFunctionEventInvokeConfig(functionName); } diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index b8c3e298c..1088ffe42 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1224,6 +1224,19 @@ class AwsProvider { }, ], }, + lambda: { + type: 'object', + properties: { + applicationLogLevel: { + type: 'string', + enum: ['DEBUG', 'ERROR', 'FATAL', 'INFO', 'TRACE', 'WARN'], + }, + logFormat: { type: 'string', enum: ['JSON', 'Text'] }, + logGroup: { type: 'string', pattern: '^[.\\-_/#A-Za-z0-9]{1,512}$' }, + systemLogLevel: { type: 'string', enum: ['DEBUG', 'INFO', 'WARN'] }, + }, + additionalProperties: false, + }, }, }, memorySize: { $ref: '#/definitions/awsLambdaMemorySize' }, @@ -1394,6 +1407,19 @@ class AwsProvider { disableLogs: { type: 'boolean' }, environment: { $ref: '#/definitions/awsLambdaEnvironment' }, ephemeralStorageSize: { type: 'integer', minimum: 512, maximum: 10240 }, + logs: { + type: 'object', + properties: { + applicationLogLevel: { + type: 'string', + enum: ['DEBUG', 'ERROR', 'FATAL', 'INFO', 'TRACE', 'WARN'], + }, + logFormat: { type: 'string', enum: ['JSON', 'Text'] }, + logGroup: { type: 'string', pattern: '^[.\\-_/#A-Za-z0-9]{1,512}$' }, + systemLogLevel: { type: 'string', enum: ['DEBUG', 'INFO', 'WARN'] }, + }, + additionalProperties: false, + }, fileSystemConfig: { type: 'object', properties: { diff --git a/test/unit/lib/plugins/aws/package/compile/functions.test.js b/test/unit/lib/plugins/aws/package/compile/functions.test.js index 8a34d194f..c00cdb5e2 100644 --- a/test/unit/lib/plugins/aws/package/compile/functions.test.js +++ b/test/unit/lib/plugins/aws/package/compile/functions.test.js @@ -1709,6 +1709,15 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { handler: 'index.handler', ephemeralStorageSize: 1024, }, + fnLogs: { + handler: 'target.handler', + logs: { + applicationLogLevel: 'DEBUG', + logFormat: 'JSON', + logGroup: 'helloLoggingLogGroup', + systemLogLevel: 'DEBUG', + }, + }, fnUrl: { handler: 'target.handler', url: true, @@ -2263,6 +2272,374 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { ).to.deep.equal({ Size: ephemeralStorageSize }); }); + it('should support `functions[].logs`', () => { + const logs = serviceConfig.functions.fnLogs.logs; + + expect(logs.applicationLogLevel).to.be.a('string'); + expect(logs.logFormat).to.be.a('string'); + expect(logs.logGroup).to.be.a('string'); + expect(logs.systemLogLevel).to.be.a('string'); + + expect( + cfResources[naming.getLambdaLogicalId('fnLogs')].Properties.LoggingConfig + ).to.deep.equal({ + ApplicationLogLevel: logs.applicationLogLevel, + LogFormat: logs.logFormat, + LogGroup: logs.logGroup, + SystemLogLevel: logs.systemLogLevel, + }); + }); + + it('should accept `DEBUG` as a valid applicationLogLevel value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + applicationLogLevel: 'DEBUG', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should accept `ERROR` as a valid applicationLogLevel value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + applicationLogLevel: 'ERROR', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should accept `FATAL` as a valid applicationLogLevel value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + applicationLogLevel: 'FATAL', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should accept `INFO` as a valid applicationLogLevel value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + applicationLogLevel: 'INFO', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should accept `TRACE` as a valid applicationLogLevel value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + applicationLogLevel: 'TRACE', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should accept `WARN` as a valid applicationLogLevel value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + applicationLogLevel: 'WARN', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should reject invalid applicationLogLevel value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + applicationLogLevel: 'INVALID', + }, + }, + }, + }, + command: 'package', + }).catch((error) => { + expect(error).to.have.property('code', 'INVALID_NON_SCHEMA_COMPLIANT_CONFIGURATION'); + }); + }); + + it('should accept `JSON` as a valid logFormat value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + logFormat: 'JSON', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should accept `Text` as a valid logFormat value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + logFormat: 'Text', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should reject invalid logFormat value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + logFormat: 'INVALID', + }, + }, + }, + }, + command: 'package', + }).catch((error) => { + expect(error).to.have.property('code', 'INVALID_NON_SCHEMA_COMPLIANT_CONFIGURATION'); + }); + }); + + it('should accept valid logGroup value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + logGroup: 'valid', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should reject invalid logGroup value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + logGroup: '', + }, + }, + }, + }, + command: 'package', + }).catch((error) => { + expect(error).to.have.property('code', 'INVALID_NON_SCHEMA_COMPLIANT_CONFIGURATION'); + }); + }); + + it('should accept `DEBUG` as a valid systemLogLevel value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + systemLogLevel: 'DEBUG', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should accept `INFO` as a valid systemLogLevel value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + systemLogLevel: 'INFO', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should accept `WARN` as a valid systemLogLevel value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + systemLogLevel: 'WARN', + }, + }, + }, + }, + command: 'package', + }).then((result) => { + expect(result).to.be.ok; + }); + }); + + it('should reject invalid systemLogLevel value', () => { + return runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + logs: { + systemLogLevel: 'INVALID', + }, + }, + }, + }, + command: 'package', + }).catch((error) => { + expect(error).to.have.property('code', 'INVALID_NON_SCHEMA_COMPLIANT_CONFIGURATION'); + }); + }); + + it('should support `provider.logs.lambda`', async () => { + const { cfTemplate, awsNaming } = await runServerless({ + fixture: 'function', + configExt: { + provider: { + logs: { + lambda: { + logFormat: 'JSON', + applicationLogLevel: 'INFO', + systemLogLevel: 'WARN', + logGroup: '/aws/lambda/provider-log-group', + }, + }, + }, + }, + command: 'package', + }); + + const { LoggingConfig } = + cfTemplate.Resources[awsNaming.getLambdaLogicalId('basic')].Properties; + + expect(LoggingConfig).to.deep.equal({ + LogFormat: 'JSON', + ApplicationLogLevel: 'INFO', + SystemLogLevel: 'WARN', + LogGroup: '/aws/lambda/provider-log-group', + }); + }); + + it('should prefer `functions[].logs` over `provider.logs.lambda`', async () => { + const { cfTemplate, awsNaming } = await runServerless({ + fixture: 'function', + configExt: { + provider: { + logs: { + lambda: { + logFormat: 'Text', + applicationLogLevel: 'ERROR', + }, + }, + }, + functions: { + basic: { + logs: { + logFormat: 'JSON', + applicationLogLevel: 'DEBUG', + }, + }, + }, + }, + command: 'package', + }); + + const { LoggingConfig } = + cfTemplate.Resources[awsNaming.getLambdaLogicalId('basic')].Properties; + + expect(LoggingConfig).to.deep.equal({ + LogFormat: 'JSON', + ApplicationLogLevel: 'DEBUG', + }); + }); + it('should support `functions[].fileSystemConfig` (with vpc configured on function)', () => { const functionServiceConfig = serviceConfig.functions.fnFileSystemConfig; const fileSystemCfConfig =