Skip to content

Commit cbc911c

Browse files
committed
feat(logging): add v3 cloudwatch client public API definitions
1 parent 10d1107 commit cbc911c

11 files changed

Lines changed: 497 additions & 0 deletions

aws-cloudwatch/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

aws-cloudwatch/build.gradle.kts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
plugins {
17+
alias(libs.plugins.amplify.android.library)
18+
alias(libs.plugins.kotlin.serialization)
19+
alias(libs.plugins.amplify.publishing)
20+
}
21+
22+
apply(from = rootProject.file("configuration/checkstyle.gradle"))
23+
24+
android {
25+
namespace = "com.amplifyframework.cloudwatch"
26+
}
27+
28+
dependencies {
29+
implementation(project(":foundation"))
30+
implementation(project(":foundation-bridge"))
31+
32+
implementation(libs.androidx.appcompat)
33+
implementation(libs.aws.cloudwatchlogs)
34+
implementation(libs.aws.http)
35+
implementation(libs.kotlin.serializationJson)
36+
37+
testImplementation(libs.test.junit)
38+
testImplementation(libs.test.mockk)
39+
testImplementation(libs.test.robolectric)
40+
testImplementation(libs.test.androidx.junit)
41+
testImplementation(libs.test.androidx.core)
42+
testImplementation(libs.test.kotlin.coroutines)
43+
testImplementation(libs.test.kotest.assertions)
44+
testImplementation(project(":testutils"))
45+
testImplementation(project(":aws-cloudwatch"))
46+
47+
androidTestImplementation(project(":testutils"))
48+
androidTestImplementation(libs.test.androidx.core)
49+
androidTestImplementation(project(":core"))
50+
androidTestImplementation(project(":aws-core"))
51+
androidTestImplementation(project(":aws-auth-cognito"))
52+
androidTestImplementation(libs.test.androidx.runner)
53+
androidTestImplementation(libs.test.kotlin.coroutines)
54+
androidTestImplementation(libs.test.androidx.junit)
55+
androidTestImplementation(libs.test.kotest.assertions)
56+
androidTestImplementation(project(":aws-cloudwatch"))
57+
}

