Implement logging module

This commit is contained in:
Kirill Kamakin
2022-08-05 18:58:21 +02:00
parent 057651c60f
commit ba5f7322ab
11 changed files with 207 additions and 3 deletions

View File

@@ -68,6 +68,7 @@ dependencies {
implementation(project(":database")) implementation(project(":database"))
implementation(project(":datastore")) implementation(project(":datastore"))
implementation(project(":logging"))
implementation(libs.android.material.material) implementation(libs.android.material.material)

View File

@@ -4,22 +4,23 @@ import gq.kirmanak.mealient.data.add.AddRecipeDataSource
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.extensions.logAndMapErrors import gq.kirmanak.mealient.extensions.logAndMapErrors
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class AddRecipeDataSourceImpl @Inject constructor( class AddRecipeDataSourceImpl @Inject constructor(
private val addRecipeServiceFactory: ServiceFactory<AddRecipeService>, private val addRecipeServiceFactory: ServiceFactory<AddRecipeService>,
private val logger: Logger,
) : AddRecipeDataSource { ) : AddRecipeDataSource {
override suspend fun addRecipe(recipe: AddRecipeRequest): String { override suspend fun addRecipe(recipe: AddRecipeRequest): String {
Timber.v("addRecipe() called with: recipe = $recipe") logger.v { "addRecipe() called with: recipe = $recipe" }
val service = addRecipeServiceFactory.provideService() val service = addRecipeServiceFactory.provideService()
val response = logAndMapErrors( val response = logAndMapErrors(
block = { service.addRecipe(recipe) }, logProvider = { "addRecipe: can't add recipe" } block = { service.addRecipe(recipe) }, logProvider = { "addRecipe: can't add recipe" }
) )
Timber.v("addRecipe() response = $response") logger.v { "addRecipe() response = $response" }
return response return response
} }
} }

1
logging/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

14
logging/build.gradle.kts Normal file
View File

@@ -0,0 +1,14 @@
plugins {
id("gq.kirmanak.mealient.library")
id("dagger.hilt.android.plugin")
id("kotlin-kapt")
}
android {
namespace = "gq.kirmanak.mealient.logging"
}
dependencies {
implementation(libs.google.dagger.hiltAndroid)
kapt(libs.google.dagger.hiltCompiler)
}

View File

@@ -0,0 +1,11 @@
package gq.kirmanak.mealient.logging
interface Appender {
fun isLoggable(logLevel: LogLevel): Boolean
fun isLoggable(logLevel: LogLevel, tag: String): Boolean
fun log(logLevel: LogLevel, tag: String, message: String)
}

View File

@@ -0,0 +1,3 @@
package gq.kirmanak.mealient.logging
enum class LogLevel { VERBOSE, DEBUG, INFO, WARNING, ERROR }

View File

@@ -0,0 +1,59 @@
package gq.kirmanak.mealient.logging
import android.os.Build
import android.util.Log
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LogcatAppender @Inject constructor() : Appender {
private val isLoggable: Boolean by lazy { BuildConfig.DEBUG }
override fun isLoggable(logLevel: LogLevel): Boolean = isLoggable
override fun isLoggable(logLevel: LogLevel, tag: String): Boolean = isLoggable
override fun log(logLevel: LogLevel, tag: String, message: String) {
// Tag length limit was removed in API 26.
val logTag = if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) {
tag
} else {
tag.substring(0, MAX_TAG_LENGTH)
}
if (message.length < MAX_LOG_LENGTH) {
Log.println(logLevel.priority, logTag, message)
return
}
// Split by line, then ensure each line can fit into Log's maximum length.
var i = 0
val length = message.length
while (i < length) {
var newline = message.indexOf('\n', i)
newline = if (newline != -1) newline else length
do {
val end = newline.coerceAtMost(i + MAX_LOG_LENGTH)
val part = message.substring(i, end)
Log.println(logLevel.priority, logTag, part)
i = end
} while (i < newline)
i++
}
}
companion object {
private const val MAX_LOG_LENGTH = 4000
private const val MAX_TAG_LENGTH = 23
}
}
private val LogLevel.priority: Int
get() = when (this) {
LogLevel.VERBOSE -> Log.VERBOSE
LogLevel.DEBUG -> Log.DEBUG
LogLevel.INFO -> Log.INFO
LogLevel.WARNING -> Log.WARN
LogLevel.ERROR -> Log.ERROR
}

View File

@@ -0,0 +1,16 @@
package gq.kirmanak.mealient.logging
typealias MessageSupplier = () -> String
interface Logger {
fun v(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
fun d(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
fun i(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
fun w(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
fun e(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
}

View File

@@ -0,0 +1,75 @@
package gq.kirmanak.mealient.logging
import android.util.Log
import java.util.regex.Pattern
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LoggerImpl @Inject constructor(
private val appenders: Set<@JvmSuppressWildcards Appender>,
) : Logger {
override fun v(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) {
log(LogLevel.VERBOSE, tag, messageSupplier, throwable)
}
override fun d(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) {
log(LogLevel.DEBUG, tag, messageSupplier, throwable)
}
override fun i(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) {
log(LogLevel.INFO, tag, messageSupplier, throwable)
}
override fun w(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) {
log(LogLevel.WARNING, tag, messageSupplier, throwable)
}
override fun e(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) {
log(LogLevel.ERROR, tag, messageSupplier, throwable)
}
private fun log(
logLevel: LogLevel,
tag: String?,
messageSupplier: MessageSupplier,
t: Throwable?
) {
var logTag: String? = null
var message: String? = null
for (appender in appenders) {
if (appender.isLoggable(logLevel).not()) continue
logTag = logTag ?: tag ?: Throwable().stackTrace
.first { element -> !IGNORED_CLASSES.any { element.className.contains(it) } }
.let(::createStackElementTag)
if (appender.isLoggable(logLevel, logTag).not()) continue
message = message ?: (messageSupplier() + createStackTrace(t))
appender.log(logLevel, logTag, message)
}
}
private fun createStackTrace(throwable: Throwable?): String =
throwable?.let { Log.getStackTraceString(it) }
?.takeUnless { it.isBlank() }
?.let { "\n" + it }
.orEmpty()
private fun createStackElementTag(element: StackTraceElement): String {
var tag = element.className.substringAfterLast('.')
val m = ANONYMOUS_CLASS.matcher(tag)
if (m.find()) {
tag = m.replaceAll("")
}
return tag
}
companion object {
private val IGNORED_CLASSES = listOf(Logger::class.java.name, LoggerImpl::class.java.name)
private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")
}
}

View File

@@ -0,0 +1,22 @@
package gq.kirmanak.mealient.logging
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface LoggingModule {
@Binds
@Singleton
fun bindLogger(loggerImpl: LoggerImpl): Logger
@Binds
@Singleton
@IntoSet
fun bindLogcatAppender(logcatAppender: LogcatAppender): Appender
}

View File

@@ -22,3 +22,4 @@ rootProject.name = "Mealient"
include(":app") include(":app")
include(":database") include(":database")
include(":datastore") include(":datastore")
include(":logging")