What logging solution do you use in your iOS/Andro...
# multiplatform
s
What logging solution do you use in your iOS/Android projects? I see that the examples use Napier, but there is no support for macOS and judging from the old opened issues the project seems currently unmaintained. Kermit looks like a good option, but is only a pre-release. So what do you recommend?
1
From older answers of the same questions I found also https://github.com/copper-leaf/clog and https://github.com/LighthouseGames/KmLogging , but I can't decide what to try out first.
Looks like everyone writes his own KMM logging framework and maybe someone should put up a survey to find out what's the best option here.
Unfortunately both lack macOS Support.
Ok, there a lot of good ideas for logging, but according to https://kamp.petuska.dev Kermit is the only real option here because of macOS support
m
I use kermit 🙂 My app is in production and I do not have any problem so far
👍 1
🙏 1
s
I used slf4j before and injecting the logger instance as a dependency feels a bit unfamilar
m
I do not inject it actually, i have it as a variable in shared codebase, and access it from everywhere 🙂 The real project is this: https://github.com/CurrencyConverterCalculator/CCC but the code is in my submodule here: https://github.com/SubMob/LogMob/blob/3298989615/logmob/src/commonMain/kotlin/com/github/mustafaozhan/logmob/Logger.kt#L6
🙏 1
s
Oh, yes, that's an good idea! Thank you
@Mustafa Ozhan LogMob looks really nice. Actually that's something I'm looking for: Multiplatform log + Firebase Crashlytics. Would you consider to add an macOS target?
Building a XCFramework for iOS & macOS would be what I need
m
Hmm I will think about it and let you know, it should be so hard to add macOS target, but you should know that Firebase Crashlytics implementation is only for Android for now
I have plans to add CrashKiOS into Logmob, for iOS crash reporting (to collect crash details in shared code)
s
Looked at CrashkitIOS this morning and it looks good 👍 Will need that, too
macOS target is not that hard. The implementation is also NSLog. Just creating the XCFramework is a bit tricky
This is how I do it:
Copy code
ios {
    binaries {
        framework {
            baseName = "shared"
            embedBitcode("bitcode")
        }
    }
}
macosX64("macos") {
    binaries {
        framework {
            baseName = "shared"
        }
    }
}
Copy code
val buildXcFramework by tasks.registering {
    dependsOn("deleteXcFramework")

    group = "build"

    val mode = System.getenv("CONFIGURATION") ?: "DEBUG"

    val frameworks = arrayOf("iosArm64", "iosX64", "macos")
        .map { kotlin.targets.getByName<KotlinNativeTarget>(it).binaries.getFramework(mode) }

    inputs.property("mode", mode)

    dependsOn(frameworks.map { it.linkTask })

    doLast { buildXcFramework(frameworks) }
}

fun Task.buildXcFramework(frameworks: List<org.jetbrains.kotlin.gradle.plugin.mpp.Framework>) {

    val buildArgs: () -> List<String> = {
        val arguments = mutableListOf("-create-xcframework")
        frameworks.forEach {
            arguments += "-framework"
            arguments += "${it.outputDirectory}/${project.name}.framework"
        }
        arguments += "-output"
        arguments += xcFrameworkPath
        arguments
    }

    exec {
        executable = "xcodebuild"
        args = buildArgs()
    }
}