aws-cloudwatch/gradle.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
POM_ARTIFACT_ID=aws-cloudwatch
2+
POM_NAME=Amplify Framework for Android - CloudWatch
3+
POM_DESCRIPTION=Amplify Framework for Android - Standalone CloudWatch Client
4+
POM_PACKAGING=aar
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package com.amplifyframework.cloudwatch
16+
17+
import android.content.Context
18+
import aws.sdk.kotlin.services.cloudwatchlogs.CloudWatchLogsClient
19+
import com.amplifyframework.annotations.ExperimentalAmplifyApi
20+
import com.amplifyframework.foundation.credentials.AwsCredentials
21+
import com.amplifyframework.foundation.credentials.AwsCredentialsProvider
22+
import com.amplifyframework.foundation.logging.AmplifyLogging
23+
import com.amplifyframework.foundation.logging.LogLevel
24+
import com.amplifyframework.foundation.logging.LogMessage
25+
import com.amplifyframework.foundation.logging.LogSink
26+
import com.amplifyframework.foundation.result.Result
27+
import kotlinx.coroutines.flow.MutableSharedFlow
28+
import kotlinx.coroutines.flow.SharedFlow
29+
import kotlinx.coroutines.flow.asSharedFlow
30+
31+
/**
32+
* A standalone client for sending log events to Amazon CloudWatch Logs.
33+
*
34+
* Provides namespace-based logging with automatic batching, local file persistence via
35+
* log rotation, and configurable flush strategies.
36+
*
37+
* Implements [LogSink] so it can be registered with [AmplifyLogging.addSink] to capture
38+
* all framework log messages and forward them to CloudWatch.
39+
*
40+
* Use a single client instance per (region, log group). CloudWatch log streams are keyed
41+
* by device and user identifier, not by client instance, so two clients targeting the same
42+
* region and log group would write to the same streams and share the same local storage
43+
* directory, resulting in interleaved writes.
44+
*
45+
* Example usage:
46+
* ```kotlin
47+
* val cloudWatch = AmplifyCloudWatchClient(
48+
* context = applicationContext,
49+
* region = "us-east-1",
50+
* credentialsProvider = credentialsProvider,
51+
* options = AmplifyCloudWatchClientOptions(logGroupName = "/app/my-android-app")
52+
* )
53+
*
54+
* // Register as a sink to capture all AmplifyLogging messages
55+
* AmplifyLogging.addSink(cloudWatch)
56+
*
57+
* cloudWatch.flushLogs()
58+
* ```
59+
*
60+
* @param context An Android [Context] used to locate the on-device log store
61+
* @param region The AWS region of the target log group
62+
* @param credentialsProvider Provides AWS credentials for CloudWatch Logs calls
63+
* @param options Configuration options for the client
64+
*/
65+
@ExperimentalAmplifyApi
66+
class AmplifyCloudWatchClient(
67+
context: Context,
68+
region: String,
69+
credentialsProvider: AwsCredentialsProvider<AwsCredentials>,
70+
options: AmplifyCloudWatchClientOptions
71+
) : LogSink {
72+
73+
private val eventsFlow = MutableSharedFlow<LoggingEvent>()
74+
75+
/**
76+
* A stream of [LoggingEvent]s (flush failures, write failures, etc.).
77+
*/
78+
val events: SharedFlow<LoggingEvent> = eventsFlow.asSharedFlow()
79+
80+
// region LogSink
81+
82+
override fun isEnabledFor(level: LogLevel): Boolean = TODO("Not yet implemented")
83+
84+
override fun emit(message: LogMessage): Unit = TODO("Not yet implemented")
85+
86+
// endregion
87+
88+
// region Lifecycle
89+
90+
/** Enable logging and automatic flushing. */
91+
fun enable(): Unit = TODO("Not yet implemented")
92+
93+
/** Disable logging and automatic flushing. */
94+
fun disable(): Unit = TODO("Not yet implemented")
95+
96+
/** Flush all pending log entries to CloudWatch. */
97+
suspend fun flushLogs(): Result<FlushData, AmplifyCloudWatchException> = TODO("Not yet implemented")
98+
99+
/** Returns the underlying AWS CloudWatch Logs SDK client. */
100+
fun getCloudWatchLogsClient(): CloudWatchLogsClient = TODO("Not yet implemented")
101+
102+
// endregion
103+
104+
// region User identity
105+
106+
/**
107+
* Set the current user identifier. Affects log stream naming and user-specific log
108+
* level filtering. Pass `null` on sign-out.
109+
*/
110+
fun setUserIdentifier(identifier: String?): Unit = TODO("Not yet implemented")
111+
112+
/**
113+
* Update the logging constraints. Affects log level filtering for all namespaces.
114+
*/
115+
fun setLoggingConstraints(constraints: LoggingConstraints): Unit = TODO("Not yet implemented")
116+
117+
// endregion
118+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package com.amplifyframework.cloudwatch
16+
17+
import aws.sdk.kotlin.services.cloudwatchlogs.CloudWatchLogsClient
18+
import com.amplifyframework.annotations.ExperimentalAmplifyApi
19+
import com.amplifyframework.foundation.config.SdkClientConfigurationProvider
20+
21+
/** Provides custom configuration for the underlying [CloudWatchLogsClient]. */
22+
typealias CloudWatchLogsClientConfigurationProvider = SdkClientConfigurationProvider<CloudWatchLogsClient.Config.Builder>
23+
24+
private const val DEFAULT_LOCAL_STORE_MAX_SIZE_IN_MB = 5
25+
26+
/**
27+
* Configuration options for [AmplifyCloudWatchClient].
28+
*
29+
* @param logGroupName The CloudWatch log group to send log events to
30+
* @param localStoreMaxSizeInMB Maximum size of the on-device log store, in megabytes (default: 5)
31+
* @param flushStrategy Strategy for automatically flushing cached log entries
32+
* @param loggingConstraints Constraints controlling which messages are captured
33+
* @param configureClient Optional customization of the underlying [CloudWatchLogsClient]
34+
*/
35+
@ExperimentalAmplifyApi
36+
data class AmplifyCloudWatchClientOptions internal constructor(
37+
val logGroupName: String,
38+
val localStoreMaxSizeInMB: Int,
39+
val flushStrategy: FlushStrategy,
40+
val loggingConstraints: LoggingConstraints,
41+
val configureClient: CloudWatchLogsClientConfigurationProvider? = null
42+
) {
43+
companion object {
44+
/**
45+
* Creates a new builder for configuring [AmplifyCloudWatchClientOptions].
46+
*
47+
* @return A new builder instance with default values
48+
*/
49+
@JvmStatic
50+
fun builder() = Builder()
51+
52+
@JvmSynthetic
53+
operator fun invoke(func: Builder.() -> Unit) = Builder().apply(func).build()
54+
}
55+
56+
/**
57+
* Builder for [AmplifyCloudWatchClientOptions].
58+
*
59+
* [logGroupName] is required and must be set before [build] is called.
60+
*/
61+
class Builder internal constructor() {
62+
var logGroupName: String? = null
63+
@JvmSynthetic set
64+
65+
var localStoreMaxSizeInMB: Int = DEFAULT_LOCAL_STORE_MAX_SIZE_IN_MB
66+
@JvmSynthetic set
67+
68+
var flushStrategy: FlushStrategy = FlushStrategy.Interval()
69+
@JvmSynthetic set
70+
71+
var loggingConstraints: LoggingConstraints = LoggingConstraints()
72+
@JvmSynthetic set
73+
74+
var configureClient: CloudWatchLogsClientConfigurationProvider? = null
75+
@JvmSynthetic private set
76+
77+
/** Sets the CloudWatch log group to send log events to. Required. */
78+
fun logGroupName(value: String) = apply { logGroupName = value }
79+
80+
/** Sets the maximum size of the on-device log store, in megabytes. */
81+
fun localStoreMaxSizeInMB(value: Int) = apply { localStoreMaxSizeInMB = value }
82+
83+
/** Sets the strategy for automatically flushing cached log entries. */
84+
fun flushStrategy(value: FlushStrategy) = apply { flushStrategy = value }
85+
86+
/** Sets the constraints controlling which messages are captured. */
87+
fun loggingConstraints(value: LoggingConstraints) = apply { loggingConstraints = value }
88+
89+
/** Sets a custom configuration provider for the underlying [CloudWatchLogsClient]. */
90+
fun configureClient(value: CloudWatchLogsClientConfigurationProvider?) = apply { configureClient = value }
91+
92+
/**
93+
* Configures the underlying [CloudWatchLogsClient] using a DSL-style lambda.
94+
*
95+
* @param value Lambda with receiver on [CloudWatchLogsClient.Config.Builder]
96+
* @return This builder instance
97+
*/
98+
@JvmSynthetic
99+
fun configureClient(value: CloudWatchLogsClient.Config.Builder.() -> Unit) = apply {
100+
configureClient = CloudWatchLogsClientConfigurationProvider { it.value() }
101+
}
102+
103+
/**
104+
* Builds the [AmplifyCloudWatchClientOptions] with the configured values.
105+
*
106+
* @return Configured options instance
107+
* @throws IllegalArgumentException if [logGroupName] was not set
108+
*/
109+
fun build() = AmplifyCloudWatchClientOptions(
110+
requireNotNull(logGroupName) { "logGroupName is required" },
111+
localStoreMaxSizeInMB,
112+
flushStrategy,
113+
loggingConstraints,
114+
configureClient
115+
)
116+
}
117+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package com.amplifyframework.cloudwatch
16+
17+
import com.amplifyframework.foundation.exceptions.AmplifyException
18+
19+
/**
20+
* Base exception for all standalone CloudWatch client operations.
21+
*
22+
* This is a sealed hierarchy. Callers can exhaustively match on the subtype to
23+
* determine the category of failure:
24+
* - [AmplifyCloudWatchStorageException] — local file I/O or log-rotation errors
25+
* - [AmplifyCloudWatchServiceException] — CloudWatch Logs API call failed
26+
* - [AmplifyCloudWatchConfigurationException] — client is misconfigured
27+
* - [AmplifyCloudWatchUnknownException] — unexpected / uncategorized errors
28+
*
29+
* @param message Error message describing what went wrong
30+
* @param recoverySuggestion Suggested action to resolve the error
31+
* @param cause Underlying cause of the exception
32+
*/
33+
sealed class AmplifyCloudWatchException(
34+
message: String,
35+
recoverySuggestion: String,
36+
cause: Throwable? = null
37+
) : AmplifyException(message, recoverySuggestion, cause)
38+
39+
/** Local file I/O or log-rotation error. */
40+
class AmplifyCloudWatchStorageException(
41+
message: String,
42+
recoverySuggestion: String,
43+
cause: Throwable? = null
44+
) : AmplifyCloudWatchException(message, recoverySuggestion, cause)
45+
46+
/** A CloudWatch Logs API call failed. */
47+
class AmplifyCloudWatchServiceException(
48+
message: String,
49+
recoverySuggestion: String,
50+
cause: Throwable? = null
51+
) : AmplifyCloudWatchException(message, recoverySuggestion, cause)
52+
53+
/** The client is misconfigured. */
54+
class AmplifyCloudWatchConfigurationException(
55+
message: String,
56+
recoverySuggestion: String,
57+
cause: Throwable? = null
58+
) : AmplifyCloudWatchException(message, recoverySuggestion, cause)
59+
60+
/** Unexpected / uncategorized error. */
61+
class AmplifyCloudWatchUnknownException(
62+
message: String,
63+
recoverySuggestion: String,
64+
cause: Throwable? = null
65+
) : AmplifyCloudWatchException(message, recoverySuggestion, cause)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package com.amplifyframework.cloudwatch
16+
17+
import com.amplifyframework.annotations.ExperimentalAmplifyApi
18+
19+
/**
20+
* The successful result of a [AmplifyCloudWatchClient.flushLogs] call.
21+
*
22+
* @param success Whether the flush completed successfully
23+
*/
24+
@ExperimentalAmplifyApi
25+
data class FlushData(val success: Boolean = true)

0 commit comments

Comments
 (0)