diff --git a/packages/logging_cloudwatch/amplify_logging_cloudwatch/.gitignore b/packages/logging_cloudwatch/amplify_logging_cloudwatch/.gitignore index 65c34dc86e..f7752a753f 100644 --- a/packages/logging_cloudwatch/amplify_logging_cloudwatch/.gitignore +++ b/packages/logging_cloudwatch/amplify_logging_cloudwatch/.gitignore @@ -1,6 +1,8 @@ # Files and directories created by pub. .dart_tool/ .packages +.flutter-plugins +.flutter-plugins-dependencies # Conventional directory for build outputs. build/ diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart index 0070be9b5a..649e1a7517 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart @@ -1,7 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export 'file_storage.vm.dart' if (dart.library.html) 'file_storage.web.dart'; +export 'file_storage_stub.dart' + if (dart.library.io) 'file_storage_vm.dart' + if (dart.library.html) 'file_storage_web.dart'; /// File storage interface for saving and loading constraint locally abstract interface class FileStorage { diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_stub.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_stub.dart new file mode 100644 index 0000000000..8f71d4c5e8 --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_stub.dart @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_core/amplify_core.dart'; +import 'package:aws_logging_cloudwatch/src/file_storage/file_storage.dart'; + +/// File storage implementation for saving and loading constraint locally +class FileStorageImpl implements FileStorage { + /// File storage implementation for saving and loading constraint locally + FileStorageImpl(this.pathProvider); + + /// Path provider to get the application support path + final AppPathProvider pathProvider; + + @override + Future loadConstraint(String fileName) async { + throw UnimplementedError(); + } + + @override + Future saveConstraintLocally(String fileName, String content) async { + throw UnimplementedError(); + } +} diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.vm.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_vm.dart similarity index 95% rename from packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.vm.dart rename to packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_vm.dart index 05e51fd45c..7deb4ec801 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.vm.dart +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_vm.dart @@ -8,7 +8,7 @@ import 'package:aws_logging_cloudwatch/src/file_storage/file_storage.dart'; import 'package:path/path.dart' as p; /// File storage implementation for saving and loading constraint locally -final class FileStorageImpl implements FileStorage { +class FileStorageImpl implements FileStorage { /// File storage implementation for saving and loading constraint locally FileStorageImpl(this.pathProvider); diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.web.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_web.dart similarity index 94% rename from packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.web.dart rename to packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_web.dart index bbed8305b8..4bdf07efec 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.web.dart +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_web.dart @@ -7,7 +7,7 @@ import 'package:amplify_core/amplify_core.dart'; import 'package:aws_logging_cloudwatch/src/file_storage/file_storage.dart'; /// File storage implementation for saving and loading constraint locally -final class FileStorageImpl implements FileStorage { +class FileStorageImpl implements FileStorage { /// File storage implementation for saving and loading constraint locally // ignore: avoid_unused_constructor_parameters FileStorageImpl(AppPathProvider pathProvider); diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.dart index 540036ba9f..5f85de6a9c 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.dart +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.dart @@ -63,8 +63,27 @@ class LoggingConstraint with AWSDebuggable { factory LoggingConstraint.fromJson(Map json) => _$LoggingConstraintFromJson(json); + // /// Converts an [LoggingConstraint] instance to a [Map]. + // Map toJson() => _$LoggingConstraintToJson(this); + /// Converts an [LoggingConstraint] instance to a [Map]. - Map toJson() => _$LoggingConstraintToJson(this); + Map toJson() { + final jsonMap = { + 'defaultLogLevel': + defaultLogLevel.toString().split('.').last, // Convert enum to string + 'categoryLogLevel': categoryLogLevel?.map( + (key, value) => MapEntry(key, value.toString().split('.').last), + ), + }; + + if (userLogLevel != null) { + jsonMap['userLogLevel'] = userLogLevel!.map( + (key, value) => MapEntry(key, value.toJson()), + ); + } + + return jsonMap; + } /// The default [LogLevel] for sending logs to CloudWatch. final LogLevel defaultLogLevel; @@ -93,7 +112,18 @@ class UserLogLevel { _$UserLogLevelFromJson(json); /// Converts a [UserLogLevel] instance to a [Map]. - Map toJson() => _$UserLogLevelToJson(this); + Map toJson() => { + 'defaultLogLevel': defaultLogLevel + ?.toString() + .split('.') + .last, // Convert enum to string + 'categoryLogLevel': categoryLogLevel?.map( + (key, value) => MapEntry( + key, + value.toString().split('.').last, + ), // Convert enum to string + ), + }; /// The default [LogLevel] for sending logs to CloudWatch. final LogLevel? defaultLogLevel; diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.g.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.g.dart index 6fd6cd2a90..2ec0bf735a 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.g.dart +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.g.dart @@ -15,6 +15,9 @@ LoggingConstraint _$LoggingConstraintFromJson(Map json) => (json['categoryLogLevel'] as Map?)?.map( (k, e) => MapEntry(k, $enumDecode(_$LogLevelEnumMap, e)), ), + userLogLevel: (json['userLogLevel'] as Map?)?.map( + (k, e) => MapEntry(k, UserLogLevel.fromJson(e as Map)), + ), ); Map _$LoggingConstraintToJson(LoggingConstraint instance) => @@ -22,6 +25,7 @@ Map _$LoggingConstraintToJson(LoggingConstraint instance) => 'defaultLogLevel': _$LogLevelEnumMap[instance.defaultLogLevel]!, 'categoryLogLevel': instance.categoryLogLevel ?.map((k, e) => MapEntry(k, _$LogLevelEnumMap[e]!)), + 'userLogLevel': instance.userLogLevel, }; const _$LogLevelEnumMap = { diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart index 8f7a0e4e4e..ae9e2a2ccb 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart @@ -10,7 +10,9 @@ import 'package:amplify_core/amplify_core.dart'; import 'package:aws_common/aws_common.dart'; import 'package:aws_logging_cloudwatch/aws_logging_cloudwatch.dart'; -import 'package:aws_logging_cloudwatch/src/file_storage/file_storage.dart'; +import 'package:aws_logging_cloudwatch/src/file_storage/file_storage_stub.dart' + if (dart.library.io) 'package:aws_logging_cloudwatch/src/file_storage/file_storage_vm.dart' + if (dart.library.html) 'package:aws_logging_cloudwatch/src/file_storage/file_storage_web.dart'; import 'package:aws_signature_v4/aws_signature_v4.dart'; import 'package:meta/meta.dart'; @@ -35,21 +37,23 @@ base class BaseRemoteLoggingConstraintProvider BaseRemoteLoggingConstraintProvider({ required DefaultRemoteConfiguration config, required AWSCredentialsProvider credentialsProvider, - FileStorage? fileStorage, + FileStorageImpl? fileStorage, + AWSHttpClient? awsHttpClient, }) : _fileStorage = fileStorage, _config = config, - _credentialsProvider = credentialsProvider { - init(); + _credentialsProvider = credentialsProvider, + _awsHttpClient = awsHttpClient ?? AWSHttpClient() { + _init(); } - final FileStorage? _fileStorage; + final FileStorageImpl? _fileStorage; final DefaultRemoteConfiguration _config; final AWSCredentialsProvider _credentialsProvider; LoggingConstraint? _loggingConstraint; - final AWSHttpClient _awsHttpClient = AWSHttpClient(); + final AWSHttpClient _awsHttpClient; // The timer to refresh the constraint periodically. Timer? _timer; @@ -64,20 +68,8 @@ base class BaseRemoteLoggingConstraintProvider /// Initializes the [BaseRemoteLoggingConstraintProvider] by fetching /// the constraint from the endpoint initially and then /// starting the refresh timer afterwards. - Future init() async { - // Check local storage first. - if (_fileStorage != null) { - final localConstraint = - await _fileStorage!.loadConstraint('remoteloggingconstraints.json'); - if (localConstraint != null) { - _loggingConstraint = LoggingConstraint.fromJson( - jsonDecode(localConstraint) as Map, - ); - } - } - await _fetchAndCacheConstraintFromEndpoint(); - await _refreshConstraintPeriodically(); - return null; + void _init() { + _refreshConstraintPeriodically(); } /// Creates a request to fetch the constraint from the endpoint. @@ -112,14 +104,18 @@ base class BaseRemoteLoggingConstraintProvider jsonEncode(fetchedConstraint.toJson()), ); } + } else { + await _loadConstraintFromLocalCache(); } } on Exception catch (exception) { - throw Exception( + logger.debug( 'Failed to fetch logging constraint from ${_config.endpoint}: $exception', ); - } on Error catch (error) { + await _loadConstraintFromLocalCache(); + } on Error catch (error, stackTrace) { logger.error( 'Error while fetching logging constraint from ${_config.endpoint}: $error', + stackTrace, ); } } @@ -128,8 +124,18 @@ base class BaseRemoteLoggingConstraintProvider @override LoggingConstraint? get loggingConstraint => _loggingConstraint; + Future _loadConstraintFromLocalCache() async { + final localConstraint = + await _fileStorage!.loadConstraint('remoteloggingconstraints.json'); + if (localConstraint != null) { + _loggingConstraint = LoggingConstraint.fromJson( + jsonDecode(localConstraint) as Map, + ); + } + } + /// Refreshes the constraint from the endpoint periodically. - Future _refreshConstraintPeriodically() async { + void _refreshConstraintPeriodically() { if (_isRunning) { return; } @@ -141,6 +147,7 @@ base class BaseRemoteLoggingConstraintProvider _config.refreshInterval, (_) => _fetchAndCacheConstraintFromEndpoint(), ); + Timer.run(_fetchAndCacheConstraintFromEndpoint); } } @@ -155,6 +162,7 @@ final class DefaultRemoteLoggingConstraintProvider required super.config, required this.credentialsProvider, super.fileStorage, + super.awsHttpClient, }) : super(credentialsProvider: credentialsProvider); /// The credentials provider to use for signing the request. diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/test/remote_constraint_provider_test.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/test/remote_constraint_provider_test.dart new file mode 100644 index 0000000000..ded004352d --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/test/remote_constraint_provider_test.dart @@ -0,0 +1,220 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:convert'; + +import 'package:amplify_core/amplify_core.dart'; +import 'package:aws_logging_cloudwatch/aws_logging_cloudwatch.dart'; +import 'package:aws_logging_cloudwatch/src/file_storage/file_storage.dart' + if (dart.library.io) 'package:aws_logging_cloudwatch/src/file_storage/file_storage_vm.dart' + if (dart.library.html) 'package:aws_logging_cloudwatch/src/file_storage/file_storage_web.dart'; +// import 'package:aws_logging_cloudwatch/src/file_storage/file_storage_vm.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +const sampleJson = ''' + { + "defaultLogLevel": "error", + "categoryLogLevel": { + "API": "debug", + "AUTH": "debug" + }, + "userLogLevel": { + "cognito-sub-xyz-123": { + "defaultLogLevel": "verbose", + "categoryLogLevel": { + "API": "verbose", + "AUTH": "verbose" + } + } + } + } + '''; + +class MockFileStorage extends Mock implements FileStorageImpl { + MockFileStorage(); + + @override + Future saveConstraintLocally(String fileName, String content) async {} +} + +class MockAWSHttpClient extends Mock implements AWSHttpClient {} + +class MockAWSHttpOperation extends Mock + implements AWSHttpOperation {} + +class MockAWSCredentialsProvider extends Mock + implements AWSCredentialsProvider {} + +class PathProvider extends Mock implements AppPathProvider { + PathProvider(); + + @override + Future getApplicationSupportPath() async { + return ''; + } + + @override + Future getTemporaryPath() async { + return ''; + } +} + +final fakeRequest = AWSHttpRequest( + method: AWSHttpMethod.get, + uri: Uri.parse('https://fakewebsite.com'), + headers: const {}, + body: utf8.encode('sample'), +); + +void main() { + group('RemoteLoggingConstraintProvider', () { + late BaseRemoteLoggingConstraintProvider provider; + late FileStorageImpl mockFileStorage; + late MockAWSHttpClient mockAWSHttpClient; + late MockAWSCredentialsProvider mockCredentialsProvider; + late MockAWSHttpOperation mockOperation; + + const sampleJson = ''' + { + "defaultLogLevel": "error", + "categoryLogLevel": { + "API": "debug", + "AUTH": "debug" + }, + "userLogLevel": { + "cognito-sub-xyz-123": { + "defaultLogLevel": "verbose", + "categoryLogLevel": { + "API": "verbose", + "AUTH": "verbose" + } + } + } + } + '''; + + setUp(() { + mockFileStorage = MockFileStorage(); + mockAWSHttpClient = MockAWSHttpClient(); + mockCredentialsProvider = MockAWSCredentialsProvider(); + mockOperation = MockAWSHttpOperation(); + + registerFallbackValue(fakeRequest); + + // mock the response from the endpoint + when(() => mockOperation.response).thenAnswer((_) async { + return AWSHttpResponse( + statusCode: 200, + body: utf8.encode(sampleJson), + ); + }); + + // mock the call to createRequest + when(() => mockAWSHttpClient.send(any())).thenAnswer((_) { + return mockOperation; + }); + + when(() => mockFileStorage.loadConstraint(any())) + .thenAnswer((_) async => Future.value(sampleJson)); + + provider = BaseRemoteLoggingConstraintProvider( + config: const DefaultRemoteConfiguration( + refreshInterval: Duration(seconds: 10), + endpoint: 'https://example.com', + region: 'us-west-2', + ), + credentialsProvider: mockCredentialsProvider, + fileStorage: mockFileStorage, + awsHttpClient: mockAWSHttpClient, + ); + }); + + test('initializes _loggingConstraint from endpoint', () async { + await Future.delayed(const Duration(seconds: 3)); + + // Verify that _loggingConstraint exists + expect( + provider.loggingConstraint!.toJson(), + equals(json.decode(sampleJson)), + ); + }); + + test( + 'fetches _loggingConstraint from local storage and returns null if there are no constraints in local storage', + () async { + when(() => mockOperation.response).thenAnswer((_) async { + return AWSHttpResponse( + statusCode: 400, + body: utf8.encode('NO RESPONSE'), + ); + }); + + // mock load constraint returns null + when(() => mockFileStorage.loadConstraint(any())) + .thenAnswer((_) async => Future.value(null)); + + await Future.delayed(const Duration(seconds: 3)); + + // Verify that _loggingConstraint is set + expect(provider.loggingConstraint, equals(null)); + }); + + test('uses local storage if endpoint fails', () async { + when(() => mockOperation.response).thenAnswer((_) async { + return AWSHttpResponse( + statusCode: 400, + body: utf8.encode('NO RESPONSE'), + ); + }); + + when(() => mockFileStorage.loadConstraint(any())) + .thenAnswer((_) async => Future.value(sampleJson)); + + await Future.delayed(const Duration(seconds: 3)); + + // Verify that _loggingConstraint uses local storage + expect( + provider.loggingConstraint!.toJson(), + equals(json.decode(sampleJson)), + ); + }); + + test('updates constraints when endpoint returns updated constraints', + () async { + const updatedJson = ''' + { + "defaultLogLevel": "debug", + "categoryLogLevel": { + "API": "debug", + "AUTH": "error" + }, + "userLogLevel": { + "cognito-sub-xyz-123": { + "defaultLogLevel": "verbose", + "categoryLogLevel": { + "API": "error", + "AUTH": "debug" + } + } + } + } + '''; + + when(() => mockOperation.response).thenAnswer((_) async { + return AWSHttpResponse( + statusCode: 200, + body: utf8.encode(updatedJson), + ); + }); + + await Future.delayed(const Duration(seconds: 3)); + + // Verify that _loggingConstraint got updated + expect( + provider.loggingConstraint!.toJson(), + equals(json.decode(updatedJson)), + ); + }); + }); +}