├── LICENSE ├── README.md ├── SentryLogBackend.kt └── Ulog.kt /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Danny Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # µlog 2 | 3 | Simple, fast, and efficient logging facade for Android apps. 4 | 5 | Inspired by [Timber](https://github.com/JakeWharton/timber) and [Logcat](https://github.com/square/logcat). 6 | 7 | ## Features 8 | 9 | - Lazy message evaluation 10 | - Pluggable backends (like Timber) 11 | - Fast automatic class name tags for debug 12 | - Easy to specify explicit tags when necessary (optional Kotlin named argument) 13 | - Debug and verbose logs completely optimized out from release builds 14 | 15 | These features improve performance (no more expensive toString calls accidentally left in release builds), reduce code size, and help make obfuscation more effective for sensitive code. 16 | 17 | ## Usage 18 | 19 | µlog isn’t available as a library because one of its key features, stripping debug and verbose logs in release builds, depends on BuildConfig. Instead, simply add [Ulog.kt](Ulog.kt) to your project and adjust the BuildConfig import. 20 | 21 | Install a backend at early init, e.g. in Application#onCreate: 22 | 23 | ```kotlin 24 | Ulog.installBackend(SystemLogBackend()) 25 | ``` 26 | 27 | Then log away: 28 | 29 | ```kotlin 30 | logV { "Call func" } 31 | logD { "Debug: $data" } 32 | logI(TAG) { "Starting service" } 33 | logW(TAG, e) { "Unexpected error" } 34 | logE(e) { "Failed to fetch data" } 35 | ``` 36 | -------------------------------------------------------------------------------- /SentryLogBackend.kt: -------------------------------------------------------------------------------- 1 | package dev.kdrag0n.app.debug 2 | 3 | import android.util.Log 4 | import dev.kdrag0n.app.log.LogBackend 5 | import io.sentry.Breadcrumb 6 | import io.sentry.IHub 7 | import io.sentry.SentryEvent 8 | import io.sentry.SentryLevel 9 | import io.sentry.protocol.Message 10 | 11 | // Based on Sentry's official Timber integration 12 | class SentryLogBackend( 13 | private val hub: IHub, 14 | private val minEventLevel: SentryLevel, 15 | private val minBreadcrumbLevel: SentryLevel 16 | ) : LogBackend { 17 | override fun print(tag: String, priority: Int, message: String, exception: Throwable?) { 18 | if (message.isEmpty() && exception == null) { 19 | return 20 | } 21 | 22 | val level = getSentryLevel(priority) 23 | val sentryMessage = Message().apply { 24 | this.message = message 25 | } 26 | 27 | captureEvent(level, tag, sentryMessage, exception) 28 | addBreadcrumb(level, sentryMessage, exception) 29 | } 30 | 31 | /** 32 | * do not log if it's lower than min. required level. 33 | */ 34 | private fun isLoggable( 35 | level: SentryLevel, 36 | minLevel: SentryLevel 37 | ): Boolean = level.ordinal >= minLevel.ordinal 38 | 39 | /** 40 | * Captures an event with the given attributes 41 | */ 42 | private fun captureEvent( 43 | sentryLevel: SentryLevel, 44 | tag: String?, 45 | msg: Message, 46 | throwable: Throwable? 47 | ) { 48 | if (isLoggable(sentryLevel, minEventLevel)) { 49 | val sentryEvent = SentryEvent().apply { 50 | level = sentryLevel 51 | throwable?.let { setThrowable(it) } 52 | tag?.let { 53 | setTag("UlogTag", it) 54 | } 55 | message = msg 56 | logger = "Ulog" 57 | } 58 | 59 | hub.captureEvent(sentryEvent) 60 | } 61 | } 62 | 63 | /** 64 | * Adds a breadcrumb 65 | */ 66 | private fun addBreadcrumb( 67 | sentryLevel: SentryLevel, 68 | msg: Message, 69 | throwable: Throwable? 70 | ) { 71 | // checks the breadcrumb level 72 | if (isLoggable(sentryLevel, minBreadcrumbLevel)) { 73 | val throwableMsg = throwable?.message 74 | val breadCrumb = when { 75 | msg.message != null -> Breadcrumb().apply { 76 | level = sentryLevel 77 | category = "Ulog" 78 | message = msg.formatted ?: msg.message 79 | } 80 | throwableMsg != null -> Breadcrumb.error(throwableMsg).apply { 81 | category = "exception" 82 | } 83 | else -> null 84 | } 85 | 86 | breadCrumb?.let { hub.addBreadcrumb(it) } 87 | } 88 | } 89 | } 90 | 91 | private fun getSentryLevel(priority: Int): SentryLevel { 92 | return when (priority) { 93 | Log.ASSERT -> SentryLevel.FATAL 94 | Log.ERROR -> SentryLevel.ERROR 95 | Log.WARN -> SentryLevel.WARNING 96 | Log.INFO -> SentryLevel.INFO 97 | Log.DEBUG -> SentryLevel.DEBUG 98 | Log.VERBOSE -> SentryLevel.DEBUG 99 | else -> SentryLevel.DEBUG 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Ulog.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * µlog: simple, fast, and efficient logging facade for Kotlin 3 | * 4 | * - Fast: lazy message evaluation, no stack traces used for automatic tags (like square/logcat) 5 | * - Extensible with backends like Timber, but with a simpler API 6 | * - Easier to specify explicit tags than Timber 7 | * - Debug and verbose logs optimized out at compile time 8 | * (completely removed from release builds) 9 | * 10 | * Licensed under the MIT License (MIT) 11 | * 12 | * Copyright (c) 2022 Danny Lin 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining a copy 15 | * of this software and associated documentation files (the "Software"), to deal 16 | * in the Software without restriction, including without limitation the rights 17 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | * copies of the Software, and to permit persons to whom the Software is 19 | * furnished to do so, subject to the following conditions: 20 | * 21 | * The above copyright notice and this permission notice shall be included in all 22 | * copies or substantial portions of the Software. 23 | * 24 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | * SOFTWARE. 31 | */ 32 | 33 | @file:Suppress("FunctionName") // _ indicates internal 34 | 35 | package dev.kdrag0n.app.log 36 | 37 | import android.app.Application 38 | import android.util.Log 39 | import dev.kdrag0n.app.BuildConfig 40 | import org.jetbrains.annotations.ApiStatus.Internal 41 | import java.io.PrintWriter 42 | import java.io.StringWriter 43 | 44 | // APIs must be public for inline calls 45 | object Ulog { 46 | // perf 47 | @Internal @JvmStatic var backends = emptyArray() 48 | 49 | const val LOG_VERBOSE = false 50 | @JvmStatic val LOG_DEBUG = BuildConfig.DEBUG 51 | 52 | fun installBackend(backend: LogBackend) { 53 | backends += backend 54 | } 55 | 56 | @JvmStatic 57 | @Internal 58 | fun _formatException(e: Throwable) = e.let { 59 | val sw = StringWriter(256) 60 | val pw = PrintWriter(sw, false) 61 | e.printStackTrace(pw) 62 | pw.flush() 63 | '\n' + sw.toString() 64 | } 65 | 66 | @JvmStatic 67 | @Internal 68 | fun _print(tag: String, priority: Int, msg: String, exception: Throwable?) { 69 | backends.forEach { 70 | it.print(tag, priority, msg, exception) 71 | } 72 | } 73 | 74 | // Change this if you have more stringent min API requirements 75 | @JvmStatic 76 | @Internal 77 | fun _getDefaultTag(): String = Application.getProcessName() 78 | } 79 | 80 | interface LogBackend { 81 | fun print(tag: String, priority: Int, message: String, exception: Throwable?) 82 | } 83 | 84 | class SystemLogBackend : LogBackend { 85 | override fun print(tag: String, priority: Int, message: String, exception: Throwable?) { 86 | val finalMsg = if (exception != null) message + Ulog._formatException(exception) else message 87 | Log.println(priority, tag, finalMsg) 88 | } 89 | } 90 | 91 | // Must be public for inline calls 92 | @Internal 93 | fun Any.__className_ulog_internal(): String { 94 | val clazz = this::class.java 95 | val name = clazz.simpleName 96 | // Slow path for anonymous classes 97 | return name.ifEmpty { clazz.name.split('.').last() } 98 | } 99 | 100 | /* 101 | * Generic (all levels) 102 | * Can't be optimized much as this needs to support all priorities. 103 | */ 104 | inline fun log( 105 | tag: String? = null, 106 | priority: Int = Log.DEBUG, 107 | exception: Throwable? = null, 108 | message: () -> String = { "" }, 109 | ) { 110 | val msg = message() 111 | val finalTag = tag ?: Ulog._getDefaultTag() 112 | Ulog._print(finalTag, priority, msg, exception) 113 | } 114 | inline fun log( 115 | exception: Throwable, 116 | tag: String? = null, 117 | priority: Int = Log.DEBUG, 118 | message: () -> String = { "" }, 119 | ) = log(tag, priority, exception, message) 120 | inline fun log( 121 | priority: Int, 122 | tag: String? = null, 123 | exception: Throwable? = null, 124 | message: () -> String = { "" }, 125 | ) = log(tag, priority, exception, message) 126 | 127 | inline fun Any.log( 128 | tag: String? = null, 129 | priority: Int = Log.DEBUG, 130 | exception: Throwable? = null, 131 | message: () -> String = { "" }, 132 | ) { 133 | val msg = message() 134 | val finalTag = tag ?: __className_ulog_internal() 135 | Ulog._print(finalTag, priority, msg, exception) 136 | } 137 | inline fun Any.log( 138 | exception: Throwable, 139 | tag: String? = null, 140 | priority: Int = Log.DEBUG, 141 | message: () -> String = { "" }, 142 | ) = log(tag, priority, exception, message) 143 | inline fun Any.log( 144 | priority: Int, 145 | tag: String? = null, 146 | exception: Throwable? = null, 147 | message: () -> String = { "" }, 148 | ) = log(tag, priority, exception, message) 149 | 150 | /* 151 | * Verbose 152 | * Optimized out at compile time when possible. 153 | */ 154 | inline fun logV( 155 | tag: String? = null, 156 | exception: Throwable? = null, 157 | message: () -> String = { "" }, 158 | ) { 159 | if (!Ulog.LOG_VERBOSE) return 160 | 161 | val msg = message() 162 | val finalTag = tag ?: Ulog._getDefaultTag() 163 | Ulog._print(finalTag, Log.VERBOSE, msg, exception) 164 | } 165 | inline fun logV( 166 | exception: Throwable, 167 | tag: String? = null, 168 | message: () -> String = { "" }, 169 | ) = logV(tag, exception, message) 170 | 171 | inline fun Any.logV( 172 | tag: String? = null, 173 | exception: Throwable? = null, 174 | message: () -> String = { "" }, 175 | ) { 176 | if (!Ulog.LOG_VERBOSE) return 177 | 178 | val msg = message() 179 | val finalTag = tag ?: __className_ulog_internal() 180 | Ulog._print(finalTag, Log.VERBOSE, msg, exception) 181 | } 182 | inline fun Any.logV( 183 | exception: Throwable, 184 | tag: String? = null, 185 | message: () -> String = { "" }, 186 | ) = logV(tag, exception, message) 187 | 188 | /* 189 | * Debug 190 | */ 191 | inline fun logD( 192 | tag: String? = null, 193 | exception: Throwable? = null, 194 | message: () -> String = { "" }, 195 | ) { 196 | if (!Ulog.LOG_DEBUG) return 197 | 198 | val msg = message() 199 | val finalTag = tag ?: Ulog._getDefaultTag() 200 | Ulog._print(finalTag, Log.DEBUG, msg, exception) 201 | } 202 | inline fun logD( 203 | exception: Throwable, 204 | tag: String? = null, 205 | message: () -> String = { "" }, 206 | ) = logD(tag, exception, message) 207 | 208 | inline fun Any.logD( 209 | tag: String? = null, 210 | exception: Throwable? = null, 211 | message: () -> String = { "" }, 212 | ) { 213 | if (!Ulog.LOG_DEBUG) return 214 | 215 | val msg = message() 216 | val finalTag = tag ?: __className_ulog_internal() 217 | Ulog._print(finalTag, Log.DEBUG, msg, exception) 218 | } 219 | inline fun Any.logD( 220 | exception: Throwable, 221 | tag: String? = null, 222 | message: () -> String = { "" }, 223 | ) = logD(tag, exception, message) 224 | 225 | /* 226 | * Other levels 227 | * (no special optimizations) 228 | */ 229 | inline fun logI( 230 | tag: String? = null, 231 | exception: Throwable? = null, 232 | message: () -> String = { "" }, 233 | ) = log(tag, Log.INFO, exception, message) 234 | inline fun logI( 235 | exception: Throwable, 236 | tag: String? = null, 237 | message: () -> String = { "" }, 238 | ) = log(tag, Log.INFO, exception, message) 239 | inline fun Any.logI( 240 | tag: String? = null, 241 | exception: Throwable? = null, 242 | message: () -> String = { "" }, 243 | ) = log(tag, Log.INFO, exception, message) 244 | inline fun Any.logI( 245 | exception: Throwable, 246 | tag: String? = null, 247 | message: () -> String = { "" }, 248 | ) = log(tag, Log.INFO, exception, message) 249 | 250 | inline fun logW( 251 | exception: Throwable? = null, 252 | tag: String? = null, 253 | message: () -> String = { "" }, 254 | ) = log(tag, Log.WARN, exception, message) 255 | inline fun logW( 256 | tag: String, 257 | exception: Throwable? = null, 258 | message: () -> String = { "" }, 259 | ) = log(tag, Log.WARN, exception, message) 260 | inline fun Any.logW( 261 | exception: Throwable? = null, 262 | tag: String? = null, 263 | message: () -> String = { "" }, 264 | ) = log(tag, Log.WARN, exception, message) 265 | inline fun Any.logW( 266 | tag: String, 267 | exception: Throwable? = null, 268 | message: () -> String = { "" }, 269 | ) = log(tag, Log.WARN, exception, message) 270 | 271 | inline fun logE( 272 | exception: Throwable? = null, 273 | tag: String? = null, 274 | message: () -> String = { "" }, 275 | ) = log(tag, Log.ERROR, exception, message) 276 | inline fun logE( 277 | tag: String, 278 | exception: Throwable? = null, 279 | message: () -> String = { "" }, 280 | ) = log(tag, Log.ERROR, exception, message) 281 | inline fun Any.logE( 282 | exception: Throwable? = null, 283 | tag: String? = null, 284 | message: () -> String = { "" }, 285 | ) = log(tag, Log.ERROR, exception, message) 286 | inline fun Any.logE( 287 | tag: String, 288 | exception: Throwable? = null, 289 | message: () -> String = { "" }, 290 | ) = log(tag, Log.ERROR, exception, message) 291 | --------------------------------------------------------------------------------