Skip to content

Enhance logging and task execution handling in Android#1

Open
tiper wants to merge 5 commits intomainfrom
experimental
Open

Enhance logging and task execution handling in Android#1
tiper wants to merge 5 commits intomainfrom
experimental

Conversation

@tiper
Copy link
Copy Markdown
Contributor

@tiper tiper commented Mar 27, 2026

No description provided.

@tiper tiper requested a review from Copilot March 27, 2026 14:14
@tiper tiper self-assigned this Mar 27, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the core multiplatform logger to queue log/appender operations on a single-threaded coroutine worker, adds lazy message overloads for log calls, and tweaks platform appenders and build toolchain settings.

Changes:

  • Replaces mutex/runBlocking synchronization in LOG with a coroutine/Channel-backed serial work queue and adds lambda-based logging overloads.
  • Adjusts Apple/Android appenders’ formatting behavior (stack trace concatenation; chunk splitting output).
  • Updates Gradle JVM toolchain from 8 to 11.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 7 comments.

File Description
src/commonMain/kotlin/com/mindera/lodge/LOG.kt Serializes add/remove/log operations via a coroutine worker; adds lazy-message overloads; deduplicates appenders by loggerId.
src/appleMain/kotlin/com/mindera/lodge/appenders/OSAppender.kt Combines message + stack trace into a single OS log call.
src/androidMain/kotlin/com/mindera/lodge/appenders/LogcatAppender.kt Simplifies chunk splitting logic for long messages.
build.gradle.kts Moves compilation toolchain to JDK 11.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 41 to 45
override fun log(severity: SEVERITY, tag: String, t: Throwable?, log: String) {
with(severity.toLevel()) {
val prefix = severity.prefix(tag)
log(prefix + log)
t?.let {
log(prefix + it.stackTraceToString())
}
log(t?.let { prefix + "$log\n" + it.stackTraceToString() } ?: (prefix + log))
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When t is non-null, the new implementation logs prefix + log + "\n" + stackTrace but does not prefix the stack trace (previously it was logged as a separate prefixed line). If consumers rely on the prefix for filtering/reading logs, consider keeping the prefix on the stack trace as well (or keep two log calls) and avoid the unnecessary "$log" interpolation.

Copilot uses AI. Check for mistakes.
Comment on lines 57 to 62
val chunks = ceil((1f * length / maxLineLength).toDouble()).toInt()
for (i in 1..chunks) {
val start = maxLineLength * (i - 1)
val max = maxLineLength * i
if (max < length) {
it.add("[Chunk $i of $chunks] " + substring(maxLineLength * (i - 1), max))
} else {
it.add("[Chunk $i of $chunks] " + substring(maxLineLength * (i - 1)))
}
it.add(if (max < length) substring(start, max) else substring(start))
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toChunks() no longer includes any chunk marker (the previous "[Chunk i of chunks]" prefix was removed), which makes multi-part messages hard to reconstruct in Logcat. Consider restoring a chunk indicator (or otherwise tagging chunks) so long messages remain debuggable; also consider whether the Throwable should be logged only once rather than repeated for every chunk.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +59
fun add(appender: Appender) = tasks.trySend {
if (appenders.none { it.loggerId == appender.loggerId }) {
appenders.add(appender)
} else {
log("LOG", WARN, null) { "Appender '${appender.loggerId}' discarded: an appender with that id is already registered." }
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add(appender: Appender) now returns the result of tasks.trySend(...) (a ChannelResult<Unit>), which changes the public API from returning Unit and makes it inconsistent with the other add/remove methods. Consider switching back to a block body returning Unit (and optionally handling a failed send explicitly) to avoid a breaking signature change for consumers.

Suggested change
fun add(appender: Appender) = tasks.trySend {
if (appenders.none { it.loggerId == appender.loggerId }) {
appenders.add(appender)
} else {
log("LOG", WARN, null) { "Appender '${appender.loggerId}' discarded: an appender with that id is already registered." }
fun add(appender: Appender) {
tasks.trySend {
if (appenders.none { it.loggerId == appender.loggerId }) {
appenders.add(appender)
} else {
log("LOG", WARN, null) { "Appender '${appender.loggerId}' discarded: an appender with that id is already registered." }
}

Copilot uses AI. Check for mistakes.
Comment on lines +32 to 33
private val tasks = Channel<() -> Unit>(UNLIMITED)

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Channel.Factory.UNLIMITED for the work queue means logging spikes can grow the queue without bound and potentially exhaust memory. For a logger, consider a bounded capacity with an explicit onBufferOverflow strategy (drop oldest/latest) or a non-blocking backpressure approach, and treat appender add/remove separately if they must be lossless.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +46
private var delayMillis = 0L

/**
* A dedicated coroutine that pulls lambdas out of `tasks` and executes
* them one-by-one, preserving the exact order in which they were queued.
*/
@Suppress("unused")
private val job = CoroutineScope(SupervisorJob() + Default.limitedParallelism(1)).apply {
launch(start = UNDISPATCHED) {
for (task in tasks) {
task()
delay(delayMillis)
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delayMillis is only used in the worker loop but is never written anywhere in the codebase, so the delay(delayMillis) currently adds complexity without providing behavior. Either remove the delay entirely, or add an explicit (documented) API for configuring it so the field isn’t dead code.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to 48
@Suppress("unused")
private val job = CoroutineScope(SupervisorJob() + Default.limitedParallelism(1)).apply {
launch(start = UNDISPATCHED) {
for (task in tasks) {
task()
delay(delayMillis)
}
}
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The job property is not a Job (it’s a CoroutineScope returned from apply) and is only kept for side effects. Renaming it to reflect what it actually is (e.g., workerScope/worker) will reduce confusion for future maintainers.

Copilot uses AI. Check for mistakes.
Comment on lines 350 to 366
private fun log(
tag: String,
severity: SEVERITY,
t: Throwable?,
text: String
) = mutex.withLock {
if(appenders.isNotEmpty()) {
val log = "[T#$threadName] | $text"
appenders.forEach {
if (it.minLogLevel.ordinal > severity.ordinal) return@forEach
it.log(severity, tag, t, log)
message: () -> String,
) {
val originalThread = threadName
tasks.trySend {
if (appenders.isNotEmpty()) {
val log = "[T#$originalThread] | ${message()}"
appenders.forEach {
if (it.minLogLevel.ordinal > severity.ordinal) return@forEach
it.log(severity, tag, t, log)
}
}
}
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add/remove/log are now fire-and-forget enqueues (trySend) rather than completing synchronously as they did with the previous Mutex.withLock(runBlocking) approach. This is a behavior change for callers (e.g., add() returning before the appender is actually registered, logs potentially not flushed before process exit). Consider documenting this explicitly and/or providing a synchronous/flush option, and handle trySend failures so logs aren’t silently dropped if the channel is closed.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants