ログ表示あれこれ
いい感じのLoggerが無いので結局自作になりそう
Logcatに表示
String
まずは基本のStringのおさらい
// String型は""
var text = "text"
var num = 0
val test = "test"
// 変数を挿入するときは$をつける
text = "$test text $num"
// NG 変数testtextと解釈されて存在してないからエラー
//text = "$testtext $num"
// OK 英数字以外で区切る
text = "$test:text$num"
// OK または${}で囲う
text = "${test}text$num"
// 式を代入するときは${}で囲う
text = "${++num} ${num + 1}"
// 結合は+フォーマット指定はString.format()
text += String.format(" %s %04d", test, num)
// 数値の桁あわせは以下でも
//text = "${num.toString().padStart(4, '0')}"
text = num.toString().padStart(4, '0')
// 16進数表示(大文字)
text = Integer.toHexString(num.toInt()).uppercase()
// Java同様Kotlinもkotlin.text.StringBuilderが使える
val text_ = StringBuilder(text)
text_.append(test)
text_.append(num)
text = text_.toString()
println
logcatに表示する標準機能
// I/System.out: (ログの内容)
println("...")
android.util.Log
// V/(TAG): (ログの内容)
Log.v("TAG","...")
// D/(TAG): (ログの内容)
Log.d("TAG","...")
// I/(TAG): (ログの内容)
Log.i("TAG","...")
// W/(TAG): (ログの内容)
Log.w("TAG","...")
// E/(TAG): (ログの内容)Timber
Log.e("TAG","...")
// E/(TAG): (ログの内容)
Log.wtf("TAG","...")
出力したい内容 / Exceptionハンドリング
Thread ID / Line番号 / Class Name / Methord Name
StackTraceElementで情報が取れるが、Thread.currentThread().getStackTrace() / Throwable().stackTraceで内容が変わる
どのindexのElementを使うかは目的に合わせて実装が必要
val threadId = Integer.toHexString(Thread.currentThread().id.toInt()).uppercase().padStart(4, '0')
val threadName = Thread.currentThread().name ?: ""
// Thread.currentThread().getStackTrace().forEach {
Throwable().stackTrace.forEach {
val text = String.format("%s[%s] L%04d %s %s", threadName, threadId , it.lineNumber, it.className, it.methodName)
println(text)
}
DefaultUncaughtExceptionHandler
/**
* アプリ全体でthrowしたExceptionをハンドリングします
*
* ApplicationクラスのonCreateなどで呼び出してください
*
* 例.
* ```kotlin
* setUncaughtExceptionHandler(isExit = true) { thread, throwable ->
* println(thread.toString())
* throwable.printStackTrace()
* }
* ```
*
* @param isExit 意図しないException発生時はアプリが不安定になることが多いためアプリ終了をサポートしています
*/
fun setUncaughtExceptionHandler(isExit: Boolean = true, process: (thread: Thread, throwable: Throwable) -> Unit) {
val handler = Thread.UncaughtExceptionHandler { thread, throwable ->
process(thread, throwable)
//TODO: ApplicationやActivityのonCreate()完了前にハンドリングしてしまった場合無限ループになる
if (isExit) android.os.Process.killProcess(android.os.Process.myPid())
}
Thread.setDefaultUncaughtExceptionHandler(handler)
}
Logger(自作)
アプリのパフォーマンスにも影響あるのでファイル出力は要件が無い限りデバック時のみの利用が良いかも
ファイルサイズやファイルのインクリメントとかは配慮してないです
クラッシュやANRはGoogle Play ConsoleとかFirebase Crashlyticsとかを利用がよさそう
/**
* getCallMethodElement()を実行したメソッドを呼び出したメソッドのStackTraceElementを返します
*
* ```kotlin
* val element = getCallMethodElement()
* element?.let {
* println(String.format("line:%04d class:%s method:%s", it.lineNumber, it.className, it.methodName))
* }
* ```
*/
fun getCallMethodElement(): StackTraceElement? {
val element: StackTraceElement = Throwable().stackTrace[1]
var find = false
Thread.currentThread().stackTrace.forEach {
if (find) {
val target: String = it.methodName.split("$")[0]
if (target != element.methodName) return it
}
if (it.className.equals(element.className) && it.methodName.equals(element.methodName)) find = true
}
return null
}
class Logger {
enum class LogLevel(val value: Int) {
VERBOSE(Log.VERBOSE),
DEBUG(Log.DEBUG),
INFO(Log.VERBOSE),
WARN(Log.WARN),
ERROR(Log.ERROR),
ASSERT(Log.ASSERT);
}
companion object {
private var TAG: String = ""
private var fileName: String? = null
@Volatile private var queue: ArrayDeque<String>? = null
private var scope: CoroutineScope? = null
fun initialize(tag: String = "", fileName: String? = null) {
TAG = tag
this.fileName = fileName
if (fileName != null) {
queue = ArrayDeque()
scope = MainScope()
}
}
private fun output(context: Context) {
queue?.let {
while (it.isNotEmpty()) {
val text: String = it.first()
it.removeFirst()
context.openFileOutput(fileName, Context.MODE_PRIVATE or Context.MODE_APPEND).use { stream ->
stream.write(text.toByteArray())
}
}
}
}
private fun write(context: Context, text: String) {
queue?.let {
val now = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
val line = String.format("%s %s\n", now.format(formatter), text)
it.add(line)
scope?.launch {
output(context)
}
}
}
private fun log(level: Int, tag: String, message: String, element: StackTraceElement?, context: Context? = null) {
var text = message
element?.let {
text = String.format("[%04x] L%04d %s#%s: %s", Thread.currentThread().id, it.lineNumber, it.className, it.methodName, message)
}
when (level) {
LogLevel.VERBOSE.value -> Log.v(tag, text)
LogLevel.DEBUG.value -> Log.d(tag, text)
LogLevel.INFO.value -> Log.i(tag, text)
LogLevel.WARN.value -> Log.w(tag, text)
LogLevel.ERROR.value -> Log.e(tag, text)
LogLevel.ASSERT.value -> Log.wtf(tag, text)
else -> Log.v(tag, text)
}
context?.let {
val lv = when (level) {
LogLevel.VERBOSE.value -> "V"
LogLevel.DEBUG.value -> "E"
LogLevel.INFO.value -> "I"
LogLevel.WARN.value -> "W"
LogLevel.ERROR.value -> "E"
LogLevel.ASSERT.value -> "C"
else -> "V"
}
write(it, String.format("%s %s", lv, text))
}
}
private fun log(level: LogLevel, tag: String, message: String, element: StackTraceElement?, context: Context? = null) {
log(level.value, tag, message, element, context)
}
fun v(message: String, context: Context? = null) {
log(LogLevel.VERBOSE, TAG, message, getCallMethodElement(), context)
}
fun d(message: String, context: Context? = null) {
log(LogLevel.DEBUG, TAG, message, getCallMethodElement(), context)
}
fun i(message: String, context: Context? = null) {
log(LogLevel.INFO, TAG, message, getCallMethodElement(), context)
}
fun w(message: String, context: Context? = null) {
log(LogLevel.WARN, TAG, message, getCallMethodElement(), context)
}
fun e(message: String, context: Context? = null) {
log(LogLevel.ERROR, TAG, message, getCallMethodElement(), context)
}
fun wtf(message: String, context: Context? = null) {
log(LogLevel.ASSERT, TAG, message, getCallMethodElement(), context)
}
}
}
サードベンダーのLogger
Timber
TimberはLogcatの拡張でLogcatの出力はandroid.util.Logと同じ
TAGを省略できたりフォーマットを変えるのに便利だけどサードベンダーに依存したくないから試すだけ
Project Structureで以下をimplementation
com.jakewharton.timber
class TimberDebugTree(private val context: Context, private val tag: String): Timber.DebugTree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
super.log(priority, String.format("%s: %s", this.tag, tag), message, t)
}
override fun createStackElementTag(element: StackTraceElement): String {
return String.format("%s@%s[%04x] L%d", element.className, element.methodName, Thread.currentThread().id, element.lineNumber)
}
}
fun initializeTimber(context: Context, tag: String, isDebug: Boolean) {
if (isDebug) {
Timber.plant(TimberDebugTree(context, tag))
}
}
class MainActivity : ComponentActivity() {
...
initializeTimber(this, "TAG", BuildConfig.DEBUG)
Timber.v("...")
Timber.d("...")
Timber.i("...")
Timber.w("...")
Timber.e("...")
Timber.wtf("...")
}
log4j2 Kotlin(使えない)
https://logging.apache.org/log4j/kotlin/index.html
https://github.com/apache/logging-log4j-kotlin
以下のエラーが出て使えない…設定が悪いのだろうか?
LogManager.getContext()がSimpleLoggerContextになってて、原因はlog4j-coreのclasspathが通ってないかららしいが…
ERROR StatusLogger Unable to load services for service class org.apache.logging.log4j.spi.Provider
結構粘ったけど無理して使うほどのものでも無いので不採用
android {
packagingOptions {
resources {
excludes += '/META-INF/DEPENDENCIES'
}
}
}
dependencies {
implementation 'org.apache.logging.log4j:log4j-api-kotlin:1.2.0'
implementation 'org.apache.logging.log4j:log4j-api:2.19.0'
implementation 'org.apache.logging.log4j:log4j-core:2.19.0'
}
import org.apache.logging.log4j.kotlin.Logging
class MainActivity : ComponentActivity(), Logging {
...
logger.debug("...")
logger.info("...")
logger.warn("...")
logger.error("...")
logger.fatal("...")
}
log4j2.xmlをAndroid StudioのProjectのどこに置いても読み込まれないらしいのでAssetsに置く例
val xml = assets.open("log4j2.xml").reader(charset=Charsets.UTF_8).use{ it.readText() }
val stream: InputStream = ByteArrayInputStream(xml.toByteArray())
val source: ConfigurationSource? = ConfigurationSource(stream)
source?.let {
val context = LogManager.getContext(false)
val factory = org.apache.logging.log4j.core.config.xml.XmlConfigurationFactory.getInstance()
// Castできずエラー
val config = factory.getConfiguration(context as LoggerContext?, it)
Configurator.initialize(config)
config.start()
}
val logger = LogManager.getLogger("test")
logger.debug("...")
Android Studio Dolphin 2021.3.1 Patch 1 built on September 30, 2022