Remotely change log levels
You can remotely configure the Amplify Logger, enabling you to make changes to your logging levels or user allow list in your deployed applications.
Set up remote configuration backend resources
Below is an example CDK construct that provisions the Amazon CloudWatch, AWS API Gateway, AWS Lambda, and AWS S3 bucket. The CDK construct also deploys the remote configuration file to the AWS S3 bucket which you can then update to change the configuration level or user allow list.
Permissions
The CDK construct creates IAM policies and assigns them to the Amplify authenticated and unauthenticated roles.
- The API Gateway endpoint can only be invoked by user with the Amplify authenticated or unauthenticated roles.
- The AWSCloudWatch logs can only be created and sent by users with the Amplify authenticated or unauthenticated roles. The permission policy cannot be modified to be more restrictive and scoped to specific users.
- The lambda invocation is only allowed from the provisioned AWS API Gateway resource.
- The S3 bucket containing the
remoteloggingconstraints.json
can only be read by the Lambda execution role.
Replace the placeholder values with your own values:
<log-group-name>
is the log group that logs will be sent to. Note that this CDK construct sample includes logic to create the CloudWatch log group that you may have already created in previous steps.<s3-bucket-name>
is the S3 bucket that will hold the logging constraints json file that gets fetched remotely.<amplify-authenticated-role-name>
and<amplify-unauthenticated-role-name>
are Amplify roles created as part of Amplify Auth configuration via Amplify CLI.
Resource dependencies
lambdaConfig
provides the location and lambda for reading from S3. An example is provided in the lambda handler section.configFileName
andloggingConfigLocation
provides the location and file name of the log level configuration file that is deployed to S3. An example is provided in the Creating remote configuration file section.
import * as cdk from "aws-cdk-lib"import { Construct } from "constructs"import * as apigateway from "aws-cdk-lib/aws-apigateway"import * as lambda from "aws-cdk-lib/aws-lambda"import * as s3 from "aws-cdk-lib/aws-s3"import * as logs from "aws-cdk-lib/aws-logs"import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment"import * as path from "path"import * as iam from "aws-cdk-lib/aws-iam"
export class RemoteLoggingConstraintsConstruct extends Construct { constructor(scope: Construct, id: string, props: RemoteLoggingConstraintProps) { super(scope, id)
// ** provision CloudWatch Log Group to send logs ** const region = cdk.Stack.of(this).region const account = cdk.Stack.of(this).account const logGroupName = <log-group-name> const authRoleName = <amplify-authenticated-role-name> const unAuthRoleName = <amplify-unauthenticated-role-name>
new logs.LogGroup(this, 'Log Group', { logGroupName: logGroupName, retention: logs.RetentionDays.INFINITE })
const authRole = iam.Role.fromRoleName(this, "Auth-Role", authRoleName) const unAuthRole = iam.Role.fromRoleName(this, "UnAuth-Role", unAuthRoleName) const logResource = `arn:aws:logs:${region}:${account}:log-group:${logGroupName}:log-stream:*` const logIAMPolicy = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, resources: [logResource], actions: ["logs:PutLogEvents", "logs:DescribeLogStreams", "logs:CreateLogStream"] })
authRole.addToPrincipalPolicy(logIAMPolicy) unAuthRole.addToPrincipalPolicy(logIAMPolicy)
// ** provision resource to support remote configuration (API Gateway, S3 bucket, and Lambda) ** const <loggingConfigLocation> = 'resources/config/remoteloggingconstraints.json' const <lambdaConfig> = 'resources/lambda/remoteconfig.js' const <configFileName> = 'remoteloggingconstraints.json'
const remoteConfigBucket = new s3.Bucket(this, 'AmplifyRemoteLogging-Bucket', { publicReadAccess: false, versioned: true, bucketName: <s3-bucket-name> });
new BucketDeployment(this, `AmplifyRemoteLogging-BucketDeployment`, { sources: [ Source.asset(path.dirname(path.join(<loggingConfigLocation>))), ], destinationBucket: remoteConfigBucket });
const handler = new lambda.Function(this, "AmplifyRemoteLogging-Handler", { runtime: lambda.Runtime.NODEJS_18_X, code: lambda.Code.fromAsset(path.dirname(path.join(<lambdaConfig>))), handler: "remotelogging.main", environment: { BUCKET: <s3-bucket-name>, KEY: <configFileName> } })
remoteConfigBucket.grantRead(handler)
const api = new apigateway.RestApi(this, "AmplifyRemoteLogging-API", { restApiName: "Logging API", description: "API Gateway for Remote Logging" })
const getRemoteLoggingIntegration = new apigateway.LambdaIntegration(handler) const loggingConstraints = api.root.addResource('loggingconstraints') const getLoggingConstraints = loggingConstraints.addMethod('GET', getRemoteLoggingIntegration, { authorizationType: apigateway.AuthorizationType.IAM })
const apiInvokePolicy = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, resources: [ getLoggingConstraints.methodArn ], actions: ['execute-api:Invoke'] })
authRole.addToPrincipalPolicy(apiInvokePolicy) unAuthRole.addToPrincipalPolicy(apiInvokePolicy)
new cdk.CfnOutput(this, 'APIEndpoint', { value: `https://${api.restApiId}.execute-api.${region}.amazonaws.com/prod/loggingconstraints`, }); new cdk.CfnOutput(this, 'CloudWatchLogGroupName', { value: logGroupName }); new cdk.CfnOutput(this, 'CloudWatchRegion', { value: region }); }}
The API endpoint, CloudWatch log group, and region will be printed out in the terminal. You can use this information to setup the Amplify library.
Sample Lambda handler
Below is a sample lambda that reads and returns the remoteloggingconstraints.json
from AWS S3. Note that the configuration is cached by version via the usages of ETag in this example. This lets lambda be more efficient and save bandwidth, as it does not need to resend a full configuration file if the content was not changed.
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');const s3 = new S3Client({});const bucketName = process.env.BUCKET;const key = process.env.KEY;let cachedConfig = { expiresOn: 0, ETag: '', config: ''};
exports.main = async function (event, context) { try { if (event.httpMethod === 'GET') { if (!cachedConfig.config || Date.now() > cachedConfig.expiresOn) { // refresh cache if cache is invalid const command = new GetObjectCommand({ Bucket: bucketName, Key: key }); const s3Resp = await s3.send(command); await setCachedConfig(s3Resp); }
if (event.headers['If-None-Match'] === cachedConfig.ETag) { // return 304 not modified if config has not changed return { statusCode: 304 }; } else { // return updated/modified config with latest ETag return { statusCode: 200, headers: { "'ETag'": cachedConfig.ETag }, body: cachedConfig.config }; } } } catch (error) { const resp = error.stack || JSON.stringify(error, null, 2); return { statusCode: 400, headers: {}, body: JSON.stringify(resp) }; }};
const setCachedConfig = async (s3Resp) => { cachedConfig = { expiresOn: Date.now() + 600 * 1000, //10 minutes ETag: s3Resp.ETag.replace(/\"/gi, ''), //remove \" from string config: await s3Resp.Body.transformToString() };};
Creating remote configuration file
Below is a sample remote config file that overwrites the local file in the mobile application. This file will be deployed to S3. Once deployed, you can change your application log levels by editing this file in S3.
{ "defaultLogLevel": "ERROR", "categoryLogLevel": { "API": "DEBUG", "AUTH": "DEBUG" }, "userLogLevel": { "cognito-sub-xyz-123": { "defaultLogLevel": "VERBOSE", "categoryLogLevel": { "API": "VERBOSE", "AUTH": "VERBOSE" } } }}
Enable remote configuration in your app
To enable Amplify Logger to fetch remote log levels, you will need to provide the API endpoint that has the log levels and the refresh interval for updating the remote configuration locally on the user's device.
In your application, update the amplifyconfiguration_logging
file by adding a new json section defaultRemoteConfiguration
:
{ "awsCloudWatchLoggingPlugin": { "enable": true, "logGroupName": "<log-group-name>", "region": "<region>", "localStoreMaxSizeInMB": 1, "flushIntervalInSeconds": 60, "loggingConstraints": { "defaultLogLevel": "ERROR" }, "defaultRemoteConfiguration": { "endpoint": "<your-api-endpoint>", "refreshIntervalInSeconds": 1200 } }}
Specify the remote config provider as a parameter when constructing the AWSCloudWatchLoggingPlugin
instance.
do { let endpointUrl: URL = URL(string: "<your-api-endpoint>")! let remoteConfigProvider = DefaultRemoteLoggingConstraintsProvider(endpoint: endpointUrl, region: "<region>") let loggingPlugin = AWSCloudWatchLoggingPlugin(remoteLoggingConstraintsProvider: remoteConfigProvider) try Amplify.add(plugin: loggingPlugin) try Amplify.configure(with: .amplifyOutputs)} catch { assert(false, "Error initializing Amplify: \(error)")}