Skip to content

Commit 4473aec

Browse files
committed
[ECO-4998] feat: add logger abstraction
1 parent 4861812 commit 4473aec

File tree

11 files changed

+200
-9
lines changed

11 files changed

+200
-9
lines changed

chat-android/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ android {
2727
}
2828

2929
compileOptions {
30+
isCoreLibraryDesugaringEnabled = true
3031
sourceCompatibility = JavaVersion.VERSION_17
3132
targetCompatibility = JavaVersion.VERSION_17
3233
}
@@ -45,7 +46,7 @@ buildConfig {
4546
dependencies {
4647
api(libs.ably.android)
4748
implementation(libs.gson)
48-
49+
coreLibraryDesugaring(libs.desugar.jdk.libs)
4950
testImplementation(libs.junit)
5051
testImplementation(libs.mockk)
5152
testImplementation(libs.coroutine.test)

chat-android/src/main/java/com/ably/chat/ChatApi.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ private const val PROTOCOL_VERSION_PARAM_NAME = "v"
1515
private const val RESERVED_ABLY_CHAT_KEY = "ably-chat"
1616
private val apiProtocolParam = Param(PROTOCOL_VERSION_PARAM_NAME, API_PROTOCOL_VERSION.toString())
1717

18-
internal class ChatApi(private val realtimeClient: RealtimeClient, private val clientId: String) {
18+
internal class ChatApi(
19+
private val realtimeClient: RealtimeClient,
20+
private val clientId: String,
21+
private val logger: Logger,
22+
) {
1923

2024
/**
2125
* Get messages from the Chat Backend
@@ -134,6 +138,15 @@ internal class ChatApi(private val realtimeClient: RealtimeClient, private val c
134138
}
135139

136140
override fun onError(reason: ErrorInfo?) {
141+
logger.error(
142+
"ChatApi.makeAuthorizedRequest(); failed to make request",
143+
staticContext = mapOf(
144+
"url" to url,
145+
"statusCode" to reason?.statusCode.toString(),
146+
"errorCode" to reason?.code.toString(),
147+
"errorMessage" to reason?.message.toString(),
148+
),
149+
)
137150
// (CHA-M3e)
138151
continuation.resumeWithException(AblyException.fromErrorInfo(reason))
139152
}
@@ -159,6 +172,15 @@ internal class ChatApi(private val realtimeClient: RealtimeClient, private val c
159172
}
160173

161174
override fun onError(reason: ErrorInfo?) {
175+
logger.error(
176+
"ChatApi.makeAuthorizedPaginatedRequest(); failed to make request",
177+
staticContext = mapOf(
178+
"url" to url,
179+
"statusCode" to reason?.statusCode.toString(),
180+
"errorCode" to reason?.code.toString(),
181+
"errorMessage" to reason?.message.toString(),
182+
),
183+
)
162184
continuation.resumeWithException(AblyException.fromErrorInfo(reason))
163185
}
164186
},

chat-android/src/main/java/com/ably/chat/ChatClient.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@file:Suppress("StringLiteralDuplication", "NotImplementedDeclaration")
1+
@file:Suppress("NotImplementedDeclaration")
22

33
package com.ably.chat
44

@@ -45,7 +45,17 @@ internal class DefaultChatClient(
4545
override val clientOptions: ClientOptions,
4646
) : ChatClient {
4747

48-
private val chatApi = ChatApi(realtime, clientId)
48+
private val logger: Logger = if (clientOptions.logHandler != null) {
49+
CustomLogger(
50+
clientOptions.logHandler,
51+
clientOptions.logLevel,
52+
buildLogContext(),
53+
)
54+
} else {
55+
AndroidLogger(clientOptions.logLevel, buildLogContext())
56+
}
57+
58+
private val chatApi = ChatApi(realtime, clientId, logger.withContext(tag = "AblyChatAPI"))
4959

5060
override val rooms: Rooms = DefaultRooms(
5161
realtimeClient = realtime,
@@ -58,4 +68,16 @@ internal class DefaultChatClient(
5868

5969
override val clientId: String
6070
get() = realtime.auth.clientId
71+
72+
private fun buildLogContext() = LogContext(
73+
tag = "ChatClient",
74+
staticContext = mapOf(
75+
"clientId" to clientId,
76+
"instanceId" to generateUUID(),
77+
),
78+
dynamicContext = mapOf(
79+
"connectionId" to { realtime.connection.id },
80+
"connectionState" to { realtime.connection.state.name },
81+
),
82+
)
6183
}

chat-android/src/main/java/com/ably/chat/ClientOptions.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.ably.chat
22

3-
import io.ably.lib.util.Log.LogHandler
4-
53
/**
64
* Configuration options for the chat client.
75
*/
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package com.ably.chat
2+
3+
import android.util.Log
4+
import java.time.LocalDateTime
5+
6+
fun interface LogHandler {
7+
fun log(message: String, level: LogLevel, throwable: Throwable?, context: LogContext)
8+
}
9+
10+
data class LogContext(
11+
val tag: String,
12+
val staticContext: Map<String, String> = mapOf(),
13+
val dynamicContext: Map<String, () -> String> = mapOf(),
14+
)
15+
16+
internal interface Logger {
17+
val context: LogContext
18+
fun withContext(
19+
tag: String? = null,
20+
staticContext: Map<String, String> = mapOf(),
21+
dynamicContext: Map<String, () -> String> = mapOf(),
22+
): Logger
23+
24+
fun log(
25+
message: String,
26+
level: LogLevel,
27+
throwable: Throwable? = null,
28+
newTag: String? = null,
29+
newStaticContext: Map<String, String> = mapOf(),
30+
)
31+
}
32+
33+
internal fun Logger.trace(
34+
message: String,
35+
throwable: Throwable? = null,
36+
tag: String? = null,
37+
staticContext: Map<String, String> = mapOf(),
38+
) {
39+
log(message, LogLevel.Trace, throwable, tag, staticContext)
40+
}
41+
42+
internal fun Logger.debug(
43+
message: String,
44+
throwable: Throwable? = null,
45+
tag: String? = null,
46+
staticContext: Map<String, String> = mapOf(),
47+
) {
48+
log(message, LogLevel.Debug, throwable, tag, staticContext)
49+
}
50+
51+
internal fun Logger.info(message: String, throwable: Throwable? = null, tag: String? = null, staticContext: Map<String, String> = mapOf()) {
52+
log(message, LogLevel.Info, throwable, tag, staticContext)
53+
}
54+
55+
internal fun Logger.warn(message: String, throwable: Throwable? = null, tag: String? = null, staticContext: Map<String, String> = mapOf()) {
56+
log(message, LogLevel.Warn, throwable, tag, staticContext)
57+
}
58+
59+
internal fun Logger.error(
60+
message: String,
61+
throwable: Throwable? = null,
62+
tag: String? = null,
63+
staticContext: Map<String, String> = mapOf(),
64+
) {
65+
log(message, LogLevel.Error, throwable, tag, staticContext)
66+
}
67+
68+
internal fun LogContext.mergeWith(
69+
tag: String? = null,
70+
staticContext: Map<String, String> = mapOf(),
71+
dynamicContext: Map<String, () -> String> = mapOf(),
72+
): LogContext {
73+
return LogContext(
74+
tag = tag ?: this.tag,
75+
staticContext = this.staticContext + staticContext,
76+
dynamicContext = this.dynamicContext + dynamicContext,
77+
)
78+
}
79+
80+
internal class AndroidLogger(
81+
private val minimalVisibleLogLevel: LogLevel,
82+
override val context: LogContext,
83+
) : Logger {
84+
85+
override fun withContext(tag: String?, staticContext: Map<String, String>, dynamicContext: Map<String, () -> String>): Logger {
86+
return AndroidLogger(
87+
minimalVisibleLogLevel = minimalVisibleLogLevel,
88+
context = context.mergeWith(tag, staticContext, dynamicContext),
89+
)
90+
}
91+
92+
override fun log(message: String, level: LogLevel, throwable: Throwable?, newTag: String?, newStaticContext: Map<String, String>) {
93+
if (level.logLevelValue <= minimalVisibleLogLevel.logLevelValue) return
94+
val finalContext = context.mergeWith(newTag, newStaticContext)
95+
val tag = finalContext.tag
96+
val completeContext = finalContext.staticContext + finalContext.dynamicContext.mapValues { it.value() }
97+
98+
val contextString = ", context: $completeContext"
99+
val formattedMessage = "[${LocalDateTime.now()}] $tag ${level.name} ably-chat: ${message}$contextString"
100+
when (level) {
101+
// We use Logcat's info level for Trace and Debug
102+
LogLevel.Trace -> Log.i(tag, formattedMessage, throwable)
103+
LogLevel.Debug -> Log.i(tag, formattedMessage, throwable)
104+
LogLevel.Info -> Log.i(tag, formattedMessage, throwable)
105+
LogLevel.Warn -> Log.w(tag, formattedMessage, throwable)
106+
LogLevel.Error -> Log.e(tag, formattedMessage, throwable)
107+
LogLevel.Silent -> {}
108+
}
109+
}
110+
}
111+
112+
internal class CustomLogger(
113+
private val logHandler: LogHandler,
114+
private val minimalVisibleLogLevel: LogLevel,
115+
override val context: LogContext,
116+
) : Logger {
117+
118+
override fun withContext(tag: String?, staticContext: Map<String, String>, dynamicContext: Map<String, () -> String>): Logger {
119+
return CustomLogger(
120+
logHandler = logHandler,
121+
minimalVisibleLogLevel = minimalVisibleLogLevel,
122+
context = context.mergeWith(tag, staticContext, dynamicContext),
123+
)
124+
}
125+
126+
override fun log(message: String, level: LogLevel, throwable: Throwable?, newTag: String?, newStaticContext: Map<String, String>) {
127+
if (level.logLevelValue <= minimalVisibleLogLevel.logLevelValue) return
128+
val finalContext = context.mergeWith(newTag, newStaticContext)
129+
logHandler.log(
130+
message = message,
131+
level = level,
132+
throwable = throwable,
133+
context = finalContext,
134+
)
135+
}
136+
}

chat-android/src/main/java/com/ably/chat/Utils.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.ably.lib.realtime.CompletionListener
55
import io.ably.lib.types.AblyException
66
import io.ably.lib.types.ChannelOptions
77
import io.ably.lib.types.ErrorInfo
8+
import java.util.UUID
89
import kotlin.coroutines.resume
910
import kotlin.coroutines.resumeWithException
1011
import kotlin.coroutines.suspendCoroutine
@@ -62,6 +63,8 @@ fun ChatChannelOptions(init: (ChannelOptions.() -> Unit)? = null): ChannelOption
6263
return options
6364
}
6465

66+
fun generateUUID() = UUID.randomUUID().toString()
67+
6568
/**
6669
* A value that can be evaluated at a later time, similar to `kotlinx.coroutines.Deferred` or a JavaScript Promise.
6770
*

chat-android/src/test/java/com/ably/chat/ChatApiTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import org.junit.Test
1313
class ChatApiTest {
1414

1515
private val realtime = mockk<RealtimeClient>(relaxed = true)
16-
private val chatApi = ChatApi(realtime, "clientId")
16+
private val chatApi =
17+
ChatApi(realtime, "clientId", logger = EmptyLogger(LogContext(tag = "TEST")))
1718

1819
/**
1920
* @nospec

chat-android/src/test/java/com/ably/chat/MessagesTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class MessagesTest {
2828
private val realtimeClient = mockk<RealtimeClient>(relaxed = true)
2929
private val realtimeChannels = mockk<Channels>(relaxed = true)
3030
private val realtimeChannel = spyk<Channel>(buildRealtimeChannel())
31-
private val chatApi = spyk(ChatApi(realtimeClient, "clientId"))
31+
private val chatApi = spyk(ChatApi(realtimeClient, "clientId", EmptyLogger(LogContext(tag = "TEST"))))
3232
private lateinit var messages: DefaultMessages
3333

3434
private val channelStateListenerSlot = slot<ChannelStateListener>()

chat-android/src/test/java/com/ably/chat/TestUtils.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,8 @@ fun mockOccupancyApiResponse(realtimeClientMock: RealtimeClient, response: JsonE
4949
)
5050
}
5151
}
52+
53+
internal class EmptyLogger(override val context: LogContext) : Logger {
54+
override fun withContext(tag: String?, staticContext: Map<String, String>, dynamicContext: Map<String, () -> String>): Logger = this
55+
override fun log(message: String, level: LogLevel, throwable: Throwable?, newTag: String?, newStaticContext: Map<String, String>) = Unit
56+
}

example/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ android {
3737
}
3838
}
3939
compileOptions {
40+
isCoreLibraryDesugaringEnabled = true
4041
sourceCompatibility = JavaVersion.VERSION_1_8
4142
targetCompatibility = JavaVersion.VERSION_1_8
4243
}
@@ -67,7 +68,7 @@ dependencies {
6768
implementation(libs.androidx.ui.graphics)
6869
implementation(libs.androidx.ui.tooling.preview)
6970
implementation(libs.androidx.material3)
70-
implementation(libs.konfetti.compose)
71+
coreLibraryDesugaring(libs.desugar.jdk.libs)
7172
testImplementation(libs.junit)
7273
androidTestImplementation(libs.androidx.junit)
7374
androidTestImplementation(libs.androidx.espresso.core)

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[versions]
55
ably-chat = "0.0.1"
66
ably = "1.2.43"
7+
desugar-jdk-libs = "2.1.2"
78
junit = "4.13.2"
89
agp = "8.5.2"
910
detekt = "1.23.6"
@@ -22,6 +23,7 @@ coroutine = "1.8.1"
2223
build-config = "5.4.0"
2324

2425
[libraries]
26+
desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar-jdk-libs" }
2527
junit = { group = "junit", name = "junit", version.ref = "junit" }
2628
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
2729
ably-android = { module = "io.ably:ably-android", version.ref = "ably" }

0 commit comments

Comments
 (0)