LogcatEngine is an Android library designed to efficiently capture and display your application's logcat output. The library utilizes C++17 at the native layer to monitor logcat via Linux Epoll and delivers it to the Kotlin layer via a Unix Pipe, keeping log capture work off the UI thread.
- High-Throughput Capture: Written in C++17 using event-driven
epoll, bounded pipes, and low-allocation line scanning. - Modern Kotlin API: Provides raw and structured
Flowstreams, integrating seamlessly with Coroutines and Jetpack Compose. - Watchdog & Resilience: Automatically monitors and restarts the logcat process if it's terminated by the system.
- Backpressure Handling: Uses a bounded
SharedFlowbuffer with aDROP_OLDESTpolicy to prevent memory overflow during log storms. - Memory Optimized: Employs NIO's
DirectByteBufferand Native Buffers to bypass Java GC pressure. - Hot-Swappable Line Filters: Update regex or plain text filters in real-time without restarting the capture thread.
- Fail-Open Factory: Provides a no-op engine when the native library is unavailable.
LogcatEngine is a bounded, best-effort capture library. It is designed to keep applications responsive during log storms, not to provide a lossless audit log.
The native writer uses non-blocking all-or-drop pipe writes, and the Kotlin
stream uses a bounded SharedFlow buffer with DROP_OLDEST. If consumers are
slow, older lines may be dropped so log capture does not block the engine or
grow memory without limit. The in-memory history is also bounded by
LogcatConfig.historyLimit.
Callers should treat the stream as recent diagnostic context. App behavior should not depend on every logcat line being delivered.
LogcatEngine: The public facade returned byLogcatEngineFactory.LogManagerremains available as the legacy singleton.- JNI Layer: Bridges Kotlin calls to the C++ core.
- Native Engine (C++17):
- Forks a dedicated
logcatprocess usingfork()andexecv(). - Uses
epollto wait for data on the pipe without polling when idle. - Processes streams at the byte level and scans newline boundaries with low-allocation buffers.
- Forks a dedicated
- Data Pipeline:
- The file descriptor (FD) is returned to Kotlin.
LogManagerreads from the pipe using an NIO Channel onDispatchers.IO.- Binary data is decoded and emitted via raw and structured
Flowstreams.
In your Activity or Service, initialize the engine with a typed config. To capture logs only from your app, pass the current PID.
tags accepts logcat's complete tag filter syntax, such as MyTag:V *:S;
when tags is set, it controls priorities and minLevel is not appended.
class MainActivity : ComponentActivity() {
private val engine = LogcatEngineFactory.create()
private lateinit var session: LogcatSession
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
session = engine.start(
LogcatConfig.currentProcess(
minLevel = LogLevel.Debug,
filter = LogFilter.None,
)
)
}
}Use collectAsState to listen to the session streams and update the UI.
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import com.core.logcat.capture.core.LogFilter
import com.core.logcat.capture.core.LogLevel
import com.core.logcat.capture.core.LogcatEngineFactory
import com.core.logcat.capture.core.LogcatConfig
import com.core.logcat.capture.core.LogcatSession
@Composable
fun LogConsole(session: LogcatSession) {
// Collect raw logs from the Flow
val latestLog by session.rawLogs.collectAsState(initial = "...")
// Or collect parsed logs from session.logs
// Engine status is available from session.state
// Recent parsed logs are available from session.history()
// UI to display the logs (e.g., in a LazyColumn)
// ...
}It's crucial to stop the engine when it's no longer needed to release the native thread and close the pipe. Call session.stop() in onDestroy.
class MainActivity : ComponentActivity() {
// ...
override fun onDestroy() {
// Stop the engine and release resources
session.stop()
super.onDestroy()
}
}For coroutine-based cleanup, call session.stopAndJoin() from a coroutine.
You can also export the bounded in-memory history:
session.exportHistory(file, LogExportFormat.JsonLines)The :core module is configured as a publishable Android AAR:
./gradlew :core:publishReleasePublicationToLocalStaticMavenRepositoryThe artifact coordinates are currently:
implementation("io.github.phuongtran:logcat-engine-core:1.3")This publishing setup is intentionally local/static for now. Release signing, license policy, and public repository hosting should be finalized before a real external release.
Read docs/doc.md for scope, runtime model, and delivery contract. Common product and integration questions are in docs/faq.md. Build, publish, and diagnostic commands are in docs/testing.md.
The MainActivity.kt file in the app module provides a complete example of how to integrate LogcatEngine into a UI built with Compose. It includes:
- Engine initialization and cleanup.
- Opt-in warning-log simulation.
- Displaying logs in a high-performance
LazyColumn. - Whole-screen scrolling with a back-to-top control.
- Color-coding log lines based on their level (Debug, Error, Warning).
You can run the app module to see the library in action.