From 5b50aa8803559fb2d1413db4095551491ece8cc9 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Mon, 27 Oct 2025 19:28:45 +0100 Subject: [PATCH 1/3] feat(AWS Lambda): support advanced logging configuration - Add logs property to function schema with applicationLogLevel, logFormat, logGroup, systemLogLevel - Implement LoggingConfig compilation in functions.js - Add comprehensive tests for all log level values and validation - Update documentation with examples Enables AWS Lambda advanced logging controls including JSON format logs, custom log levels, and custom log groups as announced in: https://aws.amazon.com/blogs/compute/introducing-advanced-logging-controls-for-aws-lambda-functions/ Closes #12264 --- docs/guides/functions.md | 15 + docs/guides/serverless.yml.md | 6 + lib/plugins/aws/package/compile/functions.js | 24 ++ lib/plugins/aws/provider.js | 13 + .../aws/package/compile/functions.test.js | 315 ++++++++++++++++++ 5 files changed, 373 insertions(+) diff --git a/docs/guides/functions.md b/docs/guides/functions.md index 230cb0a36..be6d081be 100644 --- a/docs/guides/functions.md +++ b/docs/guides/functions.md @@ -873,6 +873,21 @@ functions: ephemeralStorageSize: 1024 ``` +## Logging Configuration + +[Configuring Lambda advanced logging options](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced) + +```yml +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..d6c15999a 100644 --- a/docs/guides/serverless.yml.md +++ b/docs/guides/serverless.yml.md @@ -722,6 +722,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..edaf8f39a 100644 --- a/lib/plugins/aws/package/compile/functions.js +++ b/lib/plugins/aws/package/compile/functions.js @@ -632,6 +632,30 @@ class AwsCompileFunctions { } } + if (functionObject.logs) { + const loggingConfig = {}; + + if (functionObject.logs.applicationLogLevel) { + loggingConfig.ApplicationLogLevel = functionObject.logs.applicationLogLevel; + } + + if (functionObject.logs.logFormat) { + loggingConfig.LogFormat = functionObject.logs.logFormat; + } + + if (functionObject.logs.logGroup) { + loggingConfig.LogGroup = functionObject.logs.logGroup; + } + + if (functionObject.logs.systemLogLevel) { + loggingConfig.SystemLogLevel = functionObject.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..d130463e9 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1394,6 +1394,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..07303ee63 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,312 @@ 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 `functions[].fileSystemConfig` (with vpc configured on function)', () => { const functionServiceConfig = serviceConfig.functions.fnFileSystemConfig; const fileSystemCfConfig = From 0624dc235d0155ada8f5bac6face2b09ec3ab53e Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Mon, 27 Oct 2025 19:35:40 +0100 Subject: [PATCH 2/3] feat(AWS Lambda): add provider-level logs configuration support - Add provider.logs.lambda schema with same properties as function-level - Implement inheritance: function logs override provider logs - Add tests for provider-level config and override behavior - Update docs to show both provider and function-level examples - Fix logGroup pattern to properly escape dash character Functions without explicit logs config now inherit from provider.logs.lambda if configured, matching behavior of other provider-level properties like tracing and kmsKeyArn. --- docs/guides/functions.md | 12 ++++ docs/guides/serverless.yml.md | 11 ++++ lib/plugins/aws/package/compile/functions.js | 22 ++++--- lib/plugins/aws/provider.js | 15 ++++- .../aws/package/compile/functions.test.js | 60 +++++++++++++++++++ 5 files changed, 110 insertions(+), 10 deletions(-) diff --git a/docs/guides/functions.md b/docs/guides/functions.md index be6d081be..e02a3923f 100644 --- a/docs/guides/functions.md +++ b/docs/guides/functions.md @@ -877,7 +877,19 @@ functions: [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 diff --git a/docs/guides/serverless.yml.md b/docs/guides/serverless.yml.md index d6c15999a..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 diff --git a/lib/plugins/aws/package/compile/functions.js b/lib/plugins/aws/package/compile/functions.js index edaf8f39a..e5c498749 100644 --- a/lib/plugins/aws/package/compile/functions.js +++ b/lib/plugins/aws/package/compile/functions.js @@ -632,23 +632,27 @@ class AwsCompileFunctions { } } - if (functionObject.logs) { + const logs = + functionObject.logs || + (this.serverless.service.provider.logs && this.serverless.service.provider.logs.lambda); + + if (logs) { const loggingConfig = {}; - if (functionObject.logs.applicationLogLevel) { - loggingConfig.ApplicationLogLevel = functionObject.logs.applicationLogLevel; + if (logs.applicationLogLevel) { + loggingConfig.ApplicationLogLevel = logs.applicationLogLevel; } - if (functionObject.logs.logFormat) { - loggingConfig.LogFormat = functionObject.logs.logFormat; + if (logs.logFormat) { + loggingConfig.LogFormat = logs.logFormat; } - if (functionObject.logs.logGroup) { - loggingConfig.LogGroup = functionObject.logs.logGroup; + if (logs.logGroup) { + loggingConfig.LogGroup = logs.logGroup; } - if (functionObject.logs.systemLogLevel) { - loggingConfig.SystemLogLevel = functionObject.logs.systemLogLevel; + if (logs.systemLogLevel) { + loggingConfig.SystemLogLevel = logs.systemLogLevel; } if (Object.keys(loggingConfig).length) { diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index d130463e9..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' }, @@ -1402,7 +1415,7 @@ class AwsProvider { enum: ['DEBUG', 'ERROR', 'FATAL', 'INFO', 'TRACE', 'WARN'], }, logFormat: { type: 'string', enum: ['JSON', 'Text'] }, - logGroup: { type: 'string', pattern: '^[.-_/#A-Za-z0-9]{1,512}$' }, + logGroup: { type: 'string', pattern: '^[.\\-_/#A-Za-z0-9]{1,512}$' }, systemLogLevel: { type: 'string', enum: ['DEBUG', 'INFO', 'WARN'] }, }, additionalProperties: false, 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 07303ee63..a89b1689f 100644 --- a/test/unit/lib/plugins/aws/package/compile/functions.test.js +++ b/test/unit/lib/plugins/aws/package/compile/functions.test.js @@ -2578,6 +2578,66 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { }); }); + 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 = From 0fcf158942701762de41a21a15d577265fc23da5 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Tue, 28 Oct 2025 12:02:58 +0100 Subject: [PATCH 3/3] style: fix prettier formatting issues --- test/unit/lib/plugins/aws/package/compile/functions.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 a89b1689f..c00cdb5e2 100644 --- a/test/unit/lib/plugins/aws/package/compile/functions.test.js +++ b/test/unit/lib/plugins/aws/package/compile/functions.test.js @@ -2596,7 +2596,8 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { command: 'package', }); - const { LoggingConfig } = cfTemplate.Resources[awsNaming.getLambdaLogicalId('basic')].Properties; + const { LoggingConfig } = + cfTemplate.Resources[awsNaming.getLambdaLogicalId('basic')].Properties; expect(LoggingConfig).to.deep.equal({ LogFormat: 'JSON', @@ -2630,7 +2631,8 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { command: 'package', }); - const { LoggingConfig } = cfTemplate.Resources[awsNaming.getLambdaLogicalId('basic')].Properties; + const { LoggingConfig } = + cfTemplate.Resources[awsNaming.getLambdaLogicalId('basic')].Properties; expect(LoggingConfig).to.deep.equal({ LogFormat: 'JSON',