├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.MD ├── build.sbt ├── gpg.sh ├── project ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── com │ └── github │ └── mlangc │ └── slf4zio │ └── api │ ├── LogMessage.scala │ ├── LogSpec.scala │ ├── LoggingSupport.scala │ ├── MDZIO.scala │ └── package.scala └── test ├── resources └── logback-test.xml └── scala └── com └── github └── mlangc └── slf4zio ├── LogbackInitializationTimeout.scala ├── LogbackTestAppender.scala ├── LogbackTestUtils.scala ├── LoggingStrategiesBenchmark.scala ├── ReadmeExamplesTest.scala └── api ├── LoggingServiceTest.scala ├── LoggingSupportTest.scala ├── MDZIOTest.scala └── RawPerfLogTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .sbtopts 3 | project/.sbt 4 | .idea 5 | .bloop/ 6 | .vscode/ 7 | metals.sbt 8 | .metals/ 9 | .bsp/ 10 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.5.9 2 | maxColumn = 120 3 | runner.dialect = scala213 4 | align.preset = none 5 | 6 | rewrite.rules = [Imports] 7 | rewrite.imports.expand = true 8 | rewrite.imports.sort = original 9 | 10 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | # SLF4ZIO 2 | *Integrates SLF4J with ZIO in a simple manner.* 3 | 4 | [![Latest Release](https://img.shields.io/maven-central/v/com.github.mlangc/slf4zio_2.13?color=green&label=latest-release)](https://oss.sonatype.org/content/repositories/releases/com/github/mlangc/slf4zio_2.13) 5 | [![Latest Snapshot](https://img.shields.io/nexus/s/com.github.mlangc/slf4zio_2.13?label=latest-snapshot&server=https%3A%2F%2Foss.sonatype.org)](https://oss.sonatype.org/content/repositories/snapshots/com/github/mlangc/slf4zio_2.13/) 6 | 7 | ## When to Use 8 | If your code is based on [ZIO](https://zio.dev/) and you want to log with [SLF4J](https://www.slf4j.org/) without additional abstractions getting 9 | in your way. 10 | 11 | ## How to Use 12 | The library supports three different coding styles, that you can mix and match according to your needs. They are listed here in the order of 13 | my personal preference: 14 | 15 | ### 1. Using the LoggingSupport Convenience Trait 16 | 17 | ````scala 18 | import com.github.mlangc.slf4zio.api._ 19 | import zio._ 20 | 21 | object SomeObject extends LoggingSupport { 22 | def doStuff: Task[Unit] = { 23 | for { 24 | _ <- logger.warnIO("What the heck") 25 | _ <- ZIO.ifZIO(Random.nextBoolean)( 26 | logger.infoIO("Uff, that was close"), 27 | logger.errorIO("Game over", new IllegalStateException("This is the end")) 28 | ) 29 | 30 | _ <- ZIO.attempt { 31 | // logger is just a plain SLF4J logger; you can therefore use it from 32 | // effectful code directly: 33 | logger.trace("Wink wink nudge nudge") 34 | } 35 | 36 | _ <- ZIO 37 | .sleep(8.millis) 38 | .as(23) 39 | .perfLog( 40 | // See below for more examples with `LogSpec` 41 | LogSpec 42 | .onSucceed[Int]((d, i) => debug"Finally done with $i after ${d.render}") 43 | .withThreshold(5.millis) 44 | ) 45 | } yield () 46 | } 47 | } 48 | ```` 49 | 50 | #### Side Note 51 | Note that the `logger` field in the `LoggingSupport` trait is lazy. Since the implicit class 52 | that implements the various `**IO` methods, like `debugIO`, `infoIO` and so forth, wraps the 53 | logger using a [by-name parameter](https://docs.scala-lang.org/tour/by-name-parameters.html), 54 | logger initialization won't happen before `unsafeRun`, if you don't access the logger directly 55 | from non effectful code. To ensure referential transparency for creating an object of a class that 56 | inherits the `LoggingSupport` trait even with outright broken or strange logger implementations, 57 | you have wrap the creation of the object in an effect of its own. It might make more sense to use 58 | another logger implementation though. For practical purposes, I would consider obtaining a 59 | logger to be a pure operation as soon as the logging framework has finished its initialization, 60 | and not care too much about this subtlety. 61 | 62 | ### 2. Creating Loggers as Needed 63 | 64 | ```scala 65 | import com.github.mlangc.slf4zio.api._ 66 | import zio._ 67 | 68 | val effect: Task[Unit] = { 69 | // ... 70 | class SomeClass 71 | // ... 72 | for { 73 | logger <- makeLogger[SomeClass] 74 | _ <- logger.debugIO("Debug me tender") 75 | // ... 76 | _ <- ZIO.attempt { 77 | // Note that makeLogger just returns a plain SLF4J logger; you can therefore use it from 78 | // effectful code directly: 79 | logger.info("Don't be shy") 80 | // ... 81 | logger.warn("Please take me home") 82 | // ... 83 | } 84 | // ... 85 | _ <- logger.perfLogZIO(ZIO.sleep(10.millis))( 86 | // See below for more examples with `LogSpec` 87 | LogSpec.onSucceed(d => info"Feeling relaxed after sleeping ${d.render}") 88 | ) 89 | } yield () 90 | } 91 | ``` 92 | 93 | ### 3. Using the Logging Service 94 | 95 | ```scala 96 | import com.github.mlangc.slf4zio.api._ 97 | import zio._ 98 | 99 | val effect: RIO[Logging, Unit] = 100 | for { 101 | _ <- logging.warnIO("Surprise, surprise") 102 | plainLogger <- logging.logger 103 | _ <- ZIO.attempt { 104 | plainLogger.debug("Shhh...") 105 | plainLogger.warn("The devil always comes in disguise") 106 | } 107 | _ <- logging.traceIO("...") 108 | getNumber = ZIO.succeed(42) 109 | // See below for more examples with `LogSpec` 110 | _ <- getNumber.perfLogZ(LogSpec.onSucceed(d => debug"Got number after ${d.render}")) 111 | } yield () 112 | ``` 113 | 114 | ### Using Markers 115 | 116 | ```scala 117 | import com.github.mlangc.slf4zio.api._ 118 | import zio._ 119 | 120 | val effect: RIO[Logging, Unit] = 121 | for { 122 | marker <- getMarker("[MARKER]") 123 | _ <- logging.infoIO(marker, "Here we are") 124 | logger <- logging.logger 125 | _ <- logger.debugIO(marker, "Wat?") 126 | _ <- ZIO.attempt { 127 | logger.warn(marker, "Don't worry") 128 | } 129 | } yield () 130 | ``` 131 | 132 | ### Performance Logging 133 | Apart from providing ZIO aware wrappers for [SLF4J](https://www.slf4j.org/), the library might also 134 | help you with performance related logging. The examples from above are meant to give you the overall 135 | idea. Here is another snippet, that is meant to illustrate how to build complex `LogSpec`s from simple 136 | ones, utilizing the underlying monoidial structure: 137 | 138 | ```scala 139 | import com.github.mlangc.slf4zio.api._ 140 | import zio._ 141 | 142 | // Simple specs can be combined using the `++` to obtain more complex specs 143 | val logSpec1: LogSpec[Throwable, Int] = 144 | LogSpec.onSucceed[Int]((d, a) => info"Succeeded after ${d.render} with $a") ++ 145 | LogSpec.onError[Throwable]((d, th) => error"Failed after ${d.render} with $th") ++ 146 | LogSpec.onTermination((d, c) => error"Fatal failure after ${d.render}: ${c.prettyPrint}") 147 | 148 | // A threshold can be applied to a LogSpec. Nothing will be logged, unless the threshold is exceeded. 149 | val logSpec2: LogSpec[Any, Any] = 150 | LogSpec 151 | .onSucceed(d => warn"Operation took ${d.render}") 152 | .withThreshold(1.milli) 153 | 154 | // Will behave like logSpec1 and eventually log a warning as specified in logSpec2 155 | val logSpec3: LogSpec[Throwable, Int] = logSpec1 ++ logSpec2 156 | 157 | val effect: ZIO[Logging, Nothing, Unit] = for { 158 | _ <- ZIO.sleep(5.micros).perfLogZ(LogSpec.onSucceed(d => debug"Done after ${d.render}")) 159 | _ <- ZIO.sleep(1.milli).as(42).perfLogZ(logSpec1) 160 | _ <- ZIO.sleep(2.milli).perfLogZ(logSpec2) 161 | _ <- ZIO.sleep(3.milli).as(23).perfLogZ(logSpec3) 162 | } yield () 163 | ``` 164 | 165 | ### Using MDC convenience APIs 166 | SLF4ZIO also ships with a set of convenience APIs for `org.slf4j.MDC`. Note however, that traditional 167 | MDC implementations are based on thread local data, which doesn't work at all with ZIO, where a 168 | single `zio.Fiber` might run on different threads during its lifetime, and a single thread might 169 | accommodate multiple fibers. **If you want to use MDC logging in your ZIO based application, it is 170 | critical to use a fiber aware MDC implementation, as provided for example by 171 | [zio-interop-log4j2](https://github.com/mlangc/zio-interop-log4j2). `MDZIO` is just a collection of 172 | convenience APIs for interacting with `org.slf4j.MDC` that doesn't add any functionality of its own.** 173 | 174 | ## Alternatives 175 | If you want to track logging effects using the [ZIO Environment](http://degoes.net/articles/zio-environment) exclusively, consider using 176 | [zio-logging](https://github.com/zio/zio-logging). If you are into Tagless Final, 177 | take a look at [log4cats](https://github.com/ChristopherDavenport/log4cats). 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "slf4zio" 2 | 3 | organization := "com.github.mlangc" 4 | 5 | scalaVersion := "2.13.8" 6 | 7 | crossScalaVersions := Seq("2.12.16", "2.11.12", "2.13.8") 8 | 9 | ThisBuild / dynverSonatypeSnapshots := true 10 | 11 | // See https://tpolecat.github.io/2017/04/25/scalac-flags.html 12 | val scala212Opts = Seq( 13 | "-deprecation", // Emit warning and location for usages of deprecated APIs. 14 | "-encoding", 15 | "utf-8", // Specify character encoding used by source files. 16 | "-explaintypes", // Explain type errors in more detail. 17 | "-feature", // Emit warning and location for usages of features that should be imported explicitly. 18 | "-language:existentials", // Existential types (besides wildcard types) can be written and inferred 19 | "-language:experimental.macros", // Allow macro definition (besides implementation and application) 20 | "-language:higherKinds", // Allow higher-kinded types 21 | "-language:implicitConversions", // Allow definition of implicit functions called views 22 | "-unchecked", // Enable additional warnings where generated code depends on assumptions. 23 | "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access. 24 | "-Xfatal-warnings", // Fail the compilation if there are any warnings. 25 | "-Xfuture", // Turn on future language features. 26 | "-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver. 27 | "-Xlint:by-name-right-associative", // By-name parameter of right associative operator. 28 | "-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error. 29 | "-Xlint:delayedinit-select", // Selecting member of DelayedInit. 30 | "-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element. 31 | "-Xlint:inaccessible", // Warn about inaccessible types in method signatures. 32 | "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id. 33 | "-Xlint:nullary-unit", // Warn when nullary methods return Unit. 34 | "-Xlint:option-implicit", // Option.apply used implicit view. 35 | "-Xlint:package-object-classes", // Class or object defined in package object. 36 | "-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds. 37 | "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. 38 | "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component. 39 | "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope. 40 | "-Xlint:unsound-match", // Pattern match may not be typesafe. 41 | "-Yno-adapted-args", // Do not adapt an argument list (either by inserting () or creating a tuple) to match the receiver. 42 | "-Ypartial-unification", // Enable partial unification in type constructor inference 43 | "-Ywarn-dead-code", // Warn when dead code is identified. 44 | "-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined. 45 | "-Ywarn-inaccessible", // Warn about inaccessible types in method signatures. 46 | "-Ywarn-nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'. 47 | "-Ywarn-nullary-unit", // Warn when nullary methods return Unit. 48 | "-Ywarn-numeric-widen", // Warn when numerics are widened. 49 | "-Ywarn-unused:implicits", // Warn if an implicit parameter is unused. 50 | "-Ywarn-unused:imports", // Warn if an import selector is not referenced. 51 | "-Ywarn-unused:locals", // Warn if a local definition is unused. 52 | "-Ywarn-unused:params", // Warn if a value parameter is unused. 53 | "-Ywarn-unused:patvars", // Warn if a variable bound in a pattern is unused. 54 | "-Ywarn-unused:privates", // Warn if a private member is unused. 55 | "-Ywarn-value-discard" // Warn when non-Unit expression results are unused. 56 | ) 57 | 58 | // See https://nathankleyn.com/2019/05/13/recommended-scalac-flags-for-2-13/ 59 | val scala213Opts = Seq( 60 | "-deprecation", // Emit warning and location for usages of deprecated APIs. 61 | "-explaintypes", // Explain type errors in more detail. 62 | "-feature", // Emit warning and location for usages of features that should be imported explicitly. 63 | "-language:existentials", // Existential types (besides wildcard types) can be written and inferred 64 | "-language:experimental.macros", // Allow macro definition (besides implementation and application) 65 | "-language:higherKinds", // Allow higher-kinded types 66 | "-language:implicitConversions", // Allow definition of implicit functions called views 67 | "-unchecked", // Enable additional warnings where generated code depends on assumptions. 68 | "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access. 69 | "-Xfatal-warnings", // Fail the compilation if there are any warnings. 70 | "-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver. 71 | "-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error. 72 | "-Xlint:delayedinit-select", // Selecting member of DelayedInit. 73 | "-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element. 74 | "-Xlint:inaccessible", // Warn about inaccessible types in method signatures. 75 | "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id. 76 | "-Xlint:nullary-unit", // Warn when nullary methods return Unit. 77 | "-Xlint:option-implicit", // Option.apply used implicit view. 78 | "-Xlint:package-object-classes", // Class or object defined in package object. 79 | "-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds. 80 | "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. 81 | "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component. 82 | "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope. 83 | "-Ywarn-dead-code", // Warn when dead code is identified. 84 | "-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined. 85 | "-Ywarn-numeric-widen", // Warn when numerics are widened. 86 | "-Ywarn-unused:implicits", // Warn if an implicit parameter is unused. 87 | "-Ywarn-unused:imports", // Warn if an import selector is not referenced. 88 | "-Ywarn-unused:locals", // Warn if a local definition is unused. 89 | "-Ywarn-unused:params", // Warn if a value parameter is unused. 90 | "-Ywarn-unused:patvars", // Warn if a variable bound in a pattern is unused. 91 | "-Ywarn-unused:privates", // Warn if a private member is unused. 92 | "-Ywarn-value-discard", // Warn when non-Unit expression results are unused. 93 | "-Ybackend-parallelism", 94 | "8", // Enable paralellisation — change to desired number! 95 | "-Ycache-plugin-class-loader:last-modified", // Enables caching of classloaders for compiler plugins 96 | "-Ycache-macro-class-loader:last-modified" // and macro definitions. This can lead to performance improvements. 97 | ) 98 | 99 | val scala211Opts = Seq( 100 | "-deprecation", 101 | "-encoding", 102 | "UTF-8", 103 | "-feature", 104 | "-language:existentials", 105 | "-language:higherKinds", 106 | "-language:implicitConversions", 107 | "-unchecked", 108 | "-Xfatal-warnings", 109 | "-Xlint", 110 | "-Yno-adapted-args", 111 | "-Ywarn-dead-code", 112 | "-Ywarn-numeric-widen", 113 | "-Ywarn-value-discard", 114 | "-Xfuture", 115 | "-Ywarn-unused-import" 116 | ) 117 | 118 | scalacOptions ++= { 119 | if (scalaVersion.value.startsWith("2.12.")) scala212Opts 120 | else if (scalaVersion.value.startsWith("2.11.")) scala211Opts 121 | else scala213Opts 122 | } 123 | 124 | Compile / console / scalacOptions --= Seq("-Ywarn-unused:imports", "-Xfatal-warnings") 125 | 126 | val silencerVersion = "1.7.9" 127 | libraryDependencies ++= Seq( 128 | compilerPlugin("com.github.ghik" % "silencer-plugin" % silencerVersion cross CrossVersion.full), 129 | "com.github.ghik" % "silencer-lib" % silencerVersion % Provided cross CrossVersion.full 130 | ) 131 | 132 | libraryDependencies += "org.slf4j" % "slf4j-api" % "1.7.30" 133 | 134 | resolvers += 135 | "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots" 136 | 137 | val zioVersion = "2.0.0" 138 | libraryDependencies += "dev.zio" %% "zio" % zioVersion 139 | libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.4.0" % Test 140 | libraryDependencies += "com.storm-enroute" %% "scalameter" % "0.19" % Test 141 | 142 | libraryDependencies ++= Seq( 143 | "dev.zio" %% "zio-test" % zioVersion % "test", 144 | "dev.zio" %% "zio-test-sbt" % zioVersion % "test" 145 | ) 146 | 147 | testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")) 148 | 149 | publishMavenStyle := true 150 | publishTo := sonatypePublishToBundle.value 151 | 152 | // License of your choice 153 | licenses := Seq("APL2" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt")) 154 | 155 | // Where is the source code hosted 156 | import xerial.sbt.Sonatype._ 157 | sonatypeProjectHosting := Some(GitHubHosting("mlangc", "slf4zio", "m.langer798@gmail.com")) 158 | 159 | developers := List( 160 | Developer( 161 | id = "mlangc", 162 | name = "Matthias Langer", 163 | email = "m.langer798@gmail.com", 164 | url = url("https://mlangc.wordpress.com/") 165 | ) 166 | ) 167 | 168 | Global / PgpKeys.gpgCommand := (baseDirectory.value / "gpg.sh").getAbsolutePath 169 | -------------------------------------------------------------------------------- /gpg.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | gpg --pinentry-mode loopback $@ -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.7.1 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.13") 2 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") 3 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") 4 | addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1") 5 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mlangc/slf4zio/api/LogMessage.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio.api 2 | 3 | import org.slf4j.event.Level 4 | 5 | case class LogMessage(text: String, level: Level, suppressed: Boolean = false) 6 | 7 | object LogMessage { 8 | val Suppressed = LogMessage("", Level.TRACE, true) 9 | 10 | def trace(text: String) = LogMessage(text, Level.TRACE) 11 | def debug(text: String) = LogMessage(text, Level.DEBUG) 12 | def info(text: String) = LogMessage(text, Level.INFO) 13 | def warn(text: String) = LogMessage(text, Level.WARN) 14 | def error(text: String) = LogMessage(text, Level.ERROR) 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mlangc/slf4zio/api/LogSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio.api 2 | 3 | import zio.duration2DurationOps 4 | import zio.Cause 5 | import zio.Duration 6 | 7 | case class LogSpec[-E, -A]( 8 | onError: List[(Duration, E) => LogMessage] = Nil, 9 | onSucceed: List[(Duration, A) => LogMessage] = Nil, 10 | onTermination: List[(Duration, Cause[Nothing]) => LogMessage] = Nil 11 | ) { 12 | 13 | def combine[E2 <: E, A2 <: A](other: LogSpec[E2, A2]): LogSpec[E2, A2] = 14 | LogSpec( 15 | onError = onError ::: other.onError, 16 | onSucceed = onSucceed ::: other.onSucceed, 17 | onTermination = onTermination ::: other.onTermination 18 | ) 19 | 20 | def ++[E2 <: E, A2 <: A](other: LogSpec[E2, A2]) = combine(other) 21 | 22 | def withThreshold(threshold: Duration): LogSpec[E, A] = { 23 | LogSpec( 24 | onError.map(f => (d: Duration, e: E) => if (d < threshold) LogMessage.Suppressed else f(d, e)), 25 | onSucceed.map(f => (d: Duration, e: A) => if (d < threshold) LogMessage.Suppressed else f(d, e)), 26 | onTermination.map(f => (d: Duration, c: Cause[Nothing]) => if (d < threshold) LogMessage.Suppressed else f(d, c)) 27 | ) 28 | } 29 | 30 | def isNoOp: Boolean = 31 | this == LogSpec.NoOp 32 | } 33 | 34 | object LogSpec { 35 | val NoOp: LogSpec[Any, Any] = LogSpec(Nil, Nil, Nil) 36 | 37 | def onSucceed(msg: Duration => LogMessage): LogSpec[Any, Any] = 38 | onSucceed((d, _) => msg(d)) 39 | 40 | def onSucceed[A](msg: (Duration, A) => LogMessage): LogSpec[Any, A] = 41 | LogSpec(onSucceed = List(msg)) 42 | 43 | def onError[E](msg: (Duration, E) => LogMessage): LogSpec[E, Any] = 44 | LogSpec(onError = List(msg)) 45 | 46 | def onError(msg: Duration => LogMessage): LogSpec[Any, Any] = 47 | onError((d, _) => msg(d)) 48 | 49 | def onTermination(msg: (Duration, Cause[Nothing]) => LogMessage): LogSpec[Any, Any] = 50 | LogSpec(onTermination = List(msg)) 51 | 52 | def onTermination(msg: Duration => LogMessage): LogSpec[Any, Any] = 53 | onTermination((d, _) => msg(d)) 54 | 55 | def onTerminationOrError[E](msg: (Duration, Cause[E]) => LogMessage): LogSpec[E, Any] = 56 | onError[E]((d, e) => msg(d, Cause.fail(e))) ++ onTermination(msg) 57 | 58 | def onTerminationOrError(msg: Duration => LogMessage): LogSpec[Any, Any] = 59 | onTerminationOrError((d, _) => msg(d)) 60 | } 61 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mlangc/slf4zio/api/LoggingSupport.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio.api 2 | 3 | import org.slf4j.Logger 4 | import zio.ZIO 5 | 6 | trait LoggingSupport { outer => 7 | @transient 8 | protected final lazy val logger: Logger = getLogger(getClass) 9 | 10 | protected implicit final class ZioLoggerOps[R, E, A](zio: ZIO[R, E, A]) { 11 | def perfLog[E1 >: E](spec: LogSpec[E1, A]): ZIO[R, E, A] = 12 | ZIO.environmentWithZIO[R] { r => 13 | val io = zio.provideEnvironment(r) 14 | io.perfLogZ(spec) 15 | .provideLayer(Logging.forLogger(logger)) 16 | } 17 | } 18 | 19 | protected final def perfLog[A](thunk: => A)(spec: LogSpec[Throwable, A]): A = 20 | logger.perfLog(thunk)(spec) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mlangc/slf4zio/api/MDZIO.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio.api 2 | 3 | import com.github.ghik.silencer.silent 4 | import org.slf4j.MDC 5 | import scala.collection.JavaConverters._ 6 | import zio.UIO 7 | import zio.ZIO 8 | 9 | /** Convenience APIs for interacting with the MDC context. 10 | * 11 | * ==Important== 12 | * Make sure that you use a fiber aware MDC implementation, as provided for example by zio-interop-log4j2. Using the convenience wrappers found 14 | * here won't make the underlying MDC context implementation aware of ZIO fibers. 15 | */ 16 | @silent("JavaConverters") 17 | abstract class MDZIO { 18 | final def put(key: String, value: String): UIO[Unit] = 19 | ZIO.succeed(MDC.put(key, value)) 20 | 21 | final def get(key: String): UIO[Option[String]] = 22 | ZIO.succeed(Option(MDC.get(key))) 23 | 24 | final def remove(key: String): UIO[Unit] = 25 | ZIO.succeed(MDC.remove(key)) 26 | 27 | final def clear(): UIO[Unit] = 28 | ZIO.succeed(MDC.clear()) 29 | 30 | final def putAll(pairs: (String, String)*): UIO[Unit] = 31 | putAll(pairs) 32 | 33 | final def putAll(pairs: Iterable[(String, String)]): UIO[Unit] = 34 | ZIO.foreachDiscard(pairs)((put _).tupled) 35 | 36 | final def removeAll(keys: Iterable[String]): UIO[Unit] = 37 | ZIO.foreachDiscard(keys)(remove) 38 | 39 | /** Puts the given key value pairs in the context, executes the given action, and restores the original context. 40 | */ 41 | final def doWith[R, E, A](pairs: Iterable[(String, String)])(zio: ZIO[R, E, A]): ZIO[R, E, A] = 42 | for { 43 | state1 <- ZIO.succeed(pairs.toMap) 44 | state0 <- getAll(state1.keys) 45 | newKeys = state1.keySet.diff(state0.keySet) 46 | a <- (putAll(state1) *> zio).ensuring(removeAll(newKeys) *> putAll(state0)) 47 | } yield a 48 | 49 | final def doWith[R, E, A](pairs: (String, String)*)(zio: ZIO[R, E, A]): ZIO[R, E, A] = 50 | doWith(pairs)(zio) 51 | 52 | final def getContextMap: UIO[Option[Map[String, String]]] = 53 | ZIO.succeed(Option(MDC.getCopyOfContextMap).map(_.asScala.toMap)) 54 | 55 | final def setContextMap(map: Map[String, String]): UIO[Unit] = 56 | ZIO.succeed(MDC.setContextMap(map.asJava)) 57 | 58 | private def getAll(keys: Iterable[String]): UIO[Map[String, String]] = 59 | ZIO.foldLeft(keys)(Map.empty[String, String]) { (acc, key) => 60 | get(key).map(_.fold(acc)(v => acc + (key -> v))) 61 | } 62 | } 63 | 64 | /** See also [[Logging.Service.mdzio]] if you want all logging related calls go through the service. 65 | */ 66 | object MDZIO extends MDZIO 67 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mlangc/slf4zio/api/package.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio 2 | 3 | import org.slf4j.event.Level 4 | import org.slf4j.Logger 5 | import org.slf4j.LoggerFactory 6 | import org.slf4j.Marker 7 | import org.slf4j.MarkerFactory 8 | import scala.reflect.ClassTag 9 | import scala.util.Failure 10 | import scala.util.Success 11 | import scala.util.Try 12 | import zio.Cause 13 | import zio.Clock 14 | import zio.Duration 15 | import zio.UIO 16 | import zio.ULayer 17 | import zio.URIO 18 | import zio.ZIO 19 | import zio.ZLayer 20 | 21 | package object api { 22 | implicit final class Slf4jLoggerOps(logger: => Logger) { 23 | def traceIO(msg: => String): UIO[Unit] = ZIO.succeed { 24 | if (logger.isTraceEnabled()) 25 | logger.trace(msg) 26 | } 27 | 28 | def debugIO(msg: => String): UIO[Unit] = ZIO.succeed { 29 | if (logger.isDebugEnabled) 30 | logger.debug(msg) 31 | } 32 | 33 | def infoIO(msg: => String): UIO[Unit] = ZIO.succeed { 34 | if (logger.isInfoEnabled()) 35 | logger.info(msg) 36 | } 37 | 38 | def warnIO(msg: => String): UIO[Unit] = ZIO.succeed { 39 | if (logger.isWarnEnabled()) 40 | logger.warn(msg) 41 | } 42 | 43 | def errorIO(msg: => String): UIO[Unit] = ZIO.succeed { 44 | if (logger.isErrorEnabled()) 45 | logger.error(msg) 46 | } 47 | 48 | def traceIO(msg: => String, th: Throwable): UIO[Unit] = ZIO.succeed { 49 | if (logger.isTraceEnabled()) 50 | logger.trace(msg, th) 51 | } 52 | 53 | def debugIO(msg: => String, th: Throwable): UIO[Unit] = ZIO.succeed { 54 | if (logger.isDebugEnabled) 55 | logger.debug(msg, th) 56 | } 57 | 58 | def infoIO(msg: => String, th: Throwable): UIO[Unit] = ZIO.succeed { 59 | if (logger.isInfoEnabled) 60 | logger.info(msg, th) 61 | } 62 | 63 | def warnIO(msg: => String, th: Throwable): UIO[Unit] = ZIO.succeed { 64 | if (logger.isWarnEnabled) 65 | logger.warn(msg, th) 66 | } 67 | 68 | def errorIO(msg: => String, th: Throwable): UIO[Unit] = ZIO.succeed { 69 | if (logger.isErrorEnabled) 70 | logger.error(msg, th) 71 | } 72 | 73 | def traceIO(marker: Marker, msg: => String): UIO[Unit] = ZIO.succeed { 74 | if (logger.isTraceEnabled(marker)) 75 | logger.trace(marker, msg) 76 | } 77 | 78 | def debugIO(marker: Marker, msg: => String): UIO[Unit] = ZIO.succeed { 79 | if (logger.isDebugEnabled(marker)) 80 | logger.debug(marker, msg) 81 | } 82 | 83 | def infoIO(marker: Marker, msg: => String): UIO[Unit] = ZIO.succeed { 84 | if (logger.isInfoEnabled(marker)) 85 | logger.info(marker, msg) 86 | } 87 | 88 | def warnIO(marker: Marker, msg: => String): UIO[Unit] = ZIO.succeed { 89 | if (logger.isWarnEnabled(marker)) 90 | logger.warn(marker, msg) 91 | } 92 | 93 | def errorIO(marker: Marker, msg: => String): UIO[Unit] = ZIO.succeed { 94 | if (logger.isErrorEnabled(marker)) 95 | logger.error(marker, msg) 96 | } 97 | 98 | def traceIO(marker: Marker, msg: => String, th: Throwable): UIO[Unit] = ZIO.succeed { 99 | if (logger.isTraceEnabled(marker)) 100 | logger.trace(marker, msg, th) 101 | } 102 | 103 | def debugIO(marker: Marker, msg: => String, th: Throwable): UIO[Unit] = ZIO.succeed { 104 | if (logger.isDebugEnabled) 105 | logger.debug(marker, msg, th) 106 | } 107 | 108 | def infoIO(marker: Marker, msg: => String, th: Throwable): UIO[Unit] = ZIO.succeed { 109 | if (logger.isInfoEnabled) 110 | logger.info(marker, msg, th) 111 | } 112 | 113 | def warnIO(marker: Marker, msg: => String, th: Throwable): UIO[Unit] = ZIO.succeed { 114 | if (logger.isWarnEnabled) 115 | logger.warn(marker, msg, th) 116 | } 117 | 118 | def errorIO(marker: Marker, msg: => String, th: Throwable): UIO[Unit] = ZIO.succeed { 119 | if (logger.isErrorEnabled) 120 | logger.error(marker, msg, th) 121 | } 122 | 123 | def logIO(msg: LogMessage): UIO[Unit] = 124 | ZIO.succeed(log(msg)) 125 | 126 | def log(msg: LogMessage): Unit = 127 | if (msg.suppressed) () 128 | else 129 | msg.level match { 130 | case Level.ERROR => logger.error(msg.text) 131 | case Level.WARN => logger.warn(msg.text) 132 | case Level.INFO => logger.info(msg.text) 133 | case Level.DEBUG => logger.debug(msg.text) 134 | case Level.TRACE => logger.trace(msg.text) 135 | } 136 | 137 | def perfLogZIO[R, E, A, E2 >: E, A2 >: A](zio: ZIO[R, E, A])(spec: LogSpec[E2, A2]): ZIO[R, E, A] = 138 | if (spec.isNoOp) zio 139 | else 140 | Clock.nanoTime.flatMap { t0 => 141 | def handleError(cause: Cause[E]): ZIO[Any, E, Nothing] = 142 | if (spec.onError.isEmpty && spec.onTermination.isEmpty) ZIO.failCause(cause) 143 | else 144 | Clock.nanoTime.flatMap { t1 => 145 | val d = Duration.fromNanos(t1 - t0) 146 | 147 | val msgs = cause.failureOrCause match { 148 | case Right(failure) => spec.onTermination.map(_(d, failure)) 149 | case Left(e) => spec.onError.map(_(d, e)) 150 | } 151 | 152 | ZIO.foreach(msgs)(m => logger.logIO(m)) *> ZIO.failCause(cause) 153 | } 154 | 155 | def handleSuccess(a: A): ZIO[Any, Nothing, A] = 156 | if (spec.onSucceed.isEmpty) ZIO.succeed(a) 157 | else { 158 | for { 159 | t1 <- Clock.nanoTime 160 | d = Duration.fromNanos(t1 - t0) 161 | msgs = spec.onSucceed.map(_(d, a)) 162 | _ <- ZIO.foreach(msgs)(m => logger.logIO(m)) 163 | } yield a 164 | } 165 | 166 | zio.foldCauseZIO(handleError, handleSuccess) 167 | } 168 | 169 | def perfLogIO[R, E, A](zio: ZIO[R, E, A])(spec: LogSpec[E, A]): ZIO[R, E, A] = 170 | ZIO.environmentWithZIO[R] { r => 171 | val io = zio.provideEnvironment(r) 172 | perfLogZIO(io)(spec).provideLayer(ZLayer.succeed(Clock.ClockLive)) 173 | } 174 | 175 | def perfLog[A](thunk: => A)(spec: LogSpec[Throwable, A]): A = 176 | if (spec.isNoOp) thunk 177 | else { 178 | val t0 = System.nanoTime() 179 | val res = Try(thunk) 180 | 181 | def onError(th: Throwable): Nothing = 182 | if (spec.onError.isEmpty) throw th 183 | else { 184 | val t1 = System.nanoTime() 185 | val d = Duration.fromNanos(t1 - t0) 186 | val msgs = spec.onError.map(_(d, th)) 187 | msgs.foreach(logger.log) 188 | throw th 189 | } 190 | 191 | def onSuccess(a: A): A = 192 | if (spec.onSucceed.isEmpty) a 193 | else { 194 | val t1 = System.nanoTime() 195 | val d = Duration.fromNanos(t1 - t0) 196 | val msgs = spec.onSucceed.map(_(d, a)) 197 | msgs.foreach(logger.log) 198 | a 199 | } 200 | 201 | res match { 202 | case Failure(e) => onError(e) 203 | case Success(a) => onSuccess(a) 204 | } 205 | } 206 | } 207 | 208 | object Logging { 209 | trait Service[-R] { 210 | final def traceIO(msg: => String): URIO[R, Unit] = 211 | withUnderlying(_.traceIO(msg)) 212 | 213 | final def debugIO(msg: => String): URIO[R, Unit] = 214 | withUnderlying(_.debugIO(msg)) 215 | 216 | final def infoIO(msg: => String): URIO[R, Unit] = 217 | withUnderlying(_.infoIO(msg)) 218 | 219 | final def warnIO(msg: => String): URIO[R, Unit] = 220 | withUnderlying(_.warnIO(msg)) 221 | 222 | final def errorIO(msg: => String): URIO[R, Unit] = 223 | withUnderlying(_.errorIO(msg)) 224 | 225 | final def traceIO(msg: => String, th: Throwable): URIO[R, Unit] = 226 | withUnderlying(_.traceIO(msg, th)) 227 | 228 | final def debugIO(msg: => String, th: Throwable): URIO[R, Unit] = 229 | withUnderlying(_.debugIO(msg, th)) 230 | 231 | final def infoIO(msg: => String, th: Throwable): URIO[R, Unit] = 232 | withUnderlying(_.infoIO(msg, th)) 233 | 234 | final def warnIO(msg: => String, th: Throwable): URIO[R, Unit] = 235 | withUnderlying(_.warnIO(msg, th)) 236 | 237 | final def errorIO(msg: => String, th: Throwable): URIO[R, Unit] = 238 | withUnderlying(_.errorIO(msg, th)) 239 | 240 | final def traceIO(marker: Marker, msg: => String): URIO[R, Unit] = 241 | withUnderlying(_.traceIO(marker, msg)) 242 | 243 | final def debugIO(marker: Marker, msg: => String): URIO[R, Unit] = 244 | withUnderlying(_.debugIO(marker, msg)) 245 | 246 | final def infoIO(marker: Marker, msg: => String): URIO[R, Unit] = 247 | withUnderlying(_.infoIO(marker, msg)) 248 | 249 | final def warnIO(marker: Marker, msg: => String): URIO[R, Unit] = 250 | withUnderlying(_.warnIO(marker, msg)) 251 | 252 | final def errorIO(marker: Marker, msg: => String): URIO[R, Unit] = 253 | withUnderlying(_.errorIO(marker, msg)) 254 | 255 | final def traceIO(marker: Marker, msg: => String, th: Throwable): URIO[R, Unit] = 256 | withUnderlying(_.traceIO(marker, msg, th)) 257 | 258 | final def debugIO(marker: Marker, msg: => String, th: Throwable): URIO[R, Unit] = 259 | withUnderlying(_.debugIO(marker, msg, th)) 260 | 261 | final def infoIO(marker: Marker, msg: => String, th: Throwable): URIO[R, Unit] = 262 | withUnderlying(_.infoIO(marker, msg, th)) 263 | 264 | final def warnIO(marker: Marker, msg: => String, th: Throwable): URIO[R, Unit] = 265 | withUnderlying(_.warnIO(marker, msg, th)) 266 | 267 | final def errorIO(marker: Marker, msg: => String, th: Throwable): URIO[R, Unit] = 268 | withUnderlying(_.errorIO(marker, msg, th)) 269 | 270 | final def logIO(msg: => LogMessage): URIO[R, Unit] = 271 | withUnderlying(_.logIO(msg)) 272 | 273 | final def mdzio: MDZIO = MDZIO 274 | 275 | def logger: URIO[R, Logger] 276 | 277 | private def withUnderlying(op: Logger => UIO[Unit]): URIO[R, Unit] = 278 | logger.flatMap(op) 279 | } 280 | 281 | def forClass(clazz: Class[_]): ULayer[Logging] = ZLayer.succeed { 282 | new Service[Any] { 283 | @transient 284 | private lazy val theLogger = getLogger(clazz) 285 | 286 | def logger: UIO[Logger] = ZIO.succeed(theLogger) 287 | } 288 | } 289 | 290 | def forLogger(getLogger: => Logger): ULayer[Logging] = ZLayer.succeed { 291 | new Service[Any] { 292 | def logger: UIO[Logger] = ZIO.succeed(getLogger) 293 | } 294 | } 295 | 296 | def global: ULayer[Logging] = forClass(Logging.getClass) 297 | 298 | val any: ZLayer[Logging, Nothing, Logging] = 299 | ZLayer.environment[Logging] 300 | } 301 | 302 | type Logging = Logging.Service[Any] 303 | 304 | val logging: Logging.Service[Logging] = new Logging.Service[Logging] { 305 | def logger: URIO[Logging, Logger] = 306 | ZIO.environmentWithZIO[Logging](_.get[Logging.Service[Any]].logger) 307 | } 308 | 309 | def getLogger[T](implicit classTag: ClassTag[T]): Logger = 310 | getLogger(classTag.runtimeClass) 311 | 312 | def getLogger(clazz: Class[_]): Logger = 313 | LoggerFactory.getLogger(clazz) 314 | 315 | def getLogger(name: String): Logger = 316 | LoggerFactory.getLogger(name) 317 | 318 | def getMarker(name: String): UIO[Marker] = 319 | ZIO.succeed(MarkerFactory.getMarker(name)) 320 | 321 | def makeLogger(name: String): UIO[Logger] = 322 | ZIO.succeed(getLogger(name)) 323 | 324 | def makeLogger[T](implicit classTag: ClassTag[T]): UIO[Logger] = 325 | ZIO.succeed(getLogger[T]) 326 | 327 | def makeLogger[T](clazz: Class[_]): UIO[Logger] = 328 | ZIO.succeed(getLogger(clazz)) 329 | 330 | implicit final class LogMessageInterpolator(val stringContext: StringContext) extends AnyVal { 331 | def trace(args: Any*): LogMessage = { 332 | LogMessage.trace(stringContext.s(args: _*)) 333 | } 334 | 335 | def debug(args: Any*): LogMessage = { 336 | LogMessage.debug(stringContext.s(args: _*)) 337 | } 338 | 339 | def info(args: Any*): LogMessage = { 340 | LogMessage.info(stringContext.s(args: _*)) 341 | } 342 | 343 | def warn(args: Any*): LogMessage = { 344 | LogMessage.warn(stringContext.s(args: _*)) 345 | } 346 | 347 | def error(args: Any*): LogMessage = { 348 | LogMessage.error(stringContext.s(args: _*)) 349 | } 350 | } 351 | 352 | implicit final class ZioLoggingOps[R, E, A](val zio: ZIO[R, E, A]) { 353 | def perfLogZ[E2 >: E, A2 >: A](spec: LogSpec[E2, A2]): ZIO[R with Logging, E, A] = 354 | ZIO 355 | .environmentWithZIO[Logging](_.get[Logging.Service[Any]].logger) 356 | .flatMap(_.perfLogZIO(zio)(spec)) 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss} %-5level %marker %logger{0} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mlangc/slf4zio/LogbackInitializationTimeout.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio 2 | 3 | import zio.duration2DurationOps 4 | import zio.Duration 5 | 6 | case class LogbackInitializationTimeout(elapsed: Duration) 7 | extends RuntimeException(s"Logback initialization timed out after ${elapsed.render}") 8 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mlangc/slf4zio/LogbackTestAppender.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent 4 | import ch.qos.logback.core.AppenderBase 5 | import java.util.concurrent.atomic.AtomicReference 6 | import java.util.function.UnaryOperator 7 | import zio.UIO 8 | import zio.ZIO 9 | 10 | class LogbackTestAppender extends AppenderBase[ILoggingEvent] { 11 | def append(eventObject: ILoggingEvent): Unit = { 12 | LogbackTestAppender.eventsRef.updateAndGet(new UnaryOperator[List[ILoggingEvent]] { 13 | def apply(evts: List[ILoggingEvent]): List[ILoggingEvent] = eventObject :: evts 14 | }) 15 | 16 | () 17 | } 18 | } 19 | 20 | object LogbackTestAppender { 21 | private val eventsRef = new AtomicReference(List.empty[ILoggingEvent]) 22 | 23 | def events: UIO[List[ILoggingEvent]] = 24 | ZIO.succeed(eventsRef.get()) 25 | 26 | def eventsFor(clazz: Class[_]): UIO[List[ILoggingEvent]] = 27 | events.map(_.filter(_.getLoggerName == clazz.getCanonicalName)) 28 | } 29 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mlangc/slf4zio/LogbackTestUtils.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio 2 | 3 | import ch.qos.logback.classic.LoggerContext 4 | import org.slf4j.LoggerFactory 5 | import zio.durationInt 6 | import zio.Duration 7 | import zio.IO 8 | import zio.Schedule 9 | import zio.UIO 10 | import zio.ZIO 11 | 12 | object LogbackTestUtils { 13 | def waitForLogbackInitialization: IO[LogbackInitializationTimeout, Unit] = { 14 | val schedule: Schedule[Any, Boolean, (Boolean, Duration)] = 15 | (Schedule.recurUntil[Boolean](identity) <* (Schedule.spaced(1.milli) <* Schedule.recurs(500))) && Schedule.elapsed 16 | 17 | logbackInitialized.repeat(schedule).flatMap { 18 | case (true, _) => ZIO.unit 19 | case (false, elapsed) => ZIO.fail(LogbackInitializationTimeout(elapsed)) 20 | } 21 | } 22 | 23 | def logbackInitialized: UIO[Boolean] = ZIO.succeed { 24 | LoggerFactory.getILoggerFactory.isInstanceOf[LoggerContext] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mlangc/slf4zio/LoggingStrategiesBenchmark.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio 2 | 3 | import org.scalameter.picklers.noPickler._ 4 | import org.scalameter.Bench 5 | import org.scalameter.Gen 6 | import org.slf4j.event.Level 7 | import org.slf4j.Logger 8 | import org.slf4j.LoggerFactory 9 | import zio.Runtime 10 | import zio.UIO 11 | import zio.Unsafe 12 | import zio.ZIO 13 | 14 | object LoggingStrategiesBenchmark extends Bench.LocalTime { 15 | private implicit class LoggerOpsAnyVal(val logger: Logger) extends AnyVal { 16 | def debugAnyValIO(msg: => String): UIO[Unit] = ZIO.succeed { 17 | if (logger.isDebugEnabled) 18 | logger.debug(msg) 19 | } 20 | } 21 | 22 | private implicit class LoggerOpsLazy(logger: => Logger) { 23 | def debugLazyIO(msg: => String): UIO[Unit] = ZIO.succeed { 24 | if (logger.isDebugEnabled) 25 | logger.debug(msg) 26 | } 27 | } 28 | 29 | private val nLogs = Gen.enumeration("nlogs")(1, 100) 30 | 31 | private lazy val debugLogger = getLogger(Level.DEBUG) 32 | private lazy val infoLogger = getLogger(Level.INFO) 33 | 34 | private def logStr = "--benchmark me--" 35 | 36 | performance of "debugLogger" in { 37 | measure method "debug" in { 38 | using(nLogs) in nTimes(debugLogger.debug(logStr)) 39 | } 40 | 41 | measure method "debugAnyValIO" in { 42 | using(nLogs) in runNtimesIO(debugLogger.debugAnyValIO(logStr)) 43 | } 44 | 45 | measure method "debugLazyIO" in { 46 | using(nLogs) in runNtimesIO(debugLogger.debugLazyIO(logStr)) 47 | } 48 | } 49 | 50 | performance of "infoLogger" in { 51 | measure method "debug" in { 52 | using(nLogs) in nTimes(infoLogger.debug(logStr)) 53 | } 54 | 55 | measure method "debugAnyValIO" in { 56 | using(nLogs) in runNtimesIO(infoLogger.debugAnyValIO(logStr)) 57 | } 58 | 59 | measure method "debugLazyIO" in { 60 | using(nLogs) in runNtimesIO(infoLogger.debugLazyIO(logStr)) 61 | } 62 | } 63 | 64 | private def nTimes(block: => Unit)(n: Int): Unit = { 65 | 1.to(n).foreach(_ => block) 66 | } 67 | 68 | private def getLogger(level: Level): Logger = 69 | LoggerFactory.getLogger(getClass.getCanonicalName + "." + level.toString.toLowerCase) 70 | 71 | private def runNtimesIO(io: UIO[Unit])(n: Int): Unit = 72 | Unsafe.unsafe(implicit u => Runtime.default.unsafe.run(nTimesIO(n)(io)).getOrThrowFiberFailure()) 73 | 74 | private def nTimesIO[R, E](n: Int)(io: ZIO[R, E, Unit]): ZIO[R, E, Unit] = n match { 75 | case 1 => io 76 | case n if n > 1 => io *> nTimesIO(n - 1)(io) 77 | case _ => ZIO.unit 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mlangc/slf4zio/ReadmeExamplesTest.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio 2 | 3 | import com.github.mlangc.slf4zio.api.Logging 4 | import zio.test._ 5 | import zio.test.Assertion._ 6 | 7 | object ReadmeExamplesTest extends ZIOSpecDefault { 8 | def spec = suite("ReadmeExamplesTest")( 9 | test("creating loggers as needed") { 10 | import com.github.mlangc.slf4zio.api._ 11 | import zio._ 12 | 13 | val effect: Task[Unit] = { 14 | // ... 15 | class SomeClass 16 | // ... 17 | for { 18 | logger <- makeLogger[SomeClass] 19 | _ <- logger.debugIO("Debug me tender") 20 | // ... 21 | _ <- ZIO.attempt { 22 | // Note that makeLogger just returns a plain SLF4J logger; you can therefore use it from 23 | // effectful code directly: 24 | logger.info("Don't be shy") 25 | // ... 26 | logger.warn("Please take me home") 27 | // ... 28 | } 29 | // ... 30 | _ <- logger.perfLogZIO(ZIO.sleep(10.millis))( 31 | // See below for more examples with `LogSpec` 32 | LogSpec.onSucceed(d => info"Feeling relaxed after sleeping ${d.render}") 33 | ) 34 | } yield () 35 | } 36 | 37 | assertZIO(effect)(isUnit) 38 | }, 39 | test("Using the convenience trait") { 40 | import com.github.mlangc.slf4zio.api._ 41 | import zio._ 42 | 43 | object SomeObject extends LoggingSupport { 44 | def doStuff: Task[Unit] = { 45 | for { 46 | _ <- logger.warnIO("What the heck") 47 | _ <- ZIO.ifZIO(Random.nextBoolean)( 48 | logger.infoIO("Uff, that was close"), 49 | logger.errorIO("Game over", new IllegalStateException("This is the end")) 50 | ) 51 | 52 | _ <- ZIO.attempt { 53 | // logger is just a plain SLF4J logger; you can therefore use it from 54 | // effectful code directly: 55 | logger.trace("Wink wink nudge nudge") 56 | } 57 | 58 | _ <- ZIO 59 | .sleep(8.millis) 60 | .as(23) 61 | .perfLog( 62 | // See below for more examples with `LogSpec` 63 | LogSpec 64 | .onSucceed[Int]((d, i) => debug"Finally done with $i after ${d.render}") 65 | .withThreshold(5.millis) 66 | ) 67 | } yield () 68 | } 69 | } 70 | 71 | assertZIO(SomeObject.doStuff.ignore)(isUnit) 72 | }, 73 | test("Using the service") { 74 | import com.github.mlangc.slf4zio.api._ 75 | import zio._ 76 | 77 | val effect: RIO[Logging, Unit] = 78 | for { 79 | _ <- logging.warnIO("Surprise, surprise") 80 | plainLogger <- logging.logger 81 | _ <- ZIO.attempt { 82 | plainLogger.debug("Shhh...") 83 | plainLogger.warn("The devil always comes in disguise") 84 | } 85 | _ <- logging.traceIO("...") 86 | getNumber = ZIO.succeed(42) 87 | // See below for more examples with `LogSpec` 88 | _ <- getNumber.perfLogZ(LogSpec.onSucceed(d => debug"Got number after ${d.render}")) 89 | } yield () 90 | 91 | assertZIO(effect)(isUnit) 92 | }, 93 | test("Performance Logging - Using the Logging Service") { 94 | import com.github.mlangc.slf4zio.api._ 95 | import zio._ 96 | 97 | // Simple specs can be combined using the `++` to obtain more complex specs 98 | val logSpec1: LogSpec[Throwable, Int] = 99 | LogSpec.onSucceed[Int]((d, a) => info"Succeeded after ${d.render} with $a") ++ 100 | LogSpec.onError[Throwable]((d, th) => error"Failed after ${d.render} with $th") ++ 101 | LogSpec.onTermination((d, c) => error"Fatal failure after ${d.render}: ${c.prettyPrint}") 102 | 103 | // A threshold can be applied to a LogSpec. Nothing will be logged, unless the threshold is exceeded. 104 | val logSpec2: LogSpec[Any, Any] = 105 | LogSpec 106 | .onSucceed(d => warn"Operation took ${d.render}") 107 | .withThreshold(1.milli) 108 | 109 | // Will behave like logSpec1 and eventually log a warning as specified in logSpec2 110 | val logSpec3: LogSpec[Throwable, Int] = logSpec1 ++ logSpec2 111 | 112 | val effect: ZIO[Logging, Nothing, Unit] = for { 113 | _ <- ZIO.sleep(5.micros).perfLogZ(LogSpec.onSucceed(d => debug"Done after ${d.render}")) 114 | _ <- ZIO.sleep(1.milli).as(42).perfLogZ(logSpec1) 115 | _ <- ZIO.sleep(2.milli).perfLogZ(logSpec2) 116 | _ <- ZIO.sleep(3.milli).as(23).perfLogZ(logSpec3) 117 | } yield () 118 | 119 | assertZIO(effect)(isUnit) 120 | }, 121 | test("Working with Markers") { 122 | import com.github.mlangc.slf4zio.api._ 123 | import zio._ 124 | 125 | val effect: RIO[Logging, Unit] = 126 | for { 127 | marker <- getMarker("[MARKER]") 128 | _ <- logging.infoIO(marker, "Here we are") 129 | logger <- logging.logger 130 | _ <- logger.debugIO(marker, "Wat?") 131 | _ <- ZIO.attempt { 132 | logger.warn(marker, "Don't worry") 133 | } 134 | } yield () 135 | 136 | assertZIO(effect)(isUnit) 137 | } 138 | ).provideCustomLayer(Logging.forClass(getClass)) @@ TestAspect.withLiveClock 139 | } 140 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mlangc/slf4zio/api/LoggingServiceTest.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio.api 2 | 3 | import ch.qos.logback.classic.Level 4 | import com.github.mlangc.slf4zio.LogbackTestAppender 5 | import com.github.mlangc.slf4zio.LogbackTestUtils 6 | import zio._ 7 | import zio.test._ 8 | import zio.test.Assertion._ 9 | import zio.ZIO 10 | 11 | object LoggingServiceTest extends ZIOSpecDefault { 12 | def spec = suite("LoggingService")( 13 | test("Logging with thresholds") { 14 | val spec = { 15 | LogSpec.onError(_ => warn"ERROR") ++ 16 | LogSpec.onSucceed(_ => info"OK") ++ 17 | LogSpec.onTermination(_ => error"UPS") 18 | }.withThreshold(1.second) 19 | 20 | def consume(d: Duration) = 21 | TestClock.adjust(d) 22 | 23 | def consumeAndFail(d: Duration) = 24 | consume(d) *> ZIO.fail(new RuntimeException("Test")) 25 | 26 | def consumeAndDie(d: Duration) = 27 | consume(d) *> ZIO.dieMessage("Test") 28 | 29 | (for { 30 | _ <- consume(1.milli).perfLogZ(spec) 31 | _ <- consume(1.second).perfLogZ(spec) 32 | _ <- consumeAndFail(999.millis + 999.micros + 999.nanos).perfLogZ(spec).ignore 33 | _ <- consumeAndFail(1.second + 1.nano).perfLogZ(spec).ignore 34 | _ <- consumeAndDie(Duration.Zero).perfLogZ(spec).catchAllCause(_ => ZIO.unit) 35 | _ <- consumeAndDie(1.second).perfLogZ(spec).catchAllCause(_ => ZIO.unit) 36 | evts <- LogbackTestAppender.events.map(evts => 37 | evts.filter(evt => evt.getLoggerName == getClass.getCanonicalName) 38 | ) 39 | warnEvts = evts.filter(_.getLevel == Level.WARN) 40 | infoEvts = evts.filter(_.getLevel == Level.INFO) 41 | errEvts = evts.filter(_.getLevel == Level.ERROR) 42 | } yield { 43 | assert(warnEvts)(hasSize(equalTo(1))) && 44 | assert(infoEvts)(hasSize(equalTo(1))) && 45 | assert(errEvts)(hasSize(equalTo(1))) 46 | }) 47 | } 48 | ).provideCustomLayer((Logging.forClass(getClass))) @@ 49 | TestAspect.before(live(LogbackTestUtils.waitForLogbackInitialization.orDie)) @@ 50 | TestAspect.timeout(15.seconds) 51 | } 52 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mlangc/slf4zio/api/LoggingSupportTest.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio.api 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent 4 | import ch.qos.logback.classic.Level 5 | import com.github.mlangc.slf4zio.LogbackInitializationTimeout 6 | import com.github.mlangc.slf4zio.LogbackTestAppender 7 | import com.github.mlangc.slf4zio.LogbackTestUtils 8 | import zio.duration2DurationOps 9 | import zio.test._ 10 | import zio.test.Assertion 11 | import zio.test.Assertion.equalTo 12 | import zio.test.Assertion.hasSize 13 | import zio.test.ZIOSpecDefault 14 | import zio.IO 15 | import zio.ZIO 16 | 17 | object LoggingSupportTest extends ZIOSpecDefault with LoggingSupport { 18 | def spec = suite("LoggingSupport")( 19 | test("Make sure something is logged") { 20 | for { 21 | _ <- logger.infoIO("Test") 22 | evts <- getLogEvents 23 | } yield assert(evts)(Assertion.isNonEmpty) 24 | }, 25 | suite("Performance logging")( 26 | suite("ZIO syntax")( 27 | test("Success only") { 28 | for { 29 | _ <- ZIO.unit.perfLog(LogSpec.onSucceed(d => debug"Simple ${d.render}")) 30 | _ <- ZIO.succeed(42).perfLog(LogSpec.onSucceed((d, a) => debug"Simple (${d.render}, $a)")) 31 | evts <- getLogEvents(_.getMessage.startsWith("Simple")) 32 | } yield assert(evts.size)(equalTo(2)) 33 | }, 34 | test("Success and failures") { 35 | val spec: LogSpec[Throwable, Any] = LogSpec.onSucceed(d => info"Success after ${d.render}") ++ 36 | LogSpec.onError[Throwable]((d, e) => warn"Error $e after ${d.render}") ++ 37 | LogSpec.onTermination((d, c) => error"Fatal error ${c.prettyPrint} after ${d.render}") 38 | 39 | for { 40 | _ <- ZIO.unit.perfLog(spec) 41 | _ <- ZIO.fail(new RuntimeException).perfLog(spec).ignore 42 | _ <- ZIO.die(new RuntimeException).perfLog(spec).catchAllCause(_ => ZIO.unit) 43 | succEvents <- getLogEvents(evt => evt.getLevel == Level.INFO && evt.getMessage.startsWith("Success")) 44 | errEvents <- getLogEvents(evt => evt.getLevel == Level.WARN && evt.getMessage.startsWith("Error")) 45 | termEvents <- getLogEvents(evt => evt.getLevel == Level.ERROR && evt.getMessage.startsWith("Fatal")) 46 | } yield { 47 | assert(succEvents)(hasSize(equalTo(1))) && 48 | assert(errEvents)(hasSize(equalTo(1))) && 49 | assert(termEvents)(hasSize(equalTo(1))) 50 | } 51 | } 52 | ) 53 | ) 54 | ) @@ TestAspect.before(live(LogbackTestUtils.waitForLogbackInitialization).orDie) 55 | 56 | private def getLogEvents(p: ILoggingEvent => Boolean): IO[LogbackInitializationTimeout, List[ILoggingEvent]] = 57 | LogbackTestAppender.events.map { evts => 58 | evts.filter(evt => p(evt) && evt.getLoggerName.contains(getClass.getSimpleName)) 59 | } 60 | 61 | private def getLogEvents: IO[LogbackInitializationTimeout, List[ILoggingEvent]] = 62 | getLogEvents(_ => true) 63 | } 64 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mlangc/slf4zio/api/MDZIOTest.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio.api 2 | 3 | import zio.test._ 4 | import zio.test.Assertion.equalTo 5 | import zio.test.Assertion.isNone 6 | import zio.test.ZIOSpecDefault 7 | import zio.ZIO 8 | 9 | object MDZIOTest extends ZIOSpecDefault { 10 | def spec = suite("MDZIO")( 11 | test("put, get & clear works properly") { 12 | val (x, y) = ("x", "y") 13 | val (a, b) = ("a", "b") 14 | 15 | for { 16 | _ <- MDZIO.putAll(x -> a, y -> b) 17 | v1 <- MDZIO.get(x) 18 | v2 <- MDZIO.get(y) 19 | _ <- MDZIO.clear() 20 | v3 <- MDZIO.get(x) 21 | v4 <- MDZIO.get(y) 22 | } yield assert((v1, v2, v3, v4))(equalTo((Some(a), Some(b), None, None))) 23 | }, 24 | test("doWith") { 25 | val (d, e, f, g) = ("d", "e", "f", "g") 26 | val (k, l, m, n) = ("k", "l", "m", "n") 27 | val m0 = "m0" 28 | val km0 = List("k", "m0") 29 | val defg = List(d, e, f, g) 30 | val klmn = List(k, l, m, n) 31 | 32 | for { 33 | _ <- MDZIO.putAll(d -> k, e -> m0) 34 | v1 <- MDZIO.doWith(e -> l, f -> m, g -> n)(ZIO.foreach(defg)(MDZIO.get)) 35 | v2 <- ZIO.foreach(defg)(MDZIO.get) 36 | } yield assert(v1.flatten)(equalTo(klmn)) && assert(v2.flatten)(equalTo(km0)) 37 | }, 38 | test("setContextMap") { 39 | val (r, s) = ("r", "s") 40 | 41 | for { 42 | _ <- MDZIO.put(r, s) 43 | _ <- MDZIO.setContextMap(Map.empty) 44 | v <- MDZIO.get(r) 45 | } yield assert(v)(isNone) 46 | } 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mlangc/slf4zio/api/RawPerfLogTest.scala: -------------------------------------------------------------------------------- 1 | package com.github.mlangc.slf4zio.api 2 | 3 | import com.github.mlangc.slf4zio.LogbackTestAppender 4 | import com.github.mlangc.slf4zio.LogbackTestUtils 5 | import zio.duration2DurationOps 6 | import zio.durationInt 7 | import zio.test._ 8 | import zio.ZIO 9 | 10 | object RawPerfLogTest extends ZIOSpecDefault { 11 | def spec = suite("Raw Performance Log")( 12 | test("Simple Examples") { 13 | val spec1 = 14 | LogSpec.onSucceed[String]((d, s) => info"Good: (${d.render}, $s)") ++ 15 | LogSpec.onError[Throwable]((d, e) => error"Not good: (${d.render}, $e)") ++ 16 | LogSpec.onTermination((d, c) => error"Unexpected: (${d.render}, $c}") 17 | 18 | val spec2 = 19 | spec1.withThreshold(1.hour) 20 | 21 | val ok = "!!OK!!" 22 | val error = "!!ERROR!!" 23 | 24 | for { 25 | logger <- makeLogger(getClass) 26 | _ <- ZIO.attempt(logger.perfLog(ok)(spec1)) 27 | _ <- ZIO.attempt(logger.perfLog[String](throw new RuntimeException(error))(spec1)).ignore 28 | _ <- ZIO.attempt(logger.perfLog(ok)(spec2)) 29 | _ <- ZIO.attempt(logger.perfLog[String](throw new RuntimeException(error))(spec2)).ignore 30 | evts <- LogbackTestAppender.eventsFor(this.getClass) 31 | } yield assert(evts)(Assertion.hasSize(Assertion.equalTo(2))) 32 | } 33 | ) @@ TestAspect.before(LogbackTestUtils.waitForLogbackInitialization.orDie) @@ TestAspect.withLiveClock 34 | } 35 | --------------------------------------------------------------------------------