refactor(logging): introduce custom FastLogger and replace direct Android logging

This commit is contained in:
2026-05-30 18:31:00 +03:00
parent fc3b3cfcb3
commit 10453287a7
32 changed files with 353 additions and 151 deletions
@@ -1,13 +1,13 @@
package dev.meloda.fast.common.model
enum class LogLevel(val value: Int) {
enum class NetworkLogLevel(val value: Int) {
NONE(0),
BASIC(1),
HEADERS(2),
BODY(3);
companion object {
fun parse(value: Int): LogLevel = entries.firstOrNull { it.value == value }
fun parse(value: Int): NetworkLogLevel = entries.firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown log level with value: $value")
}
}
@@ -3,7 +3,7 @@ package dev.meloda.fast.datastore
import android.content.SharedPreferences
import androidx.core.content.edit
import dev.meloda.fast.common.model.DarkMode
import dev.meloda.fast.common.model.LogLevel
import dev.meloda.fast.common.model.NetworkLogLevel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -238,11 +238,11 @@ object AppSettings {
)
set(value) = put(SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT, value)
var networkLogLevel: LogLevel
var networkLogLevel: NetworkLogLevel
get() = get(
SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL,
SettingsKeys.DEFAULT_NETWORK_LOG_LEVEL
).let(LogLevel::parse)
).let(NetworkLogLevel::parse)
set(level) = put(SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, level.value)
var showDebugCategory: Boolean
@@ -1,8 +1,8 @@
package dev.meloda.fast.domain
import android.util.Log
import dev.meloda.fast.database.dao.ConvoDao
import dev.meloda.fast.database.dao.MessageDao
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.model.LongPollParsedEvent
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
@@ -11,6 +11,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlin.coroutines.CoroutineContext
class LongPollEventsHandler(
private val logger: FastLogger,
private val convoUseCase: ConvoUseCase,
private val messagesUseCase: MessagesUseCase,
private val convoDao: ConvoDao,
@@ -20,8 +21,7 @@ class LongPollEventsHandler(
private val exceptionHandler =
CoroutineExceptionHandler { _, throwable ->
Log.e("LongPollUpdatesParser", "error: $throwable")
throwable.printStackTrace()
logger.error(this::class, "CoroutineException", throwable)
}
private val coroutineContext: CoroutineContext
@@ -45,8 +45,8 @@ class LongPollEventsHandler(
isArchived = event.convo.isArchived
)
Log.d(
"LongPollEventsHandler",
logger.debug(
this::class,
"isArchived ${event.convo.isArchived}: updated $affectedRows rows."
)
}
@@ -57,7 +57,10 @@ class LongPollEventsHandler(
cmId = event.toCmId
)
Log.d("LongPollEventsHandler", "updateLastCmId: updated $affectedRows rows.")
logger.debug(
this::class,
"updateLastCmId: updated $affectedRows rows."
)
}
is LongPollParsedEvent.ChatMajorChanged -> {
@@ -66,7 +69,10 @@ class LongPollEventsHandler(
majorId = event.majorId
)
Log.d("LongPollEventsHandler", "updateMajorId: updated $affectedRows rows.")
logger.debug(
this::class,
"updateMajorId: updated $affectedRows rows."
)
}
is LongPollParsedEvent.ChatMinorChanged -> {
@@ -75,7 +81,10 @@ class LongPollEventsHandler(
minorId = event.minorId
)
Log.d("LongPollEventsHandler", "updateMinorId: updated $affectedRows rows.")
logger.debug(
this::class,
"updateMinorId: updated $affectedRows rows."
)
}
is LongPollParsedEvent.Interaction -> {
@@ -93,7 +102,10 @@ class LongPollEventsHandler(
isDeleted = true
)
Log.d("LongPollEventsHandler", "markDeleted: updated $affectedRows rows.")
logger.debug(
this::class,
"markDeleted: updated $affectedRows rows."
)
}
is LongPollParsedEvent.MessageEdited -> {
@@ -107,7 +119,10 @@ class LongPollEventsHandler(
isImportant = event.marked
)
Log.d("LongPollEventsHandler", "markImportant: updated $affectedRows rows.")
logger.debug(
this::class,
"markImportant: updated $affectedRows rows."
)
}
is LongPollParsedEvent.MessageMarkedAsNotSpam -> {
@@ -121,7 +136,10 @@ class LongPollEventsHandler(
isSpam = true
)
Log.d("LongPollEventsHandler", "markSpam: updated $affectedRows rows.")
logger.debug(
this::class,
"markSpam: updated $affectedRows rows."
)
}
is LongPollParsedEvent.MessageRestored -> {
@@ -143,7 +161,10 @@ class LongPollEventsHandler(
unreadCount = event.unreadCount
)
Log.d("LongPollEventsHandler", "inMessageRead: updated $affectedRows rows.")
logger.debug(
this::class,
"inMessageRead: updated $affectedRows rows."
)
}
is LongPollParsedEvent.OutgoingMessageRead -> {
@@ -153,7 +174,10 @@ class LongPollEventsHandler(
unreadCount = event.unreadCount
)
Log.d("LongPollEventsHandler", "outMessageRead: updated $affectedRows rows.")
logger.debug(
this::class,
"outMessageRead: updated $affectedRows rows."
)
}
is LongPollParsedEvent.UnreadCounter -> {
@@ -1,6 +1,5 @@
package dev.meloda.fast.domain
import android.util.Log
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.asInt
import dev.meloda.fast.common.extensions.asLong
@@ -8,6 +7,7 @@ import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.toList
import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.processState
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.model.ApiEvent
import dev.meloda.fast.model.ConvoFlags
import dev.meloda.fast.model.InteractionType
@@ -27,6 +27,7 @@ import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
class LongPollUpdatesParser(
private val logger: FastLogger,
private val convoUseCase: ConvoUseCase,
private val messagesUseCase: MessagesUseCase
) {
@@ -34,8 +35,7 @@ class LongPollUpdatesParser(
private val exceptionHandler =
CoroutineExceptionHandler { _, throwable ->
Log.e("LongPollUpdatesParser", "error: $throwable")
throwable.printStackTrace()
logger.error(this::class, "CoroutineException", throwable)
}
private val coroutineContext: CoroutineContext
@@ -51,7 +51,7 @@ class LongPollUpdatesParser(
return when (val eventType = ApiEvent.parseOrNull(eventId)) {
null -> {
Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
logger.debug(this::class, "parseNextUpdate(): unknownEvent: $event")
emptyList()
}
@@ -83,7 +83,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseMessageSetFlags(): $eventType: $event")
val cmId = event[1].asLong()
val flags = event[2].asInt()
@@ -171,7 +171,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseMessageClearFlags(): $eventType: $event")
val cmId = event[1].asLong()
val flags = event[2].asInt()
@@ -246,7 +246,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseMessageNew(): $eventType: $event")
val cmId = event[1].asLong()
val peerId = event[4].asLong()
@@ -284,7 +284,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseMessageEdit(): $eventType: $event")
val cmId = event[1].asLong()
val peerId = event[3].asLong()
@@ -305,7 +305,8 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseMessageReadIncoming(): $eventType: $event")
val peerId = event[1].asLong()
val cmId = event[2].asLong()
val unreadCount = event[3].asInt()
@@ -323,7 +324,8 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseMessageReadOutgoing(): $eventType: $event")
val peerId = event[1].asLong()
val cmId = event[2].asLong()
val unreadCount = event[3].asInt()
@@ -342,7 +344,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseChatClearFlags(): $eventType: $event")
val peerId = event[1].asLong()
val flags = event[2].asInt()
@@ -402,7 +404,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseChatSetFlags(): $eventType: $event")
val peerId = event[1].asLong()
val flags = event[2].asInt()
@@ -462,7 +464,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseMessagesDeleted(): $eventType: $event")
val peerId = event[1].asLong()
val cmId = event[2].asLong()
@@ -479,7 +481,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseChatMajorChanged(): $eventType: $event")
val peerId = event[1].asLong()
val majorId = event[2].asInt()
@@ -496,7 +498,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseChatMinorChanged(): $eventType: $event")
val peerId = event[1].asLong()
val minorId = event[2].asInt()
@@ -513,7 +515,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
Log.d("LongPollUpdatesParser", "$eventType: $event")
logger.debug(this::class, "parseInteraction(): $eventType: $event")
val interactionType = when (eventType) {
ApiEvent.TYPING -> InteractionType.Typing
@@ -556,7 +558,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> {
Log.d("LongPollUpdatesParser", "$eventType $event")
logger.debug(this::class, "parseUnreadCounterUpdate(): $eventType: $event")
val unreadCount = event[1].asInt()
val unreadUnmutedCount = event[2].asInt()
@@ -583,7 +585,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
Log.d("LongPollUpdatesParser", "$eventType $event")
logger.debug(this::class, "parseMessageUpdated(): $eventType: $event")
val cmId = event[1].asLong()
val peerId = event[4].asLong()
@@ -605,7 +607,7 @@ class LongPollUpdatesParser(
eventType: ApiEvent,
event: List<Any>
): List<LongPollParsedEvent> = suspendCancellableCoroutine { continuation ->
Log.d("LongPollUpdatesParser", "$eventType $event")
logger.debug(this::class, "parseMessageCacheClear(): $eventType: $event")
val messageId = event[1].asLong()
@@ -639,7 +641,7 @@ class LongPollUpdatesParser(
).listenValue(this) { state ->
state.processState(
error = { error ->
Log.e("LongPollUpdatesParser", "loadMessage: error: $error")
logger.error(this::class, "loadMessage(): ERROR: $error")
continuation.resume(null)
},
success = { response ->
@@ -668,7 +670,7 @@ class LongPollUpdatesParser(
).listenValue(coroutineScope) { state ->
state.processState(
error = { error ->
Log.e("LongPollUpdatesParser", "loadConvo: error: $error")
logger.error(this::class, "loadConvo(): ERROR: $error")
continuation.resume(null)
},
success = { response ->
+1
View File
@@ -0,0 +1 @@
/build
+11
View File
@@ -0,0 +1,11 @@
plugins {
alias(libs.plugins.fast.android.library)
}
android {
namespace = "dev.meloda.fast.logger"
}
dependencies {
implementation(libs.koin.android)
}
@@ -0,0 +1,17 @@
package dev.meloda.fast.logger;
enum class FastLogLevel {
VERBOSE,
DEBUG,
INFO,
WARNING,
ERROR,
ASSERT;
companion object {
fun parse(value: Int): FastLogLevel {
if (value !in 0..5) throw IllegalArgumentException("Unknown LogLevel value $value")
return entries.first { it.ordinal == value }
}
}
}
@@ -0,0 +1,108 @@
package dev.meloda.fast.logger
import android.util.Log
import kotlin.reflect.KClass
class FastLogger {
companion object {
@Volatile
private lateinit var instance: FastLogger
fun setInstance(logger: FastLogger) {
if (::instance.isInitialized) {
throw IllegalStateException("FastLogger has already been initialized.")
}
instance = logger
}
fun getInstance(): FastLogger {
if (!::instance.isInitialized) {
throw UninitializedPropertyAccessException("FastLogger is not initialized.")
}
return instance
}
}
private var logLevel: FastLogLevel = FastLogLevel.ERROR
fun setLogLevel(logLevel: FastLogLevel) {
Log.v(this::class.java.simpleName, "Set LogLevel from ${this.logLevel} to $logLevel")
this.logLevel = logLevel
}
fun verbose(clazz: Class<*>, message: String, throwable: Throwable? = null) {
verbose(clazz.simpleName, message, throwable)
}
fun verbose(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.VERBOSE)) {
Log.v(tag, message, throwable)
}
}
fun debug(clazz: KClass<*>, message: String, throwable: Throwable? = null) {
debug(clazz.java, message, throwable)
}
fun debug(clazz: Class<*>, message: String, throwable: Throwable? = null) {
debug(clazz.simpleName, message, throwable)
}
fun debug(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.DEBUG)) {
Log.d(tag, message, throwable)
}
}
fun info(clazz: KClass<*>, message: String, throwable: Throwable? = null) {
info(clazz.java, message, throwable)
}
fun info(clazz: Class<*>, message: String, throwable: Throwable? = null) {
info(clazz.simpleName, message, throwable)
}
fun info(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.INFO)) {
Log.i(tag, message, throwable)
}
}
fun warning(clazz: Class<*>, message: String, throwable: Throwable? = null) {
warning(clazz.simpleName, message, throwable)
}
fun warning(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.WARNING)) {
Log.w(tag, message, throwable)
}
}
fun error(clazz: KClass<*>, message: String, throwable: Throwable? = null) {
error(clazz.java, message, throwable)
}
fun error(clazz: Class<*>, message: String, throwable: Throwable? = null) {
error(clazz.simpleName, message, throwable)
}
fun error(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.ERROR)) {
Log.e(tag, message, throwable)
}
}
fun assert(clazz: Class<*>, message: String, throwable: Throwable? = null) {
assert(clazz.simpleName, message, throwable)
}
fun assert(tag: String, message: String, throwable: Throwable? = null) {
if (shouldLog(FastLogLevel.ASSERT)) {
Log.wtf(tag, message, throwable)
}
}
private fun shouldLog(level: FastLogLevel): Boolean = level.ordinal >= logLevel.ordinal
}
@@ -0,0 +1,8 @@
package dev.meloda.fast.logger
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val loggerModule = module {
singleOf(::FastLogger)
}
@@ -1,7 +1,5 @@
package dev.meloda.fast.model.api.data
import android.util.Log
enum class AttachmentType(var value: String) {
UNKNOWN("unknown"),
PHOTO("photo"),
@@ -42,10 +40,6 @@ enum class AttachmentType(var value: String) {
it.value == value
} ?: UNKNOWN
if (parsedValue == UNKNOWN) {
Log.e("AttachmentType", "Unknown attachment type: $value")
}
return parsedValue
}
}
+1
View File
@@ -15,6 +15,7 @@ dependencies {
api(projects.core.common)
api(projects.core.model)
api(projects.core.datastore)
api(projects.core.logger)
implementation(libs.moshi.kotlin)
implementation(libs.koin.android)
@@ -1,10 +1,10 @@
package dev.meloda.fast.network
import android.util.Log
import com.slack.eithernet.ApiException
import com.slack.eithernet.errorType
import com.slack.eithernet.toType
import com.squareup.moshi.JsonDataException
import dev.meloda.fast.logger.FastLogger
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
@@ -16,7 +16,10 @@ import java.lang.reflect.Type
*
* допускает Unit как SuccessType в случае невозможности каста ответа в ErrorType
*/
class ResponseConverterFactory(private val converter: JsonConverter) : Converter.Factory() {
class ResponseConverterFactory(
private val converter: JsonConverter,
private val logger: FastLogger
) : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
@@ -29,6 +32,7 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
successType = type,
errorRaw = errorRaw,
converter = converter,
logger = logger
)
}
@@ -36,6 +40,7 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
private val successType: Type,
private val errorRaw: Class<*>,
private val converter: JsonConverter,
private val logger: FastLogger
) : Converter<ResponseBody, Any?> {
override fun convert(value: ResponseBody): Any? {
val string = value.string()
@@ -53,7 +58,7 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
},
onFailure = { failure ->
if (failure is JsonDataException) {
Log.d("ResponseBodyConverter", "convertJsonDataException: $failure")
logger.error(this::class, "convert(): ERROR", failure)
throw ApiException(
RestApiError(
errorCode = -1,
@@ -68,10 +73,11 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
converter.fromJson(errorRaw, string)
}.fold(
onSuccess = { errorModel ->
Log.d("ResponseBodyConverter", "convert: $errorModel")
logger.debug(this::class, "convert(): errorModel: $errorModel")
throw ApiException(errorModel)
},
onFailure = { exception ->
logger.error(this::class, "convert(): INNER: ERROR", exception)
if (!isUnit) {
throw exception
} else {
@@ -7,6 +7,7 @@ import com.slack.eithernet.integration.retrofit.ApiResultConverterFactory
import com.squareup.moshi.Moshi
import dev.meloda.fast.common.AppConstants
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.network.JsonConverter
import dev.meloda.fast.network.MoshiConverter
import dev.meloda.fast.network.OAuthResultCallFactory
@@ -123,7 +124,12 @@ private fun Scope.buildRetrofit(client: OkHttpClient): Retrofit {
.baseUrl("${AppConstants.URL_API}/")
.addConverterFactory(ApiResultConverterFactory)
.addCallAdapterFactory(ApiResultCallAdapterFactory)
.addConverterFactory(ResponseConverterFactory(get<JsonConverter>()))
.addConverterFactory(
ResponseConverterFactory(
get<JsonConverter>(),
get<FastLogger>()
)
)
.addConverterFactory(MoshiConverterFactory.create(get()))
.client(client)
.build()
@@ -1,9 +1,9 @@
package dev.meloda.fast.network.interceptor
import android.util.Log
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.CaptchaTokenResult
import dev.meloda.fast.logger.FastLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -14,11 +14,7 @@ import org.json.JSONObject
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicReference
class Error14HandlingInterceptor(
// private val domains: Set<String> = emptySet(),
) : Interceptor {
private val cookie = AtomicReference<String?>(null)
class Error14HandlingInterceptor(private val logger: FastLogger) : Interceptor {
private companion object {
private const val CAPTCHA_ERROR_CODE = 14
@@ -26,6 +22,8 @@ class Error14HandlingInterceptor(
private val executor = Executors.newSingleThreadExecutor()
}
private val cookie = AtomicReference<String?>(null)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().withCookie()
val response = chain.proceed(request)
@@ -41,23 +39,23 @@ class Error14HandlingInterceptor(
executor.submit {
AppSettings.setCaptchaRedirectUri(redirectUri)
Log.d("Error14Interceptor", "passCaptchaAndGetToken: $redirectUri")
logger.debug(this::class, "passCaptchaAndGetToken: $redirectUri")
var job: Job? = null
job = AppSettings.getCaptchaResultFlow()
.listenValue(CoroutineScope(Dispatchers.IO)) {
Log.d("Error14Interceptor", "passCaptchaAndGetToken: $it")
logger.debug(this::class, "passCaptchaAndGetToken: $it")
if (it != CaptchaTokenResult.Initial) {
synchronized(tokenResult) {
Log.d(
"Error14Interceptor",
logger.debug(
this::class,
"passCaptchaAndGetToken: SYNCHRONIZED: $it"
)
tokenResult.set(wrapResult(it))
tokenResult.notifyAll()
job?.cancel()
Log.d(
"Error14Interceptor",
logger.debug(
this::class,
"passCaptchaAndGetToken: NULL RESULT"
)
AppSettings.setCaptchaResult(CaptchaTokenResult.Initial)
@@ -71,7 +69,7 @@ class Error14HandlingInterceptor(
tokenResult.wait()
}
Log.d("Error14Interceptor", "passCaptchaAndGetToken: GET VALUE")
logger.debug(this::class, "passCaptchaAndGetToken: GET VALUE")
tokenResult.get().getOrThrow()
}
}
+1
View File
@@ -12,6 +12,7 @@ android {
dependencies {
api(projects.core.common)
api(projects.core.model)
api(projects.core.logger)
implementation(projects.core.presentation)
implementation(libs.haze)
@@ -0,0 +1,6 @@
package dev.meloda.fast.ui.common
import androidx.compose.runtime.compositionLocalOf
import dev.meloda.fast.logger.FastLogger
val LocalLogger = compositionLocalOf { FastLogger.getInstance() }