From dc4378e567cb7a1f9ccaecdd4f84bef37d28decc Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Mon, 14 Sep 2026 12:13:05 -0700 Subject: [PATCH 1/2] feat: add cloudwatch client docs for Swift and Kotlin --- src/directory/directory.mjs | 7 + .../logging/cloudwatch/index.mdx | 86 +++ .../frontend/logging/cloudwatch/index.mdx | 512 ++++++++++++++++++ 3 files changed, 605 insertions(+) create mode 100644 src/pages/[platform]/build-a-backend/add-aws-services/logging/cloudwatch/index.mdx create mode 100644 src/pages/[platform]/frontend/logging/cloudwatch/index.mdx diff --git a/src/directory/directory.mjs b/src/directory/directory.mjs index a3fdb7d1f52..8e6b7ef36ab 100644 --- a/src/directory/directory.mjs +++ b/src/directory/directory.mjs @@ -617,6 +617,10 @@ export const directory = { { path: 'src/pages/[platform]/build-a-backend/add-aws-services/logging/view-logs/index.mdx', section: 'backend' + }, + { + path: 'src/pages/[platform]/build-a-backend/add-aws-services/logging/cloudwatch/index.mdx', + section: 'backend' } ] }, @@ -1038,6 +1042,9 @@ export const directory = { }, { path: 'src/pages/[platform]/frontend/logging/sdk/index.mdx' + }, + { + path: 'src/pages/[platform]/frontend/logging/cloudwatch/index.mdx' } ] }, diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/logging/cloudwatch/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/logging/cloudwatch/index.mdx new file mode 100644 index 00000000000..153fc3fe45b --- /dev/null +++ b/src/pages/[platform]/build-a-backend/add-aws-services/logging/cloudwatch/index.mdx @@ -0,0 +1,86 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'CloudWatch Logs', + description: 'Set up an Amazon CloudWatch log group and configure IAM permissions for the Amplify CloudWatch client.', + platforms: [ + 'android', + 'swift' + ], +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + +Use the [AWS Cloud Development Kit (AWS CDK)](https://docs.aws.amazon.com/cdk/latest/guide/home.html) to create an [Amazon CloudWatch log group](https://aws.amazon.com/cloudwatch/) and grant your app the permissions it needs. For more on adding custom AWS resources to your Amplify backend, see [Custom resources](/[platform]/build-a-backend/add-aws-services/custom-resources/). + +## Set up a CloudWatch log group + +```ts title="amplify/backend.ts" +import { defineBackend } from "@aws-amplify/backend"; +import { auth } from "./auth/resource"; +import { data } from "./data/resource"; +import { Policy, PolicyStatement } from "aws-cdk-lib/aws-iam"; +import { LogGroup } from "aws-cdk-lib/aws-logs"; + +const backend = defineBackend({ + auth, + data, +}); + +const loggingStack = backend.createStack("logging-stack"); + +// Create a CloudWatch log group +const logGroup = new LogGroup(loggingStack, "LogGroup", { + logGroupName: "/app/my-app", +}); + +// Grant log write permissions to authenticated users +const loggingPolicy = new Policy(loggingStack, "LoggingPolicy", { + statements: [ + new PolicyStatement({ + actions: [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams", + ], + resources: [logGroup.logGroupArn], + }), + ], +}); + +backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(loggingPolicy); +``` + +If you are not using the CDK, ensure your authenticated IAM role has permission to write to your target log group: + +```json +{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Resource": "arn:aws:logs:::log-group::*" + }] +} +``` + +For more information, see the [Amazon CloudWatch Logs documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html). + +## Next steps + +Use the [CloudWatch client](/[platform]/frontend/logging/cloudwatch/) to send logs from your app. diff --git a/src/pages/[platform]/frontend/logging/cloudwatch/index.mdx b/src/pages/[platform]/frontend/logging/cloudwatch/index.mdx new file mode 100644 index 00000000000..68ad86c272c --- /dev/null +++ b/src/pages/[platform]/frontend/logging/cloudwatch/index.mdx @@ -0,0 +1,512 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'CloudWatch client', + description: 'A standalone client for sending application logs to Amazon CloudWatch Logs with offline support, automatic batching, and configurable flushing.', + platforms: [ + 'swift', + 'android' + ], +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + +`AmplifyCloudWatchClient` is a standalone client for sending application logs to [Amazon CloudWatch Logs](https://aws.amazon.com/cloudwatch/). It provides: + +- Local persistence for offline support +- Automatic batching and interval-based flushing (default: every 60 seconds) +- Integration with Amplify logging as a log sink +- Per-namespace and per-user log level constraints +- Enable/disable toggle that silently drops new logs while preserving cached ones + + + +This is an experimental API. These public APIs are subject to change and are not meant for production code. + + + +You must opt in by importing the module with the `AmplifyExperimental` SPI: + +```swift +@_spi(AmplifyExperimental) import AmplifyCloudWatchClient +``` + + + + + +You must opt in by annotating your usage with `@OptIn(ExperimentalAmplifyApi::class)`. + + + + + + + +This is a standalone client, separate from the Amplify Logging category plugin. It communicates directly with the CloudWatch Logs API. + + + + + +Before using this client, ensure your backend is configured with the required log group and IAM permissions. See [Set up CloudWatch Logs](/[platform]/build-a-backend/add-aws-services/logging/cloudwatch/). + + + +## Getting started + +### Installation + + + +Add the dependency to your module's `build.gradle.kts`: + +```kotlin +dependencies { + implementation("com.amplifyframework:aws-cloudwatch:ANDROID_VERSION") +} +``` + + + + + +Add `AmplifyCloudWatchClient` to your project using Swift Package Manager. In Xcode, go to **File > Add Package Dependencies** and enter the repository URL for the [Amplify Swift library](https://github.com/aws-amplify/amplify-swift): `https://github.com/aws-amplify/amplify-swift`. + + + +### Initialize the client + +The `logGroupName` is required. It must match a CloudWatch log group your app has permission to write to. + + + +```kotlin +import com.amplifyframework.cloudwatch.AmplifyCloudWatchClient +import com.amplifyframework.cloudwatch.AmplifyCloudWatchClientOptions + +val cloudWatch = AmplifyCloudWatchClient( + context = applicationContext, + region = "us-east-1", + credentialsProvider = credentialsProvider, + options = AmplifyCloudWatchClientOptions { + logGroupName = "/app/my-android-app" + } +) +``` + + + + + +```swift +@_spi(AmplifyExperimental) import AmplifyCloudWatchClient + +let cloudWatch = try AmplifyCloudWatchClient( + region: "us-east-1", + credentialsProvider: credentialsProvider, + options: .init(logGroupName: "/app/my-ios-app") +) +``` + + + +### Configuration options + +You can customize the client behavior through the options object: + + + +| Option | Default | Description | +|---|---|---| +| `logGroupName` | *(required)* | The CloudWatch log group logs are sent to. | +| `localStoreMaxSizeInMB` | 5 | Maximum size of the local log cache in MB. | +| `flushStrategy` | `FlushStrategy.Interval()` (60s) | Automatic flush interval. Use `FlushStrategy.None` for manual-only flushing. | +| `loggingConstraints` | `LoggingConstraints()` | Per-namespace and per-user log level rules. | +| `configureClient` | `null` | Escape hatch to customize the underlying AWS SDK `CloudWatchLogsClient`. | + +```kotlin +import com.amplifyframework.cloudwatch.AmplifyCloudWatchClientOptions +import com.amplifyframework.cloudwatch.FlushStrategy +import com.amplifyframework.cloudwatch.LoggingConstraints +import com.amplifyframework.logging.LogLevel +import kotlin.time.Duration.Companion.seconds + +val options = AmplifyCloudWatchClientOptions { + logGroupName = "/app/my-android-app" + localStoreMaxSizeInMB = 10 + flushStrategy = FlushStrategy.Interval(30.seconds) + loggingConstraints = LoggingConstraints(defaultLogLevel = LogLevel.Verbose) + configureClient { + retryStrategy { maxAttempts = 10 } + } +} +``` + +To disable automatic flushing: + +```kotlin +options = AmplifyCloudWatchClientOptions { + logGroupName = "/app/my-android-app" + flushStrategy = FlushStrategy.None +} +``` + + + + + +| Option | Default | Description | +|---|---|---| +| `logGroupName` | *(required)* | The CloudWatch log group logs are sent to. | +| `localStoreMaxSizeInMB` | 5 | Maximum size of the local log cache in MB. | +| `flushStrategy` | `.interval()` (60s) | Automatic flush interval. Use `.none` for manual-only flushing. | +| `loggingConstraints` | `LoggingConstraints()` | Per-namespace and per-user log level rules. | +| `configureClient` | `nil` | Closure to customize the underlying `CloudWatchLogsClientConfig`. | + +```swift +let cloudWatch = try AmplifyCloudWatchClient( + region: "us-east-1", + credentialsProvider: credentialsProvider, + options: .init( + logGroupName: "/app/my-ios-app", + localStoreMaxSizeInMB: 10, + flushStrategy: .interval(30), + loggingConstraints: LoggingConstraints(defaultLogLevel: .verbose), + configureClient: { config in + // Customize the underlying CloudWatchLogsClientConfig + } + ) +) +``` + +To disable automatic flushing: + +```swift +options: .init(logGroupName: "/app/my-ios-app", flushStrategy: .none) +``` + + + +## Usage + +### Register as a logging sink + + + +The client implements AmplifyFoundation's [`LogSinkBehavior`](https://github.com/aws-amplify/amplify-swift/blob/main/AmplifyFoundation/Sources/Logging/LogSinkBehavior.swift) protocol. Register it once to capture every message logged through Amplify logging: + + + + + +The client implements AmplifyFoundation's [`LogSink`](https://github.com/aws-amplify/amplify-android/blob/main/foundation/src/commonMain/kotlin/com/amplifyframework/foundation/logging/LogSink.kt) interface. Register it once to capture every message logged through Amplify logging: + + + + + +```kotlin +import com.amplifyframework.logging.AmplifyLogging + +AmplifyLogging.addSink(cloudWatch) +``` + + + + + +```swift +AmplifyLogging.addSink(cloudWatch) + +let log = AmplifyLogging.logger(for: "Storage") +log.info("Upload started") +``` + + + +### Emit a message directly + +You can also send a log message to the client without going through Amplify logging: + + + +```kotlin +import com.amplifyframework.logging.LogLevel +import com.amplifyframework.logging.LogMessage + +cloudWatch.emit(LogMessage(LogLevel.Error, "MyNamespace", "Something went wrong", null)) +``` + + + + + +```swift +cloudWatch.emit(message: LogMessage(logLevel: .error, namespace: "MyNamespace", message: "Something went wrong")) +``` + + + +Messages emitted while the client is disabled are silently dropped. + +### Flush logs + +The client automatically flushes cached logs at the configured interval (default: 60 seconds). You can also trigger a manual flush: + + + +```kotlin +when (val result = cloudWatch.flushLogs()) { + is Result.Success -> println("Flushed: ${result.data.flushed}") + is Result.Failure -> println("Flush error: ${result.error}") +} +``` + + + + + +```swift +try await cloudWatch.flushLogs() +``` + + + +Manual flushes work even when the client is disabled, allowing you to drain cached logs without re-enabling collection. + +### Enable and disable + +You can toggle log collection and automatic flushing at runtime. When disabled, new logs are silently dropped but already-cached logs remain in storage. + + + +```kotlin +cloudWatch.disable() +// Logs are dropped, auto-flush paused + +cloudWatch.enable() +// Collection and auto-flush resume +``` + + + + + +```swift +cloudWatch.disable() +// Logs are dropped, auto-flush paused + +cloudWatch.enable() +// Collection and auto-flush resume +``` + + + +### Set the user identifier + +Associate cached and future logs with a user identifier, for example after sign-in. + + + +Pass `null` to clear it on sign-out. + + + + + +Pass `nil` to clear it on sign-out. + + + + + +```kotlin +cloudWatch.setUserIdentifier("user-123") +``` + + + + + +```swift +cloudWatch.setUserIdentifier("user-123") +``` + + + +### Set logging constraints + +Update the log level rules at runtime, for example after fetching remote configuration: + + + +```kotlin +import com.amplifyframework.cloudwatch.LoggingConstraints +import com.amplifyframework.logging.LogLevel + +cloudWatch.setLoggingConstraints( + LoggingConstraints( + defaultLogLevel = LogLevel.Warn, + namespaceLogLevel = mapOf("Storage" to LogLevel.Debug) + ) +) +``` + + + + + +```swift +cloudWatch.setLoggingConstraints( + LoggingConstraints( + defaultLogLevel: .warn, + namespaceLogLevel: ["Storage": .debug] + ) +) +``` + + + +### Observe events + +The client surfaces write and flush failures through an events stream so you can react to persistent problems: + + + +```kotlin +import com.amplifyframework.cloudwatch.LoggingEvent +import kotlinx.coroutines.flow.collect + +cloudWatch.events.collect { event -> + when (event) { + is LoggingEvent.WriteLogFailure -> { /* handle write failure */ } + is LoggingEvent.FlushLogFailure -> { /* handle flush failure */ } + } +} +``` + + + + + +```swift +import Combine + +let cancellable = cloudWatch.events.sink { event in + switch event { + case .writeLogFailure(let context, let error): + // handle write failure + break + case .flushLogFailure(let context, let error): + // handle flush failure + break + } +} +``` + + + +## Advanced + +### Escape hatch + +Access the underlying AWS SDK `CloudWatchLogsClient` for operations not covered by this client's API: + + + +```kotlin +val sdkClient = cloudWatch.getCloudWatchLogsClient() +// Use sdkClient for direct CloudWatch Logs API calls +``` + + + + + +```swift +let sdkClient = cloudWatch.getCloudWatchLogsClient() +// Use sdkClient for direct CloudWatch Logs API calls +``` + + + +### Error handling + +Operations surface errors through a sealed error hierarchy: + + + +| Error type | Description | +|---|---| +| `AmplifyCloudWatchStorageException` | Local storage error (file I/O, log rotation). | +| `AmplifyCloudWatchServiceException` | A CloudWatch Logs API call failed. | +| `AmplifyCloudWatchConfigurationException` | The client was misconfigured. | +| `AmplifyCloudWatchUnknownException` | Unexpected or uncategorized error. | + +`flushLogs()` returns `Result`: + +```kotlin +when (val result = cloudWatch.flushLogs()) { + is Result.Success -> { /* success */ } + is Result.Failure -> when (result.error) { + is AmplifyCloudWatchStorageException -> { /* storage error */ } + is AmplifyCloudWatchServiceException -> { /* service error */ } + is AmplifyCloudWatchConfigurationException -> { /* misconfiguration */ } + is AmplifyCloudWatchUnknownException -> { /* unexpected error */ } + } +} +``` + + + + + +| Error type | Description | +|---|---| +| `CloudWatchError.storage` | Local storage error (file I/O, log rotation). | +| `CloudWatchError.service` | A CloudWatch Logs API call failed. | +| `CloudWatchError.configuration` | The client was misconfigured. | +| `CloudWatchError.unknown` | Unexpected or uncategorized error. | + +The initializer and `flushLogs()` throw `CloudWatchError`: + +```swift +do { + try await cloudWatch.flushLogs() +} catch let error as CloudWatchError { + switch error { + case .storage(let desc, _, _): + print("Storage error: \(desc)") + case .service(let desc, _, _): + print("Service error: \(desc)") + case .configuration(let desc, _, _): + print("Configuration error: \(desc)") + case .unknown(let desc, _, _): + print("Unknown error: \(desc)") + } +} +``` + + + +### CloudWatch Logs service limits + +The client batches logs to stay within the CloudWatch Logs `PutLogEvents` limits: + +| Limit | Value | +|---|---| +| Max batch size | 1 MB | +| Max events per batch | 10,000 | +| Max single event size | 256 KB | From 5591d5eecb29304164dfbeab60d7639a50c45dae Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Mon, 14 Sep 2026 12:28:07 -0700 Subject: [PATCH 2/2] move CloudWatch client section to first --- src/directory/directory.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/directory/directory.mjs b/src/directory/directory.mjs index 8e6b7ef36ab..c812c8d9894 100644 --- a/src/directory/directory.mjs +++ b/src/directory/directory.mjs @@ -1019,6 +1019,9 @@ export const directory = { { path: 'src/pages/[platform]/frontend/logging/index.mdx', children: [ + { + path: 'src/pages/[platform]/frontend/logging/cloudwatch/index.mdx' + }, { path: 'src/pages/[platform]/frontend/logging/send-logs/index.mdx' }, @@ -1042,9 +1045,6 @@ export const directory = { }, { path: 'src/pages/[platform]/frontend/logging/sdk/index.mdx' - }, - { - path: 'src/pages/[platform]/frontend/logging/cloudwatch/index.mdx' } ] },