diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c2564b9..99556026 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,8 @@ koin-core = "3.5.3" koin-android = "3.5.3" koin-test = "3.5.3" coroutines = "1.7.3" +kotlinx-datetime = "0.6.1" +kotlinx-io = "0.5.4" roboelectric = "4.10.3" buildConfig = "4.1.2" mavenPublish = "0.27.0" @@ -61,6 +63,8 @@ testhelp = { module = "co.touchlab:testhelp", version.ref = "testhelp" } koin = { module = "io.insert-koin:koin-core", version.ref = "koin-core" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin-android" } coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } +kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" } roboelectric = { module = "org.robolectric:robolectric", version.ref = "roboelectric" } koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin-test" } @@ -90,4 +94,4 @@ android = [ "androidx-navigationFragment", "androidx-navigationUI", "androidx-coordinatorLayout", -] \ No newline at end of file +] diff --git a/kermit-core/src/appleMain/kotlin/co/touchlab/kermit/OSLogWriter.kt b/kermit-core/src/appleMain/kotlin/co/touchlab/kermit/OSLogWriter.kt index a5306f7f..853f53c1 100644 --- a/kermit-core/src/appleMain/kotlin/co/touchlab/kermit/OSLogWriter.kt +++ b/kermit-core/src/appleMain/kotlin/co/touchlab/kermit/OSLogWriter.kt @@ -12,8 +12,6 @@ package co.touchlab.kermit import co.touchlab.kermit.darwin.* import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.ptr -import platform.darwin.OS_LOG_DEFAULT import platform.darwin.OS_LOG_TYPE_DEBUG import platform.darwin.OS_LOG_TYPE_DEFAULT import platform.darwin.OS_LOG_TYPE_ERROR @@ -27,12 +25,12 @@ import kotlin.experimental.ExperimentalNativeApi */ open class OSLogWriter internal constructor( private val messageStringFormatter: MessageStringFormatter, - private val darwinLogger: DarwinLogger + private val darwinLogger: DarwinLogger, ) : LogWriter() { - constructor(messageStringFormatter: MessageStringFormatter = DefaultFormatter) : this( + constructor(messageStringFormatter: MessageStringFormatter = DefaultFormatter, subsystem: String = "", category: String = "", publicLogging: Boolean = false) : this( messageStringFormatter, - DarwinLoggerActual + DarwinLoggerActual(subsystem, category, publicLogging), ) override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { @@ -54,9 +52,8 @@ open class OSLogWriter internal constructor( } } - @OptIn(ExperimentalNativeApi::class) open fun logThrowable(osLogSeverity: os_log_type_t, throwable: Throwable) { - darwinLogger.log(osLogSeverity, throwable.getStackTrace().joinToString("\n")) + darwinLogger.log(osLogSeverity, throwable.stackTraceToString()) } private fun kermitSeverityToOsLogType(severity: Severity): os_log_type_t = when (severity) { @@ -77,9 +74,18 @@ internal interface DarwinLogger { } @OptIn(ExperimentalForeignApi::class) -private object DarwinLoggerActual : DarwinLogger { - private val logger = darwin_log_create("", "")!! +private class DarwinLoggerActual(subsystem: String, category: String, publicLogging: Boolean) : DarwinLogger { + private val logger = darwin_log_create(subsystem, category)!! + // see https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code?language=objc + // iOS considers everything coming from Kermit as a dynamic string, so without publicLogging=true, all logs are + // private + private val darwinLogFn: (osLogSeverity: os_log_type_t, message: String) -> Unit = if (publicLogging) { + { osLogSeverity, message -> darwin_log_public_with_type(logger, osLogSeverity, message) } + } else { + { osLogSeverity, message -> darwin_log_with_type(logger, osLogSeverity, message) } + } + override fun log(osLogSeverity: os_log_type_t, message: String) { - darwin_log_with_type(logger, osLogSeverity, message) + darwinLogFn(osLogSeverity, message) } -} \ No newline at end of file +} diff --git a/kermit-core/src/appleMain/kotlin/co/touchlab/kermit/XcodeSeverityWriter.kt b/kermit-core/src/appleMain/kotlin/co/touchlab/kermit/XcodeSeverityWriter.kt index 67ba394d..dd872927 100644 --- a/kermit-core/src/appleMain/kotlin/co/touchlab/kermit/XcodeSeverityWriter.kt +++ b/kermit-core/src/appleMain/kotlin/co/touchlab/kermit/XcodeSeverityWriter.kt @@ -22,10 +22,9 @@ open class XcodeSeverityWriter(private val messageStringFormatter: MessageString override fun formatMessage(severity: Severity, tag: Tag, message: Message): String = "${emojiPrefix(severity)} ${messageStringFormatter.formatMessage(null, tag, message)}" - @OptIn(ExperimentalNativeApi::class) override fun logThrowable(osLogSeverity: os_log_type_t, throwable: Throwable) { // oslog cuts off longer strings, so for local development, println is more useful - println(throwable.getStackTrace().joinToString("\n")) + println(throwable.stackTraceToString()) } //If this looks familiar, yes, it came directly from Napier :) https://github.com/AAkira/Napier#darwinios-macos-watchos-tvosintelapple-silicon diff --git a/kermit-core/src/nativeInterop/cInterop/os_log.def b/kermit-core/src/nativeInterop/cInterop/os_log.def index baed7c77..62f4dd67 100644 --- a/kermit-core/src/nativeInterop/cInterop/os_log.def +++ b/kermit-core/src/nativeInterop/cInterop/os_log.def @@ -19,3 +19,12 @@ darwin_os_log_t darwin_log_create(const char *subsystem, const char *category) { void darwin_log_with_type(darwin_os_log_t log, os_log_type_t type, const char *msg) { os_log_with_type((os_log_t)log, type, "%s", msg); } + +/** + * Uses format specifier %{public}s to make logging public. + * See https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code?language=objc. + * We cannot pass the format specifier from Kotlin, as the API requires the value to be a string constant. + */ +void darwin_log_public_with_type(darwin_os_log_t log, os_log_type_t type, const char *msg) { + os_log_with_type((os_log_t)log, type, "%{public}s", msg); +} diff --git a/kermit-io/api/android/kermit-io.api b/kermit-io/api/android/kermit-io.api new file mode 100644 index 00000000..b4918998 --- /dev/null +++ b/kermit-io/api/android/kermit-io.api @@ -0,0 +1,28 @@ +public class co/touchlab/kermit/io/RollingFileLogWriter : co/touchlab/kermit/LogWriter { + public fun (Lco/touchlab/kermit/io/RollingFileLogWriterConfig;Lco/touchlab/kermit/MessageStringFormatter;Lkotlinx/datetime/Clock;Lkotlinx/io/files/FileSystem;)V + public synthetic fun (Lco/touchlab/kermit/io/RollingFileLogWriterConfig;Lco/touchlab/kermit/MessageStringFormatter;Lkotlinx/datetime/Clock;Lkotlinx/io/files/FileSystem;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun log (Lco/touchlab/kermit/Severity;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V +} + +public final class co/touchlab/kermit/io/RollingFileLogWriterConfig { + public fun (Ljava/lang/String;Lkotlinx/io/files/Path;JIZZ)V + public synthetic fun (Ljava/lang/String;Lkotlinx/io/files/Path;JIZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lkotlinx/io/files/Path; + public final fun component3 ()J + public final fun component4 ()I + public final fun component5 ()Z + public final fun component6 ()Z + public final fun copy (Ljava/lang/String;Lkotlinx/io/files/Path;JIZZ)Lco/touchlab/kermit/io/RollingFileLogWriterConfig; + public static synthetic fun copy$default (Lco/touchlab/kermit/io/RollingFileLogWriterConfig;Ljava/lang/String;Lkotlinx/io/files/Path;JIZZILjava/lang/Object;)Lco/touchlab/kermit/io/RollingFileLogWriterConfig; + public fun equals (Ljava/lang/Object;)Z + public final fun getLogFileName ()Ljava/lang/String; + public final fun getLogFilePath ()Lkotlinx/io/files/Path; + public final fun getLogTag ()Z + public final fun getMaxLogFiles ()I + public final fun getPrependTimestamp ()Z + public final fun getRollOnSize ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/kermit-io/api/jvm/kermit-io.api b/kermit-io/api/jvm/kermit-io.api new file mode 100644 index 00000000..b4918998 --- /dev/null +++ b/kermit-io/api/jvm/kermit-io.api @@ -0,0 +1,28 @@ +public class co/touchlab/kermit/io/RollingFileLogWriter : co/touchlab/kermit/LogWriter { + public fun (Lco/touchlab/kermit/io/RollingFileLogWriterConfig;Lco/touchlab/kermit/MessageStringFormatter;Lkotlinx/datetime/Clock;Lkotlinx/io/files/FileSystem;)V + public synthetic fun (Lco/touchlab/kermit/io/RollingFileLogWriterConfig;Lco/touchlab/kermit/MessageStringFormatter;Lkotlinx/datetime/Clock;Lkotlinx/io/files/FileSystem;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun log (Lco/touchlab/kermit/Severity;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V +} + +public final class co/touchlab/kermit/io/RollingFileLogWriterConfig { + public fun (Ljava/lang/String;Lkotlinx/io/files/Path;JIZZ)V + public synthetic fun (Ljava/lang/String;Lkotlinx/io/files/Path;JIZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lkotlinx/io/files/Path; + public final fun component3 ()J + public final fun component4 ()I + public final fun component5 ()Z + public final fun component6 ()Z + public final fun copy (Ljava/lang/String;Lkotlinx/io/files/Path;JIZZ)Lco/touchlab/kermit/io/RollingFileLogWriterConfig; + public static synthetic fun copy$default (Lco/touchlab/kermit/io/RollingFileLogWriterConfig;Ljava/lang/String;Lkotlinx/io/files/Path;JIZZILjava/lang/Object;)Lco/touchlab/kermit/io/RollingFileLogWriterConfig; + public fun equals (Ljava/lang/Object;)Z + public final fun getLogFileName ()Ljava/lang/String; + public final fun getLogFilePath ()Lkotlinx/io/files/Path; + public final fun getLogTag ()Z + public final fun getMaxLogFiles ()I + public final fun getPrependTimestamp ()Z + public final fun getRollOnSize ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/kermit-io/build.gradle.kts b/kermit-io/build.gradle.kts new file mode 100644 index 00000000..79d21a2b --- /dev/null +++ b/kermit-io/build.gradle.kts @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024 Touchlab + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("com.android.library") + kotlin("multiplatform") + id("com.vanniktech.maven.publish") +} + +kotlin { + androidTarget { + publishAllLibraryVariants() + } + jvm() + + macosX64() + macosArm64() + iosX64() + iosArm64() + iosSimulatorArm64() + watchosArm32() + watchosArm64() + watchosSimulatorArm64() + watchosDeviceArm64() + watchosX64() + tvosArm64() + tvosSimulatorArm64() + tvosX64() + + mingwX64() + linuxX64() + linuxArm64() + + androidNativeArm32() + androidNativeArm64() + androidNativeX86() + androidNativeX64() + + @Suppress("OPT_IN_USAGE") + applyDefaultHierarchyTemplate { + common { + group("commonJvm") { + withAndroidTarget() + withJvm() + } + } + } + + sourceSets { + commonMain.dependencies { + implementation(project(":kermit-core")) + + api(libs.kotlinx.datetime) + api(libs.kotlinx.io) + implementation(libs.coroutines) + } + + commonTest.dependencies { + implementation(kotlin("test")) + implementation(project(":kermit-test")) + } + + getByName("commonJvmTest").dependencies { + implementation(kotlin("test-junit")) + } + + getByName("androidUnitTest").dependencies { + implementation(libs.androidx.runner) + implementation(libs.roboelectric) + } + } +} + +android { + namespace = "co.touchlab.kermit.io" + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" +} diff --git a/kermit-io/src/commonMain/kotlin/co/touchlab/kermit/io/RollingFileLogWriter.kt b/kermit-io/src/commonMain/kotlin/co/touchlab/kermit/io/RollingFileLogWriter.kt new file mode 100644 index 00000000..e97e66cd --- /dev/null +++ b/kermit-io/src/commonMain/kotlin/co/touchlab/kermit/io/RollingFileLogWriter.kt @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2024 Touchlab + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package co.touchlab.kermit.io + +import co.touchlab.kermit.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.datetime.Clock +import kotlinx.datetime.format +import kotlinx.datetime.format.DateTimeComponents +import kotlinx.io.* +import kotlinx.io.files.FileSystem +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem + +/** + * Implements a log writer that writes log messages to a rolling file. + * + * It also deletes old log files when the maximum number of log files is reached. We simply keep + * approximately [RollingFileLogWriterConfig.rollOnSize] bytes in each log file, + * and delete the oldest file when we have more than [RollingFileLogWriterConfig.maxLogFiles]. + * + * Formatting is governed by the passed [MessageStringFormatter], but we do prepend a timestamp by default. + * Turn this off via [RollingFileLogWriterConfig.prependTimestamp] + * + * Writes to the file are done by a different coroutine. The main reason for this is to make writes to the + * log file sink thread-safe, and so that file rolling can be performed without additional synchronization + * or locking. The channel that buffers log messages is currently unbuffered, so logging threads will block + * until the I/O is complete. However, buffering could easily be introduced to potentially increase logging + * throughput. The envisioned usage scenarios for this class probably do not warrant this. + * + * The recommended way to obtain the logPath on Android is: + * + * ```kotlin + * Path(context.filesDir.path) + * ``` + * + * and on iOS this wil return the application's sandboxed document directory: + * + * ```kotlin + * (NSFileManager.defaultManager.URLsForDirectory(NSDocumentDirectory, NSUserDomainMask).last() as NSURL).path!! + * ``` + * + * However, you can use any path that is writable by the application. This would generally be implemented by + * platform-specific code. + */ +open class RollingFileLogWriter( + private val config: RollingFileLogWriterConfig, + private val messageStringFormatter: MessageStringFormatter = DefaultFormatter, + private val clock: Clock = Clock.System, + private val fileSystem: FileSystem = SystemFileSystem, +) : LogWriter() { + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) + private val coroutineScope = CoroutineScope( + newSingleThreadContext("RollingFileLogWriter") + + SupervisorJob() + + CoroutineName("RollingFileLogWriter") + + CoroutineExceptionHandler { _, throwable -> + // can't log it, we're the logger -- print to standard error + println("RollingFileLogWriter: Uncaught exception in writer coroutine") + throwable.printStackTrace() + } + ) + + private val loggingChannel: Channel = Channel() + + init { + coroutineScope.launch { + writer() + } + } + + override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { + bufferLog( + formatMessage( + severity = severity, + tag = Tag(tag), + message = Message(message) + ), throwable + ) + } + + private fun bufferLog(message: String, throwable: Throwable?) { + val log = buildString { + append(clock.now().format(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET)) + append(" ") + appendLine(message) + if (throwable != null) { + appendLine(throwable.stackTraceToString()) + } + } + loggingChannel.trySendBlocking(Buffer().apply { writeString(log) }) + } + + private fun formatMessage(severity: Severity, tag: Tag?, message: Message): String = + messageStringFormatter.formatMessage(severity, if (config.logTag) tag else null, message) + + private fun maybeRollLogs(size: Long): Boolean { + return if (size > config.rollOnSize) { + rollLogs() + true + } else false + } + + private fun rollLogs() { + if (fileSystem.exists(pathForLogIndex(config.maxLogFiles - 1))) { + fileSystem.delete(pathForLogIndex(config.maxLogFiles - 1)) + } + (0..<(config.maxLogFiles - 1)).reversed().forEach { + val sourcePath = pathForLogIndex(it) + val targetPath = pathForLogIndex(it + 1) + if (fileSystem.exists(sourcePath)) { + try { + fileSystem.atomicMove(sourcePath, targetPath) + } catch (e: IOException) { + // we can't log it, we're the logger -- print to standard error + println("RollingFileLogWriter: Failed to roll log file $sourcePath to $targetPath (sourcePath exists=${fileSystem.exists(sourcePath)})") + e.printStackTrace() + } + } + } + } + + private fun pathForLogIndex(index: Int): Path = + Path(config.logFilePath, if (index == 0) "${config.logFileName}.log" else "${config.logFileName}-$index.log") + + private suspend fun writer() { + val logFilePath = pathForLogIndex(0) + + if (fileSystem.exists(logFilePath)) { + maybeRollLogs(fileSizeOrZero(logFilePath)) + } + + fun createNewLogSink(): Sink = fileSystem + .sink(logFilePath, append = true) + .buffered() + + var currentLogSink: Sink = createNewLogSink() + + while (currentCoroutineContext().isActive) { + // wait for data to be available, flush periodically + val result = loggingChannel.receiveCatching() + + // check if logs need rolling + val rolled = maybeRollLogs(fileSizeOrZero(logFilePath)) + if (rolled) { + currentLogSink.close() + currentLogSink = createNewLogSink() + } + + result.getOrNull()?.transferTo(currentLogSink) + + // we could improve performance by flushing less frequently at the cost of potential data loss, + // but this is a safe default + currentLogSink.flush() + } + } + + private fun fileSizeOrZero(path: Path) = fileSystem.metadataOrNull(path)?.size ?: 0 +} diff --git a/kermit-io/src/commonMain/kotlin/co/touchlab/kermit/io/RollingFileLogWriterConfig.kt b/kermit-io/src/commonMain/kotlin/co/touchlab/kermit/io/RollingFileLogWriterConfig.kt new file mode 100644 index 00000000..5ce242a3 --- /dev/null +++ b/kermit-io/src/commonMain/kotlin/co/touchlab/kermit/io/RollingFileLogWriterConfig.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 Touchlab + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package co.touchlab.kermit.io + +import kotlinx.io.files.Path + +data class RollingFileLogWriterConfig( + val logFileName: String, + val logFilePath: Path, + val rollOnSize: Long = 10 * 1024 * 1024, // 10MB + val maxLogFiles: Int = 5, + val logTag: Boolean = true, + val prependTimestamp: Boolean = true, +) diff --git a/samples/sample-production/build.gradle.kts b/samples/sample-production/build.gradle.kts index 23dac572..5f2b9444 100644 --- a/samples/sample-production/build.gradle.kts +++ b/samples/sample-production/build.gradle.kts @@ -23,10 +23,10 @@ plugins { allprojects { repositories { + mavenLocal() mavenCentral() maven(url = "https://oss.sonatype.org/content/repositories/snapshots") google() - mavenLocal() } } diff --git a/samples/sample/KermitSampleIOS/KermitSampleIOS/ContentView.swift b/samples/sample/KermitSampleIOS/KermitSampleIOS/ContentView.swift index 912b01aa..56cdb73e 100644 --- a/samples/sample/KermitSampleIOS/KermitSampleIOS/ContentView.swift +++ b/samples/sample/KermitSampleIOS/KermitSampleIOS/ContentView.swift @@ -15,10 +15,23 @@ import shared struct ContentView: View { - let common: SampleCommon + let common: SampleMobile init() { - self.common = SampleCommon() + let filePath = NSHomeDirectory() + "/Documents/" + let fileName = "KermitSampleLogs" + ContentView.createLoggingFile(withName: fileName, atPath: filePath) + + self.common = SampleMobile(filePathString: filePath, logFileName: fileName) + } + + private static func createLoggingFile(withName name:String, atPath filePath: String){ + let absouluteFilePath = "\(filePath)\(name).log" + if (FileManager.default.createFile(atPath: absouluteFilePath, contents: nil, attributes: nil)) { + print("File created successfully.") + } else { + print("File not created.") + } } var body: some View { diff --git a/samples/sample/KermitSampleIOS/Podfile.lock b/samples/sample/KermitSampleIOS/Podfile.lock index eb34e809..c1dd52e2 100644 --- a/samples/sample/KermitSampleIOS/Podfile.lock +++ b/samples/sample/KermitSampleIOS/Podfile.lock @@ -9,8 +9,8 @@ EXTERNAL SOURCES: :path: "../shared" SPEC CHECKSUMS: - shared: 317794cafa8cc02021e4c781c7235ace20c9dfdc + shared: 983dc25845ffd9786066f42f1d094f72b41f2e37 PODFILE CHECKSUM: eb18a5a396ff91b77d2f3b607dba9c4cf5f57893 -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/samples/sample/app/src/main/java/co/touchlab/KermitSample/FirstFragment.kt b/samples/sample/app/src/main/java/co/touchlab/KermitSample/FirstFragment.kt index 47228420..116e8c6a 100644 --- a/samples/sample/app/src/main/java/co/touchlab/KermitSample/FirstFragment.kt +++ b/samples/sample/app/src/main/java/co/touchlab/KermitSample/FirstFragment.kt @@ -15,7 +15,7 @@ import android.view.View import androidx.fragment.app.Fragment import co.touchlab.KermitSample.databinding.FragmentFirstBinding import co.touchlab.kermit.Logger -import co.touchlab.kermitsample.SampleCommon +import co.touchlab.kermitsample.SampleMobile /** * A simple [Fragment] subclass as the default destination in the navigation. @@ -25,7 +25,7 @@ class FirstFragment : Fragment(R.layout.fragment_first) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Logger.withTag("FirstFragment").v("First fragment loaded") - val sample = SampleCommon() + val sample = SampleMobile(context?.filesDir?.path ?: "") val binding = FragmentFirstBinding.bind(view) binding.btnClickCount.setOnClickListener { sample.onClickI() } binding.btnException.setOnClickListener { sample.logException() } diff --git a/samples/sample/build.gradle.kts b/samples/sample/build.gradle.kts index 0c378781..a2c9aa89 100644 --- a/samples/sample/build.gradle.kts +++ b/samples/sample/build.gradle.kts @@ -24,10 +24,10 @@ version = "1.0-SNAPSHOT" allprojects { repositories { + mavenLocal() mavenCentral() maven(url = "https://oss.sonatype.org/content/repositories/snapshots") google() - mavenLocal() } } tasks.register("ciTest") { diff --git a/samples/sample/shared/build.gradle.kts b/samples/sample/shared/build.gradle.kts index 92de0620..e738f83c 100644 --- a/samples/sample/shared/build.gradle.kts +++ b/samples/sample/shared/build.gradle.kts @@ -8,6 +8,7 @@ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -49,6 +50,8 @@ kotlin { nodejs() } + applyDefaultHierarchyTemplate() + sourceSets { commonMain.dependencies { implementation("co.touchlab:kermit:${KERMIT_VERSION}") @@ -59,9 +62,21 @@ kotlin { implementation("co.touchlab:kermit-test:${KERMIT_VERSION}") } - iosMain.dependencies { - // Only if you want to talk to Kermit from Swift - api("co.touchlab:kermit-simple:${KERMIT_VERSION}") + val mobileMain by creating { + dependsOn(commonMain.get()) + dependencies { + implementation("co.touchlab:kermit-io:${KERMIT_VERSION}") + } + } + androidMain { + dependsOn(mobileMain) + } + iosMain { + dependsOn(mobileMain) + dependencies { + // Only if you want to talk to Kermit from Swift + api("co.touchlab:kermit-simple:${KERMIT_VERSION}") + } } } cocoapods { @@ -69,7 +84,6 @@ kotlin { homepage = "https://www.touchlab.co" framework { isStatic = true - // Only if you want to talk to Kermit from Swift export("co.touchlab:kermit-simple:${KERMIT_VERSION}") } diff --git a/samples/sample/shared/src/commonMain/kotlin/co/touchlab/kermitsample/SampleCommon.kt b/samples/sample/shared/src/commonMain/kotlin/co/touchlab/kermitsample/SampleCommon.kt index 07bd40e3..8719d13a 100644 --- a/samples/sample/shared/src/commonMain/kotlin/co/touchlab/kermitsample/SampleCommon.kt +++ b/samples/sample/shared/src/commonMain/kotlin/co/touchlab/kermitsample/SampleCommon.kt @@ -12,8 +12,9 @@ package co.touchlab.kermitsample import co.touchlab.kermit.Logger -class SampleCommon { +open class SampleCommon { private var count = 0 + fun onClickI() { count++ Logger.i { "Common click count: $count" } diff --git a/samples/sample/shared/src/mobileMain/kotlin/co/touchlab/kermitsample/SampleMobile.kt b/samples/sample/shared/src/mobileMain/kotlin/co/touchlab/kermitsample/SampleMobile.kt new file mode 100644 index 00000000..297bfd7a --- /dev/null +++ b/samples/sample/shared/src/mobileMain/kotlin/co/touchlab/kermitsample/SampleMobile.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 Touchlab + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package co.touchlab.kermitsample + +import co.touchlab.kermit.Logger +import co.touchlab.kermit.io.RollingFileLogWriter +import co.touchlab.kermit.io.RollingFileLogWriterConfig +import kotlinx.io.files.Path + +class SampleMobile(filePathString: String, logFileName: String = "KermitSampleLogs") : SampleCommon() { + init { + Logger.addLogWriter( + RollingFileLogWriter( + config = RollingFileLogWriterConfig( + logFileName = logFileName, + logFilePath = Path(filePathString), + ) + ) + ) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 232e31b1..87acbbca 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,7 @@ */ include(":kermit-core") include(":kermit") +include(":kermit-io") include(":kermit-simple") include(":kermit-test") @@ -40,4 +41,4 @@ pluginManagement { } } -rootProject.name = "kermit-root" \ No newline at end of file +rootProject.name = "kermit-root"