FlexiLogger
2.1.3indexedFlexible, configurable logging with console output, crash-reporting hooks, file logging, automatic long-message chunking, level-based filtering, and HTTP client integrations (OkHttp, Ktor).
Flexible, configurable logging with console output, crash-reporting hooks, file logging, automatic long-message chunking, level-based filtering, and HTTP client integrations (OkHttp, Ktor).
Kotlin Multiplatform logging library providing flexible, configurable logging with support for console output, crash reporting integration, and file logging.
Supported Platforms: Android, iOS, JVM (Desktop), JavaScript (Browser/Node.js)
@JvmOverloadsAdd the dependency to your project:
Kotlin Multiplatform:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.projectdelta6:flexilogger:2.1.3")
// Optional: Ktor HTTP logging (all platforms)
implementation("io.github.projectdelta6:flexilogger-ktor:2.1.3")
}
// Optional: OkHttp HTTP logging (JVM/Android only)
jvmMain.dependencies {
implementation("io.github.projectdelta6:flexilogger-okhttp:2.1.3")
}
androidMain.dependencies {
implementation("io.github.projectdelta6:flexilogger-okhttp:2.1.3")
}
}
}
Android/JVM only:
dependencies {
implementation("io.github.projectdelta6:flexilogger:2.1.3")
implementation("io.github.projectdelta6:flexilogger-okhttp:2.1.3") // Optional
}
For projects still using the Android-only 1.x versions, JitPack remains available:
Add JitPack to your settings.gradle.kts:
Create a class that extends FlexiLog. For convenience, create it as a Kotlin object named Log:
When integrating with crash reporting tools like Sentry or Crashlytics, the default behavior often shows the logging library's internal code as the error source instead of where Log.e() was actually called.
FlexiLogger solves this by capturing the actual call site and passing it to your report() methods via the CallSite parameter.
Override the CallSite-aware report methods:
CallSite Properties:
Platform Support:
null (not supported)null (not supported)Skipping Additional Packages:
FlexiLogger automatically skips its own internal frames and any class extending FlexiLog. If you have additional wrapper classes to skip:
object Log : FlexiLog() {
override fun getAdditionalSkipPackages(): List<String> =
listOf("com.myapp.util.LogWrapper")
}
import com.example.Log
// Using 'this' as the tag (extracts class name)
Log.d(this, "Debug message")
Log.i(this, "Info message")
Log.w(this, "Warning message")
Log.e(, , exception)
Log.d(, )
Log.e(, , exception)
Log.e(, , forceReport = )
// Only logs if condition is true
Log.onCondition(BuildConfig.DEBUG) { logger ->
logger.d(this, "This only logs in debug builds")
}
// Suspend version for coroutines
Log.onConditionSuspend(isVerboseMode) { logger ->
logger.v(this, "Verbose logging enabled")
}
Use LoggerWithLevel for subsystem-specific log filtering:
// Create a logger that only logs WARNING and above
val networkLogger = Log.withLevel(LoggingLevel.W)
networkLogger.d("Network", "This won't be logged") // Filtered out
networkLogger.w("Network", "This will be logged") // Passes through
networkLogger.e("Network", "This will be logged") // Passes through
Override the file logging methods to enable persistent logging:
import com.duck.flexilogger.okhttp.FlexiLogHttpLoggingInterceptorLogger
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
val loggingInterceptor = HttpLoggingInterceptor(
FlexiLogHttpLoggingInterceptorLogger.with(Log, LoggingLevel.D)
).apply {
level = HttpLoggingInterceptor.Level.BODY
}
val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
import com.duck.flexilogger.ktor.FlexiLoggerPlugin
import com.duck.flexilogger.ktor.installFlexiLogger
import io.ktor.client.*
// Simple installation
val client = HttpClient {
installFlexiLogger(Log, LoggingLevel.D)
}
// Or with full configuration
val client = HttpClient {
install(FlexiLoggerPlugin) {
logger = Log
level = LoggingLevel.D
tag = "HTTP"
logHeaders = true
logBody = false
sanitizedHeaders = setOf("Authorization", "Cookie")
}
}
enum class LogType {
D, // Debug
E, // Error
I, // Info
V, // Verbose
W, // Warning
WTF // What a Terrible Failure
}
Hierarchical levels for filtering (higher value = more verbose):
enum class LoggingLevel(val level: Int) {
V(5), // Verbose - logs everything
I(4), // Info
D(3), // Debug
W(2), // Warning
E(1), // Error only
NONE(0) // Logs nothing
}
The caller parameter can be:
Any - class name is extracted automaticallyString - used directly as the tagClass<*> - (JVM/Android only) class name is extracteddata class CallSite(
val className: String, // Fully qualified class name
val methodName: String, // Method name
val fileName: String?, // Source file name (nullable)
val lineNumber:
) {
simpleClassName: String
: String
}
No consumer keep rules are required. FlexiLogger and its flexilogger-okhttp /
flexilogger-ktor integrations use no runtime reflection on consumer types, no
serialization, and no by-name enum lookups, so R8 full-mode (including the consuming
app's classes) is safe out of the box. ProGuard/R8 applies only to the Android/JVM
artifacts; the iOS and JS targets are unaffected.
JVM bytecode fix. The *-jvm artifacts for 2.0.0–2.1.2 were accidentally compiled
to Java 24 bytecode, so pure-JVM (non-Android) consumers on a JDK below 24 hit
UnsupportedClassVersionError. 2.1.3 restores Java 11 bytecode. If you worked around this
by bumping your build to JDK 24, you can revert that once on . — Android consumers were never affected.
Class name resolution fix (behaviour change). Passing a class reference as the
caller now resolves to the represented class name on every platform:
Log.d(MyActivity::class.java, "msg") // tag: "MyActivity" (previously "Class")
Log.d(MyActivity::class, "msg") // tag: "MyActivity" (previously wrong on all platforms)
Previously these were silently mis-tagged because the Any overload shadows the
Class<*> extension. If you relied on the old (incorrect) "Class"/KClass tag
text — unlikely — your log tags will change. Passing this or a String tag is
unaffected.
(JVM/Android) is deprecated; use directly (with a fix). The log overloads on / (/////) are also deprecated — the overload now handles / callers, so keeps working unchanged. All are kept for legacy Java interop.
Version 2.1.0 adds call site capture for crash reporting. This is fully backward compatible:
report() implementations continue to work unchangedreport(..., callSite: CallSite?) methodsIf you're upgrading from the Android-only version:
com.duck.flexihttplogger → com.duck.flexilogger.okhttpReleases are published to Maven Central via the Vanniktech Maven Publish plugin, orchestrated by publish.sh:
# Dry run — runs every step (checks, clean, tests, coverage gate) EXCEPT the upload
./publish.sh --dry-run # or -n
# Full release — same gates, then confirms and publishes to Maven Central
./publish.sh
Before publishing, the script:
master../gradlew allTests koverVerify — the full test suite and the coverage floor —
and aborts the release if either fails.On a successful publish, the script tags the released commit vX.Y.Z (from flexiLoggerVersion)
and pushes the tag to GitHub.
See CHANGELOG.md for the version history. Bump flexiLoggerVersion in
gradle/libs.versions.toml and update the README install snippets before releasing.
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
dependencyResolutionManagement {
repositories {
maven { url = uri("https://jitpack.io") }
}
}
build.gradle.kts:
dependencies {
implementation("com.github.projectdelta6.FlexiLogger:FlexiLogger:1.x.x")
implementation("com.github.projectdelta6.FlexiLogger:FlexiHttpLogger:1.x.x") // Optional
}
Replace 1.x.x with the desired 1.x release tag.
package com.example
import com.duck.flexilogger.FlexiLog
import com.duck.flexilogger.LogType
object Log : FlexiLog() {
override fun canLogToConsole(type: LogType): Boolean {
// Return true to output logs to the platform console
// Android: android.util.Log
// iOS: NSLog
// JVM: java.util.logging
// JS: console.log/warn/error
return true // or: BuildConfig.DEBUG on Android
}
override fun shouldReport(type: LogType): Boolean {
// Return true for log types you want sent to crash reporting
return type == LogType.E || type == LogType.WTF
}
override fun shouldReportException(tr: Throwable): Boolean {
// Filter which exceptions get reported
// Return false to ignore network errors, cancellations, etc.
return true
}
override fun report(type: LogType, tag: String, msg: String) {
// Implement crash reporting (Sentry, Crashlytics, etc.)
// Note: For call site info, override the CallSite version instead
}
override fun report(type: LogType, tag: String, msg: String, tr: Throwable) {
// Implement crash reporting with exception
// Note: For call site info, override the CallSite version instead
}
}
import com.duck.flexilogger.CallSite
import com.duck.flexilogger.FlexiLog
import com.duck.flexilogger.LogType
import io.sentry.Sentry
import io.sentry.SentryEvent
import io.sentry.protocol.Message
import io.sentry.protocol.SentryException
import io.sentry.protocol.SentryStackFrame
import io.sentry.protocol.SentryStackTrace
object Log : FlexiLog() {
// ... other overrides ...
// Override these methods to receive call site information
override fun report(type: LogType, tag: String, msg: String, callSite: CallSite?) {
Sentry.captureEvent(SentryEvent().apply {
message = Message().apply { message = msg }
logger = tag
// Use callSite to show the actual source location in Sentry
callSite?.let { site ->
exceptions = listOf(
SentryException().apply {
this.type = "LoggedError"
this.value = msg
this.module = site.className
this.stacktrace = SentryStackTrace(listOf(
SentryStackFrame().apply {
module = site.className
function = site.methodName
filename = site.fileName
lineno = site.lineNumber
isInApp = true
}
))
}
)
}
})
}
override fun report(type: LogType, tag: String, msg: String, tr: Throwable, callSite: CallSite?) {
Sentry.captureEvent(SentryEvent().apply {
message = Message().apply { message = msg }
logger = tag
throwable = tr
// Add call site as context when there's already a throwable
callSite?.let { site ->
setTag("log_call_site", "${site.simpleClassName}.${site.methodName}")
setExtra("log_location", site.toFormattedString())
}
})
}
}
| Property | Description | Example |
|---|
className | Fully qualified class name | com.example.app.data.DataRepo |
simpleClassName | Class name without package | DataRepo |
methodName | Method name | fetchData |
fileName | Source file name (nullable) | DataRepo.kt |
lineNumber | Line number (-1 if unavailable) | 123 |
toFormattedString() | Formatted display string | DataRepo.fetchData(DataRepo.kt:123) |
object Log : FlexiLog() {
// ... other overrides ...
override fun shouldLogToFile(type: LogType): Boolean {
return true // or filter by type
}
override fun writeLogToFile(
timestamp: Long,
type: LogType,
tag: String,
msg: String,
tr: Throwable?
) {
// Implement your file writing logic
val logLine = "[$timestamp] ${type.name}/$tag: $msg"
// Write to file...
}
}
| Method | Description |
|---|
i(caller, msg?, tr?) | Info log |
d(caller, msg?, tr?) | Debug log |
v(caller, msg?, tr?) | Verbose log |
w(caller, msg?, tr?) | Warning log |
e(caller, msg?, tr?, forceReport?) | Error log |
wtf(caller, msg?, tr?) | What a Terrible Failure log |
onCondition(condition, log) | Conditional logging |
withLevel(level) | Create level-filtered logger |
| Method | Description |
|---|
canLogToConsole(type) | Required. Whether to log to console |
shouldReport(type) | Required. Whether to send to crash reporting |
shouldReportException(tr) | Required. Filter exceptions for reporting |
report(type, tag, msg) | Required. Handle crash report without throwable |
report(type, tag, msg, tr) | Required. Handle crash report with throwable |
report(type, tag, msg, callSite?) | Handle crash report with call site info |
report(type, tag, msg, tr, callSite?) | Handle crash report with throwable and call site |
getAdditionalSkipPackages() | Additional packages to skip for call site detection |
shouldLogToFile(type) | Whether to write to file (default: false) |
writeLogToFile(...) | Handle file writing |
| Platform | Console Output | Class Name Extraction |
|---|
| Android | android.util.Log | obj::class.java.simpleName |
| JVM | java.util.logging.Logger | obj::class.java.simpleName |
| iOS | NSLog | obj::class.simpleName |
| JS | console.log/info/warn/error | obj::class.simpleName |
2.1.3FlexiLog.getClassName(clazz: Class<*>)clazz.simpleNameReplaceWithClass<*>FlexiLogLoggerWithLevelidvewwtfAnyClassKClassLog.d(MyClass::class.java, msg)Surfaced from shared tags and platforms — no rankings paid for.