Name:
interface
Value:
Amplify has re-imagined the way frontend developers build fullstack applications. Develop and deploy without the hassle.

Page updated Apr 29, 2024

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.

The logging configurations you set remotely will overwrite the local log level and persist for future app sessions.

Set up remote configuration backend resources

You can use the Amplify CLI to add custom CDK 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.

  1. The API Gateway endpoint can only be invoked by user with the Amplify authenticated or unauthenticated roles.
  2. 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.
  3. The lambda invocation is only allowed from the provisioned AWS API Gateway resource.
  4. 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 and loggingConfigLocation 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
}
}
}
LoggingConstraints loggingConstraints = new LoggingConstraints(LogLevel.WARN);
RemoteLoggingConstraintProvider remoteLoggingConstraintProvider = new DefaultRemoteLoggingConstraintProvider(<endpoint-url>, <region>)
AWSCloudWatchLoggingPluginConfiguration config = new AWSCloudWatchLoggingPluginConfiguration (<log-group-name>, <region>, loggingConstraints);
Amplify.addPlugin(new AWSCloudWatchLoggingPlugin(config, remoteLoggingConstraintProvider));
val loggingConstraints = LoggingConstraints(defaultLogLevel = LogLevel.WARN)
val remoteLoggingConstraintProvider = DefaultRemoteLoggingConstraintProvider(<endpoint-url>, <region>)
val config = AWSCloudWatchLoggingPluginConfiguration(logGroupName = <log-group-name>, region = <region>, loggingConstraints = loggingConstraints)
Amplify.addPlugin(AWSCloudWatchLoggingPlugin(config, remoteLoggingConstraintProvider))
LoggingConstraints loggingConstraints = new LoggingConstraints(LogLevel.WARN);
RemoteLoggingConstraintProvider remoteLoggingConstraintProvider = new DefaultRemoteLoggingConstraintProvider(<endpoint-url>, <region>)
AWSCloudWatchLoggingPluginConfiguration config = new AWSCloudWatchLoggingPluginConfiguration (<log-group-name>,<region>, loggingConstraints);
Amplify.addPlugin(new AWSCloudWatchLoggingPlugin(config, remoteLoggingConstraintProvider));