Skip to content

Enhance logging and task execution handling in Android#1

Open
tiper wants to merge 6 commits into
mainfrom
experimental
Open

Enhance logging and task execution handling in Android#1
tiper wants to merge 6 commits into
mainfrom
experimental

Conversation

@tiper

@tiper tiper commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

No description provided.

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

Copilot AI left a comment

Copy link
Copy Markdown

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 thread src/appleMain/kotlin/com/mindera/lodge/appenders/OSAppender.kt
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." }

Copilot AI Mar 27, 2026

Copy link

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)

Copilot AI Mar 27, 2026

Copy link

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)
}

Copilot AI Mar 27, 2026

Copy link

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)
}
}
}

Copilot AI Mar 27, 2026

Copy link

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)
}
}
}
}

Copilot AI Mar 27, 2026

Copy link

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 6 comments.


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

private val job = CoroutineScope(SupervisorJob() + Default.limitedParallelism(1)).apply {
launch(start = UNDISPATCHED) {
for (task in tasks) {
task()
Comment on lines +55 to 61
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." }
}
}
Comment on lines +85 to 96
fun remove(id: String) {
tasks.trySend { appenders.removeAll { id == it.loggerId } }
}

/**
* Remove log appenders
*
* @param ids Log ids of each of the loggers enabled by the order sent
*/
fun remove(ids: Set<String>) = mutex.withLock {
this.appenders.removeAll { ids.contains(it.loggerId) }
fun remove(ids: Set<String>) {
tasks.trySend { appenders.removeAll { ids.contains(it.loggerId) } }
}
log("LOG", WARN, null) { "Appender '${candidate.loggerId}' discarded: an appender with that id is already registered." }
}
}
}
Comment on lines +355 to 365
) {
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)
}
}
}
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