Page updated Jan 16, 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.
1import * as cdk from "aws-cdk-lib"
2import { Construct } from "constructs"
3import * as apigateway from "aws-cdk-lib/aws-apigateway"
4import * as lambda from "aws-cdk-lib/aws-lambda"
5import * as s3 from "aws-cdk-lib/aws-s3"
6import * as logs from "aws-cdk-lib/aws-logs"
7import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment"
8import * as path from "path"
9import * as iam from "aws-cdk-lib/aws-iam"
10
11export class RemoteLoggingConstraintsConstruct extends Construct {
12 constructor(scope: Construct, id: string, props: RemoteLoggingConstraintProps) {
13 super(scope, id)
14
15 // ** provision CloudWatch Log Group to send logs **
16 const region = cdk.Stack.of(this).region
17 const account = cdk.Stack.of(this).account
18 const logGroupName = <log-group-name>
19 const authRoleName = <amplify-authenticated-role-name>
20 const unAuthRoleName = <amplify-unauthenticated-role-name>
21
22 new logs.LogGroup(this, 'Log Group', {
23 logGroupName: logGroupName,
24 retention: logs.RetentionDays.INFINITE
25 })
26
27 const authRole = iam.Role.fromRoleName(this, "Auth-Role", authRoleName)
28 const unAuthRole = iam.Role.fromRoleName(this, "UnAuth-Role", unAuthRoleName)
29 const logResource = `arn:aws:logs:${region}:${account}:log-group:${logGroupName}:log-stream:*`
30 const logIAMPolicy = new iam.PolicyStatement({
31 effect: iam.Effect.ALLOW,
32 resources: [logResource],
33 actions: ["logs:PutLogEvents", "logs:DescribeLogStreams", "logs:CreateLogStream"]
34 })
35
36 authRole.addToPrincipalPolicy(logIAMPolicy)
37 unAuthRole.addToPrincipalPolicy(logIAMPolicy)
38
39 // ** provision resource to support remote configuration (API Gateway, S3 bucket, and Lambda) **
40 const <loggingConfigLocation> = 'resources/config/remoteloggingconstraints.json'
41 const <lambdaConfig> = 'resources/lambda/remoteconfig.js'
42 const <configFileName> = 'remoteloggingconstraints.json'
43
44 const remoteConfigBucket = new s3.Bucket(this, 'AmplifyRemoteLogging-Bucket', {
45 publicReadAccess: false,
46 versioned: true,
47 bucketName: <s3-bucket-name>
48 });
49
50 new BucketDeployment(this, `AmplifyRemoteLogging-BucketDeployment`, {
51 sources: [
52 Source.asset(path.dirname(path.join(<loggingConfigLocation>))),
53 ],
54 destinationBucket: remoteConfigBucket
55 });
56
57 const handler = new lambda.Function(this, "AmplifyRemoteLogging-Handler", {
58 runtime: lambda.Runtime.NODEJS_18_X,
59 code: lambda.Code.fromAsset(path.dirname(path.join(<lambdaConfig>))),
60 handler: "remotelogging.main",
61 environment: {
62 BUCKET: <s3-bucket-name>,
63 KEY: <configFileName>
64 }
65 })
66
67 remoteConfigBucket.grantRead(handler)
68
69 const api = new apigateway.RestApi(this, "AmplifyRemoteLogging-API", {
70 restApiName: "Logging API",
71 description: "API Gateway for Remote Logging"
72 })
73
74 const getRemoteLoggingIntegration = new apigateway.LambdaIntegration(handler)
75 const loggingConstraints = api.root.addResource('loggingconstraints')
76 const getLoggingConstraints = loggingConstraints.addMethod('GET', getRemoteLoggingIntegration, {
77 authorizationType: apigateway.AuthorizationType.IAM
78 })
79
80 const apiInvokePolicy = new iam.PolicyStatement({
81 effect: iam.Effect.ALLOW,
82 resources: [ getLoggingConstraints.methodArn ],
83 actions: ['execute-api:Invoke']
84 })
85
86 authRole.addToPrincipalPolicy(apiInvokePolicy)
87 unAuthRole.addToPrincipalPolicy(apiInvokePolicy)
88
89 new cdk.CfnOutput(this, 'APIEndpoint', {
90 value: `https://${api.restApiId}.execute-api.${region}.amazonaws.com/prod/loggingconstraints`,
91 });
92 new cdk.CfnOutput(this, 'CloudWatchLogGroupName', { value: logGroupName });
93 new cdk.CfnOutput(this, 'CloudWatchRegion', { value: region });
94 }
95}

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.

1const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
2const s3 = new S3Client({});
3const bucketName = process.env.BUCKET;
4const key = process.env.KEY;
5let cachedConfig = {
6 expiresOn: 0,
7 ETag: '',
8 config: ''
9};
10
11exports.main = async function (event, context) {
12 try {
13 if (event.httpMethod === 'GET') {
14 if (!cachedConfig.config || Date.now() > cachedConfig.expiresOn) {
15 // refresh cache if cache is invalid
16 const command = new GetObjectCommand({ Bucket: bucketName, Key: key });
17 const s3Resp = await s3.send(command);
18 await setCachedConfig(s3Resp);
19 }
20
21 if (event.headers['If-None-Match'] === cachedConfig.ETag) {
22 // return 304 not modified if config has not changed
23 return {
24 statusCode: 304
25 };
26 } else {
27 // return updated/modified config with latest ETag
28 return {
29 statusCode: 200,
30 headers: { "'ETag'": cachedConfig.ETag },
31 body: cachedConfig.config
32 };
33 }
34 }
35 } catch (error) {
36 const resp = error.stack || JSON.stringify(error, null, 2);
37 return {
38 statusCode: 400,
39 headers: {},
40 body: JSON.stringify(resp)
41 };
42 }
43};
44
45const setCachedConfig = async (s3Resp) => {
46 cachedConfig = {
47 expiresOn: Date.now() + 600 * 1000, //10 minutes
48 ETag: s3Resp.ETag.replace(/\"/gi, ''), //remove \" from string
49 config: await s3Resp.Body.transformToString()
50 };
51};

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.

1{
2 "defaultLogLevel": "ERROR",
3 "categoryLogLevel": {
4 "API": "DEBUG",
5 "AUTH": "DEBUG"
6 },
7 "userLogLevel": {
8 "cognito-sub-xyz-123": {
9 "defaultLogLevel": "VERBOSE",
10 "categoryLogLevel": {
11 "API": "VERBOSE",
12 "AUTH": "VERBOSE"
13 }
14 }
15 }
16}

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:

1{
2 "awsCloudWatchLoggingPlugin": {
3 "enable": true,
4 "logGroupName": "<log-group-name>",
5 "region": "<region>",
6 "localStoreMaxSizeInMB": 1,
7 "flushIntervalInSeconds": 60,
8 "loggingConstraints": {
9 "defaultLogLevel": "ERROR"
10 },
11 "defaultRemoteConfiguration": {
12 "endpoint": "<your-api-endpoint>",
13 "refreshIntervalInSeconds": 1200
14 }
15 }
16}

Specify the remote config provider as a parameter when constructing the AWSCloudWatchLoggingPlugin instance.

1do {
2 let endpointUrl: URL = URL(string: "<your-api-endpoint>")!
3 let remoteConfigProvider = DefaultRemoteLoggingConstraintsProvider(endpoint: endpointUrl, region: "<region>")
4 let loggingPlugin = AWSCloudWatchLoggingPlugin(remoteLoggingConstraintsProvider: remoteConfigProvider)
5 try Amplify.add(plugin: loggingPlugin)
6 try Amplify.configure()
7} catch {
8 assert(false, "Error initializing Amplify: \(error)")
9}