tasks.getByName("build").dependsOn(buildXcFramework)
I need one XCFramework for both because I have a "SwiftUI Multiplatform" XCode project
m
why do you need XCFramework for that ? All you need is using kermit as variable in Swift. ie. you will have a
Logger.kt
file in your shared code and inside you will have
kermit
as variable. then you will be able to reach in iOS/macOS like this
Copy code
LoggerKt.kermit.d(withMessage: {"some log"})
s
I know. I meant my KMM project exports the shared lib for iOS as XCFramework. So if you find the need to export your lib as a shared framework that code may be useful. But most likely you can just add a macOS target and be done because no one will import it a a single xcode module but rather import it into their own shared modules.
m
yea that was what i was thinking of will let you know when the library artifact will be ready
s
🙂
@Mustafa Ozhan Why do you use the "CommonLogger" instead of "NSLogLogger" in iosMain?
m
Hey no spesific reason just because of the
kermit
appending in logcat
it makes my life easier to filter logcat by
kermit
s
Ok, thanks. I was just thinking if using NSLog would be better on iOS
I like your project a lot 😃 Just a small thing I noticed in the style: the "companion object" belongs to the bottom. See https://kotlinlang.org/docs/coding-conventions.html#class-layout For example in https://github.com/SubMob/LogMob/blob/master/logmob/src/androidMain/kotlin/com/github/mustafaozhan/logmob/LogMobLogger.kt
I'm also so used to put constants in the top of a class (Java style) that I have a hard time learning to put that stuff to the bottom 😅
You project is absolutely on the right track and helps where Kermit alone falls short. 👍 One-shot enabling logging with analytics & crashlytics is what every App should need and you make it easy. I like.
m
Yea the place companion object 😄 I couldn’t get used to it, but you should know the main project is not this one so that’s why I forgot to move them to bottom 😄 this is just a submodule of https://github.com/CurrencyConverterCalculator/CCC
s
Crashlytics fails for me 😕 You don't activate the
Copy code
firebase-crashlytics-gradle
plugin that the doc says that should be added and it works? For me it says
Make sure to call FirebaseApp.initializeApp(Context) first.
but your app doesn't do that
k
I wouldn't characterize Kermit as "pre-release". We should update the docs probably. We use it quite a bit.
Reading through the messages. Is this referring to LogMob or CCC? I'm sort of thinking through a larger update to Kermit itself and collecting community thoughts.
You project is absolutely on the right track and helps where Kermit alone falls short. 👍
One-shot enabling logging with analytics & crashlytics is what every App should need and you make it easy. I like.
s
I'm referring to LogMob
I don't like creating Logger instances and giving them as a dependency and LogMob makes it a single-line to activate a centralized logging with Kermit
k
On android only, as far as I can tell?
s
CrashkitIOs integration is in the works ^^
k
Ah
s
I wish to use Kermit like Clog can be used
c
(I’m the author of Clog) In my observations, it’s hard to really make a “good” logging library, because everyone needs such vastly different things from it, and it’s difficult to reconcile all those needs without bloating the whole thing and making it overly difficult to use. I think that’s why we see so many different logging solutions in KMM now, with very little “rallying” around any particular one; it’s usually easier just to write a small logging library with the specific features you need rather than trying to hack it into an existing logging library. It’s certainly why I continue to maintain Clog separately from Kermit and others
k
I missed the middle of the conversation
In my observations, it’s hard to really make a “good” logging library, because everyone needs such vastly different things from it
This, 100%
I used to tell people interested in open source KMP to not make another logging library, then we went ahead and did it 🙂
😂 1
s
Casey, if we manage to get macOS support online it may be easier for me to use that instead of Kermit + MobLog
👍 1
But the crashlytics integration idea is nice
k
So, one of the not visible design goals of Kermit was to avoid atomic references on log calls. I just started looking at clog, but it does appear to access mutable config state on each log call, which (on native) means accessing an AR. In general, logging should be as performance invisible as possible. So, Kermit has all config at start, after which you can't modify the config (it's frozen), but it's fully immutable, and freely accessible across thread. Again, on the "can't make everybody happy" thread, if you want to change your logging config at runtime, you can take performance hits, but we decided very few client-side use cases need mutable config.
Crashlytics integration on iOS is complicated by needing to use cinterop, which is relatively painful vs the arguably lazy approach we took with CrashKiOS, which is copy/paste swift code. I can't defend that decision, other than to say I was picking my battles. Can revisit in the future 🙂
How is CLog used, btw? Like just global log calls, just calling functions (not lambdas), or both?
c
Yup, always gonna have trade-offs 😉 Clog initially started 3-4 years ago as a pure-JVM implementation of the Android Log APIs, for use in my main open-source project, Orchid. I rewrote it in KMM a year or so ago as I started to use KMM for a shared Android/iOS library at work, and a lot of tinkering with Kotlin/JS on the side. Regarding atomic access, Clog was explicitly designed to be used from a singleton, because like @Stefan Oltmann creating logger instances in every single class drives me crazy, especially because most of the time it’s just for debugging and I end up removing most of the logging calls before submitting the PR. But Clog’s loggers actually are immutable once created, and does support a Kermit-style injectable API that does not use atomics if that’s your need. I actually plan on improving that in the near-future, probably moving the global singleton into a separate artifact just to fully support that use-case
k
Ah. We had a long debate internally about the global access. That is on the todo, with sort of a global-specific module that maintains it's own logger that has some kind of mutable updater. Also letting you log directly from methods vs lambdas. We were definitely thinking mostly the runtime use and not debugging.
This will all shift a bit in the future. We were playing around with the new memory model preview this morning. Fully non-frozen mutable state is coming.
It won't make a huge difference in the logger library situation, but it does kind of let you sneak around the config update problem.
c
Clog supports a variety of logging styles, but the one it’s most focused on right now is with the global logger
Clog.i("a log here")
. That accesses the mutable atomic variable to log to the globally-configured
ClogProfile
, which is the immutable logger. To get a static logger for runtime use, you’d call
Clog.getInstance()
or
Clog.tag("some tag")
and cache that as you would any other normal logger. There are also a set of lambda-based functions
d("optional tag) { "a log message" }
but I’m probably going to end up removing those soon because they access the global internally. I’d like to make similar functions in a Compose context, which use CompositionLocals for the “global instance” instead, which would conflict with these functions
k
but I’m probably going to end up removing those soon because they access the global internally
Are you removing them because they access the global or just not changing them because you don't really use them? I mean, in theory, they wouldn't need to access the global, unless that's how you're checking log level before evaling the lambda?
Well, they would need to access the global is you can change log level at runtime, but again, there isn't much use for that in client code at actual runtime (like on a device). I don't think much in debug either, as the log viewer would be doing the filtering
c
A little of both, but mostly because of how they access the global. If you’re going to access the Clog global, it should be an explicit access, at which point you should just use the normal global API.
Also, I will just add that Clog was developed in a silo, driven pretty much entirely by my own needs without much thought beyond that as I’m not aware of anyone else actually using it at this point and I’ve not really have any change requests to it in the several years I’ve been working on it. That said, there are a lot of similarities between Kermit and Clog at the API, and even though we have had different goals for the two libraries in the past, it does sound like we’re both kinda moving more toward the same thing, and I’d be happy to have a longer conversation on if there’s a way for us to collaborate to make a de-facto “standard” logging library for KMP
k
Yeah, feel same on the similar-nees, if only because we're talking through the same exact design and tradeoffs 🙂 Will put some thought on a merged one.
We tried to design it as a public library, but then we didn't even send out a blog post about it and haven't tried to get any eyeballs on it. Just using it for our projects, and we haven't done much updating since release.
m
kermit is really great actually I absolutely love it one small concern you can have better example especially using together with CrashKiOS it will definitely make sense to have repo/article about them 🙂
s
Very interesting conversation here 🙂
😅 1
Having one go-to standard that supports all possible Kotlin/Native targets and is easy to use would be a dream come true.
@Reed Ellsworth As author of KmLogging you may also want to share your thoughts 🙂
👍 1
r
For most of the reasons in this thread is why I ended up having to write my own library, KmLogging. I wanted something that was easy to use, had next to no overhead when disabled, and be extensible
The need to support crashlytics was a usecase I used to make sure it was easily extensible and in this case extensible for only one/two platforms that actually supported crashlytics. This is included in the project's sample code for the android target.
As for supporting, a macos target, that is currently not in the library but I would be happy to work with whoever needed that support in KmLogging to get it added.
❤️ 1
I like to put lots of logs in my code and leave them in there in production. However, in production I don't want any of them to be visible. So they need to be disabled. If I put in lots of logging statements the cost of computing whether the log is enabled or not is high if you have lots of logs. If you look at this cost in all of the libraries mentioned here and all the ones I saw, their computation cost of figuring out whether it should log or not is high. For KmLogging there is no method call overhead and only one static boolean flag check whether that log level is enabled that is executed when the given log level is disabled. That means the cost of the log statement is minimal and therefore performance is no longer a reason to determine whether or not you add a log statement or not.
k
and only one static boolean flag check whether that log level is enabled
In the main thread, yes? I looked at the boolean vars in https://github.com/LighthouseGames/KmLogging/blob/main/logging/src/commonMain/kotlin/org/lighthousegames/logging/KmLogging.kt#L6 and it looks like they're thread local, unless I'm missing something. Kermit's approach to passing lambdas is similar, although you're using inline functions, which would avoid a function call (at the expense of inline function binary, perhaps).
So, while you may disable logging for verbose in config on app start, if a background thread called a verbose log, unless that thread also went through logging config, it would log all verbose calls.
r
Yes, it is ThreadLocal
k
Although, I guess other threads wouldn't have loggers
r
Lambdas removes the overhead of formatting the message. Kermit still has overhead to determine if logging is enabled. It has been a while since I looked at kermit so I don't remember the call sequence they require to compute if that level is enabled.
k
If Kermit was inline, it would be the same as what you're talking about. I'm confused. It also uses lambdas
However, in the case of thread local, no other threads will log, but they will run the lambda, because KmLogging's booleans all default to true
Kermit has always taken lambdas and check before evaluating
To better explain the thread local issue, if you wrote a verbose log lambda, and called that from a non-main thread, it would evaluate the lambda because
isLoggingVerbose
is true, but it would log it nowhere, so you'd probably not know about it
That was pretty much why thread local is a non-starter for Kermit
You'd need to config each thread
You could copy mutable config on thread init and keep that immutable local
Seems complicated, though. I don't know of any use cases on client that need dynamic logging config.
r
With multi-threaded apps you will essentially end up with the default configuration since it is unlikely that you will configure it specifically for each thread. However, the default configuration will work as expected. What you lose in a background thread is if you added something like a Crashlytics logger. The log would only go to the standard place for that platform and not go to Crashlytics.
I was just looking at kermit again and don't see any support for lambdas. Would you mind pointing me in the direction of kermit's support of lambdas.
k
I will say, making all the calls inline might help, but thread local is risky for people who aren't aware of the restrictions. Any non-main thread logger call will get evaluated, but won't do what you expect (Crashlytics, etc). Why not just freeze config?
log.d { "Inserting ${breeds.size} breeds into database" }
r
Kermits overhead is on each disabled log call is a 1-3 method calls plus the following: loggerList.forEach { if (it.isLoggable(severity)) {
What do you mean by "freeze config"? I would like to come up with a way to get background threads to re-use the config from the main thread but have not found a way that works on iOS yet.
k
Kermits overhead is on each disabled log call is a 1-3 method calls plus the following:
Agree. It's part of the internal discussion on refresh.
By "freeze config" I mean the client sets up config initially, then you never modify it, so it can be frozen. That allows shared config among threads.
It does not allow for global access, which is 1 reason why Kermit instances have no global accessor, which is another point of discussion.
To reuse config on multiple threads, you could have the thread-local init copy from something global that isn't thread-local. It would not be aware of changes after the copy, but I think that would work.
759 Views