├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── logging ├── build.gradle.kts ├── proguard.txt └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── diamondedge │ │ └── logging │ │ ├── Log.kt │ │ ├── Platform.kt │ │ └── PlatformLogger.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── diamondedge │ │ └── logging │ │ ├── FixedLogLevel.kt │ │ ├── KmLog.kt │ │ ├── KmLogging.kt │ │ ├── KmModuleLog.kt │ │ ├── LogFactory.kt │ │ ├── LogLevel.kt │ │ ├── LogLevelController.kt │ │ ├── Logger.kt │ │ ├── Platform.kt │ │ ├── PlatformLogger.kt │ │ ├── PrintLogger.kt │ │ ├── TagProvider.kt │ │ ├── TimingLog.kt │ │ └── VariableLogLevel.kt │ ├── iosMain │ └── kotlin │ │ └── com │ │ └── diamondedge │ │ └── logging │ │ ├── Platform.kt │ │ └── PlatformLogger.kt │ ├── jsMain │ └── kotlin │ │ └── com │ │ └── diamondedge │ │ └── logging │ │ ├── Platform.kt │ │ └── PlatformLogger.kt │ ├── jvmMain │ └── kotlin │ │ └── com │ │ └── diamondedge │ │ └── logging │ │ ├── Platform.kt │ │ ├── PlatformLogger.kt │ │ ├── Slf4jLog.kt │ │ └── Slf4jLogFactory.kt │ └── wasmJsMain │ └── kotlin │ └── com │ └── diamondedge │ └── logging │ ├── Platform.kt │ └── PlatformLogger.kt ├── sampleApp ├── README.md ├── composeApp │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ ├── google-services.json │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── diamondedge │ │ │ │ └── sample │ │ │ │ ├── App.kt │ │ │ │ ├── CrashlyticsLogger.kt │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── diamondedge │ │ │ └── sample │ │ │ ├── AppUI.kt │ │ │ └── Greeting.kt │ │ ├── commonTest │ │ └── kotlin │ │ │ └── com │ │ │ └── diamondedge │ │ │ └── sample │ │ │ └── SampleTest.kt │ │ ├── desktopMain │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── diamondedge │ │ │ │ └── sample │ │ │ │ └── main.kt │ │ └── resources │ │ │ └── logback.xml │ │ ├── iosMain │ │ └── kotlin │ │ │ └── com │ │ │ └── diamondedge │ │ │ └── sample │ │ │ └── MainViewController.kt │ │ └── wasmJsMain │ │ ├── kotlin │ │ └── com │ │ │ └── diamondedge │ │ │ └── sample │ │ │ └── main.kt │ │ └── resources │ │ ├── index.html │ │ └── styles.css └── iosApp │ ├── Configuration │ └── Config.xcconfig │ ├── iosApp.xcodeproj │ └── project.pbxproj │ └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── app-icon-1024.png │ └── Contents.json │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .kotlin 4 | /local.properties 5 | /.idea 6 | .DS_Store 7 | build 8 | /captures 9 | node_modules 10 | kotlin-js-store 11 | .externalNativeBuild 12 | notes.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020-2025 Reed Ellsworth 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin Multiplatform Logging 2 | 3 | [![ver](https://img.shields.io/maven-central/v/com.diamondedge/logging)](https://repo1.maven.org/maven2/com/diamondedge/logging/) 4 | [![Kotlin](https://img.shields.io/badge/Kotlin-2.0.21-blue.svg?logo=kotlin)](http://kotlinlang.org) 5 | ![kmm](https://img.shields.io/badge/Multiplatform-Android%20iOS%20WasmJS%20JS%20JVM-blue) 6 | [![License](https://img.shields.io/badge/License-Apache--2.0-blue)](http://www.apache.org/licenses/LICENSE-2.0) 7 | [![libs.tech recommends](https://libs.tech/project/332284926/badge.svg)](https://libs.tech/project/332284926/kmlogging) 8 | 9 | Kotlin multiplatform logging library targeting Android, iOS, JVM, WasmJS and JS. 10 | 11 | ## Features 12 | 13 | * Uses the native logging facility on each platform: Log on Android, os_log on iOS, SLF4J on JVM and 14 | console on WasmJs and JavaScript. 15 | * High performance. Very little overhead when logging is disabled. When disabled, only one boolean 16 | is evaluated and no function calls. Building the message string and running the code to calculate 17 | it is not executed. 18 | * No configuration necessary. 19 | * Can add additional loggers such as Crashlytics or replace/extend the builtin PlatformLogger with 20 | something else 21 | * Can provide custom/configurable log level control on builtin PlatformLogger such as changing the 22 | log level from Remote Config 23 | * Each logger can log at a different level. 24 | * All platforms can use the same set of loggers by configuring in common code or can use different 25 | ones on each platform by configuring in platform specific code. 26 | * It is thread-safe 27 | 28 | ## Migration from version 1.x 29 | 30 | Version 2.0 changed the package from `org.lighthousegames.logging` to `com.diamondedge.logging` 31 | To do the migration simply change the dependency to 32 | `implementation("com.diamondedge:logging:$logging_version")` 33 | and replace all occurrences of `org.lighthousegames.logging` in your code base with 34 | `com.diamondedge.logging`. 35 | 36 | ## Setup 37 | 38 | The library is available from the Maven Central repository with the current version 39 | of ![ver](https://img.shields.io/maven-central/v/com.diamondedge/logging) 40 | You should use version `2.0.21` or later of the kotlin multiplatform plugin (Older version of Kotlin 41 | are supported in older versions of the library). Place the following in the commonMain section. 42 | 43 | build.gradle.kts 44 | 45 | ```kotlin 46 | sourceSets { 47 | val commonMain by getting { 48 | dependencies { 49 | api("com.diamondedge:logging:$logging_version") 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | build.gradle 56 | 57 | ```gradle 58 | sourceSets { 59 | commonMain { 60 | dependencies { 61 | api "com.diamondedge:logging:$logging_version" 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ## Setup for non-multiplatform 68 | 69 | So, you want the best and easiest to use kotlin logging library but are not yet ready for 70 | multiplatform development then just include on the following in your `dependencies` section: 71 | 72 | ```gradle 73 | implementation("com.diamondedge:logging-android:$logging_version") 74 | implementation("com.diamondedge:logging-jvm:$logging_version") 75 | implementation("com.diamondedge:logging-js:$logging_version") 76 | ``` 77 | 78 | ## Usage 79 | 80 | Create an instance of logging class by using the convenience function `logging()`. 81 | On Android, iOS and JVM the class from where `logging()` was called will be used as the tag in the 82 | logs. For JS or when a specific tag is desired it can be supplied i.e `val log = logging("mytag")` 83 | or `val log = KmLog("mytag")` 84 | 85 | ```kotlin 86 | class MyClass { 87 | fun easyPeasy() { 88 | log.i { "use traditional Android short function name" } 89 | } 90 | 91 | fun easyPeasyLemonSqueesy() { 92 | log.info { "use longer more explicit function name" } 93 | } 94 | 95 | companion object { 96 | val log = logging() 97 | } 98 | } 99 | ``` 100 | 101 | ## Performance 102 | 103 | There are 3 aspects to logging that have significant overhead: 104 | 105 | 1. calculating the tag or class name 106 | 1. formatting the log message 107 | 1. determining whether a given log call should be logged or not 108 | 109 | KmLogging addresses and improves on each of these as compared to other logging libraries: 110 | 111 | 1. KmLogging calculates the tag just once per module The tag (class name) is only when 112 | the `val log = logging()` is executed. Most logging libraries calculate it on every log call or 113 | require you to supply it. Calculating the tag is expensive since it normally requires creating 114 | and parsing a call stack to get the class name. KmLogging chose to eliminate this performance 115 | drain by asking the developer to add an additional line of code `val log = logging()` to each 116 | class or module so this cost is only paid one time per class. 117 | 1. KmLogging does not evaluate the message string except when it will be output. Formatting the log 118 | message can require many steps and the concatenation of the strings is expensive. All of this 119 | code is captured in a lambda which is not executed unless it is determined that the result will 120 | be output to the logs. 121 | 1. KmLogging has one a single boolean check to determine if it should log. Calculating whether each 122 | level is to be logged is calculated at configuration time i.e. only once and then a boolean flag 123 | is stored for each level to signify whether that level is logging or not. The logging functions 124 | are inline and result in evaluating one boolean to determine if any logging work should be done. 125 | Most logging libraries have a lot of overhead such as many method calls and loops over the 126 | configuration objects to determine if logging should be performed or not. 127 | 128 | Since KmLogging has very little overhead when it is disabled, if you turn off logging in release 129 | builds you can leave a lot of logging in your code without paying performance penalties in 130 | production. 131 | 132 | Note: if any logger has a given log level enabled then the lambda for that log level will be 133 | evaluated. Suppose you used the default configuration and you added a Crashlytics logger that logs 134 | at info, warn and error levels. This would mean that the lambda for info, warn and error levels will 135 | be evaluated because the Crashlytics logger needs it. So in this scenario you would want to have 136 | minimal info logging code so as to not slow down the application at runtime and put most of the 137 | logging at verbose and debug levels where it will not be evaluated in release builds. 138 | 139 | ## Configuration 140 | 141 | With no configuration, logging is enabled for all log levels. 142 | 143 | ### Turn off logging for release builds 144 | 145 | If logging is not desired for release builds then use the following to turn off the logging of the 146 | default PlatformLogger 147 | 148 | ```kotlin 149 | KmLogging.setLogLevel(LogLevel.Off) 150 | 151 | // in Android this could be based on the debug flag: 152 | KmLogging.setLogLevel(if (BuildConfig.DEBUG) LogLevel.Verbose else LogLevel.Off) 153 | ``` 154 | 155 | or use `PlatformLogger` and supply it a log level controller that is disabled for all log levels: 156 | 157 | ```kotlin 158 | KmLogging.setLoggers(PlatformLogger(FixedLogLevel(false))) 159 | 160 | // in Android this could be based on the debug flag: 161 | KmLogging.setLoggers(PlatformLogger(FixedLogLevel(BuildConfig.DEBUG))) 162 | ``` 163 | 164 | ## Kotlin version support 165 | 166 | | KmLogging version | Kotlin version | 167 | |-------------------|----------------| 168 | | 2.0.3 | 2.0.21 | 169 | | 1.5.0 | 1.9.24 | 170 | | 1.4.2 | 1.8.22 | 171 | | 1.3.0 | 1.8.10 | 172 | | 1.2.1 | 1.7.21 | 173 | | 1.2.0 | 1.6.10 | 174 | | 1.1.0 | 1.5.32 | 175 | | 1.0.0 | 1.4.30 | 176 | 177 | ## Miscellaneous 178 | 179 | * When calling `KmLogging.setLoggers()` the existing loggers are removed and the supplied ones are 180 | added in. 181 | * If the existing ones should remain then `KmLogging.addLoggers()` should be used. 182 | * `PlatformLogger` uses Log on Android, os_log on iOS, SLF4j on JVM and console on WasmJS and JS. 183 | * If a custom logger is created that changes the log level dynamically such as from a Remote Config 184 | change then `KmLogging.setupLoggingFlags()` should be called when the logger's log levels were 185 | changed to calculate which log levels are enabled. KmLogging maintains variables for each log 186 | level corresponding to whether any logger is enabled at that level. This is done for performance 187 | reason so only a single boolean check will be done at runtime to minimize the overhead when 188 | running in production. The android sample app demonstrates this. 189 | 190 | ## Use in unit testing 191 | 192 | * If you want log messages to show up while unit testing then in your test class you can set up a 193 | logger that prints to the console using something like: 194 | 195 | ```kotlin 196 | @BeforeTest 197 | fun setup() { 198 | KmLogging.setLoggers(PrintLogger(FixedLogLevel(true))) 199 | } 200 | ``` 201 | 202 | * On Android unit tests will complain about not supplying default values or not being mocked. You 203 | will want to include the following: 204 | 205 | ```kotlin 206 | android { 207 | testOptions { 208 | unitTests { 209 | isReturnDefaultValues = true 210 | } 211 | } 212 | } 213 | ``` 214 | 215 | ## Logging to another system such as Crashlytics 216 | 217 | If logging is only desired at certain levels that can be setup. For example, if only the more 218 | important logs should be sent to Crashlytics to give some context to crashes then only log info 219 | level and above. That can be easily done by by defining and adding in a logger to do that. The 220 | sample android app implement this. 221 | 222 | ```kotlin 223 | class CrashlyticsLogger : Logger { 224 | override fun verbose(tag: String?, msg: String) {} 225 | 226 | override fun debug(tag: String?, msg: String) {} 227 | 228 | override fun info(tag: String?, msg: String) { 229 | FirebaseCrashlytics.getInstance().log(msg) 230 | } 231 | 232 | override fun warn(tag: String?, msg: String, t: Throwable?) { 233 | FirebaseCrashlytics.getInstance().log(msg) 234 | } 235 | 236 | override fun error(tag: String?, msg: String, t: Throwable?) { 237 | FirebaseCrashlytics.getInstance().log(msg) 238 | } 239 | 240 | override fun isLoggingVerbose(): Boolean = false 241 | 242 | override fun isLoggingDebug(): Boolean = false 243 | 244 | override fun isLoggingInfo(): Boolean = true 245 | 246 | override fun isLoggingWarning(): Boolean = true 247 | 248 | override fun isLoggingError(): Boolean = true 249 | } 250 | 251 | // at App creation time configure logging 252 | // use addLogger to keep the existing loggers 253 | 254 | KmLogging.addLogger(CrashlyticsLogger()) 255 | ``` 256 | 257 | ## Usage in iOS and Swift 258 | 259 | By default the kotlin multiplatform toolchain will not export all KmLogging classes and those that 260 | are will be prefaced with the stringLogging. 261 | If you want to use classes from Swift code you will need to direct the plugin to export the logging 262 | library in your `build.gradle.kts`: 263 | 264 | ```kotlin 265 | ios { 266 | binaries { 267 | framework { 268 | baseName = "my-shared-module-name" 269 | export("com.diamondedge:logging:$logging_version") 270 | } 271 | } 272 | } 273 | ``` 274 | 275 | Note: logging must also be included as an api dependency. 276 | See https://kotlinlang.org/docs/reference/mpp-build-native-binaries.html 277 | 278 | The code to figure out what class KmLog was instantiated from does not work from within Swift, so 279 | you will always want to pass in the class name: 280 | 281 | ```swift 282 | class MyClass { 283 | let log = KmLog(tag: "MyClass") 284 | } 285 | ``` 286 | 287 | ## Usage on JVM 288 | 289 | You will need to include a dependency on the logging library of your choice that is compatible with 290 | SLF4J. See the sample app which uses `logback`. Since SLF4J controls the log level using its own 291 | configurations it is advisable to retain the use of FixedLogLevel in the default configuration as 292 | the log level controller. The log level controller in KmLogging controls the possibility of logging 293 | occurring with SLF4J controlling whether a particular log usage is output or not. 294 | 295 | ## Usage in Libraries 296 | 297 | A best practice for libraries is to not have its logging turned on be default. If both a library and 298 | an application both use KmLogging then the library will have its logging turned on automatically. To 299 | enable or disable a library's logging independently of the application, the library needs to use a 300 | wrapper so logging can be turned on/off using a variable. 301 | 302 | Example usage with code implemented 303 | in [ChartsLogging.kt](https://github.com/ellsworthrw/DiamondCharts/blob/main/charts/src/main/java/com/diamondedge/charts/ChartsLogging.kt): 304 | 305 | ```kotlin 306 | object ChartsLogging { 307 | var enabled = true 308 | } 309 | 310 | fun moduleLogging(tag: String? = null): KmModuleLog { 311 | // string passed into createTag should be the name of the class that this function is implemented in 312 | // if it is a top level function then the class name is the file name with Kt appended 313 | val t = tag ?: KmLogging.createTag("ChartsLoggingKt").first 314 | return KmModuleLog(logging(t), ChartsLogging::enabled) 315 | } 316 | ``` 317 | 318 | The library code would then use this new function to do its logging: 319 | 320 | ```kotlin 321 | val log = moduleLogging() 322 | ``` 323 | 324 | ## Quick migration of Android Log calls 325 | 326 | Once you have adopted KmLogging, what do you do with all your existing Android code that is using 327 | the Log class? The first quick and easy step is to switch all your code to using 328 | com.diamondedge.logging.Log class which mimics the Android Log class but sends all output 329 | through KmLogging so you can turn it on and off at the same time as with all other KmLog usages and 330 | have all its benefits. To do this simply replace all occurrences of `import android.util.Log` in 331 | your code base with `import com.diamondedge.logging.Log` 332 | 333 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.androidApplication) apply false 5 | alias(libs.plugins.androidLibrary) apply false 6 | alias(libs.plugins.composeMultiplatform) apply false 7 | alias(libs.plugins.composeCompiler) apply false 8 | alias(libs.plugins.kotlinMultiplatform) apply false 9 | alias(libs.plugins.dokka) apply false 10 | } 11 | 12 | allprojects { 13 | tasks.withType(JavaCompile::class.java).configureEach { 14 | sourceCompatibility = "11" 15 | targetCompatibility = "11" 16 | } 17 | 18 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).configureEach { 19 | compilerOptions { 20 | compilerOptions { 21 | jvmTarget.set(JvmTarget.JVM_11) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Kotlin 2 | kotlin.code.style=official 3 | kotlin.daemon.jvmargs=-Xmx2048M 4 | #Gradle 5 | org.gradle.jvmargs=-Xmx2048M -XX:MaxPermSize=512m -XX:+IgnoreUnrecognizedVMOptions -Dfile.encoding=UTF-8 6 | #Android 7 | android.nonTransitiveRClass=true 8 | android.useAndroidX=true 9 | #kotlin.js.generate.executable.default=false 10 | xcodeproj=./sampleApp/iosApp 11 | #orgId=DiamondEdge1 12 | #orgName=Diamond Edge 13 | orgUrl=https://diamondedge.com/multiplatform 14 | groupId=com.diamondedge 15 | developerId=ellsworthrw 16 | developerName=Reed Ellsworth 17 | licenseName=Apache License, Version 2.0 18 | licenseUrl=http://www.apache.org/licenses/LICENSE-2.0 19 | allLicenses=["Apache-2.0"] 20 | # workaround bug in webpack 5 usage 21 | #kotlin.js.webpack.major.version=4 22 | kotlin.mpp.androidSourceSetLayoutVersion=2 23 | kotlin.js.yarn=false 24 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.5.2" 3 | android-compileSdk = "34" 4 | android-minSdk = "24" 5 | android-targetSdk = "34" 6 | androidx-activityCompose = "1.9.3" 7 | androidx-appcompat = "1.7.0" 8 | androidx-constraintlayout = "2.2.0" 9 | androidx-core-ktx = "1.15.0" 10 | androidx-lifecycle = "2.8.4" 11 | androidx-material = "1.12.0" 12 | compose-multiplatform = "1.7.0" 13 | dokka = "2.0.0" 14 | firebaseBom = "33.9.0" 15 | kotlin = "2.0.21" 16 | kotlinx-coroutines = "1.9.0" 17 | kotlinxCoroutinesAndroid = "1.5.0" 18 | kotlinxDatetime = "0.6.2" 19 | lifecycleRuntimeKtx = "2.4.0" 20 | logback = "1.5.16" 21 | slf4jApi = "2.0.16" 22 | statelyConcurrency = "2.1.0" 23 | logging = "2.0.3" 24 | 25 | [libraries] 26 | androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } 27 | firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx" } 28 | firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } 29 | firebase-config-ktx = { module = "com.google.firebase:firebase-config-ktx" } 30 | firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx" } 31 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 32 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } 33 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } 34 | androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } 35 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } 36 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 37 | androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } 38 | androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } 39 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } 40 | kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 41 | kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } 42 | logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } 43 | slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4jApi" } 44 | stately-concurrency = { module = "co.touchlab:stately-concurrency", version.ref = "statelyConcurrency" } 45 | logging = { module = "com.diamondedge:logging", version.ref = "logging" } 46 | 47 | [plugins] 48 | androidApplication = { id = "com.android.application", version.ref = "agp" } 49 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 50 | composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } 51 | composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 52 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 53 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } 54 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiamondEdge1/KmLogging/527cc0462814c7dd3a22ecfbe0e61b9d9bf8cff4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /logging/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 3 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 4 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 5 | 6 | plugins { 7 | alias(libs.plugins.kotlinMultiplatform) 8 | alias(libs.plugins.androidLibrary) 9 | id("org.jetbrains.dokka") 10 | id("com.vanniktech.maven.publish") version "0.29.0" 11 | } 12 | 13 | kotlin { 14 | androidTarget { 15 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 16 | compilerOptions { 17 | jvmTarget.set(JvmTarget.JVM_11) 18 | } 19 | publishLibraryVariants("release") 20 | } 21 | listOf( 22 | iosX64(), 23 | iosArm64(), 24 | iosSimulatorArm64() 25 | ).forEach { 26 | it.binaries.framework { 27 | baseName = "logging" 28 | isStatic = true 29 | } 30 | } 31 | 32 | jvm() 33 | 34 | @OptIn(ExperimentalWasmDsl::class) 35 | wasmJs { 36 | browser() 37 | nodejs() 38 | } 39 | js(IR) { 40 | browser() 41 | } 42 | 43 | sourceSets { 44 | val commonMain by getting { 45 | dependencies { 46 | implementation(libs.stately.concurrency) 47 | implementation(libs.kotlinx.datetime) 48 | } 49 | } 50 | 51 | val androidMain by getting 52 | 53 | val jvmMain by getting { 54 | dependencies { 55 | implementation(libs.slf4j.api) 56 | } 57 | } 58 | 59 | val iosArm64Main by getting 60 | val iosX64Main by getting 61 | val iosSimulatorArm64Main by getting 62 | val iosMain by creating { 63 | dependsOn(commonMain) 64 | iosArm64Main.dependsOn(this) 65 | iosX64Main.dependsOn(this) 66 | iosSimulatorArm64Main.dependsOn(this) 67 | } 68 | 69 | val jsMain by getting 70 | val wasmJsMain by getting 71 | } 72 | } 73 | 74 | android { 75 | namespace = "com.diamondedge.core" 76 | compileSdk = 34 77 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 78 | defaultConfig { 79 | minSdk = 21 80 | consumerProguardFiles("proguard.txt") 81 | } 82 | compileOptions { 83 | sourceCompatibility = JavaVersion.VERSION_11 84 | targetCompatibility = JavaVersion.VERSION_11 85 | } 86 | testOptions { 87 | unitTests { 88 | isReturnDefaultValues = true 89 | isIncludeAndroidResources = true 90 | } 91 | } 92 | } 93 | 94 | tasks { 95 | create("javadocJar") { 96 | println("creating javadocJar") 97 | archiveClassifier.set("javadoc") 98 | dependsOn(dokkaHtml) 99 | from(dokkaHtml.get().outputDirectory) 100 | } 101 | } 102 | 103 | extra["artifactId"] = "logging" 104 | extra["artifactVersion"] = "2.0.3" 105 | extra["libraryName"] = "KmLogging: Kotlin Multiplatform Logging" 106 | extra["libraryDescription"] = "KmLogging is a high performance, extensible and easy to use logging library for Kotlin Multiplatform development" 107 | extra["gitUrl"] = "https://github.com/DiamondEdge1/KmLogging" 108 | 109 | // defined in project's gradle.properties 110 | val groupId: String by project 111 | val licenseName: String by project 112 | val licenseUrl: String by project 113 | // optional properties 114 | val orgId: String? by project 115 | val orgName: String? by project 116 | val orgUrl: String? by project 117 | val developerName: String? by project 118 | val developerId: String? by project 119 | 120 | val artifactId: String by extra 121 | val artifactVersion: String by extra 122 | val libraryName: String by extra 123 | val libraryDescription: String by extra 124 | val gitUrl: String by extra 125 | 126 | project.group = groupId 127 | project.version = artifactVersion 128 | 129 | mavenPublishing { 130 | coordinates(groupId = groupId, artifactId = artifactId, version = artifactVersion) 131 | pom { 132 | name.set(libraryName) 133 | description.set(libraryDescription) 134 | url.set(gitUrl) 135 | 136 | licenses { 137 | license { 138 | name.set(licenseName) 139 | url.set(licenseUrl) 140 | } 141 | } 142 | scm { 143 | url.set(gitUrl) 144 | } 145 | developers { 146 | if (!developerId.isNullOrEmpty()) { 147 | developer { 148 | id.set(developerId) 149 | name.set(developerName) 150 | } 151 | } 152 | if (!orgId.isNullOrEmpty()) { 153 | developer { 154 | id.set(orgId) 155 | name.set(orgName) 156 | organization.set(orgName) 157 | organizationUrl.set(orgUrl) 158 | } 159 | } 160 | } 161 | if (!orgName.isNullOrEmpty()) { 162 | organization { 163 | name.set(orgName) 164 | if (!orgUrl.isNullOrEmpty()) 165 | url.set(orgUrl) 166 | } 167 | } 168 | } 169 | 170 | publishToMavenCentral(SonatypeHost.S01) 171 | signAllPublications() 172 | } 173 | -------------------------------------------------------------------------------- /logging/proguard.txt: -------------------------------------------------------------------------------- 1 | -keepnames class com.diamondedge.logging.KmLog 2 | -keepnames class com.diamondedge.logging.KmLogKt 3 | -------------------------------------------------------------------------------- /logging/src/androidMain/kotlin/com/diamondedge/logging/Log.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | class Log { 4 | 5 | companion object { 6 | 7 | @JvmStatic 8 | fun v(tag: String?, msg: String) { 9 | if (KmLogging.isLoggingVerbose) 10 | KmLogging.verbose(tag.toString(), msg) 11 | } 12 | 13 | @JvmStatic 14 | fun d(tag: String?, msg: String) { 15 | if (KmLogging.isLoggingDebug) 16 | KmLogging.debug(tag.toString(), msg) 17 | } 18 | 19 | @JvmStatic 20 | fun i(tag: String?, msg: String) { 21 | if (KmLogging.isLoggingInfo) 22 | KmLogging.info(tag.toString(), msg) 23 | } 24 | 25 | @JvmStatic 26 | @JvmOverloads 27 | fun w(tag: String?, msg: String, t: Throwable? = null) { 28 | if (KmLogging.isLoggingWarning) 29 | KmLogging.warn(tag.toString(), msg, t) 30 | } 31 | 32 | @JvmStatic 33 | @JvmOverloads 34 | fun e(tag: String?, msg: String, t: Throwable? = null) { 35 | if (KmLogging.isLoggingError) 36 | KmLogging.error(tag.toString(), msg, t) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /logging/src/androidMain/kotlin/com/diamondedge/logging/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | import android.os.Build 4 | 5 | internal actual fun getPlatform(): PlatformApi = AndroidPlatform() 6 | 7 | internal class AndroidPlatform : PlatformApi { 8 | override val isIOS: Boolean = false 9 | override val isAndroid: Boolean = true 10 | override val isJS: Boolean = false 11 | override val isWasmJs: Boolean = false 12 | override val isJVM: Boolean = false 13 | override val name: String = "Android" 14 | override val version: Double = Build.VERSION.SDK_INT.toDouble() 15 | override val versionName: String = Build.VERSION.RELEASE 16 | override val timeNanos: Long 17 | get() = System.nanoTime() 18 | } 19 | -------------------------------------------------------------------------------- /logging/src/androidMain/kotlin/com/diamondedge/logging/PlatformLogger.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | import android.util.Log 4 | 5 | internal actual fun getLoggerApi(): PlatformLoggerApi = AndroidPlatformLogger() 6 | 7 | internal class AndroidPlatformLogger : PlatformLoggerApi { 8 | 9 | override fun verbose(tag: String, msg: String) { 10 | Log.v(tag, msg) 11 | } 12 | 13 | override fun debug(tag: String, msg: String) { 14 | Log.d(tag, msg) 15 | } 16 | 17 | override fun info(tag: String, msg: String) { 18 | Log.i(tag, msg) 19 | } 20 | 21 | override fun warn(tag: String, msg: String, t: Throwable?) { 22 | Log.w(tag, msg, t) 23 | } 24 | 25 | override fun error(tag: String, msg: String, t: Throwable?) { 26 | Log.e(tag, msg, t) 27 | } 28 | 29 | override fun createTag(fromClass: String?): Pair { 30 | // println("createTag($fromClass)") 31 | var clsName = "NA" 32 | val stack = Thread.currentThread().stackTrace.map { it.className } 33 | stack.forEachIndexed { index, stackEntry -> 34 | // println("stack $stackEntry") 35 | if (stackEntry.endsWith("KmLogKt") && stack.size > index) { 36 | clsName = stack[index + 1] 37 | } 38 | if (fromClass != null && stackEntry.endsWith(fromClass) && stack.size > index) { 39 | clsName = stack[index + 1] 40 | } 41 | } 42 | // println("cls: $clsName") 43 | return Pair(getTag(clsName), clsName) 44 | } 45 | 46 | private fun getTag(className: String): String { 47 | var pos = className.lastIndexOf('.') 48 | pos = if (pos < 0) 0 else pos + 1 49 | return className.substring(pos) 50 | } 51 | } -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/FixedLogLevel.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | /** 4 | * All logging levels are either enabled or disabled. 5 | */ 6 | class FixedLogLevel(private val isLogging: Boolean) : LogLevelController { 7 | override fun isLoggingVerbose() = isLogging 8 | override fun isLoggingDebug() = isLogging 9 | override fun isLoggingInfo() = isLogging 10 | override fun isLoggingWarning() = isLogging 11 | override fun isLoggingError() = isLogging 12 | 13 | override fun toString(): String { 14 | return "FixedLogLevel(isLogging=$isLogging)" 15 | } 16 | } -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/KmLog.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | open class KmLog(tag: String) { 4 | val tagName = tag 5 | 6 | constructor() : this(KmLogging.createTag("KmLog").first) 7 | 8 | inline fun verbose(msg: () -> Any?) { 9 | if (KmLogging.isLoggingVerbose) 10 | verboseApi(tagName, msg().toString()) 11 | } 12 | 13 | inline fun verbose(tag: String, msg: () -> Any?) { 14 | if (KmLogging.isLoggingVerbose) 15 | verboseApi(tag, msg().toString()) 16 | } 17 | 18 | inline fun v(tag: String? = null, msg: () -> Any?) { 19 | if (KmLogging.isLoggingVerbose) 20 | verboseApi(tag ?: tagName, msg().toString()) 21 | } 22 | 23 | inline fun debug(msg: () -> Any?) { 24 | if (KmLogging.isLoggingDebug) 25 | debugApi(tagName, msg().toString()) 26 | } 27 | 28 | inline fun debug(tag: String, msg: () -> Any?) { 29 | if (KmLogging.isLoggingDebug) 30 | debugApi(tag, msg().toString()) 31 | } 32 | 33 | inline fun d(tag: String? = null, msg: () -> Any?) { 34 | if (KmLogging.isLoggingDebug) 35 | debugApi(tag ?: tagName, msg().toString()) 36 | } 37 | 38 | inline fun info(msg: () -> Any?) { 39 | if (KmLogging.isLoggingInfo) 40 | infoApi(tagName, msg().toString()) 41 | } 42 | 43 | inline fun info(tag: String, msg: () -> Any?) { 44 | if (KmLogging.isLoggingInfo) 45 | infoApi(tag, msg().toString()) 46 | } 47 | 48 | inline fun i(tag: String? = null, msg: () -> Any?) { 49 | if (KmLogging.isLoggingInfo) 50 | infoApi(tag ?: tagName, msg().toString()) 51 | } 52 | 53 | inline fun warn(msg: () -> Any?) { 54 | if (KmLogging.isLoggingWarning) 55 | warnApi(tagName, msg().toString(), null) 56 | } 57 | 58 | inline fun warn(err: Throwable?, tag: String? = null, msg: () -> Any?) { 59 | if (KmLogging.isLoggingWarning) 60 | warnApi(tag ?: tagName, msg().toString(), err) 61 | } 62 | 63 | inline fun w(err: Throwable? = null, tag: String? = null, msg: () -> Any?) { 64 | if (KmLogging.isLoggingWarning) 65 | warnApi(tag ?: tagName, msg().toString(), err) 66 | } 67 | 68 | inline fun error(msg: () -> Any?) { 69 | if (KmLogging.isLoggingError) 70 | errorApi(tagName, msg().toString(), null) 71 | } 72 | 73 | inline fun error(err: Throwable?, tag: String? = null, msg: () -> Any?) { 74 | if (KmLogging.isLoggingError) 75 | errorApi(tag ?: tagName, msg().toString(), err) 76 | } 77 | 78 | inline fun e(err: Throwable? = null, tag: String? = null, msg: () -> Any?) { 79 | if (KmLogging.isLoggingError) 80 | errorApi(tag ?: tagName, msg().toString(), err) 81 | } 82 | 83 | @PublishedApi 84 | internal fun verboseApi(tag: String, msg: String) = verbose(tag, msg) 85 | 86 | @PublishedApi 87 | internal fun debugApi(tag: String, msg: String) = debug(tag, msg) 88 | 89 | @PublishedApi 90 | internal fun infoApi(tag: String, msg: String) = info(tag, msg) 91 | 92 | @PublishedApi 93 | internal fun warnApi(tag: String, msg: String, t: Throwable?) = warn(tag, msg, t) 94 | 95 | @PublishedApi 96 | internal fun errorApi(tag: String, msg: String, t: Throwable?) = error(tag, msg, t) 97 | 98 | protected open fun verbose(tag: String, msg: String) { 99 | KmLogging.verbose(tag, msg) 100 | } 101 | 102 | protected open fun debug(tag: String, msg: String) { 103 | KmLogging.debug(tag, msg) 104 | } 105 | 106 | protected open fun info(tag: String, msg: String) { 107 | KmLogging.info(tag, msg) 108 | } 109 | 110 | protected open fun warn(tag: String, msg: String, t: Throwable? = null) { 111 | KmLogging.warn(tag, msg, t) 112 | } 113 | 114 | protected open fun error(tag: String, msg: String, t: Throwable? = null) { 115 | KmLogging.error(tag, msg, t) 116 | } 117 | 118 | override fun toString(): String { 119 | return "KmLog(tagName='$tagName')" 120 | } 121 | } 122 | 123 | /** 124 | * Create a logging object. This is the primary entry point for logging and should be called once for each file, class or object. 125 | * For classes a val can be created either as a private member of the class or as a member of the companion object. 126 | * @param tag string to be used instead of the calculated tag based on the class name or file name. 127 | */ 128 | fun logging(tag: String? = null): KmLog { 129 | if (tag != null) 130 | return logFactory.get()?.createKmLog(tag, tag) ?: KmLog(tag) 131 | val (tagCalculated, className) = KmLogging.createTag("KmLog") 132 | return logFactory.get()?.createKmLog(tagCalculated, className) ?: KmLog(tagCalculated) 133 | } 134 | -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/KmLogging.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | import co.touchlab.stately.concurrency.AtomicReference 4 | import kotlin.native.concurrent.ThreadLocal 5 | 6 | internal val logFactory: AtomicReference = AtomicReference(null) 7 | 8 | private val loggers: AtomicReference> = AtomicReference(emptyList()) 9 | 10 | @ThreadLocal 11 | object KmLogging { 12 | var isLoggingVerbose = true 13 | var isLoggingDebug = true 14 | var isLoggingInfo = true 15 | var isLoggingWarning = true 16 | var isLoggingError = true 17 | 18 | init { 19 | if (loggers.get().isEmpty()) { 20 | loggers.set(listOf(PlatformLogger(FixedLogLevel(true)))) 21 | } 22 | setupLoggingFlags() 23 | } 24 | 25 | /** 26 | * Clears the existing loggers and adds in the new ones. 27 | */ 28 | fun setLoggers(vararg loggers: Logger) { 29 | val list = ArrayList(loggers.size) 30 | for (logger in loggers) { 31 | list.add(logger) 32 | } 33 | com.diamondedge.logging.loggers.set(list) 34 | setupLoggingFlags() 35 | } 36 | 37 | fun setLogFactory(factory: LogFactory?) { 38 | logFactory.set(factory) 39 | } 40 | 41 | /** 42 | * Removes all loggers. For use by native platforms that cannot use the vararg setLoggers. 43 | */ 44 | fun clear() { 45 | loggers.set(emptyList()) 46 | } 47 | 48 | /** 49 | * Adds loggers. For use by native platforms that cannot use the vararg setLoggers. 50 | */ 51 | fun addLogger(logger: Logger) { 52 | val list = ArrayList(loggers.get()) 53 | list.add(logger) 54 | loggers.set(list) 55 | setupLoggingFlags() 56 | } 57 | 58 | /** 59 | * Removes loggers. For use by native platforms that cannot use the vararg setLoggers. 60 | */ 61 | fun removeLogger(logger: Logger) { 62 | val list = ArrayList(loggers.get()) 63 | list.remove(logger) 64 | loggers.set(list) 65 | setupLoggingFlags() 66 | } 67 | 68 | /* 69 | * Convenience method that sets the log level of the PlatformLogger if there is one. 70 | */ 71 | fun setLogLevel(level: LogLevel) { 72 | val list = ArrayList(loggers.get()) 73 | list.forEachIndexed { index, logger -> 74 | if (logger is PlatformLogger) { 75 | list[index] = PlatformLogger(VariableLogLevel(level)) 76 | } 77 | } 78 | loggers.set(list) 79 | setupLoggingFlags() 80 | } 81 | 82 | /** 83 | * Recalculate all of the flags associated with whether each log level is on or off. 84 | * Useful for when a logger's log level is dynamically set such as by a remote config service. 85 | */ 86 | fun setupLoggingFlags() { 87 | isLoggingVerbose = false 88 | isLoggingDebug = false 89 | isLoggingInfo = false 90 | isLoggingWarning = false 91 | isLoggingError = false 92 | for (logger in loggers.get()) { 93 | if (logger.isLoggingVerbose()) 94 | isLoggingVerbose = true 95 | if (logger.isLoggingDebug()) 96 | isLoggingDebug = true 97 | if (logger.isLoggingInfo()) 98 | isLoggingInfo = true 99 | if (logger.isLoggingWarning()) 100 | isLoggingWarning = true 101 | if (logger.isLoggingError()) 102 | isLoggingError = true 103 | } 104 | } 105 | 106 | fun verbose(tag: String, msg: String) { 107 | for (logger in loggers.get()) { 108 | if (logger.isLoggingVerbose()) 109 | logger.verbose(tag, msg) 110 | } 111 | } 112 | 113 | fun debug(tag: String, msg: String) { 114 | for (logger in loggers.get()) { 115 | if (logger.isLoggingDebug()) 116 | logger.debug(tag, msg) 117 | } 118 | } 119 | 120 | fun info(tag: String, msg: String) { 121 | for (logger in loggers.get()) { 122 | if (logger.isLoggingInfo()) 123 | logger.info(tag, msg) 124 | } 125 | } 126 | 127 | fun warn(tag: String, msg: String, t: Throwable? = null) { 128 | for (logger in loggers.get()) { 129 | if (logger.isLoggingWarning()) 130 | logger.warn(tag, msg, t) 131 | } 132 | } 133 | 134 | fun error(tag: String, msg: String, t: Throwable? = null) { 135 | for (logger in loggers.get()) { 136 | if (logger.isLoggingError()) 137 | logger.error(tag, msg, t) 138 | } 139 | } 140 | 141 | fun createTag(fromClass: String? = null): Pair { 142 | for (logger in loggers.get()) { 143 | if (logger is TagProvider) 144 | return logger.createTag(fromClass) 145 | } 146 | return Pair("", "") 147 | } 148 | 149 | override fun toString(): String { 150 | return "KmLogging(verbose:$isLoggingVerbose debug:$isLoggingDebug info:$isLoggingInfo warn:$isLoggingWarning error:$isLoggingError) ${loggers.get()}" 151 | } 152 | } -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/KmModuleLog.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | /** 4 | * Wrapper for KmLog that allows library modules to also use KmLogging and be able to enable or disable its logging independently 5 | * of the application that is also using KmLogging. 6 | * 7 | * Example usage with code implemented in ChartsLogging.kt 8 | * ``` 9 | * object ChartsLogging { 10 | * var enabled = true 11 | * } 12 | * 13 | * fun moduleLogging(tag: String? = null): KmModuleLog { 14 | * // string passed into createTag should be the name of the class that this function is implemented in 15 | * // if it is a top level function then the class name is the file name with Kt appended 16 | * val t = tag ?: KmLogging.createTag("ChartsLoggingKt").first 17 | * return KmModuleLog(logging(t), ChartsLogging::enabled) 18 | * } 19 | * ``` 20 | */ 21 | class KmModuleLog(val log: KmLog, val isModuleLogging: () -> Boolean) { 22 | 23 | inline fun v(msg: () -> Any?) { 24 | if (isModuleLogging()) 25 | log.verbose(msg) 26 | } 27 | 28 | inline fun v(tag: String, msg: () -> Any?) { 29 | if (isModuleLogging()) 30 | log.verbose(tag, msg) 31 | } 32 | 33 | inline fun d(msg: () -> Any?) { 34 | if (isModuleLogging()) 35 | log.debug(msg) 36 | } 37 | 38 | inline fun d(tag: String, msg: () -> Any?) { 39 | if (isModuleLogging()) 40 | log.debug(tag, msg) 41 | } 42 | 43 | inline fun i(msg: () -> Any?) { 44 | if (isModuleLogging()) 45 | log.info(msg) 46 | } 47 | 48 | inline fun i(tag: String, msg: () -> Any?) { 49 | if (isModuleLogging()) 50 | log.info(tag, msg) 51 | } 52 | 53 | inline fun w(msg: () -> Any?) { 54 | if (isModuleLogging()) 55 | log.warn(msg) 56 | } 57 | 58 | inline fun w(err: Throwable?, tag: String? = null, msg: () -> Any?) { 59 | if (isModuleLogging()) 60 | log.warn(err, tag, msg) 61 | } 62 | 63 | inline fun e(msg: () -> Any?) { 64 | if (isModuleLogging()) 65 | log.error(msg) 66 | } 67 | 68 | inline fun e(err: Throwable?, tag: String? = null, msg: () -> Any?) { 69 | if (isModuleLogging()) 70 | log.error(err, tag, msg) 71 | } 72 | } -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/LogFactory.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | interface LogFactory { 4 | fun createKmLog(tag: String, className: String): KmLog 5 | } -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/LogLevel.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | /** 4 | * Enum representing a log level where that level and all succeeding less granular levels are enabled. 5 | * Verbose is the lowest level means all levels are enabled and Error means only Error 6 | */ 7 | enum class LogLevel(val level: Int) { 8 | Verbose(ERROR_MASK or WARN_MASK or INFO_MASK or DEBUG_MASK or VERBOSE_MASK), 9 | Debug(ERROR_MASK or WARN_MASK or INFO_MASK or DEBUG_MASK), 10 | Info(ERROR_MASK or WARN_MASK or INFO_MASK), 11 | Warn(ERROR_MASK or WARN_MASK), 12 | Error(ERROR_MASK), 13 | Off(0) 14 | } 15 | 16 | const val ERROR_MASK = 0x1 17 | const val WARN_MASK = 0x2 18 | const val INFO_MASK = 0x4 19 | const val DEBUG_MASK = 0x8 20 | const val VERBOSE_MASK = 0x10 -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/LogLevelController.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | interface LogLevelController { 4 | fun isLoggingVerbose(): Boolean 5 | fun isLoggingDebug(): Boolean 6 | fun isLoggingInfo(): Boolean 7 | fun isLoggingWarning(): Boolean 8 | fun isLoggingError(): Boolean 9 | } -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | interface Logger : LoggerApi, LogLevelController 4 | -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | internal interface PlatformApi { 4 | val isIOS: Boolean 5 | val isAndroid: Boolean 6 | val isJS: Boolean 7 | val isWasmJs: Boolean 8 | val isJVM: Boolean 9 | val name: String 10 | val version: Double 11 | val versionName: String 12 | val timeNanos: Long 13 | } 14 | 15 | internal expect fun getPlatform(): PlatformApi 16 | 17 | object Platform { 18 | private val api = getPlatform() 19 | 20 | val isIOS: Boolean = api.isIOS 21 | val isAndroid: Boolean = api.isAndroid 22 | val isJS: Boolean = api.isJS 23 | val isWasmJs: Boolean = api.isWasmJs 24 | val isJVM: Boolean = api.isJVM 25 | val name: String = api.name 26 | val version: Double = api.version 27 | val versionName: String = api.versionName 28 | val timeNanos: Long = api.timeNanos 29 | } -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/PlatformLogger.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | interface LoggerApi { 4 | fun verbose(tag: String, msg: String) 5 | fun debug(tag: String, msg: String) 6 | fun info(tag: String, msg: String) 7 | fun warn(tag: String, msg: String, t: Throwable? = null) 8 | fun error(tag: String, msg: String, t: Throwable? = null) 9 | } 10 | 11 | internal interface PlatformLoggerApi : LoggerApi, TagProvider 12 | 13 | class PlatformLogger(private val logLevel: LogLevelController) : Logger, TagProvider, LogLevelController by logLevel { 14 | private val api = getLoggerApi() 15 | override fun verbose(tag: String, msg: String) = api.verbose(tag, msg) 16 | override fun debug(tag: String, msg: String) = api.debug(tag, msg) 17 | override fun info(tag: String, msg: String) = api.info(tag, msg) 18 | override fun warn(tag: String, msg: String, t: Throwable?) = api.warn(tag, msg, t) 19 | override fun error(tag: String, msg: String, t: Throwable?) = api.error(tag, msg, t) 20 | override fun createTag(fromClass: String?): Pair = api.createTag(fromClass) 21 | } 22 | 23 | internal expect fun getLoggerApi(): PlatformLoggerApi 24 | -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/PrintLogger.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | import kotlinx.datetime.Clock 4 | 5 | class PrintLogger(logLevel: LogLevelController) : Logger, LogLevelController by logLevel { 6 | 7 | override fun verbose(tag: String, msg: String) { 8 | println(message("V", tag, msg, null)) 9 | } 10 | 11 | override fun debug(tag: String, msg: String) { 12 | println(message("D", tag, msg, null)) 13 | } 14 | 15 | override fun info(tag: String, msg: String) { 16 | println(message("I", tag, msg, null)) 17 | } 18 | 19 | override fun warn(tag: String, msg: String, t: Throwable?) { 20 | println(message("W", tag, msg, t)) 21 | } 22 | 23 | override fun error(tag: String, msg: String, t: Throwable?) { 24 | println(message("E", tag, msg, t)) 25 | } 26 | 27 | private fun message(level: String, tag: String, msg: String, t: Throwable?): String { 28 | val now = Clock.System.now() 29 | val str = if (tag.isEmpty()) "$level:" else "$level/$tag:" 30 | return "$now $str $msg ${t ?: ""}" 31 | } 32 | } -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/TagProvider.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | interface TagProvider { 4 | fun createTag(fromClass: String?): Pair 5 | } -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/TimingLog.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | /** 4 | * Add logs containing how long it took to do each section. The logs will be added at the debug log level so the debug log level must be enabled. 5 | * Verbose level logging creates a new section which is also totaled by the next debug level logs. If only logging debug level then the verbose 6 | * level logs are not sent to the logs. 7 | * Usage: 8 | * val log = TimingLog("work") 9 | * ... do work A (taking 5 ms) 10 | * log.debug { "A" } 11 | * ... do work B 1 (taking 1 ms) 12 | * log.verbose { "B 1" } 13 | * ... do work B 2 (taking 3 ms) 14 | * log.verbose { "B 2" } 15 | * log.debug { "B" } 16 | * log.finish() 17 | * 18 | * If the log level is set to debug or higher then this would output: 19 | * D/Tag: work: begin TimingLog 20 | * D/Tag: work: 5 ms A 21 | * D/Tag: work: 4 ms B 22 | * D/Tag: work: end 9 ms 23 | * If the log level is set to verbose then this would output: 24 | * D/Tag: work: begin TimingLog 25 | * D/Tag: work: 5 ms A 26 | * V/Tag: work: 1 ms B 1 27 | * V/Tag: work: 3 ms B 2 28 | * D/Tag: work: 4 ms B 29 | * D/Tag: work: end 9 ms 30 | */ 31 | class TimingLog(private val label: String, tag: String? = null) { 32 | 33 | data class Timing(val time: Long, val msg: String, val isVerbose: Boolean) 34 | 35 | private val tagName = tag ?: KmLogging.createTag("TimingLog").first 36 | private val timing = ArrayList() 37 | 38 | init { 39 | add("", false) 40 | } 41 | 42 | fun add(msg: String, isVerbose: Boolean) { 43 | timing.add(Timing(Platform.timeNanos, msg, isVerbose)) 44 | } 45 | 46 | inline fun verbose(msg: () -> Any?) { 47 | if (KmLogging.isLoggingVerbose) { 48 | add(msg().toString(), true) 49 | } 50 | } 51 | 52 | inline fun debug(msg: () -> Any?) { 53 | if (KmLogging.isLoggingDebug) { 54 | add(msg().toString(), false) 55 | } 56 | } 57 | 58 | /** 59 | * Report timing logs. 60 | * If minThresholdMs is greater than 0 then only report if total time exceeds this amount 61 | * @param minThresholdMs threshold in milliseconds 62 | */ 63 | fun finish(minThresholdMs: Long = 0) { 64 | if (KmLogging.isLoggingDebug || timing.size > 1) { 65 | val first = timing[0].time 66 | var now = first 67 | var prevDebug = first 68 | var prev = first 69 | val totalTime = timing.last().time - first 70 | if (minThresholdMs > 0 && totalTime < minThresholdMs * 1_000_000) { 71 | return 72 | } 73 | KmLogging.debug(tagName, "$label: begin TimingLog") 74 | for (i in 1 until timing.size) { 75 | val t = timing[i] 76 | now = t.time 77 | val msg = t.msg 78 | val indent = if (t.isVerbose) " " else " " 79 | val timeStr = if (t.isVerbose) msDiff(now, prev) else msDiff(now, prevDebug) 80 | KmLogging.debug(tagName, "$label:$indent$timeStr $msg") 81 | prev = now 82 | if (!t.isVerbose) 83 | prevDebug = now 84 | } 85 | KmLogging.debug(tagName, "$label: end ${msDiff(now, first)}") 86 | } 87 | reset() 88 | } 89 | 90 | fun reset() { 91 | timing.clear() 92 | add("", false) 93 | } 94 | 95 | private fun msDiff(nano1: Long, nano2: Long): String { 96 | val diff = (nano1 - nano2) / 1_000_000f 97 | return "${truncateDecimal(diff, 3)} ms" 98 | } 99 | 100 | companion object { 101 | fun truncateDecimal(num: Float, decPlaces: Int): String { 102 | val str = num.toString() 103 | val dotPos = str.indexOf('.') 104 | return if (dotPos >= 0 && dotPos < str.length - decPlaces) 105 | str.substring(0..(dotPos + decPlaces)) 106 | else 107 | str 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /logging/src/commonMain/kotlin/com/diamondedge/logging/VariableLogLevel.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | /** 4 | * Each logging level is controlled by the bit mask of the given log level. 5 | */ 6 | class VariableLogLevel(private val level: Int) : LogLevelController { 7 | 8 | constructor(logLevel: LogLevel = LogLevel.Verbose) : this(logLevel.level) 9 | 10 | init { 11 | KmLogging.setupLoggingFlags() 12 | } 13 | 14 | override fun isLoggingVerbose() = level and VERBOSE_MASK > 0 15 | override fun isLoggingDebug() = level and DEBUG_MASK > 0 16 | override fun isLoggingInfo() = level and INFO_MASK > 0 17 | override fun isLoggingWarning() = level and WARN_MASK > 0 18 | override fun isLoggingError() = level and ERROR_MASK > 0 19 | 20 | override fun toString(): String { 21 | return "VariableLogLevel(level=$level verbose:${isLoggingVerbose()} debug:${isLoggingDebug()} info:${isLoggingInfo()} warn:${isLoggingWarning()} error:${isLoggingError()})" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /logging/src/iosMain/kotlin/com/diamondedge/logging/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | import platform.CoreFoundation.CFAbsoluteTimeGetCurrent 4 | import platform.UIKit.UIDevice 5 | 6 | internal actual fun getPlatform(): PlatformApi = IOSPlatform() 7 | 8 | internal class IOSPlatform : PlatformApi { 9 | override val isIOS: Boolean = true 10 | override val isAndroid: Boolean = false 11 | override val isJS: Boolean = false 12 | override val isWasmJs: Boolean = false 13 | override val isJVM: Boolean = false 14 | override val name: String = UIDevice.currentDevice.systemName 15 | override val version: Double 16 | override val versionName: String 17 | override val timeNanos: Long 18 | get() = (CFAbsoluteTimeGetCurrent() * 1_000_000_000).toLong() 19 | 20 | init { 21 | val ver = UIDevice.currentDevice.systemVersion 22 | versionName = ver 23 | version = try { 24 | ver.toDouble() 25 | } catch (e: Exception) { 26 | try { 27 | ver.substringBeforeLast('.').toDouble() 28 | } catch (e: Exception) { 29 | ver.substringBefore('.').toDouble() 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /logging/src/iosMain/kotlin/com/diamondedge/logging/PlatformLogger.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | import kotlinx.cinterop.ExperimentalForeignApi 4 | import kotlinx.cinterop.ptr 5 | import platform.Foundation.NSThread 6 | import platform.darwin.OS_LOG_DEFAULT 7 | import platform.darwin.OS_LOG_TYPE_DEBUG 8 | import platform.darwin.OS_LOG_TYPE_DEFAULT 9 | import platform.darwin.OS_LOG_TYPE_ERROR 10 | import platform.darwin.OS_LOG_TYPE_FAULT 11 | import platform.darwin.OS_LOG_TYPE_INFO 12 | import platform.darwin.__dso_handle 13 | import platform.darwin._os_log_internal 14 | 15 | internal actual fun getLoggerApi(): PlatformLoggerApi = IOSPlatformLogger() 16 | 17 | @OptIn(ExperimentalForeignApi::class) 18 | internal class IOSPlatformLogger : PlatformLoggerApi { 19 | 20 | override fun verbose(tag: String, msg: String) { 21 | _os_log_internal( 22 | __dso_handle.ptr, 23 | OS_LOG_DEFAULT, 24 | OS_LOG_TYPE_DEBUG, 25 | "%s", 26 | message("V", tag, msg) 27 | ) 28 | } 29 | 30 | override fun debug(tag: String, msg: String) { 31 | _os_log_internal( 32 | __dso_handle.ptr, 33 | OS_LOG_DEFAULT, 34 | OS_LOG_TYPE_INFO, 35 | "%s", 36 | message("D", tag, msg) 37 | ) 38 | } 39 | 40 | override fun info(tag: String, msg: String) { 41 | _os_log_internal( 42 | __dso_handle.ptr, 43 | OS_LOG_DEFAULT, 44 | OS_LOG_TYPE_DEFAULT, 45 | "%s", 46 | message("I", tag, msg) 47 | ) 48 | } 49 | 50 | override fun warn(tag: String, msg: String, t: Throwable?) { 51 | _os_log_internal( 52 | __dso_handle.ptr, 53 | OS_LOG_DEFAULT, 54 | OS_LOG_TYPE_ERROR, 55 | "%s", 56 | message("W", tag, msg, t) 57 | ) 58 | } 59 | 60 | override fun error(tag: String, msg: String, t: Throwable?) { 61 | _os_log_internal( 62 | __dso_handle.ptr, 63 | OS_LOG_DEFAULT, 64 | OS_LOG_TYPE_FAULT, 65 | "%s", 66 | message("E", tag, msg, t) 67 | ) 68 | } 69 | 70 | private fun message(level: String, tag: String, msg: String, t: Throwable? = null): String { 71 | val str = if (tag.isEmpty()) "$level: $msg" else "$level/$tag: $msg" 72 | return if (t == null) str else "$str $t" 73 | } 74 | 75 | override fun createTag(fromClass: String?): Pair { 76 | val stack = NSThread.callStackSymbols 77 | var clsName = "" 78 | stack.forEachIndexed { index, t -> 79 | val stackEntry = t.toString() 80 | // println("tag: $tag stack: $stackEntry") 81 | 82 | if (stackEntry.contains("KmLog") && stack.size > index) { 83 | val nextEntry = stack[index + 1].toString() 84 | if (!nextEntry.contains("KmLog")) 85 | clsName = nextEntry 86 | } 87 | if (fromClass != null && stackEntry.contains(fromClass) && stack.size > index) { 88 | clsName = stack[index + 1].toString() 89 | } 90 | } 91 | return Pair(getClassName(clsName), clsName) 92 | } 93 | 94 | private fun getClassName(stackEntry: String): String { 95 | var tag = "" 96 | if (stackEntry.contains(".Companion")) { 97 | tag = stackEntry.substringBefore(".Companion") 98 | } else if (stackEntry.contains("#()")) { 99 | tag = stackEntry.substringBefore("#()") 100 | } 101 | return tag.substring(tag.lastIndexOf(".") + 1) 102 | } 103 | } -------------------------------------------------------------------------------- /logging/src/jsMain/kotlin/com/diamondedge/logging/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | import kotlin.js.Date 4 | 5 | internal actual fun getPlatform(): PlatformApi = JsPlatform() 6 | 7 | internal class JsPlatform : PlatformApi { 8 | override val isIOS: Boolean = false 9 | override val isAndroid: Boolean = false 10 | override val isJS: Boolean = true 11 | override val isWasmJs: Boolean = false 12 | override val isJVM: Boolean = false 13 | override val name: String = "JavaScript" 14 | override val version: Double = 1.0 //TODO 15 | override val versionName: String = "1.0" //TODO 16 | override val timeNanos: Long 17 | get() = Date().getMilliseconds() * 1_000_000L 18 | } 19 | -------------------------------------------------------------------------------- /logging/src/jsMain/kotlin/com/diamondedge/logging/PlatformLogger.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | import kotlin.js.Date 4 | 5 | internal actual fun getLoggerApi(): PlatformLoggerApi = JsPlatformLogger() 6 | 7 | internal class JsPlatformLogger : PlatformLoggerApi { 8 | 9 | override fun verbose(tag: String, msg: String) { 10 | console.log(preface("V", tag), msg) 11 | } 12 | 13 | override fun debug(tag: String, msg: String) { 14 | console.log(preface("D", tag), msg) 15 | } 16 | 17 | override fun info(tag: String, msg: String) { 18 | console.info(preface("I", tag), msg) 19 | } 20 | 21 | override fun warn(tag: String, msg: String, t: Throwable?) { 22 | console.warn(preface("W", tag), msg, t) 23 | } 24 | 25 | override fun error(tag: String, msg: String, t: Throwable?) { 26 | console.error(preface("E", tag), msg, t) 27 | } 28 | 29 | override fun createTag(fromClass: String?): Pair { 30 | return Pair("", "") 31 | } 32 | 33 | private fun preface(level: String, tag: String): String { 34 | val d = Date() 35 | val timestamp = 36 | "${d.getMonth() + 1}/${d.getDate()} ${d2(d.getHours())}:${d2(d.getMinutes())}:${d2(d.getSeconds())}.${ 37 | d3(d.getMilliseconds()) 38 | }" 39 | val str = if (tag.isEmpty()) "$level:" else "$level/$tag:" 40 | return "$timestamp $str" 41 | } 42 | 43 | private fun d2(i: Int): String { 44 | return if (i < 10) "0$i" else i.toString() 45 | } 46 | 47 | private fun d3(i: Int): String { 48 | return if (i < 100) ("0" + if (i < 10) "0" else "") + i else i.toString() 49 | } 50 | } -------------------------------------------------------------------------------- /logging/src/jvmMain/kotlin/com/diamondedge/logging/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | internal actual fun getPlatform(): PlatformApi = JvmPlatform() 4 | 5 | internal class JvmPlatform : PlatformApi { 6 | override val isIOS: Boolean = false 7 | override val isAndroid: Boolean = false 8 | override val isJS: Boolean = false 9 | override val isWasmJs: Boolean = false 10 | override val isJVM: Boolean = true 11 | override val name: String = "JVM" 12 | override val version: Double = 1.0 //TODO 13 | override val versionName: String = "1.0" //TODO 14 | override val timeNanos: Long 15 | get() = System.nanoTime() 16 | } 17 | -------------------------------------------------------------------------------- /logging/src/jvmMain/kotlin/com/diamondedge/logging/PlatformLogger.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | internal actual fun getLoggerApi(): PlatformLoggerApi = JvmPlatformLogger() 4 | 5 | internal class JvmPlatformLogger : PlatformLoggerApi { 6 | 7 | init { 8 | KmLogging.setLogFactory(Slf4jLogFactory()) 9 | } 10 | 11 | // Note: these methods don't do anything because the logging output happens from Slf4Log class 12 | 13 | override fun verbose(tag: String, msg: String) { 14 | } 15 | 16 | override fun debug(tag: String, msg: String) { 17 | } 18 | 19 | override fun info(tag: String, msg: String) { 20 | } 21 | 22 | override fun warn(tag: String, msg: String, t: Throwable?) { 23 | } 24 | 25 | override fun error(tag: String, msg: String, t: Throwable?) { 26 | } 27 | 28 | override fun createTag(fromClass: String?): Pair { 29 | val clsName = findClassName(fromClass) 30 | return Pair(getTag(clsName), clsName) 31 | } 32 | 33 | companion object { 34 | fun findClassName(fromClass: String?): String { 35 | var clsName = "" 36 | val stack = Thread.currentThread().stackTrace.map { it.className } 37 | stack.forEachIndexed { index, stackEntry -> 38 | // println("stack $stackEntry") 39 | if (stackEntry.endsWith("KmLogKt") && stack.size > index) { 40 | clsName = stack[index + 1] 41 | } 42 | if (fromClass != null && stackEntry.endsWith(fromClass) && stack.size > index) { 43 | clsName = stack[index + 1] 44 | } 45 | } 46 | return clsName 47 | } 48 | 49 | private fun getTag(fullClassName: String): String { 50 | var pos = fullClassName.lastIndexOf('.') 51 | pos = if (pos < 0) 0 else pos + 1 52 | return fullClassName.substring(pos) 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /logging/src/jvmMain/kotlin/com/diamondedge/logging/Slf4jLog.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory 5 | 6 | class Slf4jLog(tag: String, className: String) : KmLog(tag) { 7 | 8 | private val logger: Logger = LoggerFactory.getLogger(className) 9 | 10 | override fun verbose(tag: String, msg: String) { 11 | super.verbose(tag, msg) 12 | logger.trace(msg) 13 | } 14 | 15 | override fun debug(tag: String, msg: String) { 16 | super.debug(tag, msg) 17 | logger.debug(msg) 18 | } 19 | 20 | override fun info(tag: String, msg: String) { 21 | super.info(tag, msg) 22 | logger.info(msg) 23 | } 24 | 25 | override fun warn(tag: String, msg: String, t: Throwable?) { 26 | super.warn(tag, msg, t) 27 | if (t != null) 28 | logger.warn(msg, t) 29 | else 30 | logger.warn(msg) 31 | } 32 | 33 | override fun error(tag: String, msg: String, t: Throwable?) { 34 | super.error(tag, msg, t) 35 | if (t != null) 36 | logger.error(msg, t) 37 | else 38 | logger.error(msg) 39 | } 40 | } -------------------------------------------------------------------------------- /logging/src/jvmMain/kotlin/com/diamondedge/logging/Slf4jLogFactory.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | class Slf4jLogFactory : LogFactory { 4 | 5 | override fun createKmLog(tag: String, className: String): KmLog { 6 | return Slf4jLog(tag, className) 7 | } 8 | } -------------------------------------------------------------------------------- /logging/src/wasmJsMain/kotlin/com/diamondedge/logging/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | import kotlinx.datetime.Clock 4 | 5 | internal actual fun getPlatform(): PlatformApi = WasmPlatform() 6 | 7 | internal class WasmPlatform : PlatformApi { 8 | override val isIOS: Boolean = false 9 | override val isAndroid: Boolean = false 10 | override val isJS: Boolean = false 11 | override val isWasmJs: Boolean = true 12 | override val isJVM: Boolean = false 13 | override val name: String = "Wasm" 14 | override val version: Double = 1.0 //TODO 15 | override val versionName: String = "1.0" //TODO 16 | override val timeNanos: Long 17 | get() { 18 | val now = Clock.System.now() 19 | return now.epochSeconds * 1_000_000_000L + now.nanosecondsOfSecond 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /logging/src/wasmJsMain/kotlin/com/diamondedge/logging/PlatformLogger.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.logging 2 | 3 | import kotlinx.datetime.Clock 4 | 5 | internal actual fun getLoggerApi(): PlatformLoggerApi = WasmPlatformLogger() 6 | 7 | internal class WasmPlatformLogger : PlatformLoggerApi { 8 | 9 | override fun verbose(tag: String, msg: String) { 10 | console(message("V", tag, msg, null)) 11 | } 12 | 13 | override fun debug(tag: String, msg: String) { 14 | console(message("D", tag, msg, null)) 15 | } 16 | 17 | override fun info(tag: String, msg: String) { 18 | consoleInfo(message("I", tag, msg, null)) 19 | } 20 | 21 | override fun warn(tag: String, msg: String, t: Throwable?) { 22 | consoleWarn(message("W", tag, msg, t)) 23 | } 24 | 25 | override fun error(tag: String, msg: String, t: Throwable?) { 26 | consoleError(message("E", tag, msg, t)) 27 | } 28 | 29 | override fun createTag(fromClass: String?): Pair { 30 | return Pair("", "") 31 | } 32 | 33 | private fun message(level: String, tag: String, msg: String, t: Throwable?): String { 34 | val now = Clock.System.now() 35 | val str = if (tag.isEmpty()) "$level:" else "$level/$tag:" 36 | return "$now $str $msg ${t ?: ""}" 37 | } 38 | } 39 | 40 | private fun console(msg: String): Unit = js("console.log(msg)") 41 | private fun consoleInfo(msg: String): Unit = js("console.info(msg)") 42 | private fun consoleWarn(msg: String): Unit = js("console.warn(msg)") 43 | private fun consoleError(msg: String): Unit = js("console.error(msg)") 44 | -------------------------------------------------------------------------------- /sampleApp/README.md: -------------------------------------------------------------------------------- 1 | # Kotlin Multiplatform Logging Sample App 2 | 3 | ## Android App demonstrates 4 | 5 | * Adding a logger to send logs to Crashlytics 6 | * Modifying builtin PlatformLogger to log based on Remote Config log level value 7 | 8 | ## Setup of Crashlytics and Remote Config 9 | 10 | _IMPORTANT_: To build the sample app and see Crashlytics and Remote Config in operation, you will 11 | need to add your own google-services.json into the androidApp directory. 12 | 13 | In the Firebase console: 14 | 15 | * add an app with a packageId of com.diamondedge.sample. 16 | * In the Remote Config tab, add a parameter labelled log_level and set its default value to a string 17 | matching the enum names for LogLevel 18 | 19 | Notes: 20 | 21 | * Remote Config log levels would be useful if for example you wanted to turn logging on in release 22 | builds 23 | but only for certain users such as QA testers or for all environments prior to production. 24 | * Remote Config doesn't have a way to tell you when they fetch new values so if you wanted a more 25 | dynamic level that would change while the app is running then you would have to call 26 | KmLogging.setupLoggingFlags() 27 | from a timer that would run at the same time interval as you have the Remote Config 28 | minimumFetchIntervalInSeconds set to. 29 | * Other paid feature flag systems such as LaunchDarkly, ConfigCat, etc have ways to know when config 30 | changes occur 31 | so you will know when to call KmLogging.setupLoggingFlags() to update the log levels. 32 | 33 | ## Running the sample app 34 | 35 | ### Android 36 | 37 | * Run `sampleApp.composeApp` from Android Studio as you would any other Android app. 38 | 39 | ### Desktop 40 | 41 | * Run from the commandline using: ./gradlew :sampleApp:composeApp:run 42 | 43 | ### Browser (WASM) 44 | 45 | * Run from the commandline using: ./gradlew :sampleApp:composeApp:wasmJsBrowserRun 46 | -------------------------------------------------------------------------------- /sampleApp/composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 3 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 4 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 5 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig 6 | 7 | plugins { 8 | alias(libs.plugins.kotlinMultiplatform) 9 | alias(libs.plugins.androidApplication) 10 | alias(libs.plugins.composeMultiplatform) 11 | alias(libs.plugins.composeCompiler) 12 | } 13 | 14 | kotlin { 15 | androidTarget { 16 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 17 | compilerOptions { 18 | jvmTarget.set(JvmTarget.JVM_11) 19 | } 20 | } 21 | 22 | listOf( 23 | iosX64(), 24 | iosArm64(), 25 | iosSimulatorArm64() 26 | ).forEach { iosTarget -> 27 | iosTarget.binaries.framework { 28 | baseName = "ComposeApp" 29 | isStatic = true 30 | } 31 | } 32 | 33 | jvm("desktop") 34 | 35 | @OptIn(ExperimentalWasmDsl::class) 36 | wasmJs { 37 | moduleName = "composeApp" 38 | browser { 39 | val rootDirPath = project.rootDir.path 40 | val projectDirPath = project.projectDir.path 41 | commonWebpackConfig { 42 | outputFileName = "composeApp.js" 43 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { 44 | static = (static ?: mutableListOf()).apply { 45 | // Serve sources to debug inside browser 46 | add(rootDirPath) 47 | add(projectDirPath) 48 | } 49 | } 50 | } 51 | } 52 | binaries.executable() 53 | } 54 | 55 | sourceSets { 56 | val desktopMain by getting 57 | 58 | androidMain.dependencies { 59 | implementation(compose.preview) 60 | implementation(libs.androidx.activity.compose) 61 | implementation(project.dependencies.platform(libs.firebase.bom)) 62 | implementation(libs.firebase.analytics.ktx) 63 | implementation(libs.firebase.config.ktx) 64 | implementation(libs.firebase.crashlytics.ktx) 65 | } 66 | commonMain.dependencies { 67 | implementation(compose.runtime) 68 | implementation(compose.foundation) 69 | implementation(compose.material) 70 | implementation(compose.ui) 71 | implementation(compose.components.resources) 72 | implementation(compose.components.uiToolingPreview) 73 | implementation(libs.androidx.lifecycle.viewmodel) 74 | implementation(libs.androidx.lifecycle.runtime.compose) 75 | implementation(libs.logging) 76 | // implementation(project(":logging")) 77 | } 78 | desktopMain.dependencies { 79 | implementation(compose.desktop.currentOs) 80 | implementation(libs.kotlinx.coroutines.swing) 81 | implementation(libs.slf4j.api) 82 | implementation(libs.logback.classic) 83 | } 84 | commonTest.dependencies { 85 | implementation(libs.kotlin.test) 86 | } 87 | } 88 | } 89 | 90 | android { 91 | namespace = "com.diamondedge.sample" 92 | compileSdk = libs.versions.android.compileSdk.get().toInt() 93 | 94 | defaultConfig { 95 | applicationId = "com.diamondedge.sample" 96 | minSdk = libs.versions.android.minSdk.get().toInt() 97 | targetSdk = libs.versions.android.targetSdk.get().toInt() 98 | versionCode = 1 99 | versionName = "1.0" 100 | } 101 | packaging { 102 | resources { 103 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 104 | } 105 | } 106 | buildTypes { 107 | getByName("release") { 108 | isMinifyEnabled = false 109 | } 110 | } 111 | compileOptions { 112 | sourceCompatibility = JavaVersion.VERSION_11 113 | targetCompatibility = JavaVersion.VERSION_11 114 | } 115 | testOptions { 116 | unitTests { 117 | isReturnDefaultValues = true 118 | isIncludeAndroidResources = true 119 | } 120 | } 121 | } 122 | 123 | dependencies { 124 | debugImplementation(compose.uiTooling) 125 | } 126 | 127 | compose.desktop { 128 | application { 129 | mainClass = "com.diamondedge.sample.MainKt" 130 | 131 | nativeDistributions { 132 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 133 | packageName = "com.diamondedge.sample" 134 | packageVersion = "1.0.0" 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /sampleApp/composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sampleApp/composeApp/src/androidMain/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "404711485520", 4 | "project_id": "lighthouse-games-1", 5 | "storage_bucket": "lighthouse-games-1.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:404711485520:android:1a798574f6bfa362e1a155", 11 | "android_client_info": { 12 | "package_name": "org.lighthousegames.sample" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "404711485520-lkhieg6nnmd1mema0ai6us73avdnaidp.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyBrEamfs57SbyLtAeUsVEnrCzGmqqb1X_g" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "404711485520-lkhieg6nnmd1mema0ai6us73avdnaidp.apps.googleusercontent.com", 31 | "client_type": 3 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | ], 38 | "configuration_version": "1" 39 | } -------------------------------------------------------------------------------- /sampleApp/composeApp/src/androidMain/kotlin/com/diamondedge/sample/App.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.sample 2 | 3 | import android.app.Application 4 | import com.diamondedge.logging.KmLogging 5 | import com.diamondedge.logging.Log 6 | import com.diamondedge.logging.PlatformLogger 7 | import com.diamondedge.logging.VariableLogLevel 8 | import com.diamondedge.logging.logging 9 | import com.google.firebase.ktx.Firebase 10 | import com.google.firebase.remoteconfig.ktx.get 11 | import com.google.firebase.remoteconfig.ktx.remoteConfig 12 | import com.google.firebase.remoteconfig.ktx.remoteConfigSettings 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.MainScope 15 | import kotlinx.coroutines.launch 16 | import kotlinx.coroutines.runBlocking 17 | 18 | class App : Application() { 19 | 20 | private val remoteConfig by lazy { Firebase.remoteConfig } 21 | 22 | override fun onCreate() { 23 | super.onCreate() 24 | KmLogging.setLoggers(PlatformLogger(VariableLogLevel(com.diamondedge.logging.LogLevel.Verbose)), CrashlyticsLogger()) 25 | log.info { "onCreate ${Thread.currentThread()}" } 26 | 27 | setupRemoteConfig() 28 | logOnThread() 29 | } 30 | 31 | private fun setupRemoteConfig() { 32 | val configSettings = remoteConfigSettings { 33 | minimumFetchIntervalInSeconds = /*if (BuildConfig.DEBUG) 60 else*/ 3600 34 | } 35 | remoteConfig.setConfigSettingsAsync(configSettings) 36 | applicationScope.launch(Dispatchers.IO) { 37 | remoteConfig.fetchAndActivate().addOnCompleteListener { task -> 38 | if (task.isSuccessful) { 39 | val updated = task.result 40 | val logLevel = remoteConfig["log_level"].asString() 41 | Log.i("App", "log_level: $logLevel ${Thread.currentThread()}") 42 | val level = com.diamondedge.logging.LogLevel.valueOf(logLevel) 43 | KmLogging.setLogLevel(level) 44 | log.i { "Config params updated: $updated. LogLevel: $level" } 45 | } else { 46 | log.w { "Fetch failed" } 47 | } 48 | } 49 | } 50 | } 51 | 52 | private fun logOnThread() { 53 | log.d { "logOnThread ${Thread.currentThread()}" } 54 | runBlocking { 55 | 56 | } 57 | applicationScope.launch(Dispatchers.IO) { 58 | log.d { "logOnThread coroutine: ${Thread.currentThread()} $this ${this.coroutineContext}" } 59 | } 60 | applicationScope.launch(Dispatchers.Main) { 61 | log.d { "logOnThread coroutine main ${Thread.currentThread()} $this ${this.coroutineContext}" } 62 | } 63 | } 64 | 65 | companion object { 66 | private val log = logging() 67 | var applicationScope = MainScope() 68 | } 69 | } -------------------------------------------------------------------------------- /sampleApp/composeApp/src/androidMain/kotlin/com/diamondedge/sample/CrashlyticsLogger.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.sample 2 | 3 | import com.diamondedge.logging.Logger 4 | import com.google.firebase.crashlytics.FirebaseCrashlytics 5 | 6 | class CrashlyticsLogger : Logger { 7 | 8 | override fun verbose(tag: String, msg: String) {} 9 | 10 | override fun debug(tag: String, msg: String) {} 11 | 12 | override fun info(tag: String, msg: String) { 13 | FirebaseCrashlytics.getInstance().log(message(tag, msg)) 14 | } 15 | 16 | override fun warn(tag: String, msg: String, t: Throwable?) { 17 | FirebaseCrashlytics.getInstance().log(message(tag, msg)) 18 | if (t != null) 19 | FirebaseCrashlytics.getInstance().recordException(t) 20 | } 21 | 22 | override fun error(tag: String, msg: String, t: Throwable?) { 23 | FirebaseCrashlytics.getInstance().log(message(tag, msg)) 24 | if (t != null) 25 | FirebaseCrashlytics.getInstance().recordException(t) 26 | } 27 | 28 | private fun message(tag: String, msg: String) = if (tag.isEmpty()) msg else "$tag: $msg" 29 | 30 | override fun isLoggingVerbose(): Boolean = false 31 | 32 | override fun isLoggingDebug(): Boolean = false 33 | 34 | override fun isLoggingInfo(): Boolean = true 35 | 36 | override fun isLoggingWarning(): Boolean = true 37 | 38 | override fun isLoggingError(): Boolean = true 39 | } -------------------------------------------------------------------------------- /sampleApp/composeApp/src/androidMain/kotlin/com/diamondedge/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.sample 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.tooling.preview.Preview 8 | 9 | class MainActivity : ComponentActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | 13 | setContent { 14 | AppUI() 15 | } 16 | } 17 | } 18 | 19 | @Preview 20 | @Composable 21 | fun AppAndroidPreview() { 22 | AppUI() 23 | } -------------------------------------------------------------------------------- /sampleApp/composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Sample Logging App 3 | -------------------------------------------------------------------------------- /sampleApp/composeApp/src/commonMain/kotlin/com/diamondedge/sample/AppUI.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.sample 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.material.Button 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import com.diamondedge.logging.logging 17 | import org.jetbrains.compose.ui.tooling.preview.Preview 18 | 19 | private val log = logging() 20 | 21 | @Composable 22 | @Preview 23 | fun AppUI() { 24 | MaterialTheme { 25 | var showContent by remember { mutableStateOf(false) } 26 | Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { 27 | Button(onClick = { showContent = !showContent }) { 28 | Text("Click me!") 29 | } 30 | AnimatedVisibility(showContent) { 31 | val greeting = remember { Greeting().greet() } 32 | Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { 33 | Text("Compose: $greeting") 34 | } 35 | } 36 | Button(onClick = { log.debug { "debug level msg" } }) { 37 | Text("Debug level") 38 | } 39 | Button(onClick = { log.info { "info level msg" } }) { 40 | Text("Info level") 41 | } 42 | Button(onClick = { warnException() }) { 43 | Text("Warn level") 44 | } 45 | Button(onClick = { fatalException() }) { 46 | Text("Error level") 47 | } 48 | } 49 | } 50 | } 51 | 52 | private fun warnException() { 53 | try { 54 | throw RuntimeException("Non Fatal") 55 | } catch (e: Exception) { 56 | log.warn(e) { "exception handled" } 57 | } 58 | } 59 | 60 | private fun fatalException() { 61 | try { 62 | throw RuntimeException("Fatal") 63 | } catch (e: Exception) { 64 | log.error(e) { "fatal exception encountered" } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /sampleApp/composeApp/src/commonMain/kotlin/com/diamondedge/sample/Greeting.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.sample 2 | 3 | import com.diamondedge.logging.logging 4 | 5 | class Greeting { 6 | fun greet(): String { 7 | val tlog = com.diamondedge.logging.TimingLog("Test Timing") 8 | log.i { "greeting()" } 9 | val greet = 10 | "Hello, ${com.diamondedge.logging.Platform.name} ${com.diamondedge.logging.Platform.versionName} (${com.diamondedge.logging.Platform.version})" 11 | tlog.debug { "greeting" } 12 | var s = "0" 13 | for (i in 1..1_000_000) { 14 | s = (s.toInt() + 1).toString() 15 | if (i == 700_000) 16 | tlog.verbose { "i = $i" } 17 | else if (i == 500_000) 18 | tlog.verbose { "i = $i" } 19 | else if (i == 200_000) 20 | tlog.verbose { "i = $i" } 21 | } 22 | tlog.verbose { "i = $s" } 23 | tlog.debug { "i" } 24 | tlog.finish() 25 | return greet 26 | } 27 | 28 | companion object { 29 | val log = logging() 30 | } 31 | } -------------------------------------------------------------------------------- /sampleApp/composeApp/src/commonTest/kotlin/com/diamondedge/sample/SampleTest.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.sample 2 | 3 | import com.diamondedge.logging.FixedLogLevel 4 | import com.diamondedge.logging.KmLogging 5 | import com.diamondedge.logging.PrintLogger 6 | import com.diamondedge.logging.logging 7 | import kotlin.test.BeforeTest 8 | import kotlin.test.Test 9 | import kotlin.test.assertEquals 10 | 11 | 12 | class SampleTest { 13 | @BeforeTest 14 | fun setup() { 15 | KmLogging.setLoggers(PrintLogger(FixedLogLevel(true))) 16 | } 17 | 18 | @Test 19 | fun testLogLevels() { 20 | log.v { "verbose level" } 21 | log.d { "debug level" } 22 | log.i { "info level" } 23 | assertEquals(1, 1) 24 | } 25 | 26 | companion object { 27 | val log = logging() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sampleApp/composeApp/src/desktopMain/kotlin/com/diamondedge/sample/main.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.sample 2 | 3 | import androidx.compose.ui.window.Window 4 | import androidx.compose.ui.window.application 5 | 6 | fun main() = application { 7 | Window( 8 | onCloseRequest = ::exitApplication, 9 | title = "Sample Logging App", 10 | ) { 11 | AppUI() 12 | } 13 | } -------------------------------------------------------------------------------- /sampleApp/composeApp/src/desktopMain/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sampleApp/composeApp/src/iosMain/kotlin/com/diamondedge/sample/MainViewController.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.sample 2 | 3 | import androidx.compose.ui.window.ComposeUIViewController 4 | 5 | fun MainViewController() = ComposeUIViewController { AppUI() } -------------------------------------------------------------------------------- /sampleApp/composeApp/src/wasmJsMain/kotlin/com/diamondedge/sample/main.kt: -------------------------------------------------------------------------------- 1 | package com.diamondedge.sample 2 | 3 | import androidx.compose.ui.ExperimentalComposeUiApi 4 | import androidx.compose.ui.window.ComposeViewport 5 | import kotlinx.browser.document 6 | 7 | @OptIn(ExperimentalComposeUiApi::class) 8 | fun main() { 9 | ComposeViewport(document.body!!) { 10 | AppUI() 11 | } 12 | } -------------------------------------------------------------------------------- /sampleApp/composeApp/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sample Logging App 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sampleApp/composeApp/src/wasmJsMain/resources/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | } -------------------------------------------------------------------------------- /sampleApp/iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=com.diamondedge.sample.SampleApp 3 | APP_NAME=SampleApp -------------------------------------------------------------------------------- /sampleApp/iosApp/iosApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 11 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; 12 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 13 | 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 18 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 19 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; 20 | 7555FF7B242A565900829871 /* SampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 22 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23 | AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; 24 | /* End PBXFileReference section */ 25 | 26 | /* Begin PBXFrameworksBuildPhase section */ 27 | B92378962B6B1156000C7307 /* Frameworks */ = { 28 | isa = PBXFrameworksBuildPhase; 29 | buildActionMask = 2147483647; 30 | files = ( 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | 058557D7273AAEEB004C7B11 /* Preview Content */ = { 38 | isa = PBXGroup; 39 | children = ( 40 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, 41 | ); 42 | path = "Preview Content"; 43 | sourceTree = ""; 44 | }; 45 | 42799AB246E5F90AF97AA0EF /* Frameworks */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | ); 49 | name = Frameworks; 50 | sourceTree = ""; 51 | }; 52 | 7555FF72242A565900829871 = { 53 | isa = PBXGroup; 54 | children = ( 55 | AB1DB47929225F7C00F7AF9C /* Configuration */, 56 | 7555FF7D242A565900829871 /* iosApp */, 57 | 7555FF7C242A565900829871 /* Products */, 58 | 42799AB246E5F90AF97AA0EF /* Frameworks */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | 7555FF7C242A565900829871 /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 7555FF7B242A565900829871 /* SampleApp.app */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | 7555FF7D242A565900829871 /* iosApp */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 058557BA273AAA24004C7B11 /* Assets.xcassets */, 74 | 7555FF82242A565900829871 /* ContentView.swift */, 75 | 7555FF8C242A565B00829871 /* Info.plist */, 76 | 2152FB032600AC8F00CF470E /* iOSApp.swift */, 77 | 058557D7273AAEEB004C7B11 /* Preview Content */, 78 | ); 79 | path = iosApp; 80 | sourceTree = ""; 81 | }; 82 | AB1DB47929225F7C00F7AF9C /* Configuration */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | AB3632DC29227652001CCB65 /* Config.xcconfig */, 86 | ); 87 | path = Configuration; 88 | sourceTree = ""; 89 | }; 90 | /* End PBXGroup section */ 91 | 92 | /* Begin PBXNativeTarget section */ 93 | 7555FF7A242A565900829871 /* iosApp */ = { 94 | isa = PBXNativeTarget; 95 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; 96 | buildPhases = ( 97 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */, 98 | 7555FF77242A565900829871 /* Sources */, 99 | B92378962B6B1156000C7307 /* Frameworks */, 100 | 7555FF79242A565900829871 /* Resources */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = iosApp; 107 | packageProductDependencies = ( 108 | ); 109 | productName = iosApp; 110 | productReference = 7555FF7B242A565900829871 /* SampleApp.app */; 111 | productType = "com.apple.product-type.application"; 112 | }; 113 | /* End PBXNativeTarget section */ 114 | 115 | /* Begin PBXProject section */ 116 | 7555FF73242A565900829871 /* Project object */ = { 117 | isa = PBXProject; 118 | attributes = { 119 | BuildIndependentTargetsInParallel = YES; 120 | LastSwiftUpdateCheck = 1130; 121 | LastUpgradeCheck = 1540; 122 | ORGANIZATIONNAME = orgName; 123 | TargetAttributes = { 124 | 7555FF7A242A565900829871 = { 125 | CreatedOnToolsVersion = 11.3.1; 126 | }; 127 | }; 128 | }; 129 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; 130 | compatibilityVersion = "Xcode 14.0"; 131 | developmentRegion = en; 132 | hasScannedForEncodings = 0; 133 | knownRegions = ( 134 | en, 135 | Base, 136 | ); 137 | mainGroup = 7555FF72242A565900829871; 138 | packageReferences = ( 139 | ); 140 | productRefGroup = 7555FF7C242A565900829871 /* Products */; 141 | projectDirPath = ""; 142 | projectRoot = ""; 143 | targets = ( 144 | 7555FF7A242A565900829871 /* iosApp */, 145 | ); 146 | }; 147 | /* End PBXProject section */ 148 | 149 | /* Begin PBXResourcesBuildPhase section */ 150 | 7555FF79242A565900829871 /* Resources */ = { 151 | isa = PBXResourcesBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, 155 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXResourcesBuildPhase section */ 160 | 161 | /* Begin PBXShellScriptBuildPhase section */ 162 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = { 163 | isa = PBXShellScriptBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | ); 167 | inputFileListPaths = ( 168 | ); 169 | inputPaths = ( 170 | ); 171 | name = "Compile Kotlin Framework"; 172 | outputFileListPaths = ( 173 | ); 174 | outputPaths = ( 175 | ); 176 | runOnlyForDeploymentPostprocessing = 0; 177 | shellPath = /bin/sh; 178 | shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; 179 | }; 180 | /* End PBXShellScriptBuildPhase section */ 181 | 182 | /* Begin PBXSourcesBuildPhase section */ 183 | 7555FF77242A565900829871 /* Sources */ = { 184 | isa = PBXSourcesBuildPhase; 185 | buildActionMask = 2147483647; 186 | files = ( 187 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, 188 | 7555FF83242A565900829871 /* ContentView.swift in Sources */, 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | }; 192 | /* End PBXSourcesBuildPhase section */ 193 | 194 | /* Begin XCBuildConfiguration section */ 195 | 7555FFA3242A565B00829871 /* Debug */ = { 196 | isa = XCBuildConfiguration; 197 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; 198 | buildSettings = { 199 | ALWAYS_SEARCH_USER_PATHS = NO; 200 | CLANG_ANALYZER_NONNULL = YES; 201 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 202 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 203 | CLANG_CXX_LIBRARY = "libc++"; 204 | CLANG_ENABLE_MODULES = YES; 205 | CLANG_ENABLE_OBJC_ARC = YES; 206 | CLANG_ENABLE_OBJC_WEAK = YES; 207 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 208 | CLANG_WARN_BOOL_CONVERSION = YES; 209 | CLANG_WARN_COMMA = YES; 210 | CLANG_WARN_CONSTANT_CONVERSION = YES; 211 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 212 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 213 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 214 | CLANG_WARN_EMPTY_BODY = YES; 215 | CLANG_WARN_ENUM_CONVERSION = YES; 216 | CLANG_WARN_INFINITE_RECURSION = YES; 217 | CLANG_WARN_INT_CONVERSION = YES; 218 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 219 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 220 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 221 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 222 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 223 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 224 | CLANG_WARN_STRICT_PROTOTYPES = YES; 225 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 226 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 227 | CLANG_WARN_UNREACHABLE_CODE = YES; 228 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 229 | COPY_PHASE_STRIP = NO; 230 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 231 | ENABLE_STRICT_OBJC_MSGSEND = YES; 232 | ENABLE_TESTABILITY = YES; 233 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 234 | GCC_C_LANGUAGE_STANDARD = gnu11; 235 | GCC_DYNAMIC_NO_PIC = NO; 236 | GCC_NO_COMMON_BLOCKS = YES; 237 | GCC_OPTIMIZATION_LEVEL = 0; 238 | GCC_PREPROCESSOR_DEFINITIONS = ( 239 | "DEBUG=1", 240 | "$(inherited)", 241 | ); 242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 244 | GCC_WARN_UNDECLARED_SELECTOR = YES; 245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 246 | GCC_WARN_UNUSED_FUNCTION = YES; 247 | GCC_WARN_UNUSED_VARIABLE = YES; 248 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 249 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 250 | MTL_FAST_MATH = YES; 251 | ONLY_ACTIVE_ARCH = YES; 252 | SDKROOT = iphoneos; 253 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 254 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 255 | }; 256 | name = Debug; 257 | }; 258 | 7555FFA4242A565B00829871 /* Release */ = { 259 | isa = XCBuildConfiguration; 260 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; 261 | buildSettings = { 262 | ALWAYS_SEARCH_USER_PATHS = NO; 263 | CLANG_ANALYZER_NONNULL = YES; 264 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 265 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 266 | CLANG_CXX_LIBRARY = "libc++"; 267 | CLANG_ENABLE_MODULES = YES; 268 | CLANG_ENABLE_OBJC_ARC = YES; 269 | CLANG_ENABLE_OBJC_WEAK = YES; 270 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 271 | CLANG_WARN_BOOL_CONVERSION = YES; 272 | CLANG_WARN_COMMA = YES; 273 | CLANG_WARN_CONSTANT_CONVERSION = YES; 274 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 275 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 276 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 277 | CLANG_WARN_EMPTY_BODY = YES; 278 | CLANG_WARN_ENUM_CONVERSION = YES; 279 | CLANG_WARN_INFINITE_RECURSION = YES; 280 | CLANG_WARN_INT_CONVERSION = YES; 281 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 282 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 283 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 284 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 285 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 286 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 287 | CLANG_WARN_STRICT_PROTOTYPES = YES; 288 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 289 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 290 | CLANG_WARN_UNREACHABLE_CODE = YES; 291 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 292 | COPY_PHASE_STRIP = NO; 293 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 294 | ENABLE_NS_ASSERTIONS = NO; 295 | ENABLE_STRICT_OBJC_MSGSEND = YES; 296 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 297 | GCC_C_LANGUAGE_STANDARD = gnu11; 298 | GCC_NO_COMMON_BLOCKS = YES; 299 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 300 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 301 | GCC_WARN_UNDECLARED_SELECTOR = YES; 302 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 303 | GCC_WARN_UNUSED_FUNCTION = YES; 304 | GCC_WARN_UNUSED_VARIABLE = YES; 305 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 306 | MTL_ENABLE_DEBUG_INFO = NO; 307 | MTL_FAST_MATH = YES; 308 | SDKROOT = iphoneos; 309 | SWIFT_COMPILATION_MODE = wholemodule; 310 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 311 | VALIDATE_PRODUCT = YES; 312 | }; 313 | name = Release; 314 | }; 315 | 7555FFA6242A565B00829871 /* Debug */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 319 | CODE_SIGN_IDENTITY = "Apple Development"; 320 | CODE_SIGN_STYLE = Automatic; 321 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 322 | DEVELOPMENT_TEAM = "${TEAM_ID}"; 323 | ENABLE_PREVIEWS = YES; 324 | FRAMEWORK_SEARCH_PATHS = ( 325 | "$(inherited)", 326 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 327 | ); 328 | INFOPLIST_FILE = iosApp/Info.plist; 329 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 330 | LD_RUNPATH_SEARCH_PATHS = ( 331 | "$(inherited)", 332 | "@executable_path/Frameworks", 333 | ); 334 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; 335 | PRODUCT_NAME = "${APP_NAME}"; 336 | PROVISIONING_PROFILE_SPECIFIER = ""; 337 | SWIFT_VERSION = 5.0; 338 | TARGETED_DEVICE_FAMILY = "1,2"; 339 | }; 340 | name = Debug; 341 | }; 342 | 7555FFA7242A565B00829871 /* Release */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 346 | CODE_SIGN_IDENTITY = "Apple Development"; 347 | CODE_SIGN_STYLE = Automatic; 348 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 349 | DEVELOPMENT_TEAM = "${TEAM_ID}"; 350 | ENABLE_PREVIEWS = YES; 351 | FRAMEWORK_SEARCH_PATHS = ( 352 | "$(inherited)", 353 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 354 | ); 355 | INFOPLIST_FILE = iosApp/Info.plist; 356 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 357 | LD_RUNPATH_SEARCH_PATHS = ( 358 | "$(inherited)", 359 | "@executable_path/Frameworks", 360 | ); 361 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; 362 | PRODUCT_NAME = "${APP_NAME}"; 363 | PROVISIONING_PROFILE_SPECIFIER = ""; 364 | SWIFT_VERSION = 5.0; 365 | TARGETED_DEVICE_FAMILY = "1,2"; 366 | }; 367 | name = Release; 368 | }; 369 | /* End XCBuildConfiguration section */ 370 | 371 | /* Begin XCConfigurationList section */ 372 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { 373 | isa = XCConfigurationList; 374 | buildConfigurations = ( 375 | 7555FFA3242A565B00829871 /* Debug */, 376 | 7555FFA4242A565B00829871 /* Release */, 377 | ); 378 | defaultConfigurationIsVisible = 0; 379 | defaultConfigurationName = Release; 380 | }; 381 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 382 | isa = XCConfigurationList; 383 | buildConfigurations = ( 384 | 7555FFA6242A565B00829871 /* Debug */, 385 | 7555FFA7242A565B00829871 /* Release */, 386 | ); 387 | defaultConfigurationIsVisible = 0; 388 | defaultConfigurationName = Release; 389 | }; 390 | /* End XCConfigurationList section */ 391 | }; 392 | rootObject = 7555FF73242A565900829871 /* Project object */; 393 | } -------------------------------------------------------------------------------- /sampleApp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": [ 3 | { 4 | "idiom": "universal" 5 | } 6 | ], 7 | "info": { 8 | "author": "xcode", 9 | "version": 1 10 | } 11 | } -------------------------------------------------------------------------------- /sampleApp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "app-icon-1024.png", 5 | "idiom": "universal", 6 | "platform": "ios", 7 | "size": "1024x1024" 8 | } 9 | ], 10 | "info": { 11 | "author": "xcode", 12 | "version": 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sampleApp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiamondEdge1/KmLogging/527cc0462814c7dd3a22ecfbe0e61b9d9bf8cff4/sampleApp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /sampleApp/iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "author": "xcode", 4 | "version": 1 5 | } 6 | } -------------------------------------------------------------------------------- /sampleApp/iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sampleApp/iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | CADisableMinimumFrameDurationOnPhone 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /sampleApp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "author": "xcode", 4 | "version": 1 5 | } 6 | } -------------------------------------------------------------------------------- /sampleApp/iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "KmLogging" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | pluginManagement { 5 | repositories { 6 | google { 7 | mavenContent { 8 | includeGroupAndSubgroups("androidx") 9 | includeGroupAndSubgroups("com.android") 10 | includeGroupAndSubgroups("com.google") 11 | } 12 | } 13 | mavenCentral() 14 | gradlePluginPortal() 15 | } 16 | } 17 | 18 | dependencyResolutionManagement { 19 | repositories { 20 | google { 21 | mavenContent { 22 | includeGroupAndSubgroups("androidx") 23 | includeGroupAndSubgroups("com.android") 24 | includeGroupAndSubgroups("com.google") 25 | } 26 | } 27 | mavenCentral() 28 | mavenLocal() 29 | } 30 | } 31 | 32 | include(":logging") 33 | include(":sampleApp:composeApp") 34 | --------------------------------------------------------------------------------