Skip to content

Commit

Permalink
[ECO-4998] feat: add logger abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
ttypic committed Oct 17, 2024
1 parent 7aab3fb commit ac24472
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 7 deletions.
3 changes: 2 additions & 1 deletion chat-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ android {
}

compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
Expand All @@ -45,7 +46,7 @@ buildConfig {
dependencies {
api(libs.ably.android)
implementation(libs.gson)

coreLibraryDesugaring(libs.desugar.jdk.libs)
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.coroutine.test)
Expand Down
28 changes: 27 additions & 1 deletion chat-android/src/main/java/com/ably/chat/ChatApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ private const val PROTOCOL_VERSION_PARAM_NAME = "v"
private const val RESERVED_ABLY_CHAT_KEY = "ably-chat"
private val apiProtocolParam = Param(PROTOCOL_VERSION_PARAM_NAME, API_PROTOCOL_VERSION.toString())

internal class ChatApi(private val realtimeClient: RealtimeClient, private val clientId: String) {
internal class ChatApi(
private val realtimeClient: RealtimeClient,
private val clientId: String,
private val logger: Logger,
) {

/**
* Get messages from the Chat Backend
Expand Down Expand Up @@ -134,6 +138,17 @@ internal class ChatApi(private val realtimeClient: RealtimeClient, private val c
}

override fun onError(reason: ErrorInfo?) {
logger.error(
"ChatApi.makeAuthorizedRequest(); failed to make request",
context = LogContext(
contextMap = mapOf(
"url" to url,
"statusCode" to reason?.statusCode.toString(),
"errorCode" to reason?.code.toString(),
"errorMessage" to reason?.message.toString(),
),
),
)
// (CHA-M3e)
continuation.resumeWithException(AblyException.fromErrorInfo(reason))
}
Expand All @@ -159,6 +174,17 @@ internal class ChatApi(private val realtimeClient: RealtimeClient, private val c
}

override fun onError(reason: ErrorInfo?) {
logger.error(
"ChatApi.makeAuthorizedPaginatedRequest(); failed to make request",
context = LogContext(
contextMap = mapOf(
"url" to url,
"statusCode" to reason?.statusCode.toString(),
"errorCode" to reason?.code.toString(),
"errorMessage" to reason?.message.toString(),
),
),
)
continuation.resumeWithException(AblyException.fromErrorInfo(reason))
}
},
Expand Down
8 changes: 7 additions & 1 deletion chat-android/src/main/java/com/ably/chat/ChatClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ internal class DefaultChatClient(
override val clientOptions: ClientOptions,
) : ChatClient {

private val chatApi = ChatApi(realtime, clientId)
private val logger: Logger = if (clientOptions.logHandler != null) {
CustomLogger(clientOptions.logHandler, clientOptions.logLevel)
} else {
AndroidLogger(clientOptions.logLevel)
}

private val chatApi = ChatApi(realtime, clientId, logger.withContext(LogContext(tag = "AblyChatAPI")))

override val rooms: Rooms = DefaultRooms(
realtimeClient = realtime,
Expand Down
2 changes: 0 additions & 2 deletions chat-android/src/main/java/com/ably/chat/ClientOptions.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.ably.chat

import io.ably.lib.util.Log.LogHandler

/**
* Configuration options for the chat client.
*/
Expand Down
108 changes: 108 additions & 0 deletions chat-android/src/main/java/com/ably/chat/Logger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.ably.chat

import android.util.Log
import java.time.LocalDateTime

fun interface LogHandler {
fun log(message: String, level: LogLevel, throwable: Throwable?, context: LogContext)
}

data class LogContext(val tag: String? = null, val contextMap: Map<String, String> = mapOf())

internal interface Logger {
val defaultContext: LogContext
fun withContext(additionalContext: LogContext): Logger
fun log(message: String, level: LogLevel, throwable: Throwable? = null, context: LogContext? = null)
}

internal fun Logger.trace(message: String, throwable: Throwable? = null, context: LogContext? = null) {
log(message, LogLevel.Trace, throwable, context)
}

internal fun Logger.debug(message: String, throwable: Throwable? = null, context: LogContext? = null) {
log(message, LogLevel.Debug, throwable, context)
}

internal fun Logger.info(message: String, throwable: Throwable? = null, context: LogContext? = null) {
log(message, LogLevel.Info, throwable, context)
}

internal fun Logger.warn(message: String, throwable: Throwable? = null, context: LogContext? = null) {
log(message, LogLevel.Info, throwable, context)
}

internal fun Logger.error(message: String, throwable: Throwable? = null, context: LogContext? = null) {
log(message, LogLevel.Error, throwable, context)
}

internal fun LogContext.mergeWith(other: LogContext): LogContext {
return LogContext(
tag = other.tag ?: tag,
contextMap = contextMap + other.contextMap,
)
}

internal class AndroidLogger(
private val minimalVisibleLogLevel: LogLevel,
override val defaultContext: LogContext = LogContext(),
) : Logger {

override fun withContext(additionalContext: LogContext): Logger {
return AndroidLogger(
minimalVisibleLogLevel = minimalVisibleLogLevel,
defaultContext = defaultContext.mergeWith(additionalContext),
)
}

override fun log(message: String, level: LogLevel, throwable: Throwable?, context: LogContext?) {
if (level.logLevelValue <= minimalVisibleLogLevel.logLevelValue) return
val finalContext = context?.let { defaultContext.mergeWith(it) } ?: this.defaultContext
val tag = finalContext.tag ?: "AblyChatSDK"

val contextString = if (this.defaultContext.contextMap.isEmpty()) "" else ", context: $finalContext"
val formattedMessage = "[${LocalDateTime.now()}] ${level.name} ably-chat: ${message}$contextString"
when (level) {
// We use Logcat's info level for Trace and Debug
LogLevel.Trace -> Log.i(tag, formattedMessage, throwable)
LogLevel.Debug -> Log.i(tag, formattedMessage, throwable)
LogLevel.Info -> Log.i(tag, formattedMessage, throwable)
LogLevel.Warn -> Log.w(tag, formattedMessage, throwable)
LogLevel.Error -> Log.e(tag, formattedMessage, throwable)
LogLevel.Silent -> {}
}
}
}

internal class CustomLogger(
private val logHandler: LogHandler,
private val minimalVisibleLogLevel: LogLevel,
override val defaultContext: LogContext = LogContext(),
) : Logger {

override fun withContext(additionalContext: LogContext): Logger {
return CustomLogger(
logHandler = logHandler,
minimalVisibleLogLevel = minimalVisibleLogLevel,
defaultContext = defaultContext.mergeWith(additionalContext),
)
}

override fun log(message: String, level: LogLevel, throwable: Throwable?, context: LogContext?) {
if (level.logLevelValue <= minimalVisibleLogLevel.logLevelValue) return
val finalContext = context?.let { defaultContext.mergeWith(it) } ?: this.defaultContext
logHandler.log(
message = message,
level = level,
throwable = throwable,
context = finalContext,
)
}
}

internal object EmptyLogger : Logger {
override val defaultContext: LogContext = LogContext()

override fun withContext(additionalContext: LogContext): Logger = this

override fun log(message: String, level: LogLevel, throwable: Throwable?, context: LogContext?) = Unit
}
2 changes: 1 addition & 1 deletion chat-android/src/test/java/com/ably/chat/ChatApiTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import org.junit.Test
class ChatApiTest {

private val realtime = mockk<RealtimeClient>(relaxed = true)
private val chatApi = ChatApi(realtime, "clientId")
private val chatApi = ChatApi(realtime, "clientId", logger = EmptyLogger)

/**
* @nospec
Expand Down
2 changes: 1 addition & 1 deletion chat-android/src/test/java/com/ably/chat/MessagesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class MessagesTest {
private val realtimeClient = mockk<RealtimeClient>(relaxed = true)
private val realtimeChannels = mockk<Channels>(relaxed = true)
private val realtimeChannel = spyk<Channel>(buildRealtimeChannel())
private val chatApi = spyk(ChatApi(realtimeClient, "clientId"))
private val chatApi = spyk(ChatApi(realtimeClient, "clientId", EmptyLogger))
private lateinit var messages: DefaultMessages

private val channelStateListenerSlot = slot<ChannelStateListener>()
Expand Down
2 changes: 2 additions & 0 deletions example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ android {
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
Expand Down Expand Up @@ -67,6 +68,7 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
coreLibraryDesugaring(libs.desugar.jdk.libs)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[versions]
ably-chat = "0.0.1"
ably = "1.2.43"
desugar-jdk-libs = "2.1.2"
junit = "4.13.2"
agp = "8.5.2"
detekt = "1.23.6"
Expand All @@ -21,6 +22,7 @@ coroutine = "1.8.1"
build-config = "5.4.0"

[libraries]
desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar-jdk-libs" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
ably-android = { module = "io.ably:ably-android", version.ref = "ably" }
Expand Down

0 comments on commit ac24472

Please sign in to comment.