From 79725fb40f78781f823fb1ddc6ca471b2c248704 Mon Sep 17 00:00:00 2001 From: Kha Truong <64438356+khatruong2009@users.noreply.github.com> Date: Wed, 20 Sep 2023 11:26:11 -0700 Subject: [PATCH] feat(logging): default remote config (#3643) * feat(logging): add cloudwatch logger plugin * feat(logging): default remote config chore: moved declarations to improve dart syntax chore: use logger instead of print statement chore: added sigv4signer to sign the http request chore: switched Future.delayed to Timer and implemented jsonserializable chore: roll back unnecessary changes from cloudwatch_logger_plugin chore: removed an extra line of fetchAndCacheConstraintFromEndpoint chore: moved saveConstraintLocally out of fetchConstraintFromEndpoint and moved into fetchAndCacheFromEndpoint fix: seeing if moving the queued_item_store_test.dart file into main test directory will fix testing issue Revert "fix: seeing if moving the queued_item_store_test.dart file into main test directory will fix testing issue" This reverts commit f0337131e988eb8e970335dd6a178d5584c9b5d6. chore: added ignore line for try catch block and removed unnecessary method chore: added web implementation of remote_constraint_provider fix: try changing path provider dependency to fix test fix: added flutter sdk to dependencies chore: generated workflows after adding dependencies fix: fixed indentation issue in pubspec chore: generate new workflows chore: changed file and folder names for file_storage chore: fix import for renamed files, added region as a required field to DefaultRemoteConfiguration and fixed scope variable to adjust for that. Also moved scope variable to inside fetchConstraintFromEndpoint method replaced AmplifyLogger with AWSLoggerMixin instead. chore: made signer private and used AWSHttpRequest.get method directly rather than calling it through AWSHttpRequest use stoppable timer instead of while loop chore: updated LoggingConstraint class to match the remote config file chore: added AWSHeaders.accept to the headers for the request chore: added public documentation and trailing comma chore: removed unnecessary time checks in the getter if statement chore: removed initialize() method and put it into the constructor chore: removed unnecessary lastUpdate variable now that it is no longer checked in the getter chore: added a comment and removed flutter plugin files chore: fixed some formatting chore: removed the InSeconds part of the refreshIntervalInSeconds property because it's a Duration, which isn't specific to seconds chore: fixed comment to reflect change in fetchInterval and flushInterval property name change chore: make isRunning variable privatge chore: corrected error handling in the fetchAndCacheConstraintFromEndpoint method chore: add license headers to storage implementations chore: added path for join() method to ensure windows compatibility chore: removed dart:io dependency, changed HttpHeaders.acceptHeader to AWSHeaders.accept, changed getter to just retrieve from cache chore: remove flutter dependency chore: removed http and replaced with AWSHttpClient chore: removed unnecessary stop method chore: remove .flutter-plugins files chore: moved constructor initializers into a separate init function so that I can use async await chore: added local storage check before fetching in the initialize function chore: refactored DefaultRemoteLoggingConstraintProvider to separate createRequest from the rest of the class, allowing for signed and unsigned requests to be sent chore: used private global variable for local storage key chore: refacted fileStore dart implementation * chore: dart FileStorage refactor and unit tests added * chore: made the Future.delayed more explicit * chore: added flutter path provider to amplify_logging_cloudwatch * chore: removed fluter dependency * chore: remove flutter plugin files from git ignore now that flutter dependency is removed * chore: changed names of file storage functions, logger level, and removed flutter plugin gitignore * chore: remove flutter plugin .gitignore * chore: Added completer, removed conditional import, added closeable, removed credentials provider from base class and only added to defaultconstraintprovider, changed fileStorage to use FileStorage instead of just FileStorageImpl, removed isRunning since it is never false after the first run, added completer so that delay isn't needed for tests, removed the catching of errors, added credentials provider parameter to awssigv4signer, * chore: updated aft workflows * Update packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.dart Co-authored-by: Dillon Nys <24740863+dnys1@users.noreply.github.com> * Update packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.dart Co-authored-by: Dillon Nys <24740863+dnys1@users.noreply.github.com> * chore: removed manual toJson and replace jsonSerializable with zAmplifySerializable * Update packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart Co-authored-by: Dillon Nys <24740863+dnys1@users.noreply.github.com> * Update packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart Co-authored-by: Dillon Nys <24740863+dnys1@users.noreply.github.com> * chore: use factory constructor and _fileStorage might be null in remoteConstraint * chore: fixed local storage variable and method name and also added and fixed tests * Update packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart Co-authored-by: Dillon Nys <24740863+dnys1@users.noreply.github.com> * Update packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart Co-authored-by: Dillon Nys <24740863+dnys1@users.noreply.github.com> * Update packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart Co-authored-by: Dillon Nys <24740863+dnys1@users.noreply.github.com> * Update packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart Co-authored-by: Dillon Nys <24740863+dnys1@users.noreply.github.com> * chore: add AWSSerializable and added comment to test to address Future.delayed * chore: fixed some formatting and removed todo comment * chore: removed unnecessary comments * Update packages/logging_cloudwatch/aws_logging_cloudwatch/test/remote_constraint_provider_test.dart Co-authored-by: NikaHsn * Update packages/logging_cloudwatch/aws_logging_cloudwatch/test/remote_constraint_provider_test.dart Co-authored-by: NikaHsn * Update packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart Co-authored-by: NikaHsn --------- Co-authored-by: Nika Hassani Co-authored-by: Dillon Nys <24740863+dnys1@users.noreply.github.com> Co-authored-by: NikaHsn --- .../path_provider/flutter_path_provider.dart | 23 ++ .../lib/src/cloudwatch_logger_plugin.dart | 6 +- .../lib/src/file_storage/file_storage.dart | 19 ++ .../src/file_storage/file_storage_stub.dart | 24 ++ .../lib/src/file_storage/file_storage_vm.dart | 34 +++ .../src/file_storage/file_storage_web.dart | 26 ++ .../lib/src/plugin_config.dart | 60 ++++- .../lib/src/plugin_config.g.dart | 76 ++++++ .../lib/src/remote_constraint_provider.dart | 193 +++++++++++++-- .../aws_logging_cloudwatch/pubspec.yaml | 2 + .../test/remote_constraint_provider_test.dart | 227 ++++++++++++++++++ 11 files changed, 665 insertions(+), 25 deletions(-) create mode 100644 packages/logging_cloudwatch/amplify_logging_cloudwatch/lib/src/path_provider/flutter_path_provider.dart create mode 100644 packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart create mode 100644 packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_stub.dart create mode 100644 packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_vm.dart create mode 100644 packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_web.dart create mode 100644 packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.g.dart create mode 100644 packages/logging_cloudwatch/aws_logging_cloudwatch/test/remote_constraint_provider_test.dart diff --git a/packages/logging_cloudwatch/amplify_logging_cloudwatch/lib/src/path_provider/flutter_path_provider.dart b/packages/logging_cloudwatch/amplify_logging_cloudwatch/lib/src/path_provider/flutter_path_provider.dart new file mode 100644 index 0000000000..d19c258c22 --- /dev/null +++ b/packages/logging_cloudwatch/amplify_logging_cloudwatch/lib/src/path_provider/flutter_path_provider.dart @@ -0,0 +1,23 @@ +// 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:path_provider/path_provider.dart'; + +/// {@template flutter_path_provider} +/// A [AppPathProvider] that uses the [path_provider](https://pub.dev/packages/path_provider) +/// package to get the application support and temporary paths. +/// {@endtemplate} +class FlutterPathProvider implements AppPathProvider { + @override + Future getApplicationSupportPath() async { + final directory = await getApplicationSupportDirectory(); + return directory.path; + } + + @override + Future getTemporaryPath() async { + final directory = await getTemporaryDirectory(); + return directory.path; + } +} diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/cloudwatch_logger_plugin.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/cloudwatch_logger_plugin.dart index 22fe2c1cbe..b3eecf74bb 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/cloudwatch_logger_plugin.dart +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/cloudwatch_logger_plugin.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'dart:math'; -import 'package:aws_common/aws_common.dart'; +import 'package:amplify_core/amplify_core.dart'; import 'package:aws_logging_cloudwatch/aws_logging_cloudwatch.dart'; import 'package:aws_logging_cloudwatch/src/sdk/cloud_watch_logs.dart'; import 'package:aws_logging_cloudwatch/src/stoppable_timer.dart'; @@ -76,9 +76,9 @@ class CloudWatchLoggerPlugin extends AWSLoggerPlugin region: pluginConfig.region, credentialsProvider: credentialsProvider, ) { - _timer = pluginConfig.flushIntervalInSeconds > Duration.zero + _timer = pluginConfig.flushInterval > Duration.zero ? StoppableTimer( - duration: pluginConfig.flushIntervalInSeconds, + duration: pluginConfig.flushInterval, callback: _startSyncingIfNotInProgress, onError: _onTimerError, ) 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 new file mode 100644 index 0000000000..94627270bc --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart @@ -0,0 +1,19 @@ +// 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_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 { + /// Default Constructor or FileStorage + factory FileStorage(AppPathProvider pathProvider) = FileStorageImpl; + + /// Save constraint locally to file + Future save(String fileName, String data); + + /// Load constraint from file + Future load(String fileName); +} 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..6b8971325c --- /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 load(String fileName) async { + throw UnimplementedError(); + } + + @override + Future save(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 new file mode 100644 index 0000000000..30c39773e0 --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_vm.dart @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:io'; + +import 'package:amplify_core/amplify_core.dart'; +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 +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 load(String fileName) async { + final file = + File(p.join(await pathProvider.getApplicationSupportPath(), fileName)); + if (await file.exists()) { + return file.readAsString(); + } + return null; + } + + @override + Future save(String fileName, String content) async { + final file = + File(p.join(await pathProvider.getApplicationSupportPath(), fileName)); + await file.writeAsString(content); + } +} 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 new file mode 100644 index 0000000000..4475a00b62 --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_web.dart @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:html'; + +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 + // ignore: avoid_unused_constructor_parameters + FileStorageImpl(AppPathProvider pathProvider); + + static const _prefix = 'aws.cloudwatch'; + + @override + Future load(String fileName) async { + return window.localStorage['$_prefix.$fileName']; + } + + @override + Future save(String fileName, String content) async { + window.localStorage['$_prefix.$fileName'] = content; + } +} 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 9fd10d18d0..3367eb505c 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 @@ -1,9 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import 'package:aws_common/aws_common.dart'; +import 'package:amplify_core/amplify_core.dart'; import 'package:aws_logging_cloudwatch/aws_logging_cloudwatch.dart'; +part 'plugin_config.g.dart'; + /// {@template aws_logging_cloudwatch.cloudwatch_logger_plugin_configuration} /// The configuration for `CloudWatchLoggerPlugin`. /// {@endtemplate} @@ -15,7 +17,7 @@ class CloudWatchLoggerPluginConfiguration with AWSDebuggable { required this.localLoggingConstraint, this.enable = true, this.localStoreMaxSizeInMB = 5, - this.flushIntervalInSeconds = const Duration(seconds: 60), + this.flushInterval = const Duration(seconds: 60), this.defaultRemoteConfiguration, }); @@ -31,8 +33,8 @@ class CloudWatchLoggerPluginConfiguration with AWSDebuggable { /// The max size of the local store in MB to be used for storing logs locally. final int localStoreMaxSizeInMB; - /// The duration in seconds for sending locally stored logs to CloudWatch. - final Duration flushIntervalInSeconds; + /// The duration for sending locally stored logs to CloudWatch. + final Duration flushInterval; /// {@macro aws_logging_cloudwatch.logging_constraint} final LoggingConstraint localLoggingConstraint; @@ -47,13 +49,59 @@ class CloudWatchLoggerPluginConfiguration with AWSDebuggable { /// {@template aws_logging_cloudwatch.logging_constraint} /// The logging constraint for sending logs to CloudWatch. /// {@endtemplate} -class LoggingConstraint with AWSDebuggable { +@zAmplifySerializable +class LoggingConstraint with AWSDebuggable, AWSSerializable { /// {@macro aws_logging_cloudwatch.logging_constraint} - const LoggingConstraint({this.defaultLogLevel = LogLevel.error}); + const LoggingConstraint({ + this.defaultLogLevel = LogLevel.error, + this.categoryLogLevel, + this.userLogLevel, + }); + + /// Converts a [Map] to an [LoggingConstraint] instance. + factory LoggingConstraint.fromJson(Map json) => + _$LoggingConstraintFromJson(json); + + /// Converts an [LoggingConstraint] instance to a [Map]. + @override + Map toJson() => _$LoggingConstraintToJson(this); /// The default [LogLevel] for sending logs to CloudWatch. final LogLevel defaultLogLevel; + /// The [LogLevel] for different categories. + final Map? categoryLogLevel; + + /// The [LogLevel] for different users. + final Map? userLogLevel; + @override String get runtimeTypeName => 'LoggingConstraint'; } + +/// The logging constraint for user specific log level. +@zAmplifySerializable +class UserLogLevel with AWSDebuggable, AWSSerializable { + /// The logging constraint for user specific log level. + const UserLogLevel({ + this.defaultLogLevel, + this.categoryLogLevel, + }); + + ///Converts a [Map] to a [UserLogLevel] instance. + factory UserLogLevel.fromJson(Map json) => + _$UserLogLevelFromJson(json); + + /// Converts a [UserLogLevel] instance to a [Map]. + @override + Map toJson() => _$UserLogLevelToJson(this); + + /// The default [LogLevel] for sending logs to CloudWatch. + final LogLevel? defaultLogLevel; + + /// The [LogLevel] for different categories. + final Map? categoryLogLevel; + + @override + String get runtimeTypeName => 'UserLogLevel'; +} 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 new file mode 100644 index 0000000000..64733e29d2 --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.g.dart @@ -0,0 +1,76 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'plugin_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LoggingConstraint _$LoggingConstraintFromJson(Map json) => + LoggingConstraint( + defaultLogLevel: + $enumDecodeNullable(_$LogLevelEnumMap, json['defaultLogLevel']) ?? + LogLevel.error, + categoryLogLevel: + (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) { + final val = { + 'defaultLogLevel': _$LogLevelEnumMap[instance.defaultLogLevel]!, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull( + 'categoryLogLevel', + instance.categoryLogLevel + ?.map((k, e) => MapEntry(k, _$LogLevelEnumMap[e]!))); + writeNotNull('userLogLevel', + instance.userLogLevel?.map((k, e) => MapEntry(k, e.toJson()))); + return val; +} + +const _$LogLevelEnumMap = { + LogLevel.verbose: 'verbose', + LogLevel.debug: 'debug', + LogLevel.info: 'info', + LogLevel.warn: 'warn', + LogLevel.error: 'error', + LogLevel.none: 'none', +}; + +UserLogLevel _$UserLogLevelFromJson(Map json) => UserLogLevel( + defaultLogLevel: + $enumDecodeNullable(_$LogLevelEnumMap, json['defaultLogLevel']), + categoryLogLevel: + (json['categoryLogLevel'] as Map?)?.map( + (k, e) => MapEntry(k, $enumDecode(_$LogLevelEnumMap, e)), + ), + ); + +Map _$UserLogLevelToJson(UserLogLevel instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('defaultLogLevel', _$LogLevelEnumMap[instance.defaultLogLevel]); + writeNotNull( + 'categoryLogLevel', + instance.categoryLogLevel + ?.map((k, e) => MapEntry(k, _$LogLevelEnumMap[e]!))); + return val; +} 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 7a8a36332f..ff6e0b31a1 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 @@ -1,11 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// TODO(nikahsn): remove after implementing the get loggingConstraint. -// ignore_for_file: unused_field +import 'dart:async'; +import 'dart:convert'; +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_signature_v4/aws_signature_v4.dart'; +import 'package:meta/meta.dart'; /// {@template aws_logging_cloudwatch.remote_logging_constraint_provider} /// An Interface to provide custom implementation for @@ -17,40 +21,197 @@ abstract class RemoteLoggingConstraintProvider { LoggingConstraint? get loggingConstraint; } +/// {@template aws_logging_cloudwatch.base_remote_constraints_provider} +/// Base class for [RemoteLoggingConstraintProvider] to provide +/// [LoggingConstraint] from a remote location and cache it. +/// {@endtemplate} +base class BaseRemoteLoggingConstraintProvider + with AWSDebuggable, AWSLoggerMixin + implements RemoteLoggingConstraintProvider, Closeable { + /// {@macro aws_logging_cloudwatch.base_remote_constraints_provider} + BaseRemoteLoggingConstraintProvider({ + required DefaultRemoteConfiguration config, + FileStorage? fileStorage, + }) : _fileStorage = fileStorage, + _config = config, + _awsHttpClient = AWSHttpClient() { + _init(); + } + + /// A [BaseRemoteLoggingConstraintProvider] to use only for testing. + @protected + @visibleForTesting + BaseRemoteLoggingConstraintProvider.forTesting({ + required DefaultRemoteConfiguration config, + required AWSHttpClient awsHttpClient, + FileStorage? fileStorage, + }) : _fileStorage = fileStorage, + _config = config, + _awsHttpClient = awsHttpClient { + _init(); + } + + final FileStorage? _fileStorage; + + final DefaultRemoteConfiguration _config; + + LoggingConstraint? _loggingConstraint; + + final AWSHttpClient _awsHttpClient; + + static const _cacheFileName = 'remoteloggingconstraints.json'; + + // The timer to refresh the constraint periodically. + Timer? _timer; + + /// Retrives the runtime type name used for logging. + @override + String get runtimeTypeName => 'BaseRemoteConstraintsProvider'; + + /// Initializes the [BaseRemoteLoggingConstraintProvider] by fetching + /// the constraint from the endpoint initially and then + /// starting the refresh timer afterwards. + void _init() { + _readyCompleter.complete(_refreshConstraintPeriodically()); + } + + final Completer _readyCompleter = Completer(); + + /// A future that completes when the [BaseRemoteLoggingConstraintProvider] + Future get ready => _readyCompleter.future; + + /// Creates a request to fetch the constraint from the endpoint. + /// + /// Can be overridden by subclasses to change how the request is created. + @protected + @visibleForOverriding + Future createRequest() async { + final uri = Uri.parse(_config.endpoint); + return AWSHttpRequest( + method: AWSHttpMethod.get, + uri: uri, + headers: const { + AWSHeaders.accept: 'application/json; charset=utf-8', + }, + ); + } + + /// Fetches the constraint from the endpoint and caches it. + Future _fetchAndCacheConstraintFromEndpoint() async { + try { + final request = await createRequest(); + final operation = _awsHttpClient.send(request); + final response = await operation.response; + final body = await response.decodeBody(); + if (response.statusCode != 200) { + logger + .error('Failed to fetch constraints', (response.statusCode, body)); + return; + } + final fetchedConstraint = LoggingConstraint.fromJson( + jsonDecode(body) as Map, + ); + _loggingConstraint = fetchedConstraint; + + if (_fileStorage != null) { + await _fileStorage?.save( + _cacheFileName, + jsonEncode(fetchedConstraint.toJson()), + ); + } + } on Exception catch (exception) { + logger.error( + 'Failed to fetch logging constraint from ${_config.endpoint}: $exception', + ); + } + } + + /// Returns [LoggingConstraint] from the cache or `null` if the cache is missing + /// or if the constraints could not be retrieved from the remote server. + @override + LoggingConstraint? get loggingConstraint => _loggingConstraint; + + Future _loadConstraintFromLocalStorage() async { + final localConstraint = await _fileStorage?.load(_cacheFileName); + if (localConstraint != null) { + _loggingConstraint = LoggingConstraint.fromJson( + jsonDecode(localConstraint) as Map, + ); + } + } + + /// Refreshes the constraint from the endpoint periodically. + Future _refreshConstraintPeriodically() async { + await _loadConstraintFromLocalStorage(); + _timer = Timer.periodic( + _config.refreshInterval, + (_) => _fetchAndCacheConstraintFromEndpoint(), + ); + await _fetchAndCacheConstraintFromEndpoint(); + } + + @override + void close() { + _timer?.cancel(); + _timer = null; + } +} + /// {@template aws_logging_cloudwatch.default_remote_logging_constraint_provider} /// Default implementation of [RemoteLoggingConstraintProvider] to fetch /// [LoggingConstraint] from an http endpoint periodically. /// {@endtemplate} -class DefaultRemoteLoggingConstraintProvider - implements RemoteLoggingConstraintProvider { +final class DefaultRemoteLoggingConstraintProvider + extends BaseRemoteLoggingConstraintProvider { /// {@macro aws_logging_cloudwatch.default_remote_logging_constraint_provider} DefaultRemoteLoggingConstraintProvider({ - required DefaultRemoteConfiguration config, - required AWSCredentialsProvider credentialsProvider, - }) : _config = config, - _credentialsProvider = credentialsProvider; + required super.config, + required this.credentialsProvider, + super.fileStorage, + }); - final DefaultRemoteConfiguration _config; - final AWSCredentialsProvider _credentialsProvider; + /// The credentials provider to use for signing the request. + final AWSCredentialsProvider credentialsProvider; + + /// The signer to use for signing the request. + late final AWSSigV4Signer _signer = AWSSigV4Signer( + credentialsProvider: credentialsProvider, + ); @override - // TODO(nikahsn): add implementation. - LoggingConstraint get loggingConstraint => throw UnimplementedError(); + Future createRequest() async { + final baseRequest = await super.createRequest(); + final scope = AWSCredentialScope( + region: _config.region, + service: AWSService.apiGatewayManagementApi, + ); + + final signedRequest = await _signer.sign( + baseRequest, + credentialScope: scope, + ); + + return signedRequest; + } } /// {@template aws_logging_cloudwatch.default_remote_configuration} -/// The configuration for [DefaultRemoteLoggingConstraintProvider] +/// The configuration for [BaseRemoteLoggingConstraintProvider] /// {@endtemplate} class DefaultRemoteConfiguration { /// {@macro aws_logging_cloudwatch.default_remote_configuration} const DefaultRemoteConfiguration({ required this.endpoint, - this.refreshIntervalInSeconds = const Duration(seconds: 1200), + this.refreshInterval = const Duration(seconds: 1200), + required this.region, }); /// The endpoint to fetch the `loggingConstraint`. final String endpoint; - /// The referesh interval in seconds to fetch the `loggingConstraint`. - final Duration refreshIntervalInSeconds; + /// The referesh interval to fetch the `loggingConstraint`. + final Duration refreshInterval; + + /// The region of the endpoint. + final String region; } diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/pubspec.yaml b/packages/logging_cloudwatch/aws_logging_cloudwatch/pubspec.yaml index 9d2e09f7d3..560eb790c3 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/pubspec.yaml +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/pubspec.yaml @@ -16,7 +16,9 @@ dependencies: built_value: ">=8.6.0 <8.7.0" fixnum: ^1.1.0 intl: ">=0.18.0 <1.0.0" + json_annotation: ^4.8.1 meta: ^1.9.1 + path: ^1.8.0 smithy: ^0.5.0+3 smithy_aws: ^0.5.0+3 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..cd1e4ba6ce --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/test/remote_constraint_provider_test.dart @@ -0,0 +1,227 @@ +// 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_common/testing.dart'; +import 'package:aws_logging_cloudwatch/aws_logging_cloudwatch.dart'; +import 'package:aws_logging_cloudwatch/src/file_storage/file_storage.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 FileStorage {} + +class MockAWSCredentialsProvider extends Mock + implements AWSCredentialsProvider {} + +class PathProvider implements AppPathProvider { + @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() { + late BaseRemoteLoggingConstraintProvider provider; + late FileStorage mockFileStorage; + late MockAWSHttpClient mockAWSHttpClient; + test('LoggingConstraint', () { + final sampleJsonMap = jsonDecode(sampleJson) as Map; + final loggingConstraint = LoggingConstraint.fromJson(sampleJsonMap); + expect( + loggingConstraint.toJson(), + sampleJsonMap, + ); + }); + + group('RemoteLoggingConstraintProvider', () { + setUp(() { + mockFileStorage = MockFileStorage(); + + registerFallbackValue(fakeRequest); + }); + + test('initializes loggingConstraint from endpoint', () async { + when(() => mockFileStorage.load(any())) + .thenAnswer((_) async => Future.value(sampleJson)); + + when(() => mockFileStorage.save(any(), any())).thenAnswer((_) async {}); + mockAWSHttpClient = MockAWSHttpClient((request, _) { + return AWSHttpResponse( + statusCode: 200, + body: utf8.encode(sampleJson), + ); + }); + provider = BaseRemoteLoggingConstraintProvider.forTesting( + config: const DefaultRemoteConfiguration( + refreshInterval: Duration(seconds: 1200), + endpoint: 'https://example.com', + region: 'us-west-2', + ), + fileStorage: mockFileStorage, + awsHttpClient: mockAWSHttpClient, + ); + + await provider.ready; + + expect( + provider.loggingConstraint!.toJson(), + equals(json.decode(sampleJson)), + ); + }); + + test('uses local storage if endpoint fails', () async { + when(() => mockFileStorage.load(any())) + .thenAnswer((_) async => Future.value(sampleJson)); + when(() => mockFileStorage.save(any(), any())).thenAnswer((_) async {}); + mockAWSHttpClient = MockAWSHttpClient((request, _) { + return AWSHttpResponse( + statusCode: 400, + body: utf8.encode('NO RESPONSE'), + ); + }); + provider = BaseRemoteLoggingConstraintProvider.forTesting( + config: const DefaultRemoteConfiguration( + refreshInterval: Duration(seconds: 10), + endpoint: 'https://example.com', + region: 'us-west-2', + ), + fileStorage: mockFileStorage, + awsHttpClient: mockAWSHttpClient, + ); + + await provider.ready; + + 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" + } + } + } + } + '''; + // Mocking the endpoint to return updated constraints on the second call + var callCount = 0; + var callCount2 = 0; + when(() => mockFileStorage.load(any())).thenAnswer((_) async { + callCount2++; + if (callCount2 == 1) { + return Future.value(sampleJson); + } else { + return Future.value(updatedJson); + } + }); + when(() => mockFileStorage.save(any(), any())).thenAnswer((_) async {}); + mockAWSHttpClient = MockAWSHttpClient((request, _) { + callCount++; + if (callCount == 1) { + return AWSHttpResponse( + statusCode: 200, + body: utf8.encode(sampleJson), + ); + } else { + return AWSHttpResponse( + statusCode: 200, + body: utf8.encode(updatedJson), + ); + } + }); + provider = BaseRemoteLoggingConstraintProvider.forTesting( + config: const DefaultRemoteConfiguration( + refreshInterval: Duration(seconds: 1), + endpoint: 'https://example.com', + region: 'us-west-2', + ), + fileStorage: mockFileStorage, + awsHttpClient: mockAWSHttpClient, + ); + + // Wait for the provider to refresh the constraints and make the second call to the endpoint + await Future.delayed(const Duration(seconds: 2)); + + await provider.ready; + + expect( + provider.loggingConstraint!.toJson(), + equals(json.decode(updatedJson)), + ); + }); + + test( + 'fetches loggingConstraint from local storage and returns null if there are no constraints in local storage', + () async { + when(() => mockFileStorage.load(any())) + .thenAnswer((_) async => Future.value(null)); + when(() => mockFileStorage.save(any(), any())).thenAnswer((_) async {}); + mockAWSHttpClient = MockAWSHttpClient((request, _) { + return AWSHttpResponse( + statusCode: 400, + body: utf8.encode('NO RESPONSE'), + ); + }); + provider = BaseRemoteLoggingConstraintProvider.forTesting( + config: const DefaultRemoteConfiguration( + refreshInterval: Duration(seconds: 10), + endpoint: 'https://example.com', + region: 'us-west-2', + ), + fileStorage: mockFileStorage, + awsHttpClient: mockAWSHttpClient, + ); + + await provider.ready; + + expect(provider.loggingConstraint, equals(null)); + }); + }); +}