diff --git a/.changeset/poor-moons-refuse.md b/.changeset/poor-moons-refuse.md new file mode 100644 index 00000000000..4da456357c7 --- /dev/null +++ b/.changeset/poor-moons-refuse.md @@ -0,0 +1,9 @@ +--- +'@aws-amplify/ai-constructs': minor +'@aws-amplify/backend-ai': minor +'@aws-amplify/backend-function': minor +'@aws-amplify/backend': minor +'@aws-amplify/platform-core': minor +--- + +Add options to control log settings diff --git a/package-lock.json b/package-lock.json index abf7f393397..c60a804d40e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32195,6 +32195,10 @@ "@types/is-ci": "^3.0.4", "@types/lodash.mergewith": "^4.6.2", "@types/uuid": "9.0.7" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.0.0" } }, "packages/platform-core/node_modules/uuid": { diff --git a/packages/ai-constructs/API.md b/packages/ai-constructs/API.md index a5621d37df5..0f1550885f2 100644 --- a/packages/ai-constructs/API.md +++ b/packages/ai-constructs/API.md @@ -7,12 +7,14 @@ /// import { AIConversationOutput } from '@aws-amplify/backend-output-schemas'; +import { ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda'; import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types'; import * as bedrock from '@aws-sdk/client-bedrock-runtime'; import { Construct } from 'constructs'; import { FunctionResources } from '@aws-amplify/plugin-types'; import * as jsonSchemaToTypeScript from 'json-schema-to-ts'; import { ResourceProvider } from '@aws-amplify/plugin-types'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; declare namespace __export__conversation { export { @@ -55,6 +57,10 @@ type ConversationHandlerFunctionProps = { region?: string; }>; memoryMB?: number; + logging?: { + level?: ApplicationLogLevel; + retention?: RetentionDays; + }; outputStorageStrategy?: BackendOutputStorageStrategy; }; diff --git a/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts b/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts index b0130e711f9..284024a89e4 100644 --- a/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts +++ b/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts @@ -5,6 +5,8 @@ import { ConversationHandlerFunction } from './conversation_handler_construct'; import { Template } from 'aws-cdk-lib/assertions'; import path from 'path'; import { StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage'; +import { ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; void describe('Conversation Handler Function construct', () => { void it('creates handler with log group with JWT token redacting policy', () => { @@ -284,4 +286,41 @@ void describe('Conversation Handler Function construct', () => { }, new Error('memoryMB must be a whole number between 128 and 10240 inclusive')); }); }); + + void describe('logging options', () => { + void it('sets log level', () => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + logging: { + level: ApplicationLogLevel.DEBUG, + }, + }); + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + LoggingConfig: { + ApplicationLogLevel: 'DEBUG', + LogFormat: 'JSON', + }, + }); + }); + + void it('sets log retention', () => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + logging: { + retention: RetentionDays.ONE_YEAR, + }, + }); + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Logs::LogGroup', { + RetentionInDays: 365, + }); + }); + }); }); diff --git a/packages/ai-constructs/src/conversation/conversation_handler_construct.ts b/packages/ai-constructs/src/conversation/conversation_handler_construct.ts index 995b92fed6c..e5b563a6df9 100644 --- a/packages/ai-constructs/src/conversation/conversation_handler_construct.ts +++ b/packages/ai-constructs/src/conversation/conversation_handler_construct.ts @@ -6,6 +6,7 @@ import { import { Duration, Stack, Tags } from 'aws-cdk-lib'; import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { + ApplicationLogLevel, CfnFunction, Runtime as LambdaRuntime, LoggingFormat, @@ -40,6 +41,12 @@ export type ConversationHandlerFunctionProps = { * Default is 512MB. */ memoryMB?: number; + + logging?: { + level?: ApplicationLogLevel; + retention?: RetentionDays; + }; + /** * @internal */ @@ -100,8 +107,9 @@ export class ConversationHandlerFunction bundleAwsSDK: !!this.props.entry, }, loggingFormat: LoggingFormat.JSON, + applicationLogLevelV2: this.props.logging?.level, logGroup: new LogGroup(this, 'conversationHandlerFunctionLogGroup', { - retention: RetentionDays.INFINITE, + retention: this.props.logging?.retention ?? RetentionDays.INFINITE, dataProtectionPolicy: new DataProtectionPolicy({ identifiers: [ new CustomDataIdentifier( diff --git a/packages/backend-ai/API.md b/packages/backend-ai/API.md index 2b75a01c579..fcacb75c61c 100644 --- a/packages/backend-ai/API.md +++ b/packages/backend-ai/API.md @@ -8,12 +8,17 @@ import { AiModel } from '@aws-amplify/data-schema-types'; import { ConstructFactory } from '@aws-amplify/plugin-types'; import { ConversationTurnEventVersion } from '@aws-amplify/ai-constructs/conversation'; import { FunctionResources } from '@aws-amplify/plugin-types'; +import { LogLevel } from '@aws-amplify/plugin-types'; +import { LogRetention } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; import * as runtime from '@aws-amplify/ai-constructs/conversation/runtime'; declare namespace __export__conversation { export { ConversationHandlerFunctionFactory, + ConversationHandlerFunctionLogLevel, + ConversationHandlerFunctionLogRetention, + ConversationHandlerFunctionLoggingOptions, DefineConversationHandlerFunctionProps, defineConversationHandlerFunction } @@ -36,6 +41,18 @@ type ConversationHandlerFunctionFactory = ConstructFactory; memoryMB?: number; + logging?: ConversationHandlerFunctionLoggingOptions; }; // @public (undocumented) diff --git a/packages/backend-ai/src/conversation/factory.test.ts b/packages/backend-ai/src/conversation/factory.test.ts index 9802e4944bc..a264731ffb2 100644 --- a/packages/backend-ai/src/conversation/factory.test.ts +++ b/packages/backend-ai/src/conversation/factory.test.ts @@ -203,4 +203,41 @@ void describe('ConversationHandlerFactory', () => { MemorySize: 271, }); }); + + void it('passes log level to construct', () => { + const factory = defineConversationHandlerFunction({ + entry: './test-assets/with-default-entry/handler.ts', + name: 'testHandlerName', + models: [], + logging: { + level: 'debug', + }, + }); + const lambda = factory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + LoggingConfig: { + ApplicationLogLevel: 'DEBUG', + LogFormat: 'JSON', + }, + }); + }); + + void it('passes log retention to construct', () => { + const factory = defineConversationHandlerFunction({ + entry: './test-assets/with-default-entry/handler.ts', + name: 'testHandlerName', + models: [], + logging: { + retention: '1 day', + }, + }); + const lambda = factory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Logs::LogGroup', { + RetentionInDays: 1, + }); + }); }); diff --git a/packages/backend-ai/src/conversation/factory.ts b/packages/backend-ai/src/conversation/factory.ts index 6b4242288ff..d5a88c3f03e 100644 --- a/packages/backend-ai/src/conversation/factory.ts +++ b/packages/backend-ai/src/conversation/factory.ts @@ -7,6 +7,8 @@ import { ConstructFactoryGetInstanceProps, FunctionResources, GenerateContainerEntryProps, + LogLevel, + LogRetention, ResourceProvider, } from '@aws-amplify/plugin-types'; import { @@ -17,6 +19,10 @@ import { import path from 'path'; import { CallerDirectoryExtractor } from '@aws-amplify/platform-core'; import { AiModel } from '@aws-amplify/data-schema-types'; +import { + LogLevelConverter, + LogRetentionConverter, +} from '@aws-amplify/platform-core/cdk'; class ConversationHandlerFunctionGenerator implements ConstructContainerEntryGenerator @@ -47,6 +53,18 @@ class ConversationHandlerFunctionGenerator outputStorageStrategy: this.outputStorageStrategy, memoryMB: this.props.memoryMB, }; + const logging: typeof constructProps.logging = {}; + if (this.props.logging?.level) { + logging.level = new LogLevelConverter().toCDKLambdaApplicationLogLevel( + this.props.logging.level + ); + } + if (this.props.logging?.retention) { + logging.retention = new LogRetentionConverter().toCDKRetentionDays( + this.props.logging.retention + ); + } + constructProps.logging = logging; const conversationHandlerFunction = new ConversationHandlerFunction( scope, this.props.name, @@ -115,6 +133,15 @@ class DefaultConversationHandlerFunctionFactory }; } +export type ConversationHandlerFunctionLogLevel = LogLevel; + +export type ConversationHandlerFunctionLogRetention = LogRetention; + +export type ConversationHandlerFunctionLoggingOptions = { + retention?: ConversationHandlerFunctionLogRetention; + level?: ConversationHandlerFunctionLogLevel; +}; + export type DefineConversationHandlerFunctionProps = { name: string; entry?: string; @@ -128,6 +155,7 @@ export type DefineConversationHandlerFunctionProps = { * Default is 512MB. */ memoryMB?: number; + logging?: ConversationHandlerFunctionLoggingOptions; }; /** diff --git a/packages/backend-ai/src/conversation/index.ts b/packages/backend-ai/src/conversation/index.ts index 489209c219d..69eb8d3c5cb 100644 --- a/packages/backend-ai/src/conversation/index.ts +++ b/packages/backend-ai/src/conversation/index.ts @@ -1,11 +1,17 @@ import { ConversationHandlerFunctionFactory, + ConversationHandlerFunctionLogLevel, + ConversationHandlerFunctionLogRetention, + ConversationHandlerFunctionLoggingOptions, DefineConversationHandlerFunctionProps, defineConversationHandlerFunction, } from './factory.js'; export { ConversationHandlerFunctionFactory, + ConversationHandlerFunctionLogLevel, + ConversationHandlerFunctionLogRetention, + ConversationHandlerFunctionLoggingOptions, DefineConversationHandlerFunctionProps, defineConversationHandlerFunction, }; diff --git a/packages/backend-function/API.md b/packages/backend-function/API.md index 8e0ad66d853..500972914db 100644 --- a/packages/backend-function/API.md +++ b/packages/backend-function/API.md @@ -8,6 +8,8 @@ import { AmplifyResourceGroupName } from '@aws-amplify/plugin-types'; import { BackendSecret } from '@aws-amplify/plugin-types'; import { ConstructFactory } from '@aws-amplify/plugin-types'; import { FunctionResources } from '@aws-amplify/plugin-types'; +import { LogLevel } from '@aws-amplify/plugin-types'; +import { LogRetention } from '@aws-amplify/plugin-types'; import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; import { StackProvider } from '@aws-amplify/plugin-types'; @@ -28,6 +30,22 @@ export type FunctionBundlingOptions = { minify?: boolean; }; +// @public (undocumented) +export type FunctionLoggingOptions = ({ + format: 'json'; + level?: FunctionLogLevel; +} | { + format?: 'text'; +}) & { + retention?: FunctionLogRetention; +}; + +// @public (undocumented) +export type FunctionLogLevel = LogLevel; + +// @public (undocumented) +export type FunctionLogRetention = LogRetention; + // @public (undocumented) export type FunctionProps = { name?: string; @@ -40,6 +58,7 @@ export type FunctionProps = { layers?: Record; bundling?: FunctionBundlingOptions; resourceGroupName?: AmplifyResourceGroupName; + logging?: FunctionLoggingOptions; }; // @public (undocumented) diff --git a/packages/backend-function/src/factory.test.ts b/packages/backend-function/src/factory.test.ts index c7ce48a2cfe..8b964f336c2 100644 --- a/packages/backend-function/src/factory.test.ts +++ b/packages/backend-function/src/factory.test.ts @@ -440,6 +440,39 @@ void describe('AmplifyFunctionFactory', () => { }); }); + void describe('logging options', () => { + void it('sets logging options', () => { + const lambda = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + bundling: { + minify: false, + }, + logging: { + format: 'json', + level: 'warn', + retention: '13 months', + }, + }).getInstance(getInstanceProps); + const template = Template.fromStack(lambda.stack); + // Enabling log retention adds extra lambda. + template.resourceCountIs('AWS::Lambda::Function', 2); + const lambdas = template.findResources('AWS::Lambda::Function'); + assert.ok( + Object.keys(lambdas).some((key) => key.startsWith('LogRetention')) + ); + template.hasResourceProperties('Custom::LogRetention', { + RetentionInDays: 400, + }); + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + LoggingConfig: { + ApplicationLogLevel: 'WARN', + LogFormat: 'JSON', + }, + }); + }); + }); + void describe('resourceAccessAcceptor', () => { void it('attaches policy to execution role and configures ssm environment context', () => { const functionFactory = defineFunction({ diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index 9c29357c217..e752b46d44e 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -18,6 +18,8 @@ import { ConstructFactoryGetInstanceProps, FunctionResources, GenerateContainerEntryProps, + LogLevel, + LogRetention, ResourceAccessAcceptorFactory, ResourceNameValidator, ResourceProvider, @@ -45,6 +47,7 @@ import { FunctionEnvironmentTranslator } from './function_env_translator.js'; import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; import { FunctionLayerArnParser } from './layer_parser.js'; import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js'; +import { convertLoggingOptionsToCDK } from './logging_options_parser.js'; const functionStackType = 'function-Lambda'; @@ -64,6 +67,9 @@ export type TimeInterval = | `every year`; export type FunctionSchedule = TimeInterval | CronSchedule; +export type FunctionLogLevel = LogLevel; +export type FunctionLogRetention = LogRetention; + /** * Entry point for defining a function in the Amplify ecosystem */ @@ -159,6 +165,8 @@ export type FunctionProps = { * resourceGroupName: 'auth' // to group an auth trigger with an auth resource */ resourceGroupName?: AmplifyResourceGroupName; + + logging?: FunctionLoggingOptions; }; export type FunctionBundlingOptions = { @@ -170,6 +178,18 @@ export type FunctionBundlingOptions = { minify?: boolean; }; +export type FunctionLoggingOptions = ( + | { + format: 'json'; + level?: FunctionLogLevel; + } + | { + format?: 'text'; + } +) & { + retention?: FunctionLogRetention; +}; + /** * Create Lambda functions in the context of an Amplify backend definition */ @@ -218,6 +238,7 @@ class FunctionFactory implements ConstructFactory { bundling: this.resolveBundling(), layers, resourceGroupName: this.props.resourceGroupName ?? 'function', + logging: this.props.logging ?? {}, }; }; @@ -438,6 +459,7 @@ class AmplifyFunction functionEnvironmentTypeGenerator.generateProcessEnvShim(); let functionLambda: NodejsFunction; + const cdkLoggingOptions = convertLoggingOptionsToCDK(props.logging); try { functionLambda = new NodejsFunction(scope, `${id}-lambda`, { entry: props.entry, @@ -451,6 +473,9 @@ class AmplifyFunction inject: shims, externalModules: Object.keys(props.layers), }, + logRetention: cdkLoggingOptions.retention, + applicationLogLevelV2: cdkLoggingOptions.level, + loggingFormat: cdkLoggingOptions.format, }); } catch (error) { // If the error is from ES Bundler which is executed as a child process by CDK, diff --git a/packages/backend-function/src/logging_options_parser.test.ts b/packages/backend-function/src/logging_options_parser.test.ts new file mode 100644 index 00000000000..4d6abec9589 --- /dev/null +++ b/packages/backend-function/src/logging_options_parser.test.ts @@ -0,0 +1,56 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { FunctionLoggingOptions } from './factory.js'; +import { + CDKLoggingOptions, + convertLoggingOptionsToCDK, +} from './logging_options_parser.js'; +import { ApplicationLogLevel, LoggingFormat } from 'aws-cdk-lib/aws-lambda'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; + +type TestCase = { + input: FunctionLoggingOptions; + expectedOutput: CDKLoggingOptions; +}; + +const testCases: Array = [ + { + input: {}, + expectedOutput: { + format: undefined, + level: undefined, + retention: undefined, + }, + }, + { + input: { + format: 'text', + retention: '13 months', + }, + expectedOutput: { + format: LoggingFormat.TEXT, + retention: RetentionDays.THIRTEEN_MONTHS, + level: undefined, + }, + }, + { + input: { + format: 'json', + level: 'debug', + }, + expectedOutput: { + format: LoggingFormat.JSON, + retention: undefined, + level: ApplicationLogLevel.DEBUG, + }, + }, +]; + +void describe('LoggingOptions converter', () => { + testCases.forEach((testCase, index) => { + void it(`converts to cdk options[${index}]`, () => { + const convertedOptions = convertLoggingOptionsToCDK(testCase.input); + assert.deepStrictEqual(convertedOptions, testCase.expectedOutput); + }); + }); +}); diff --git a/packages/backend-function/src/logging_options_parser.ts b/packages/backend-function/src/logging_options_parser.ts new file mode 100644 index 00000000000..ce5693d6253 --- /dev/null +++ b/packages/backend-function/src/logging_options_parser.ts @@ -0,0 +1,48 @@ +import { FunctionLoggingOptions } from './factory.js'; +import { ApplicationLogLevel, LoggingFormat } from 'aws-cdk-lib/aws-lambda'; +import { + LogLevelConverter, + LogRetentionConverter, +} from '@aws-amplify/platform-core/cdk'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; + +export type CDKLoggingOptions = { + level?: ApplicationLogLevel; + retention?: RetentionDays; + format?: LoggingFormat; +}; + +/** + * Converts logging options to CDK. + */ +export const convertLoggingOptionsToCDK = ( + loggingOptions: FunctionLoggingOptions +): CDKLoggingOptions => { + let level: ApplicationLogLevel | undefined = undefined; + if ('level' in loggingOptions) { + level = new LogLevelConverter().toCDKLambdaApplicationLogLevel( + loggingOptions.level + ); + } + const retention = new LogRetentionConverter().toCDKRetentionDays( + loggingOptions.retention + ); + const format = convertFormat(loggingOptions.format); + + return { + level, + retention, + format, + }; +}; + +const convertFormat = (format: 'json' | 'text' | undefined) => { + switch (format) { + case undefined: + return undefined; + case 'json': + return LoggingFormat.JSON; + case 'text': + return LoggingFormat.TEXT; + } +}; diff --git a/packages/platform-core/API.md b/packages/platform-core/API.md index 286ae416207..55997c87478 100644 --- a/packages/platform-core/API.md +++ b/packages/platform-core/API.md @@ -5,10 +5,22 @@ ```ts import { AppId } from '@aws-amplify/plugin-types'; +import { ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda'; import { BackendIdentifier } from '@aws-amplify/plugin-types'; import { DeepPartialAmplifyGeneratedConfigs } from '@aws-amplify/plugin-types'; +import { LogLevel } from '@aws-amplify/plugin-types'; +import { LogRetention } from '@aws-amplify/plugin-types'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; import z from 'zod'; +declare namespace __export__cdk { + export { + LogLevelConverter, + LogRetentionConverter + } +} +export { __export__cdk } + // @public export abstract class AmplifyError extends Error { constructor(name: T, classification: AmplifyErrorClassification, options: AmplifyErrorOptions, cause?: Error | undefined); @@ -121,6 +133,18 @@ export class FilePathExtractor { // @public (undocumented) export type LocalConfigurationFileName = 'usage_data_preferences.json'; +// @public +class LogLevelConverter { + // (undocumented) + toCDKLambdaApplicationLogLevel: (logLevel: LogLevel | undefined) => ApplicationLogLevel | undefined; +} + +// @public +class LogRetentionConverter { + // (undocumented) + toCDKRetentionDays: (retention: LogRetention | undefined) => RetentionDays | undefined; +} + // @public export class ObjectAccumulator { constructor(accumulator: DeepPartialAmplifyGeneratedConfigs, versionKey?: string); diff --git a/packages/platform-core/api-extractor.json b/packages/platform-core/api-extractor.json index 0f56de03f66..cc2ebea8cf9 100644 --- a/packages/platform-core/api-extractor.json +++ b/packages/platform-core/api-extractor.json @@ -1,3 +1,4 @@ { - "extends": "../../api-extractor.base.json" + "extends": "../../api-extractor.base.json", + "mainEntryPointFilePath": "/lib/index.internal.d.ts" } diff --git a/packages/platform-core/package.json b/packages/platform-core/package.json index 890f108a4b2..131980b90a2 100644 --- a/packages/platform-core/package.json +++ b/packages/platform-core/package.json @@ -10,6 +10,11 @@ "types": "./lib/index.d.ts", "import": "./lib/index.js", "require": "./lib/index.js" + }, + "./cdk": { + "types": "./lib/cdk/index.d.ts", + "import": "./lib/cdk/index.js", + "require": "./lib/cdk/index.js" } }, "main": "lib/index.js", @@ -31,5 +36,9 @@ "semver": "^7.6.3", "uuid": "^9.0.1", "zod": "^3.22.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.0.0" } } diff --git a/packages/platform-core/src/.eslintrc.json b/packages/platform-core/src/.eslintrc.json new file mode 100644 index 00000000000..0da8b97bbb6 --- /dev/null +++ b/packages/platform-core/src/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["aws-cdk-lib", "aws-cdk-lib/*", "constructs"], + "message": "Usage of CDK lib is not allowed in platform-core. Except /cdk entry point. This is to ensure that we don't load CDK eagerly from package root." + } + ] + } + ] + } +} diff --git a/packages/platform-core/src/cdk/.eslintrc.json b/packages/platform-core/src/cdk/.eslintrc.json new file mode 100644 index 00000000000..c3220a910c1 --- /dev/null +++ b/packages/platform-core/src/cdk/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-restricted-imports": "off" + } +} diff --git a/packages/platform-core/src/cdk/enum_converters.test.ts b/packages/platform-core/src/cdk/enum_converters.test.ts new file mode 100644 index 00000000000..19edc0d1441 --- /dev/null +++ b/packages/platform-core/src/cdk/enum_converters.test.ts @@ -0,0 +1,162 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { LogLevel, LogRetention } from '@aws-amplify/plugin-types'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { LogLevelConverter, LogRetentionConverter } from './enum_converters'; +import { ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda'; + +type TestCase = { + input: TSource | undefined; + expectedOutput: TTarget | undefined; +}; + +void describe('LogRetentionConverter', () => { + const testCases: Array> = [ + { + input: undefined, + expectedOutput: undefined, + }, + { + input: '1 day', + expectedOutput: RetentionDays.ONE_DAY, + }, + { + input: '3 days', + expectedOutput: RetentionDays.THREE_DAYS, + }, + { + input: '5 days', + expectedOutput: RetentionDays.FIVE_DAYS, + }, + { + input: '1 week', + expectedOutput: RetentionDays.ONE_WEEK, + }, + { + input: '2 weeks', + expectedOutput: RetentionDays.TWO_WEEKS, + }, + { + input: '1 month', + expectedOutput: RetentionDays.ONE_MONTH, + }, + { + input: '2 months', + expectedOutput: RetentionDays.TWO_MONTHS, + }, + { + input: '3 months', + expectedOutput: RetentionDays.THREE_MONTHS, + }, + { + input: '4 months', + expectedOutput: RetentionDays.FOUR_MONTHS, + }, + { + input: '5 months', + expectedOutput: RetentionDays.FIVE_MONTHS, + }, + { + input: '6 months', + expectedOutput: RetentionDays.SIX_MONTHS, + }, + { + input: '13 months', + expectedOutput: RetentionDays.THIRTEEN_MONTHS, + }, + { + input: '18 months', + expectedOutput: RetentionDays.EIGHTEEN_MONTHS, + }, + { + input: '1 year', + expectedOutput: RetentionDays.ONE_YEAR, + }, + { + input: '2 years', + expectedOutput: RetentionDays.TWO_YEARS, + }, + { + input: '3 years', + expectedOutput: RetentionDays.THREE_YEARS, + }, + { + input: '5 years', + expectedOutput: RetentionDays.FIVE_YEARS, + }, + { + input: '6 years', + expectedOutput: RetentionDays.SIX_YEARS, + }, + { + input: '7 years', + expectedOutput: RetentionDays.SEVEN_YEARS, + }, + { + input: '8 years', + expectedOutput: RetentionDays.EIGHT_YEARS, + }, + { + input: '9 years', + expectedOutput: RetentionDays.NINE_YEARS, + }, + { + input: '10 years', + expectedOutput: RetentionDays.TEN_YEARS, + }, + { + input: 'infinite', + expectedOutput: RetentionDays.INFINITE, + }, + ]; + + testCases.forEach((testCase, index) => { + void it(`converts log retention[${index}]`, () => { + const convertedValue = new LogRetentionConverter().toCDKRetentionDays( + testCase.input + ); + assert.strictEqual(convertedValue, testCase.expectedOutput); + }); + }); +}); + +void describe('LogLevelConverter', () => { + const testCases: Array> = [ + { + input: undefined, + expectedOutput: undefined, + }, + { + input: 'info', + expectedOutput: ApplicationLogLevel.INFO, + }, + { + input: 'debug', + expectedOutput: ApplicationLogLevel.DEBUG, + }, + { + input: 'error', + expectedOutput: ApplicationLogLevel.ERROR, + }, + { + input: 'warn', + expectedOutput: ApplicationLogLevel.WARN, + }, + { + input: 'trace', + expectedOutput: ApplicationLogLevel.TRACE, + }, + { + input: 'fatal', + expectedOutput: ApplicationLogLevel.FATAL, + }, + ]; + + testCases.forEach((testCase, index) => { + void it(`converts log retention[${index}]`, () => { + const convertedValue = + new LogLevelConverter().toCDKLambdaApplicationLogLevel(testCase.input); + assert.strictEqual(convertedValue, testCase.expectedOutput); + }); + }); +}); diff --git a/packages/platform-core/src/cdk/enum_converters.ts b/packages/platform-core/src/cdk/enum_converters.ts new file mode 100644 index 00000000000..3f467785a0c --- /dev/null +++ b/packages/platform-core/src/cdk/enum_converters.ts @@ -0,0 +1,97 @@ +import { LogLevel, LogRetention } from '@aws-amplify/plugin-types'; +import { ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; + +/** + * Converts LogRetention to CDK types. + */ +export class LogRetentionConverter { + toCDKRetentionDays = ( + retention: LogRetention | undefined + ): RetentionDays | undefined => { + switch (retention) { + case undefined: + return undefined; + + case '1 day': + return RetentionDays.ONE_DAY; + case '3 days': + return RetentionDays.THREE_DAYS; + case '5 days': + return RetentionDays.FIVE_DAYS; + case '1 week': + return RetentionDays.ONE_WEEK; + case '2 weeks': + return RetentionDays.TWO_WEEKS; + case '1 month': + return RetentionDays.ONE_MONTH; + case '2 months': + return RetentionDays.TWO_MONTHS; + case '3 months': + return RetentionDays.THREE_MONTHS; + case '4 months': + return RetentionDays.FOUR_MONTHS; + case '5 months': + return RetentionDays.FIVE_MONTHS; + case '6 months': + return RetentionDays.SIX_MONTHS; + case '1 year': + return RetentionDays.ONE_YEAR; + case '13 months': + return RetentionDays.THIRTEEN_MONTHS; + case '18 months': + return RetentionDays.EIGHTEEN_MONTHS; + case '2 years': + return RetentionDays.TWO_YEARS; + case '3 years': + return RetentionDays.THREE_YEARS; + case '5 years': + return RetentionDays.FIVE_YEARS; + case '6 years': + return RetentionDays.SIX_YEARS; + case '7 years': + return RetentionDays.SEVEN_YEARS; + case '8 years': + return RetentionDays.EIGHT_YEARS; + case '9 years': + return RetentionDays.NINE_YEARS; + case '10 years': + return RetentionDays.TEN_YEARS; + case 'infinite': + return RetentionDays.INFINITE; + } + }; +} + +/** + * Converts LogLevel to CDK types. + */ +export class LogLevelConverter { + toCDKLambdaApplicationLogLevel = ( + logLevel: LogLevel | undefined + ): ApplicationLogLevel | undefined => { + switch (logLevel) { + case undefined: { + return undefined; + } + case 'info': { + return ApplicationLogLevel.INFO; + } + case 'debug': { + return ApplicationLogLevel.DEBUG; + } + case 'warn': { + return ApplicationLogLevel.WARN; + } + case 'error': { + return ApplicationLogLevel.ERROR; + } + case 'fatal': { + return ApplicationLogLevel.FATAL; + } + case 'trace': { + return ApplicationLogLevel.TRACE; + } + } + }; +} diff --git a/packages/platform-core/src/cdk/index.ts b/packages/platform-core/src/cdk/index.ts new file mode 100644 index 00000000000..d5c9d704c97 --- /dev/null +++ b/packages/platform-core/src/cdk/index.ts @@ -0,0 +1,3 @@ +import { LogLevelConverter, LogRetentionConverter } from './enum_converters.js'; + +export { LogLevelConverter, LogRetentionConverter }; diff --git a/packages/platform-core/src/index.internal.ts b/packages/platform-core/src/index.internal.ts new file mode 100644 index 00000000000..336a22d74e2 --- /dev/null +++ b/packages/platform-core/src/index.internal.ts @@ -0,0 +1,12 @@ +// Suppressing to allow special prefix __export__ that is recognized by API checks. +// eslint-disable-next-line @typescript-eslint/naming-convention +import * as __export__cdk from './cdk/index.js'; + +export * from './index.js'; + +/* + Api-extractor does not ([yet](https://github.com/microsoft/rushstack/issues/1596)) support multiple package entry points + Because this package has a submodule export, we are working around this issue by including that export here and directing api-extract to this entry point instead + This allows api-extractor to pick up the submodule exports in its analysis + */ +export { __export__cdk }; diff --git a/packages/plugin-types/API.md b/packages/plugin-types/API.md index 2b78dd2ae1c..832ba6c782d 100644 --- a/packages/plugin-types/API.md +++ b/packages/plugin-types/API.md @@ -175,6 +175,12 @@ export type ImportPathVerifier = { verify: (importStack: string | undefined, expectedImportingFile: string, errorMessage: string) => void; }; +// @public (undocumented) +export type LogLevel = 'info' | 'debug' | 'warn' | 'error' | 'fatal' | 'trace'; + +// @public (undocumented) +export type LogRetention = '1 day' | '3 days' | '5 days' | '1 week' | '2 weeks' | '1 month' | '2 months' | '3 months' | '4 months' | '5 months' | '6 months' | '1 year' | '13 months' | '18 months' | '2 years' | '3 years' | '5 years' | '6 years' | '7 years' | '8 years' | '9 years' | '10 years' | 'infinite'; + // @public export type MainStackCreator = { getOrCreateMainStack: () => Stack; diff --git a/packages/plugin-types/src/index.ts b/packages/plugin-types/src/index.ts index 780c52ec02c..8d738f7b2ff 100644 --- a/packages/plugin-types/src/index.ts +++ b/packages/plugin-types/src/index.ts @@ -22,3 +22,5 @@ export * from './resource_name_validator.js'; export * from './aws_client_provider.js'; export * from './stack_provider.js'; export * from './amplify_resource_group_name.js'; +export * from './log_level.js'; +export * from './log_retention.js'; diff --git a/packages/plugin-types/src/log_level.ts b/packages/plugin-types/src/log_level.ts new file mode 100644 index 00000000000..5f4d23c0087 --- /dev/null +++ b/packages/plugin-types/src/log_level.ts @@ -0,0 +1 @@ +export type LogLevel = 'info' | 'debug' | 'warn' | 'error' | 'fatal' | 'trace'; diff --git a/packages/plugin-types/src/log_retention.ts b/packages/plugin-types/src/log_retention.ts new file mode 100644 index 00000000000..60210c721ba --- /dev/null +++ b/packages/plugin-types/src/log_retention.ts @@ -0,0 +1,24 @@ +export type LogRetention = + | '1 day' + | '3 days' + | '5 days' + | '1 week' + | '2 weeks' + | '1 month' + | '2 months' + | '3 months' + | '4 months' + | '5 months' + | '6 months' + | '1 year' + | '13 months' + | '18 months' + | '2 years' + | '3 years' + | '5 years' + | '6 years' + | '7 years' + | '8 years' + | '9 years' + | '10 years' + | 'infinite';