Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: LEGO/woof
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.4.4
Choose a base ref
...
head repository: LEGO/woof
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref

Commits on Apr 27, 2022

  1. Copy the full SHA
    2a6a965 View commit details
  2. Refresh mdoc

    hejfelix committed Apr 27, 2022
    Copy the full SHA
    b4a33cb View commit details

Commits on May 17, 2022

  1. Copy the full SHA
    26f16cd View commit details
  2. Copy the full SHA
    e4a641f View commit details
  3. Copy the full SHA
    e76f775 View commit details
  4. Upgrade http4s-core from 0.23.9 to 0.23.11 (#56)

    * Upgrade http4s-core from 0.23.9 to 0.23.11
    
    * Revert commit(s) 55119de
    
    * Upgrade http4s-core from 0.23.9 to 0.23.11
    
    Co-authored-by: Felix Bjært Hargreaves <hejfelix@users.noreply.github.com>
    scala-steward and hejfelix authored May 17, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    8c8d5f8 View commit details
  5. Upgrade slf4j-api from 1.7.35 to 1.7.36 (#48)

    * Upgrade slf4j-api from 1.7.35 to 1.7.36
    
    * Revert commit(s) bf69cab
    
    * Upgrade slf4j-api from 1.7.35 to 1.7.36
    
    Co-authored-by: Felix Bjært Hargreaves <hejfelix@users.noreply.github.com>
    scala-steward and hejfelix authored May 17, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    49d9639 View commit details
  6. Upgrade scalafmt-core from 3.4.3 to 3.5.3 (#67)

    * Upgrade scalafmt-core from 3.4.3 to 3.5.3
    
    * Reformat with scalafmt 3.5.3
    
    * Fix tests after scalafmt ugrade
    
    Co-authored-by: Felix <felix.palludan.hargreaves@lego.com>
    scala-steward and hejfelix authored May 17, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    2e59cf9 View commit details
  7. Copy the full SHA
    5340dbf View commit details

Commits on May 19, 2022

  1. Markdown linting (#69)

    * Run MarkDown linting and fix warnings.
    
    * Generate README with mdoc.
    Colin-TUE authored May 19, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    2f0e264 View commit details
  2. Add Contributing.md (#71)

    * Add Contributing.md:
    
    added section on hos to update the README.
    
    * Remove leftovers from copy-paste.
    Colin-TUE authored May 19, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    b81842d View commit details

Commits on May 20, 2022

  1. Copy the full SHA
    7caece6 View commit details
  2. Add tagless final example

    hejfelix committed May 20, 2022
    Copy the full SHA
    f110e22 View commit details
  3. Add CustomPrinter example

    hejfelix committed May 20, 2022
    Copy the full SHA
    ec9eb4f View commit details
  4. Copy the full SHA
    01dddb9 View commit details
  5. Add AtLeastLevel example

    unicojoyhug authored and hejfelix committed May 20, 2022
    Copy the full SHA
    0f01e67 View commit details
  6. Add scalajs hello world

    hejfelix committed May 20, 2022
    Copy the full SHA
    def2fc5 View commit details
  7. Copy the full SHA
    a5a4cf3 View commit details
  8. adding more LogFilter examples

    LMarkowski authored and hejfelix committed May 20, 2022
    Copy the full SHA
    862e519 View commit details
  9. Copy the full SHA
    77ec008 View commit details
  10. Copy the full SHA
    cb532a2 View commit details
  11. Add examples/FileOutput.scala

    kheino authored and hejfelix committed May 20, 2022
    Copy the full SHA
    bb48f28 View commit details
  12. Update README.md

    kheino authored and hejfelix committed May 20, 2022
    Copy the full SHA
    4b4208a View commit details
  13. Copy the full SHA
    3ff3945 View commit details
  14. format build.sbt

    hejfelix committed May 20, 2022
    Copy the full SHA
    b420136 View commit details
  15. Fix typo

    hejfelix committed May 20, 2022
    Copy the full SHA
    f9a6e25 View commit details

Commits on Jun 1, 2022

  1. Copy the full SHA
    7e6e779 View commit details
  2. Copy the full SHA
    130f786 View commit details

Commits on Jul 7, 2022

  1. Verified

    This commit was signed with the committer’s verified signature.
    scala-steward Scala Steward
    Copy the full SHA
    acd539a View commit details

Commits on Jul 8, 2022

  1. Copy the full SHA
    2d6052d View commit details
  2. Copy the full SHA
    c4a8cb1 View commit details
  3. Copy the full SHA
    7be9ee4 View commit details
  4. Copy the full SHA
    cd3f98e View commit details
  5. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    8562c43 View commit details
  6. Merge pull request #81 from scala-steward/update/cats-effect-3.3.13

    Upgrade cats-effect, cats-effect-testkit from 3.3.12 to 3.3.13
    hejfelix authored Jul 8, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    7a63acd View commit details
  7. Copy the full SHA
    a044605 View commit details
  8. Reformat with scalafmt 3.5.8

    Executed command: scalafmt --non-interactive
    scala-steward authored and hejfelix committed Jul 8, 2022
    Copy the full SHA
    960cee9 View commit details
  9. Copy the full SHA
    83c16ec View commit details

Commits on Jul 14, 2022

  1. Upgrade sbt from 1.6.2 to 1.7.1

    scala-steward authored and hejfelix committed Jul 14, 2022
    Copy the full SHA
    fcb3d70 View commit details
  2. Copy the full SHA
    9dd03a1 View commit details

Commits on Aug 8, 2022

  1. Copy the full SHA
    7e7489f View commit details
  2. Copy the full SHA
    03fcf54 View commit details

Commits on Aug 12, 2022

  1. Verified

    This commit was signed with the committer’s verified signature.
    scala-steward Scala Steward
    Copy the full SHA
    b300d51 View commit details

Commits on Aug 24, 2022

  1. Verified

    This commit was signed with the committer’s verified signature.
    scala-steward Scala Steward
    Copy the full SHA
    fd85ab4 View commit details

Commits on Sep 7, 2022

  1. Verified

    This commit was signed with the committer’s verified signature.
    scala-steward Scala Steward
    Copy the full SHA
    8ad2a23 View commit details

Commits on Sep 8, 2022

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    18a2492 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    cde9637 View commit details
  3. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    f9b0e33 View commit details

Commits on Sep 26, 2022

  1. Cross-build for Scala Native

    kubukoz authored and hejfelix committed Sep 26, 2022
    Copy the full SHA
    0c455ce View commit details
  2. Aggregate all projects

    kubukoz authored and hejfelix committed Sep 26, 2022
    Copy the full SHA
    1eb7a27 View commit details
Showing with 1,173 additions and 278 deletions.
  1. +5 −0 .git-blame-ignore-revs
  2. +6 −2 .github/workflows/release.yml
  3. +2 −2 .github/workflows/scala.yml
  4. +1 −0 .sbtopts
  5. +1 −0 .scala-steward.conf
  6. +1 −1 .scalafmt.conf
  7. +13 −1 CONTRIBUTING.md
  8. +76 −37 README.md
  9. +48 −0 REGENERATING_SIGNING_KEY.md
  10. +62 −18 build.sbt
  11. +60 −19 docs/README.md
  12. +70 −0 modules/benchmarks/FilterBenchmark.scala.output.md
  13. +49 −0 modules/benchmarks/src/main/scala/benchmarks/FilterBenchmark.scala
  14. 0 modules/core/{jvm → jvm-native}/src/main/scala/org/legogroup/woof/DefaultTimeFormat.scala
  15. 0 modules/core/{jvm → jvm-native}/src/test/scala/org/legogroup/woof/TestFormatTime.scala
  16. +59 −0 modules/core/shared/src/main/scala/org/legogroup/woof/DefaultLogger.scala
  17. +22 −10 modules/core/shared/src/main/scala/org/legogroup/woof/Filter.scala
  18. +5 −48 modules/core/shared/src/main/scala/org/legogroup/woof/Logger.scala
  19. +2 −2 modules/core/shared/src/main/scala/org/legogroup/woof/Macro.scala
  20. +1 −0 modules/core/shared/src/main/scala/org/legogroup/woof/json/JsonSupport.scala
  21. +2 −1 modules/core/shared/src/test/scala/org/legogroup/woof/FilterSuite.scala
  22. +33 −11 modules/core/shared/src/test/scala/org/legogroup/woof/LoggerSuite.scala
  23. +6 −3 modules/core/shared/src/test/scala/org/legogroup/woof/json/JsonSupportSuite.scala
  24. +11 −0 modules/examples-scalajs/index.html
  25. +20 −0 modules/examples-scalajs/src/main/scala/examples/HelloScalaJs.scala
  26. +25 −0 modules/examples/src/main/scala/examples/CustomPrinter.scala
  27. +37 −0 modules/examples/src/main/scala/examples/CustomTheme.scala
  28. +32 −0 modules/examples/src/main/scala/examples/FileOutput.scala
  29. +17 −0 modules/examples/src/main/scala/examples/HelloWorld.scala
  30. +24 −0 modules/examples/src/main/scala/examples/LogLevelFromEnv.scala
  31. +31 −0 modules/examples/src/main/scala/examples/TaglessFinal.scala
  32. +33 −0 modules/examples/src/main/scala/examples/filters/AtLeastLevel.scala
  33. +46 −0 modules/examples/src/main/scala/examples/filters/CustomFilter.scala
  34. +33 −0 modules/examples/src/main/scala/examples/filters/ExactLevel.scala
  35. +43 −0 modules/examples/src/main/scala/examples/filters/RegexFilter.scala
  36. +3 −7 modules/http4s/src/main/scala/org/legogroup/woof/http4s/CorrelationIdMiddleware.scala
  37. +4 −4 modules/http4s/src/test/scala/org/legogroup/woof/http4s/CorrelationIdMiddlewareSuite.scala
  38. +1 −0 modules/slf4j-2/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider
  39. +29 −0 modules/slf4j-2/src/main/scala/org/legogroup/woof/slf4j2/WoofLogger.scala
  40. +8 −0 modules/slf4j-2/src/main/scala/org/legogroup/woof/slf4j2/WoofLoggerServiceProvider.scala
  41. +1 −0 modules/slf4j-2/src/test/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider
  42. +105 −0 modules/slf4j-2/src/test/scala/org/legogroup/woof/slf4j2/Slf4j2Suite.scala
  43. +108 −0 modules/slf4j-common/src/main/scala/org/legogroup/woof/slf4j/Slf4jWoofLoggerImpl.scala
  44. +12 −96 modules/slf4j/src/main/scala/org/legogroup/woof/slf4j/WoofLogger.scala
  45. +3 −1 modules/slf4j/src/main/scala/org/slf4j/impl/StaticLoggerBinder.scala
  46. +12 −9 modules/slf4j/src/test/scala/org/legogroup/woof/slf4j/Slf4jSuite.scala
  47. +1 −1 project/build.properties
  48. +10 −5 project/plugins.sbt
5 changes: 5 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Scala Steward: Reformat with scalafmt 3.5.8
5b62465aa83c45f7c20487b73c633a9024569259

# Scala Steward: Reformat with scalafmt 3.7.3
c9a0b2b472e054c270e87cccf81c3cc5b96960aa
8 changes: 6 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -7,10 +7,14 @@ jobs:
publish:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: olafurpg/setup-scala@v13
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
cache: 'sbt'
- run: sbt ci-release
env:
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
4 changes: 2 additions & 2 deletions .github/workflows/scala.yml
Original file line number Diff line number Diff line change
@@ -10,9 +10,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up JDK 16
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
java-version: "16"
distribution: "adopt"
1 change: 1 addition & 0 deletions .sbtopts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-J-Xmx4G
1 change: 1 addition & 0 deletions .scala-steward.conf
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
commits.message = "Upgrade ${artifactName} from ${currentVersion} to ${nextVersion}"
updatePullRequests = "always"
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 3.4.0
version = 3.8.3

runner.dialect = scala3

14 changes: 13 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
sbt scalafmt test -mem 2048
# How to contribute

## Welcome

:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:

The following is a set of guides for contributing and feel free to propose changes to this document in a pull request.

Happy to have you onboard :+1:

## How to update the README.md

The README is generated using [mdoc]() from the README source so that all code examples are compiled, run, and its output added to the code block. When updating the README the source needs to be updated, which can be found in [/docs/](docs/README.md). Then one needs to regenerate the README using the `sbt:root> docs/mdoc` command.
113 changes: 76 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -5,29 +5,68 @@

A **pure** _(in both senses of the word!)_ **Scala 3** logging library with **no runtime reflection**.

![](dog-svgrepo-com.svg)
![logo](dog-svgrepo-com.svg)

# Table of Contents
- [Highlights](#highlights)
- [Cross platform](#cross-platform)
- [Installation](#installation)
- [Example](#example)
- [Can I use `SLF4J`?](#can-i-use-slf4j)
- [Limitations of SLF4J bindings](#limitations-of-slf4j-bindings)
- [Can I use `http4s`?](#can-i-use-http4s)
- [Structured Logging](#structured-logging)

## Highlights

* Pure **Scala 3** library
* Made with _Cats Effect_
* Macro based (_no runtime reflection_)
* ~~Can be built for _scala.js_ in the future!~~
* Cross-built for `Scala.js`
* Configured with plain Scala code

### Cross platform

| Module | JVM | scala.js | native |
|---------|-------|-----------|--------|
| core ||||
| http4s ||||
| slf4j || 🚫 | 🚫 |
| slf4j-2 || 🚫 | 🚫 |

## Installation

> build.sbt
```scala
libraryDependencies ++= Seq(
"org.legogroup" %% "woof-core" % "$VERSION",
"org.legogroup" %% "woof-slf4j" % "$VERSION", // only if you need to use Woof via slf4j
"org.legogroup" %% "woof-http4s" % "$VERSION", // only if you need to add correlation IDs in http4s
"org.legogroup" %% "woof-core" % "$VERSION",
"org.legogroup" %% "woof-slf4j" % "$VERSION", // only if you need to use Woof via slf4j 1.x.x
"org.legogroup" %% "woof-slf4j-2" % "$VERSION", // only if you need to use Woof via slf4j 2.x.x
"org.legogroup" %% "woof-http4s" % "$VERSION", // only if you need to add correlation IDs in http4s
)
```

You can see a bunch of self-contained examples in the [examples](modules/examples) sub-project. To run them, open `sbt` and run the command `examples/run`:

```
sbt:root> examples/run
Multiple main classes detected. Select one to run:
[1] examples.AtLeastLevel
[2] examples.CustomPrinter
[3] examples.CustomTheme
[4] examples.ExactLevel
[5] examples.FileOutput
[6] examples.HelloWorld
[7] examples.LogLevelFromEnv
[8] examples.RegexFilter
[9] examples.TaglessFinal
Enter number:
```

it will ask you for a number corresponding to the example you wish to run. For a self-contained `Scala.Js` example, look at [modules/examples-scalajs/src/main/scala/examples/HelloScalaJs.scala](modules/examples-scalajs/src/main/scala/examples/HelloScalaJs.scala)

## Example

```scala
@@ -61,13 +100,12 @@ and running it yields:
```scala
import cats.effect.unsafe.implicits.global
main.unsafeRunSync()
// 2022-04-21 08:47:11 [DEBUG] repl.MdocSession$.App: This is some debug (.:27)
// 2022-04-21 08:47:11 [INFO ] repl.MdocSession$.App: HEY! (.:28)
// 2022-04-21 08:47:11 [WARN ] repl.MdocSession$.App: I'm warning you (.:29)
// 2022-04-21 08:47:11 [ERROR] repl.MdocSession$.App: I give up (.:30)
// 2023-03-13 09:00:42 [DEBUG] repl.MdocSession$.MdocApp: This is some debug (README.md:27)
// 2023-03-13 09:00:42 [INFO ] repl.MdocSession$.MdocApp: HEY! (README.md:28)
// 2023-03-13 09:00:42 [WARN ] repl.MdocSession$.MdocApp: I'm warning you (README.md:29)
// 2023-03-13 09:00:42 [ERROR] repl.MdocSession$.MdocApp: I give up (README.md:30)
```


We can also re-use the program and add context to our logger:

```scala
@@ -84,14 +122,14 @@ And running with context yields:

```scala
mainWithContext.unsafeRunSync()
// 2022-04-21 08:47:11 [DEBUG] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.App: This is some debug (.:27)
// 2022-04-21 08:47:11 [INFO ] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.App: HEY! (.:28)
// 2022-04-21 08:47:11 [WARN ] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.App: I'm warning you (.:29)
// 2022-04-21 08:47:11 [ERROR] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.App: I give up (.:30)
// 2022-04-21 08:47:11 [INFO ] repl.MdocSession$.App: Now the context is gone (.:61)
// 2023-03-13 09:00:42 [DEBUG] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.MdocApp: This is some debug (README.md:27)
// 2023-03-13 09:00:42 [INFO ] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.MdocApp: HEY! (README.md:28)
// 2023-03-13 09:00:42 [WARN ] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.MdocApp: I'm warning you (README.md:29)
// 2023-03-13 09:00:42 [ERROR] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.MdocApp: I give up (README.md:30)
// 2023-03-13 09:00:42 [INFO ] repl.MdocSession$.MdocApp: Now the context is gone (README.md:61)
```

# Can I use SLF4J?
## Can I use `SLF4J`?

Yes, you can. I don't think you should (for new projects), but you can use it for interop with existing SLF4J programs! Note, however, that not everything can be implemented perfectly against the
`SLF4J` API, e.g. the filtering functionality in `woof` is much more flexible and thus does not map directly to, e.g., `isDebugEnabled`.
@@ -120,26 +158,28 @@ To use this program with woof
```scala
import org.legogroup.woof.slf4j.*
import cats.effect.std.Dispatcher
val mainSlf4j: IO[Unit] =
for
woofLogger <- DefaultLogger.makeIo(consoleOutput)
_ <- woofLogger.registerSlf4j
_ <- programWithSlf4j
yield ()
Dispatcher.sequential[IO].use{ implicit dispatcher =>
for
woofLogger <- DefaultLogger.makeIo(consoleOutput)
_ <- woofLogger.registerSlf4j
_ <- programWithSlf4j
yield ()
}
```

and running it:

```scala
mainSlf4j.unsafeRunSync()
// 2022-04-21 08:47:11 [INFO ] repl.MdocSession$App: Hello from SLF4j! (MdocSession$App.scala:81)
// 2022-04-21 08:47:11 [WARN ] repl.MdocSession$App: This is not the pure woof. (MdocSession$App.scala:82)
```
## Limitations of SLF4J bindings

### Limitations of SLF4J bindings

Currently, markers do nothing. You can get the same behaviour easily with context when using the direct `woof` api with filters and printers.

# Can I use __http4s__?
## Can I use `http4s`?

Yes you can. If you want to see internal logs from `http4s`, use the `SLF4J` module from above. If you want to use the context capabilities in `woof`, there's a module for adding correlation IDs to each request with a simple middleware.

@@ -177,20 +217,20 @@ val mainHttp4s: IO[Unit] =
yield ()
```

Finally, running it, we see that the correlation ID is added to the log message inside the routes (transparently), and that
Finally, running it, we see that the correlation ID is added to the log message inside the routes (transparently), and that
the correlation ID is also returned in the header of the response.

> NOTE: The correlation ID is _not_ present outside the routes, i.e. we have scoped it only to the service part of our code.
```scala
mainHttp4s.unsafeRunSync()
// 2022-04-21 08:47:12 [INFO ] X-Trace-Id=f9780ca5-2e41-445e-96c7-335822d2143b repl.MdocSession$.App: I got a request with trace id! :D (.:121)
// 2022-04-21 08:47:12 [INFO ] repl.MdocSession$.App: Got response headers: Headers(X-Trace-Id: f9780ca5-2e41-445e-96c7-335822d2143b) (.:142)
// 2023-03-13 09:00:43 [INFO ] X-Trace-Id=33a38390-647a-4876-9a05-7898a8f4db44 repl.MdocSession$.MdocApp: I got a request with trace id! :D (README.md:126)
// 2023-03-13 09:00:43 [INFO ] repl.MdocSession$.MdocApp: Got response headers: Headers(X-Trace-Id: 33a38390-647a-4876-9a05-7898a8f4db44) (README.md:147)
```

## Structured Logging

Structured logging is useful when your logs are collected and inspected by a monitoring system. Having a well structured log output can save you
Structured logging is useful when your logs are collected and inspected by a monitoring system. Having a well structured log output can save you
hours of reg-ex'ing your way towards the root cause of a burning issue.

`Woof` supports printing as `Json`:
@@ -210,12 +250,11 @@ And running with context yields:

```scala
contextAsJson.unsafeRunSync()
// {"level":"Debug","epochMillis":1650523632092,"timeStamp":"2022-04-21T06:47:12Z","enclosingClass":"repl.MdocSession$.App","message":"This is some debug","context":{"bar":"1337","foo":"42"}}
// {"level":"Info","epochMillis":1650523632096,"timeStamp":"2022-04-21T06:47:12Z","enclosingClass":"repl.MdocSession$.App","message":"HEY!","context":{"bar":"1337","foo":"42"}}
// {"level":"Warn","epochMillis":1650523632096,"timeStamp":"2022-04-21T06:47:12Z","enclosingClass":"repl.MdocSession$.App","message":"I'm warning you","context":{"bar":"1337","foo":"42"}}
// {"level":"Error","epochMillis":1650523632096,"timeStamp":"2022-04-21T06:47:12Z","enclosingClass":"repl.MdocSession$.App","message":"I give up","context":{"bar":"1337","foo":"42"}}
// {"level":"Info","epochMillis":1650523632097,"timeStamp":"2022-04-21T06:47:12Z","enclosingClass":"repl.MdocSession$.App","message":"Now the context is gone","context":{}}
// {"level":"Debug","epochMillis":1678694443157,"timeStamp":"2023-03-13T08:00:43Z","enclosingClass":"repl.MdocSession$.MdocApp","lineNumber":26,"message":"This is some debug","context":{"bar":"1337","foo":"42"}}
// {"level":"Info","epochMillis":1678694443159,"timeStamp":"2023-03-13T08:00:43Z","enclosingClass":"repl.MdocSession$.MdocApp","lineNumber":27,"message":"HEY!","context":{"bar":"1337","foo":"42"}}
// {"level":"Warn","epochMillis":1678694443159,"timeStamp":"2023-03-13T08:00:43Z","enclosingClass":"repl.MdocSession$.MdocApp","lineNumber":28,"message":"I'm warning you","context":{"bar":"1337","foo":"42"}}
// {"level":"Error","epochMillis":1678694443159,"timeStamp":"2023-03-13T08:00:43Z","enclosingClass":"repl.MdocSession$.MdocApp","lineNumber":29,"message":"I give up","context":{"bar":"1337","foo":"42"}}
// {"level":"Info","epochMillis":1678694443159,"timeStamp":"2023-03-13T08:00:43Z","enclosingClass":"repl.MdocSession$.MdocApp","lineNumber":168,"message":"Now the context is gone","context":{}}
```


> We are considering if we should support matching different printers with different outputs: Maybe you want human readable logs for standard out and structured logging for your monitoring tools. However, this will be a breaking change.
> We are considering if we should support matching different printers with different outputs: Maybe you want human readable logs for standard out and structured logging for your monitoring tools. However, this will be a breaking change.
48 changes: 48 additions & 0 deletions REGENERATING_SIGNING_KEY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Regenerating the GPG signing key

The key for signing releases expires every year.
To sign a new release _after the key expires_, you need to:

1. Generate a new key
2. Publish the public signature to one or more key servers: (e.g. https://keyserver.ubuntu.com/)

## Generating a new key

We use a password protected, encrypted signing key when signing releases for maven central. To refresh the key, you need to:

1. Download the secret key file from your secret manager
1. Note the encryption password from your secret manager
1. Import the key into your local gpg keychain
1. `gpg --import <path to secret key file>`
1. Enter the encryption password when prompted
1. List the secret key ids: `gpg --list-secret-keys --keyid-format LONG`
1. Note the key ID of the imported key
1. Refresh the key expiration date
1. `gpg --edit-key <key id>`
1. `expire`
1. **IMPORTANT:** When promted, write `1y`. The default is `no-expiration`, **which is not what we want**
1. You should be prompted for the pass-phrase again. Enter the same pass-phrase as before
1. `save` + enter
1. If done correctly, it should exit the `gpg` interactive prompt

## Update the key in your secret manager

1. Export the public key: `gpg --output maven.public.gpg --armor --export <key id>`
1. Export the private key: `gpg --output maven.secret.gpg --armor --export-secret-keys <key id>`
1. You will be prompted for the pass-phrase. Enter the same pass-phrase as before
1. Rename the existing public and private keys in your secret manager to something like `maven.public.gpg[expired]`
1. You can delete the `n-2`nd iteration of keys, as we are pretty sure we won't need them again at this point
1. Upload `maven.public.gpg` and `maven.secret.gpg` to your secret manager
1. **IMPORTANT** Delete `maven.public.gpg` and `maven.secret.gpg` from your local machine

## Publish the public key to a key server

1. `gpg --keyserver hkp://keyserver.ubuntu.com --send-key <key id>`

Now the downstream clients can verify the signature of the artifacts you sign with this key.

## Update the secret environment variables in the github action

1. Copy the `base64` encoded secret key from your secret manager
1. `gpg --armor --export-secret-keys $LONG_ID | base64 | pbcopy`
1. Paste the `base64` encoded secret key into the `PGP_SECRET` secret variable in the github action
80 changes: 62 additions & 18 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
Global / onChangedBuildSource := ReloadOnSourceChanges

val V = new {
val cats = "2.7.0"
val catsEffect = "3.3.11"
val circe = "0.15.0-M1"
val http4s = "0.23.9"
val munit = "0.7.29"
val munitCatsEffect = "1.0.7"
val scala = "3.1.1"
val slf4j = "1.7.35"
val scalacheck = "0.7.29"
val cats = "2.10.0"
val catsEffect = "3.5.4"
val circe = "0.14.8"
val http4s = "0.23.28"
val munit = "1.0.0-M11"
val munitCatsEffect = "2.0.0"
val scala = "3.3.4"
val slf4j = "1.7.36"
val slf4j2 = "2.0.16"
val tzdb = "2.5.0"
}

val D = new {
val slf4jApi = "org.slf4j" % "slf4j-api" % V.slf4j
val slf4jApi = "org.slf4j" % "slf4j-api" % V.slf4j
val slf4jApi2 = "org.slf4j" % "slf4j-api" % V.slf4j2

val catsCore = Def.setting("org.typelevel" %%% "cats-core" % V.cats)
val catsEffect = Def.setting("org.typelevel" %%% "cats-effect" % V.catsEffect)
val catsEffectTestKit = Def.setting("org.typelevel" %%% "cats-effect-testkit" % V.catsEffect)
val http4s = Def.setting("org.http4s" %%% "http4s-core" % V.http4s)
val munit = Def.setting("org.scalameta" %%% "munit" % V.munit)
val munitCatsEffect = Def.setting("org.typelevel" %%% "munit-cats-effect-3" % V.munitCatsEffect)
val munitCatsEffect = Def.setting("org.typelevel" %%% "munit-cats-effect" % V.munitCatsEffect)
val munitScalacheck = Def.setting("org.scalameta" %%% "munit-scalacheck" % V.munit)
val circe = Def.setting("io.circe" %%% "circe-parser" % V.circe)
val scalacheck = Def.setting("org.scalameta" %%% "munit-scalacheck" % V.scalacheck)
val tzdb = Def.setting("io.github.cquiroz" %%% "scala-java-time-tzdb" % V.tzdb)
}

/*
@@ -52,7 +57,7 @@ ThisBuild / versionScheme := Some("early-semver")
val commonSettings = Seq(
scalaVersion := V.scala,
organization := "org.legogroup",
scalacOptions ++= Seq("-source", "future"),
scalacOptions ++= Seq("-source", "future", "-deprecation"),
)

def nameForFile(file: File): String = s"woof-${file.getName()}"
@@ -72,13 +77,21 @@ lazy val docs =
lazy val root =
project
.in(file("."))
.aggregate(core.jvm, core.js, http4s.jvm, http4s.js, slf4j)
.aggregate(
List(
core,
http4s,
slf4j,
slf4j2,
slf4jCommon,
).flatMap(_.componentProjects).map(_.project): _*
)
.settings(
publish / skip := true,
)

val coreFolder = file("./modules/core")
lazy val core = crossProject(JSPlatform, JVMPlatform)
lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.in(coreFolder)
.settings(commonSettings)
.settings(
@@ -90,12 +103,13 @@ lazy val core = crossProject(JSPlatform, JVMPlatform)
D.munitCatsEffect.value % Test,
D.catsEffectTestKit.value % Test,
D.circe.value % Test,
D.scalacheck.value % Test
D.munitScalacheck.value % Test,
D.tzdb.value % Test
),
)

val http4sFolder = file("./modules/http4s")
lazy val http4s = crossProject(JSPlatform, JVMPlatform)
lazy val http4s = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(http4sFolder)
.settings(
@@ -107,4 +121,34 @@ lazy val http4s = crossProject(JSPlatform, JVMPlatform)

lazy val slf4j = woofProject(file("./modules/slf4j"))
.settings(libraryDependencies += D.slf4jApi)
.dependsOn(core.jvm % "compile->compile;test->test")
.dependsOn(slf4jCommon % "compile->compile;test->test")

lazy val slf4j2 = woofProject(file("./modules/slf4j-2"))
.settings(libraryDependencies += D.slf4jApi2)
.dependsOn(slf4jCommon % "compile->compile;test->test")

lazy val slf4jCommon =
woofProject(file("./modules/slf4j-common"))
.dependsOn(core.jvm % "compile->compile;test->test")

lazy val examples = project
.in(file("./modules/examples"))
.settings(commonSettings)
.settings(publish / skip := true)
.dependsOn(core.jvm)

lazy val examplesJs = crossProject(JSPlatform)
.withoutSuffixFor(JSPlatform)
.crossType(CrossType.Pure)
.in(file("./modules/examples-scalajs"))
.settings(commonSettings)
.settings(publish / skip := true)
.jsConfigure(_.settings(scalaJSUseMainModuleInitializer := true))
.dependsOn(core)

lazy val benchmarks = project
.in(file("./modules/benchmarks"))
.dependsOn(core.jvm)
.enablePlugins(JmhPlugin)
.settings(commonSettings)
.settings(publish / skip := true)
79 changes: 60 additions & 19 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -5,29 +5,68 @@

A **pure** _(in both senses of the word!)_ **Scala 3** logging library with **no runtime reflection**.

![](dog-svgrepo-com.svg)
![logo](dog-svgrepo-com.svg)

# Table of Contents
- [Highlights](#highlights)
- [Cross platform](#cross-platform)
- [Installation](#installation)
- [Example](#example)
- [Can I use `SLF4J`?](#can-i-use-slf4j)
- [Limitations of SLF4J bindings](#limitations-of-slf4j-bindings)
- [Can I use `http4s`?](#can-i-use-http4s)
- [Structured Logging](#structured-logging)

## Highlights

* Pure **Scala 3** library
* Made with _Cats Effect_
* Macro based (_no runtime reflection_)
* ~~Can be built for _scala.js_ in the future!~~
* Cross-built for `Scala.js`
* Configured with plain Scala code

### Cross platform

| Module | JVM | scala.js | native |
|---------|-------|-----------|--------|
| core ||||
| http4s ||||
| slf4j || 🚫 | 🚫 |
| slf4j-2 || 🚫 | 🚫 |

## Installation

> build.sbt
```scala
libraryDependencies ++= Seq(
"org.legogroup" %% "woof-core" % "$VERSION",
"org.legogroup" %% "woof-slf4j" % "$VERSION", // only if you need to use Woof via slf4j
"org.legogroup" %% "woof-http4s" % "$VERSION", // only if you need to add correlation IDs in http4s
"org.legogroup" %% "woof-core" % "$VERSION",
"org.legogroup" %% "woof-slf4j" % "$VERSION", // only if you need to use Woof via slf4j 1.x.x
"org.legogroup" %% "woof-slf4j-2" % "$VERSION", // only if you need to use Woof via slf4j 2.x.x
"org.legogroup" %% "woof-http4s" % "$VERSION", // only if you need to add correlation IDs in http4s
)
```

You can see a bunch of self-contained examples in the [examples](modules/examples) sub-project. To run them, open `sbt` and run the command `examples/run`:

```
sbt:root> examples/run
Multiple main classes detected. Select one to run:
[1] examples.AtLeastLevel
[2] examples.CustomPrinter
[3] examples.CustomTheme
[4] examples.ExactLevel
[5] examples.FileOutput
[6] examples.HelloWorld
[7] examples.LogLevelFromEnv
[8] examples.RegexFilter
[9] examples.TaglessFinal
Enter number:
```

it will ask you for a number corresponding to the example you wish to run. For a self-contained `Scala.Js` example, look at [modules/examples-scalajs/src/main/scala/examples/HelloScalaJs.scala](modules/examples-scalajs/src/main/scala/examples/HelloScalaJs.scala)

## Example

```scala mdoc:silent
@@ -63,7 +102,6 @@ import cats.effect.unsafe.implicits.global
main.unsafeRunSync()
```


We can also re-use the program and add context to our logger:

```scala mdoc:silent
@@ -82,7 +120,7 @@ And running with context yields:
mainWithContext.unsafeRunSync()
```

# Can I use SLF4J?
## Can I use `SLF4J`?

Yes, you can. I don't think you should (for new projects), but you can use it for interop with existing SLF4J programs! Note, however, that not everything can be implemented perfectly against the
`SLF4J` API, e.g. the filtering functionality in `woof` is much more flexible and thus does not map directly to, e.g., `isDebugEnabled`.
@@ -111,24 +149,28 @@ To use this program with woof
```scala mdoc:silent
import org.legogroup.woof.slf4j.*
import cats.effect.std.Dispatcher
val mainSlf4j: IO[Unit] =
for
woofLogger <- DefaultLogger.makeIo(consoleOutput)
_ <- woofLogger.registerSlf4j
_ <- programWithSlf4j
yield ()
Dispatcher.sequential[IO].use{ implicit dispatcher =>
for
woofLogger <- DefaultLogger.makeIo(consoleOutput)
_ <- woofLogger.registerSlf4j
_ <- programWithSlf4j
yield ()
}
```

and running it:

```scala mdoc
mainSlf4j.unsafeRunSync()
```
## Limitations of SLF4J bindings

### Limitations of SLF4J bindings

Currently, markers do nothing. You can get the same behaviour easily with context when using the direct `woof` api with filters and printers.

# Can I use __http4s__?
## Can I use `http4s`?

Yes you can. If you want to see internal logs from `http4s`, use the `SLF4J` module from above. If you want to use the context capabilities in `woof`, there's a module for adding correlation IDs to each request with a simple middleware.

@@ -166,7 +208,7 @@ val mainHttp4s: IO[Unit] =
yield ()
```

Finally, running it, we see that the correlation ID is added to the log message inside the routes (transparently), and that
Finally, running it, we see that the correlation ID is added to the log message inside the routes (transparently), and that
the correlation ID is also returned in the header of the response.

> NOTE: The correlation ID is _not_ present outside the routes, i.e. we have scoped it only to the service part of our code.
@@ -177,7 +219,7 @@ mainHttp4s.unsafeRunSync()

## Structured Logging

Structured logging is useful when your logs are collected and inspected by a monitoring system. Having a well structured log output can save you
Structured logging is useful when your logs are collected and inspected by a monitoring system. Having a well structured log output can save you
hours of reg-ex'ing your way towards the root cause of a burning issue.

`Woof` supports printing as `Json`:
@@ -199,5 +241,4 @@ And running with context yields:
contextAsJson.unsafeRunSync()
```


> We are considering if we should support matching different printers with different outputs: Maybe you want human readable logs for standard out and structured logging for your monitoring tools. However, this will be a breaking change.
> We are considering if we should support matching different printers with different outputs: Maybe you want human readable logs for standard out and structured logging for your monitoring tools. However, this will be a breaking change.
70 changes: 70 additions & 0 deletions modules/benchmarks/FilterBenchmark.scala.output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# The configuration and hardware info

```
Software:
System Version: macOS 13.2.1 (22D68)
Kernel Version: Darwin 22.3.0
Boot Volume: Macintosh HD
Boot Mode: Normal
Computer Name: MacBook Pro (21)
Secure Virtual Memory: Enabled
Hardware:
Model Name: MacBook Pro
Model Identifier: MacBookPro18,1
Model Number: Z14Y00047DK/A
Chip: Apple M1 Pro
Total Number of Cores: 10 (8 performance and 2 efficiency)
Memory: 32 GB
```

```
[info] # JMH version: 1.32
[info] # VM version: JDK 17.0.1, OpenJDK 64-Bit Server VM, 17.0.1+12
[info] # VM options: <none>
[info] # Blackhole mode: full + dont-inline hint
[info] # Warmup: 5 iterations, 10 s each
[info] # Measurement: 5 iterations, 10 s each
[info] # Timeout: 10 min per iteration
[info] # Threads: 1 thread, will synchronize iterations
[info] # Benchmark mode: Throughput, ops/time
```

# The results

```
[info] # Run complete. Total time: 00:25:04
[info] REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
[info] why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
[info] experiments, perform baseline and negative tests that provide experimental control, make sure
[info] the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
[info] Do not assume the numbers tell you what you want them to tell.
[info] Benchmark Mode Cnt Score Error Units
[info] FilterBenchmark.testEverything thrpt 25 255,250 ± 12,763 ops/s
[info] FilterBenchmark.testInfo thrpt 25 722,664 ± 25,608 ops/s
[info] FilterBenchmark.testNothing thrpt 25 11275,189 ± 314,631 ops/s
[success] Total time: 1509 s (25:09), completed 31 Mar 2023, 10.38.35
```

## Interpretation

My hypothesis going in was that materializing the output string was a significant part of the work, and the results seem
to corroborate this.

The benchmarks log 3 times at different levels (Info, Warn, Error) in a
tight loop of 1000 iterations. `testEverything` uses `Filter.everything`, i.e. it has to _always_ materialize the log
output. This is the worst case scenario.

`testInfo` uses the `Filter.exactLevel(LogLevel.Info)` which will only materialize `1/3` of the lines. This is almost 3
times as fast as `everything`.

`testNothing` is the best case scenario where nothing is materialized. This is more than 3 times faster
than `testEverything`, since not only
is nothing materialized, the outputs are never called, i.e. the compiler should be able to eliminate a lot of code (no
blackhole consume calls anymore!).

In general, these results are in line with the outcome I hoped for with the refactoring
from `type Filter = LogLine => Boolean` to
this new applicative style.
49 changes: 49 additions & 0 deletions modules/benchmarks/src/main/scala/benchmarks/FilterBenchmark.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package benchmarks

import cats.effect.IO
import cats.syntax.all.*
import org.legogroup.woof.{*, given}
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.infra.Blackhole
import cats.effect.unsafe.implicits.global

import scala.annotation.tailrec

class FilterBenchmark:

def test(blackhole: Blackhole)(using Filter): IO[Unit] =

val blackholeOutput: Output[IO] = new:
override def output(str: String): IO[Unit] = IO.delay(blackhole.consume(str))
override def outputError(str: String): IO[Unit] = IO.delay(blackhole.consume(str))

given Printer = ColorPrinter()
for
logger <- DefaultLogger.makeIo(blackholeOutput)
_ <-
def loop(remaining: Int): IO[Unit] =
val message = s"message$remaining"
if remaining <= 0 then IO.unit
else
for
_ <- logger.info(message)
_ <- logger.warn(message)
_ <- logger.error(message)
_ <- loop(remaining - 1)
yield ()
end loop
loop(1000)
yield ()
end for
end test

@Benchmark
def testEverything(blackhole: Blackhole): Unit = test(blackhole)(using Filter.everything).unsafeRunSync()

@Benchmark
def testNothing(blackhole: Blackhole): Unit = test(blackhole)(using Filter.nothing).unsafeRunSync()

@Benchmark
def testInfo(blackhole: Blackhole): Unit = test(blackhole)(using Filter.exactLevel(LogLevel.Info)).unsafeRunSync()

end FilterBenchmark
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.legogroup.woof

import cats.Monad
import cats.effect.IO
import cats.effect.kernel.Clock
import org.legogroup.woof.Logger.{ioStringLocal, StringLocal}
import cats.syntax.all.*

class DefaultLogger[F[_]: StringLocal: Monad: Clock](output: Output[F], outputs: Output[F]*)(using Printer, Filter)
extends Logger[F]:

val stringLocal: StringLocal[F] = summon[StringLocal[F]]
val printer: Printer = summon[Printer]
val filter: Filter = summon[Filter]

private[woof] def makeLogString(
level: LogLevel,
info: LogInfo,
message: String,
context: List[(String, String)],
): F[String] =
Clock[F].realTime
.map(d => EpochMillis(d.toMillis))
.map(now => summon[Printer].toPrint(now, level, info, message, context))

private[woof] def doOutputs(level: LogLevel, s: String): F[Unit] =
val allOutputs = outputs.prepended(output)
level match
case LogLevel.Error => allOutputs.traverse_(_.outputError(s))
case _ => allOutputs.traverse_(_.output(s))

override def doLog(level: LogLevel, message: String)(using logInfo: LogInfo): F[Unit] =
if shouldLog(level, message, filter) then logToOutputs(level, message) else ().pure

private[woof] def shouldLog(level: LogLevel, message: String, filter: Filter)(using logInfo: LogInfo): Boolean =
filter match
case Filter.AtLeastLevel(atLeastLevel) => level >= atLeastLevel
case Filter.ExactLevel(exactLevel) => level === exactLevel
case Filter.ClassRegex(regex) => regex.matches(logInfo.enclosingClass.printableName)
case Filter.MessageFilter(filter) => filter(message)
case Filter.LineNumberFilter(filter) => filter(logInfo.lineNumber)
case Filter.CompositeAnd(a, b) => shouldLog(level, message, a) && shouldLog(level, message, b)
case Filter.CompositeOr(a, b) => shouldLog(level, message, a) || shouldLog(level, message, b)
case Filter.Nothing => false
case Filter.Everything => true

private[woof] def logToOutputs(level: LogLevel, message: String)(using logInfo: LogInfo): F[Unit] = for
context <- summon[StringLocal[F]].ask
logLine <- makeLogString(level, logInfo, message, context)
_ <- doOutputs(level, logLine)
yield ()

end DefaultLogger

object DefaultLogger:
def makeIo(output: Output[IO], outputs: Output[IO]*)(using Clock[IO], Printer, Filter): IO[DefaultLogger[IO]] =
for given StringLocal[IO] <- ioStringLocal
yield new DefaultLogger[IO](output, outputs*)
end DefaultLogger
32 changes: 22 additions & 10 deletions modules/core/shared/src/main/scala/org/legogroup/woof/Filter.scala
Original file line number Diff line number Diff line change
@@ -7,22 +7,34 @@ import scala.util.matching.Regex

case class LogLine(level: LogLevel, info: LogInfo, message: String, context: List[(String, String)])

type Filter = LogLine => Boolean
enum Filter:
private[woof] case AtLeastLevel(level: LogLevel)
private[woof] case ExactLevel(level: LogLevel)
private[woof] case ClassRegex(regex: Regex)
private[woof] case MessageFilter(filter: String => Boolean)
private[woof] case LineNumberFilter(filter: Int => Boolean)
private[woof] case CompositeAnd(a: Filter, b: Filter)
private[woof] case CompositeOr(a: Filter, b: Filter)
private[woof] case Nothing
private[woof] case Everything
end Filter

object Filter:

val atLeastLevel: LogLevel => Filter = level => line => line.level >= level
val exactLevel: LogLevel => Filter = level => line => line.level == level
val regexFilter: Regex => Filter = regex => line => regex.matches(line.info.enclosingClass.printableName)
val nothing: Filter = _ => false
val everything: Filter = _ => true
val atLeastLevel: LogLevel => Filter = level => AtLeastLevel(level)
val exactLevel: LogLevel => Filter = level => ExactLevel(level)
val regexFilter: Regex => Filter = regex => ClassRegex(regex)
val nothing: Filter = Nothing
val everything: Filter = Everything
def lineNumberFilter(test: Int => Boolean): Filter = LineNumberFilter(test)
def messageFilter(test: String => Boolean): Filter = MessageFilter(test)

given Monoid[Filter] with
def empty: Filter = nothing
def combine(f: Filter, g: Filter): Filter = f or g

end Filter
extension (f: Filter)
infix def and(g: Filter): Filter = Filter.CompositeAnd(f, g)
infix def or(g: Filter): Filter = Filter.CompositeOr(f, g)

extension (f: Filter)
infix def and(g: Filter): Filter = line => f(line) && g(line)
infix def or(g: Filter): Filter = line => f(line) || g(line)
end Filter
53 changes: 5 additions & 48 deletions modules/core/shared/src/main/scala/org/legogroup/woof/Logger.scala
Original file line number Diff line number Diff line change
@@ -27,64 +27,21 @@ trait Logger[F[_]]:

end Logger

class DefaultLogger[F[_]: StringLocal: Monad: Clock](output: Output[F], outputs: Output[F]*)(using Printer, Filter)
extends Logger[F]:

val stringLocal: StringLocal[F] = summon[StringLocal[F]]
val printer: Printer = summon[Printer]
val filter: Filter = summon[Filter]

private[woof] def makeLogString(
level: LogLevel,
info: LogInfo,
message: String,
context: List[(String, String)],
): F[String] =
Clock[F].realTime
.map(d => EpochMillis(d.toMillis))
.map(now => summon[Printer].toPrint(now, level, info, message, context))

private[woof] def doOutputs(level: LogLevel, s: String): F[Unit] =
val allOutputs = outputs.prepended(output)
level match
case LogLevel.Error => allOutputs.traverse_(_.outputError(s))
case _ => allOutputs.traverse_(_.output(s))

override def doLog(level: LogLevel, message: String)(using logInfo: LogInfo): F[Unit] =
for
context <- summon[StringLocal[F]].ask
logLine <- makeLogString(level, logInfo, message, context)
_ <- doOutputs(level, logLine).whenA(summon[Filter](LogLine(level, logInfo, logLine, context)))
yield ()

end DefaultLogger

object DefaultLogger:
def makeIo(output: Output[IO], outputs: Output[IO]*)(using Clock[IO], Printer, Filter): IO[DefaultLogger[IO]] =
for given StringLocal[IO] <- ioStringLocal
yield new DefaultLogger[IO](output, outputs*)
end DefaultLogger

object Logger:

extension [F[_]: Logger, A](fa: F[A])
def withLogContext(key: String, value: String): F[A] =
Logger[F].stringLocal.local(fa)(ctx => ctx.appended((key, value)))
Logger[F].stringLocal.local(fa)(_.appended((key, value)))

def withLogContext(keyValuePairs: (String, String)*): F[A] =
Logger[F].stringLocal.local(fa)(_ ++ keyValuePairs)

type StringLocal[F[_]] = Local[F, List[(String, String)]]

def apply[F[_]](using l: Logger[F]): Logger[F] = l

given Printer = ColorPrinter()

val ioStringLocal = Local.makeIoLocal[List[(String, String)]]

@deprecated(s"Use `DefaultLogger.makeIo`") def makeIoLogger(output: Output[IO], outputs: Output[IO]*)(using
Clock[IO],
Printer,
Filter,
): IO[Logger[IO]] =
for given StringLocal[IO] <- ioStringLocal
yield new DefaultLogger[IO](output, outputs*)
val ioStringLocal: IO[StringLocal[IO]] = Local.makeIoLocal[List[(String, String)]]

end Logger
Original file line number Diff line number Diff line change
@@ -29,9 +29,9 @@ object Macro:
val nameExpr = Expr(EnclosingClass(name))

val position = Position.ofMacroExpansion
val filePath = if position.sourceFile.jpath != null then position.sourceFile.jpath else Paths.get(".")
val filePath = if position.sourceFile.path != null then position.sourceFile.path else Paths.get(".").getFileName
val lineNumber = Expr(position.startLine)
val file = Expr(filePath.getFileName.toString.split("/").takeRight(1).mkString)
val file = Expr(filePath.toString.split("/").takeRight(1).mkString)

'{ LogInfo($nameExpr, $file, $lineNumber) }
end logInfo
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ object JsonSupport:
"epochMillis" -> Json.fromLong(epochMillis.millis),
"timeStamp" -> Json.fromString(formattedTime),
"enclosingClass" -> Json.fromString(logLine.info.enclosingClass.fullName),
"lineNumber" -> Json.fromLong(logLine.info.lineNumber),
"message" -> Json.fromString(logLine.message),
"context" -> contextJson
)
Original file line number Diff line number Diff line change
@@ -77,7 +77,8 @@ class FilterSuite extends CatsEffectSuite:
test("log levels have priorities") {
import LogLevel.*
import cats.Order.catsKernelOrderingForOrder
assertEquals(LogLevel.values.toList.sorted, List(Trace, Debug, Info, Warn, Error))
val ordinalValues: List[(LogLevel, Int)] = LogLevel.values.sorted.toList.fproduct(_.ordinal)
assertEquals(ordinalValues, List(Trace -> 0, Debug -> 1, Info -> 2, Warn -> 3, Error -> 4))
}

end FilterSuite
Original file line number Diff line number Diff line change
@@ -19,14 +19,14 @@ class LoggerSuite extends CatsEffectSuite:
given Filter = Filter.everything

def clockOf(ref: Ref[IO, FiniteDuration]): Clock[IO] = new Clock[IO]:
def applicative = Applicative[IO]
def monotonic = ref.get
def realTime = ref.get
def applicative: Applicative[IO] = Applicative[IO]
def monotonic: IO[FiniteDuration] = ref.get
def realTime: IO[FiniteDuration] = ref.get

val constantClock: Clock[IO] = new Clock[IO]:
def applicative = Applicative[IO]
def monotonic = startTime.pure
def realTime = startTime.pure
def applicative: Applicative[IO] = Applicative[IO]
def monotonic: IO[FiniteDuration] = startTime.pure
def realTime: IO[FiniteDuration] = startTime.pure

test("log should make log line") {

@@ -48,7 +48,7 @@ class LoggerSuite extends CatsEffectSuite:

given Clock[IO] = constantClock
val theme = Theme.defaultTheme
given Printer = ColorPrinter(theme = theme, formatTime = testFormatTime)
given Printer = ColorPrinter(theme, formatTime = testFormatTime)
val reset = Theme.Style.Reset
val postfixFormat = theme.postfixFormat
// format: off
@@ -65,7 +65,6 @@ class LoggerSuite extends CatsEffectSuite:
end for
}


test("log concurrently") {

given Printer = NoColorPrinter(testFormatTime)
@@ -81,7 +80,7 @@ class LoggerSuite extends CatsEffectSuite:
yield assertEquals(
logs.split("\n").toList,
List(0, 200, 400, 600, 800)
.map(t => s"1987-05-31 13:37:00 [DEBUG] org.legogroup.woof.LoggerSuite: $t elapsed (LoggerSuite.scala:79)")
.map(t => s"1987-05-31 13:37:00 [DEBUG] org.legogroup.woof.LoggerSuite: $t elapsed (LoggerSuite.scala:78)")
)

executeWithStartTime(program)
@@ -97,8 +96,8 @@ class LoggerSuite extends CatsEffectSuite:
def programLogic(using Logger[IO]) = Logger[IO].info("some info")

// format: off
val expected = """1987-05-31 13:37:00 [INFO ] correlation-id=21c78595-ef21-4df0-987e-8af6aab6f346, locale=da-DK org.legogroup.woof.LoggerSuite: some info (LoggerSuite.scala:97)
1987-05-31 13:37:00 [INFO ] org.legogroup.woof.LoggerSuite: some info (LoggerSuite.scala:97)
val expected = """1987-05-31 13:37:00 [INFO ] correlation-id=21c78595-ef21-4df0-987e-8af6aab6f346, locale=da-DK org.legogroup.woof.LoggerSuite: some info (LoggerSuite.scala:96)
1987-05-31 13:37:00 [INFO ] org.legogroup.woof.LoggerSuite: some info (LoggerSuite.scala:96)
"""
// format: on
for
@@ -113,4 +112,27 @@ class LoggerSuite extends CatsEffectSuite:
yield assertEquals(output, expected)
end for
}

test("Add var-args context") {
given Clock[IO] = constantClock

given Printer = NoColorPrinter(testFormatTime)

def programLogic(using Logger[IO]) = Logger[IO].info("some info")

// format: off
val expected =
"""1987-05-31 13:37:00 [INFO ] foo=bar, baz=1337 org.legogroup.woof.LoggerSuite: some info (LoggerSuite.scala:121)
1987-05-31 13:37:00 [INFO ] org.legogroup.woof.LoggerSuite: some info (LoggerSuite.scala:121)
"""
// format: on
for
given StringLocal[IO] <- Local.makeIoLocal[List[(String, String)]]
strRef <- Ref[IO].of("")
given Logger[IO] = new DefaultLogger[IO](StringWriter(strRef))
_ <- programLogic.withLogContext(keyValuePairs = "foo" -> "bar", "baz" -> "1337")
_ <- programLogic
output <- strRef.get
yield assertEquals(output, expected)
}
end LoggerSuite
Original file line number Diff line number Diff line change
@@ -8,6 +8,8 @@ import org.legogroup.woof.json.JsonSupportSuite.epochMillisGen
import org.scalacheck.Prop.*
import org.scalacheck.*

import scala.collection.immutable.ArraySeq

class JsonSupportFunSuite extends munit.FunSuite:

test("Escape characters") {
@@ -30,7 +32,7 @@ class JsonSupportFunSuite extends munit.FunSuite:
val js = JsonSupport
val actual = js.toJsonString(logLine, epochMillis)
val expected =
"""{"level":"Debug","epochMillis":549459420000,"timeStamp":"1987-05-31T11:37:00Z","enclosingClass":"my.enclosing.Class","message":"my message","context":{"hey":"dude","foo":"bar"}}"""
"""{"level":"Debug","epochMillis":549459420000,"timeStamp":"1987-05-31T11:37:00Z","enclosingClass":"my.enclosing.Class","lineNumber":1337,"message":"my message","context":{"hey":"dude","foo":"bar"}}"""

assertEquals(actual, expected)
}
@@ -57,7 +59,8 @@ class JsonSupportSuite extends munit.ScalaCheckSuite:
val escaped = "\"" + JsonSupport.escape(s) + "\""
val parsed = io.circe.parser.parse(escaped)
val expected = Right(io.circe.Json.fromString(s))
if parsed != expected then println(s"Parsed: $parsed, expected: $expected, string: $escaped, original: $s ( ${s.toList.map(_.toInt)} )")
if parsed != expected then
println(s"Parsed: $parsed, expected: $expected, string: $escaped, original: $s ( ${s.toList.map(_.toInt)} )")

parsed == expected
}
@@ -77,7 +80,7 @@ object JsonSupportSuite:
lineNumber <- Gen.posNum[Int]
yield LogInfo(EnclosingClass(enclosingClassName.mkString(".")), fileName, lineNumber)

val levelGen = Gen.oneOf(LogLevel.values)
val levelGen = Gen.oneOf(ArraySeq.unsafeWrapArray(LogLevel.values))

val epochMillisGen = Gen.posNum[Long].map(EpochMillis.apply)

11 changes: 11 additions & 0 deletions modules/examples-scalajs/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Scala.js Hello, World</title>
</head>
<body>
<!-- include Scala.js compiled code -->
<script type="text/javascript" src="./.js/target/scala-3.1.2/examplesjs-fastopt.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package examples

import cats.effect.{IO, IOApp}
import org.legogroup.woof.{*, given}

/** To run this, open sbt and call: `examplesJs/fastOptJS` Then, open `modules/examples-scalajs/index.html` in your
* browser and look at the console
*/
object HelloScalaJs extends IOApp.Simple:

given Filter = Filter.everything
given Printer = NoColorPrinter()

def run =
for
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- Logger[IO].info("Hello, Scala.Js!")
yield ()

end HelloScalaJs
25 changes: 25 additions & 0 deletions modules/examples/src/main/scala/examples/CustomPrinter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package examples

import cats.effect.{IO, IOApp}
import org.legogroup.woof.{*, given}

object CustomPrinter extends IOApp.Simple:

given Filter = Filter.everything
given Printer with
def toPrint(
epochMillis: EpochMillis,
level: LogLevel,
info: LogInfo,
message: String,
context: List[(String, String)],
): String = s"LEVEL:${level.ordinal}, $message -- at epoch ${epochMillis.millis}"

def run =
for
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- Logger[IO].info("Hello, custom printer!")
_ <- Logger[IO].warn("Hello again, custom printer!")
_ <- Logger[IO].debug("Goodbye, custom printer!")
yield ()
end CustomPrinter
37 changes: 37 additions & 0 deletions modules/examples/src/main/scala/examples/CustomTheme.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package examples

import cats.effect.{IO, IOApp}
import org.legogroup.woof.{*, given}
import ColorPrinter.Theme
import ColorPrinter.Theme.*

object CustomTheme extends IOApp.Simple:

given Filter = Filter.everything
given Printer = ColorPrinter(customTheme)

private def customTheme: Theme = Theme(
levelFormat = {
case LogLevel.Info => Foreground.Black.withBackground(Background.Green)
case LogLevel.Warn => Foreground.Black.withBackground(Background.Yellow)
case LogLevel.Trace => Foreground.Black.withBackground(Background.White).withStyle(Style.Underlined)
case LogLevel.Error => Foreground.Black.withBackground(Background.Red).withStyle(Style.Bold)
case level => defaultTheme.levelFormat(level)
},
postfixFormat = Foreground.Cyan.withStyle(Style.Underlined),
reset = Style.Reset,
contextKey = Empty,
contextValue = Empty,
)

def run: IO[Unit] =
for
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- Logger[IO].info("just some info")
_ <- Logger[IO].debug("incoming debug")
_ <- Logger[IO].warn("warning ahead")
_ <- Logger[IO].trace("tracing")
_ <- Logger[IO].error("something went wrong")
yield ()

end CustomTheme
32 changes: 32 additions & 0 deletions modules/examples/src/main/scala/examples/FileOutput.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package examples

import cats.effect.{IO, IOApp, Resource}
import java.io.{FileWriter, PrintWriter}
import org.legogroup.woof.{*, given}

/*
* Run the program and inspect the files `woof.err`,
* and `woof.log` in the root of the project.
*/
object FileOutput extends IOApp.Simple:

given Filter = Filter.everything
given Printer = NoColorPrinter()

val fileOutput =
def writeLine(line: String, logPath: String) =
val writer = IO(PrintWriter(FileWriter(logPath, true)))
val res = Resource.make(writer)(w => IO(w.close))
res.use(w => IO(w.println(line)))
new Output[IO]:
def output(str: String) = writeLine(str, "woof.log")
def outputError(str: String) = writeLine(str, "woof.err")

def run =
for
given Logger[IO] <- DefaultLogger.makeIo(fileOutput, Output.fromConsole)
_ <- Logger[IO].info("Hello, Info!")
_ <- Logger[IO].error("Hello, Error!")
yield ()

end FileOutput
17 changes: 17 additions & 0 deletions modules/examples/src/main/scala/examples/HelloWorld.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package examples

import cats.effect.{IO, IOApp}
import org.legogroup.woof.{*, given}

object HelloWorld extends IOApp.Simple:

given Filter = Filter.everything
given Printer = ColorPrinter()

def run =
for
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- Logger[IO].info("Hello, World!")
yield ()

end HelloWorld
24 changes: 24 additions & 0 deletions modules/examples/src/main/scala/examples/LogLevelFromEnv.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package examples

import cats.effect.{IO, IOApp}
import org.legogroup.woof.{*, given}
import cats.syntax.all.*

object LogLevelFromEnv extends IOApp.Simple:

given Printer = ColorPrinter()

def run =
for
logLevelEnv <- IO.delay(sys.env.getOrElse("LOG_LEVEL", "Warn"))
logLevel <- LogLevel.valueOf(logLevelEnv).pure[IO]
given Filter = Filter.atLeastLevel(logLevel)
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- Logger[IO].trace("🔇 Hello from Trace 🔇")
_ <- Logger[IO].debug("🔇 Hello from Debug 🔊")
_ <- Logger[IO].info("🔇 Hello from Info 🔇")
_ <- Logger[IO].error("🔊 Hello from Error 🔊")
_ <- Logger[IO].warn("🔊 Hello from Warning 🔊")
yield ()

end LogLevelFromEnv
31 changes: 31 additions & 0 deletions modules/examples/src/main/scala/examples/TaglessFinal.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package examples

import org.legogroup.woof.{*, given}
import cats.syntax.all.*
import cats.Monad
import cats.effect.{IO, IOApp}

class TaglessFinal[F[_]: Logger: Monad]:

def run(): F[Unit] =
for
_ <- Logger[F].info("This")
_ <- Logger[F].warn("is")
_ <- Logger[F].debug("tagless")
_ <- Logger[F].trace("final")
yield ()

end TaglessFinal

object TaglessFinal extends IOApp.Simple:

given Filter = Filter.everything
given Printer = ColorPrinter()

def run =
for
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- TaglessFinal[IO].run()
yield ()

end TaglessFinal
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package examples.filters

import org.legogroup.woof.{*, given}
import cats.syntax.all.*
import cats.Monad
import cats.effect.{IO, IOApp}

class AtLeastLevel[F[_]: Logger: Monad]:

def run(): F[Unit] =
for
_ <- Logger[F].trace("This is [TRACE](this should not be shown.)")
_ <- Logger[F].debug("This is [DEBUG](this should not be shown.)")
_ <- Logger[F].info("This is [INFO]")
_ <- Logger[F].warn("This is [WARN]")
_ <- Logger[F].error("This is [ERROR]")
yield ()
end run

end AtLeastLevel

object AtLeastLevel extends IOApp.Simple:

given Filter = Filter.atLeastLevel(LogLevel.Info)
given Printer = ColorPrinter()

def run =
for
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- AtLeastLevel[IO].run()
yield ()

end AtLeastLevel
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package examples.filters

import org.legogroup.woof.{*, given}
import cats.effect.{IO, IOApp}
import cats.syntax.all.*

object CustomFilter extends IOApp.Simple:

given Printer = ColorPrinter()

def program(using Logger[IO]): IO[Unit] =
for
_ <- Logger[IO].info("Serious")
_ <- Logger[IO].info("business")
_ <- Logger[IO].info("logs")
_ <- Logger[IO].info("are")
_ <- Logger[IO].info("crazy")
_ <- Logger[IO].info("important")
yield ()

def evenLines: IO[Unit] =
given Filter = Filter.lineNumberFilter(_ % 2 == 0) // only print *even* lines
for
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- program
yield ()

def filterWords: IO[Unit] =
val filterWords = Set("Serious", "business", "crazy")
given Filter = Filter.messageFilter(_.split("\\s").exists(filterWords.contains))
for
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- program
yield ()

def filterNotWords: IO[Unit] =
val filterWords = Set("crazy")
given Filter = Filter.messageFilter(!_.split("\\s").exists(filterWords))
for
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- program
yield ()

def run = List(evenLines, filterWords, filterNotWords).traverse_(_ *> IO.delay(println()))

end CustomFilter
33 changes: 33 additions & 0 deletions modules/examples/src/main/scala/examples/filters/ExactLevel.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package examples.filters

import org.legogroup.woof.{*, given}
import cats.syntax.all.*
import cats.Monad
import cats.effect.{IO, IOApp}

class ExactLevel[F[_]: Logger: Monad]:

def run(): F[Unit] =
for
_ <- Logger[F].trace("This is [TRACE](this should not be shown.)")
_ <- Logger[F].debug("This is [DEBUG](this should not be shown.)")
_ <- Logger[F].info("This is [INFO](You should see only this line.)")
_ <- Logger[F].warn("This is [WARN](this should not be shown.)")
_ <- Logger[F].error("This is [ERROR](this should not be shown.)")
yield ()
end run

end ExactLevel

object ExactLevel extends IOApp.Simple:

given Filter = Filter.exactLevel(LogLevel.Info)
given Printer = ColorPrinter()

def run =
for
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- ExactLevel[IO].run()
yield ()

end ExactLevel
43 changes: 43 additions & 0 deletions modules/examples/src/main/scala/examples/filters/RegexFilter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package examples.filters

import org.legogroup.woof.{*, given}
import cats.syntax.all.*
import cats.Monad
import cats.effect.{IO, IOApp}
import scala.util.matching.Regex

class RegexFilter[F[_]: Logger: Monad]:

def run(): F[Unit] =
for
_ <- Logger[F].trace("This is [TRACE]")
_ <- Logger[F].debug("This is [DEBUG]")
yield ()
end run

end RegexFilter

class AnotherProgram[F[_]: Logger: Monad]:

def run(): F[Unit] =
for
_ <- Logger[F].trace("This is [TRACE]")
_ <- Logger[F].debug("This is [DEBUG]")
yield ()
end run

end AnotherProgram

object RegexFilter extends IOApp.Simple:

given Filter = Filter.regexFilter(".*.RegexFilter.*".r)
given Printer = ColorPrinter()

def run =
for
given Logger[IO] <- DefaultLogger.makeIo(Output.fromConsole)
_ <- RegexFilter[IO].run()
_ <- AnotherProgram[IO].run() // logs from AnotherProgram won't be shown here
yield ()

end RegexFilter
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package org.legogroup.woof.http4s

import cats.data.{Kleisli, NonEmptyList, OptionT}
import cats.effect.kernel.Sync
import cats.effect.std.UUIDGen
import cats.syntax.all.*
import cats.{Applicative, FlatMap, Monad}
import org.http4s.Header.Raw
@@ -16,23 +17,18 @@ object CorrelationIdMiddleware:

private val defaultTraceHeaderName: CIString = CIString("X-Trace-Id")

trait UUIDGen[F[_]]:
def gen: F[UUID]
given [F[_]: Sync]: UUIDGen[F] = new UUIDGen[F]:
def gen = Sync[F].delay(UUID.randomUUID)

private def getOrGenerate[F[_]: Applicative: UUIDGen](headerName: Option[CIString], request: Request[F]): F[String] =
val key = headerName.getOrElse(defaultTraceHeaderName)
request.headers
.get(key)
.map(_.head.value)
.fold(
summon[UUIDGen[F]].gen.map(_.toString),
summon[UUIDGen[F]].randomUUID.map(_.toString),
)(_.pure[F])

def middleware[F[_]: Logger: Monad: UUIDGen](headerName: Option[CIString] = None): HttpRoutes[F] => HttpRoutes[F] =
routes =>
Kleisli[([T] =>> OptionT[F, T]), Request[F], Response[F]] { request =>
Kleisli[[T] =>> OptionT[F, T], Request[F], Response[F]] { request =>
val key = headerName.getOrElse(defaultTraceHeaderName)
for
traceId <- OptionT.liftF(getOrGenerate(headerName, request))
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.legogroup.woof.http4s

import cats.data.{Kleisli, OptionT}
import cats.effect.std.UUIDGen
import cats.effect.{Clock, IO}
import cats.syntax.all.*
import cats.{Applicative, Monad}
import munit.CatsEffectSuite
import org.http4s.{HttpRoutes, Request, Response}
import org.legogroup.woof.{given, *}
import org.legogroup.woof.http4s.CorrelationIdMiddleware.UUIDGen
import org.legogroup.woof.{*, given}
import org.typelevel.ci.CIString

import java.time.ZoneId
@@ -24,9 +24,9 @@ class CorrelationIdMiddlewareSuite extends CatsEffectSuite:
given Printer = NoColorPrinter()
given Filter = Filter.everything

val testUuid = UUID.fromString("E20A27FE-5142-4E21-BA09-35BC6FB84591")
val testUuid: UUID = UUID.fromString("E20A27FE-5142-4E21-BA09-35BC6FB84591")
given UUIDGen[IO] with
def gen = testUuid.pure[IO]
def randomUUID: IO[UUID] = testUuid.pure[IO]

test("add trace id with middleware") {

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.legogroup.woof.slf4j2.WoofLoggerServiceProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.legogroup.woof.slf4j2

import cats.Id
import cats.effect.IO
import cats.effect.std.Dispatcher
import cats.effect.unsafe.IORuntime
import cats.effect.unsafe.IORuntime.global
import org.legogroup.woof.slf4j.Slf4jWoofLoggerImpl
import org.legogroup.woof.{EnclosingClass, LogInfo, LogLevel, LogLine, Logger as WLogger}
import org.slf4j.{Logger, Marker}

import java.io.File
import scala.util.Try

class WoofLogger(name: String) extends Logger with Slf4jWoofLoggerImpl[IO, Marker](name):
override def logger: Option[WLogger[IO]] = WoofLogger.logger
override def dispatcher: Option[Dispatcher[IO]] = WoofLogger.dispatcher
end WoofLogger

object WoofLogger:
var logger: Option[WLogger[IO]] = None
var dispatcher: Option[Dispatcher[IO]] = None
end WoofLogger

extension (w: WLogger[IO])
def registerSlf4j(using d: Dispatcher[IO]): IO[Unit] = IO.delay {
WoofLogger.logger = Some(w)
WoofLogger.dispatcher = Some(d)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.legogroup.woof.slf4j2

import org.slf4j.ILoggerFactory
import org.slf4j.helpers.NOP_FallbackServiceProvider

class WoofLoggerServiceProvider extends NOP_FallbackServiceProvider:
override def getLoggerFactory: ILoggerFactory = (name: String) => new WoofLogger(name)
end WoofLoggerServiceProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.legogroup.woof.slf4j2.WoofLoggerServiceProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.legogroup.woof.slf4j2

import cats.Id
import cats.effect.IO
import cats.effect.kernel.Clock
import cats.effect.std.Dispatcher
import org.legogroup.woof.*
import org.slf4j.LoggerFactory

import scala.concurrent.duration.*

class Slf4j2Suite extends munit.CatsEffectSuite:

override def munitIOTimeout = 10.minutes

val dispatcher = ResourceFunFixture(Dispatcher.sequential[IO](true))

dispatcher.test("should log stuff") { implicit dispatcher =>
given Printer = NoColorPrinter(testFormatTime)

given Filter = Filter.everything

given Clock[IO] = leetClock

for
stringOutput <- newStringWriter
woofLogger <- DefaultLogger.makeIo(stringOutput)
_ <- woofLogger.registerSlf4j
slf4jLogger <- IO.delay(LoggerFactory.getLogger(this.getClass))
_ <- IO.delay(slf4jLogger.info("HELLO, SLF4J!"))
result <- stringOutput.get
yield assertEquals(
result,
"1987-05-31 13:37:00 [INFO ] org.legogroup.woof.slf4j2.Slf4j2Suite: HELLO, SLF4J! (Slf4j2Suite.scala:30)\n",
)
end for
}

dispatcher.test("should log arrays of objects") { implicit dispatcher =>
given Printer = NoColorPrinter(testFormatTime)

given Filter = Filter.everything

given Clock[IO] = leetClock

for
stringOutput <- newStringWriter
woofLogger <- DefaultLogger.makeIo(stringOutput)
_ <- woofLogger.registerSlf4j
slf4jLogger <- IO.delay(LoggerFactory.getLogger(this.getClass))
_ <- IO.delay(slf4jLogger.info("HELLO, ARRAYS!", 1, Some(42), List(1337)))
result <- stringOutput.get
yield assertEquals(
result,
"1987-05-31 13:37:00 [INFO ] org.legogroup.woof.slf4j2.Slf4j2Suite: HELLO, ARRAYS! 1, Some(42), List(1337) (Slf4j2Suite.scala:51)\n",
)
end for
}

dispatcher.test("should respect log levels") { implicit dispatcher =>
given Printer = NoColorPrinter(testFormatTime)

given Filter = Filter.exactLevel(LogLevel.Warn)

given Clock[IO] = leetClock

for
stringWriter <- newStringWriter
woofLogger <- DefaultLogger.makeIo(stringWriter)
_ <- woofLogger.registerSlf4j
slf4jLogger <- IO.delay(LoggerFactory.getLogger(this.getClass))
_ <- IO.delay(slf4jLogger.info("INFO, SLF4J!"))
_ <- IO.delay(slf4jLogger.debug("DEBUG, SLF4J!"))
_ <- IO.delay(slf4jLogger.warn("WARN, SLF4J!"))
_ <- IO.delay(slf4jLogger.error("ERROR, SLF4J!"))
result <- stringWriter.get
yield assertEquals(
result,
"1987-05-31 13:37:00 [WARN ] org.legogroup.woof.slf4j2.Slf4j2Suite: WARN, SLF4J! (Slf4j2Suite.scala:74)\n",
)
end for
}

dispatcher.test("should not fail on null throwable") { implicit dispatcher =>
given Printer = NoColorPrinter(testFormatTime)

given Filter = Filter.everything

given Clock[IO] = leetClock

for
stringWriter <- newStringWriter
woofLogger <- DefaultLogger.makeIo(stringWriter)
_ <- woofLogger.registerSlf4j
slf4jLogger <- IO.delay(LoggerFactory.getLogger(this.getClass))
_ <- IO.delay(slf4jLogger.debug("null exception", null))
result <- stringWriter.get
yield assertEquals(
result,
"1987-05-31 13:37:00 [DEBUG] org.legogroup.woof.slf4j2.Slf4j2Suite: null exception (Slf4j2Suite.scala:96)\n",
)
end for
}

end Slf4j2Suite
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.legogroup.woof.slf4j

import cats.Functor
import cats.effect.std.Dispatcher
import org.legogroup.woof.*
import cats.syntax.all.*

import scala.util.Try

trait Slf4jWoofLoggerImpl[F[_], Marker](name: String):

private def getLogInfo() =
val stacktraceElements = (new Throwable).getStackTrace()
val lastIndex = stacktraceElements.reverse.indexWhere(s =>
s.getClassName == this.getClass.getName
) // after last mention of this class
val callingMethodIndex = stacktraceElements.size - lastIndex
val callingMethod: StackTraceElement = stacktraceElements(callingMethodIndex)
val enclosingClassName = EnclosingClass(callingMethod.getClassName)
val fileName = (enclosingClassName.fullName.replace('.', '/') + ".scala").split("\\/").takeRight(1).mkString
val lineNumber = callingMethod.getLineNumber - 1
LogInfo(enclosingClassName, fileName, lineNumber)
end getLogInfo

def getName(): String = name

def logger: Option[Logger[F]]
def dispatcher: Option[Dispatcher[F]]

private def log(level: LogLevel, msg: String): Unit =
(logger, dispatcher).mapN { (l, dispatcher) =>
val logInfo = getLogInfo()
val _ = dispatcher.unsafeRunSync(l.doLog(level, msg)(using logInfo))
}

def info(msg: String): Unit = log(LogLevel.Info, msg)
def debug(msg: String): Unit = log(LogLevel.Debug, msg)
def error(msg: String): Unit = log(LogLevel.Error, msg)
def trace(msg: String): Unit = log(LogLevel.Trace, msg)
def warn(msg: String): Unit = log(LogLevel.Warn, msg)

private def throwableMessage(msg: String, throwable: Throwable) = s"$msg " + Try(throwable.getMessage).getOrElse("")

def debug(msg: String, obj: Object): Unit = debug(s"$msg $obj")
def debug(msg: String, obj1: Object, obj2: Object): Unit = debug(s"$msg $obj1, $obj2")
def debug(msg: String, objs: Array[? <: Object]): Unit = debug(s"$msg ${objs.mkString(", ")}")
def debug(msg: String, throwable: Throwable): Unit = debug(throwableMessage(msg, throwable))
def debug(x$0: Marker, msg: String): Unit = debug(msg)
def debug(x$0: Marker, msg: String, obj: Object): Unit = debug(s"$msg, $obj")
def debug(x$0: Marker, msg: String, obj1: Object, obj2: Object): Unit = debug(s"$msg, $obj1, $obj2")
def debug(x$0: Marker, msg: String, objs: Array[? <: Object]): Unit = debug(s"$msg ${objs.mkString(", ")}")
def debug(x$0: Marker, msg: String, throwable: Throwable): Unit = debug(throwableMessage(msg, throwable))

def error(msg: String, obj: Object): Unit = error(s"$msg $obj")
def error(msg: String, obj1: Object, obj2: Object): Unit = error(s"$msg $obj1, $obj2")
def error(msg: String, objs: Array[? <: Object]): Unit = error(s"$msg ${objs.mkString(", ")}")
def error(msg: String, throwable: Throwable): Unit = error(throwableMessage(msg, throwable))
def error(x$0: Marker, msg: String): Unit = error(msg)
def error(x$0: Marker, msg: String, obj: Object): Unit = error(s"$msg $obj")
def error(x$0: Marker, msg: String, obj1: Object, obj2: Object): Unit = error(s"$msg $obj1, $obj2")
def error(x$0: Marker, msg: String, objs: Array[? <: Object]): Unit = error(s"$msg ${objs.mkString(", ")}")
def error(x$0: Marker, msg: String, throwable: Throwable): Unit = error(throwableMessage(msg, throwable))

def info(msg: String, obj: Object): Unit = info(s"$msg, $obj")
def info(msg: String, obj1: Object, obj2: Object): Unit = info(s"$msg, $obj1, $obj2")
def info(msg: String, objs: Array[? <: Object]): Unit = info(s"$msg ${objs.mkString(", ")}")
def info(msg: String, throwable: Throwable): Unit = info(throwableMessage(msg, throwable))
def info(x$0: Marker, msg: String): Unit = info(msg)
def info(x$0: Marker, msg: String, obj: Object): Unit = info(s"$msg, $obj")
def info(x$0: Marker, msg: String, obj1: Object, obj2: Object): Unit = info(s"$msg, $obj1, $obj2")
def info(x$0: Marker, msg: String, objs: Array[? <: Object]): Unit = info(s"$msg ${objs.mkString(", ")}")
def info(x$0: Marker, msg: String, throwable: Throwable): Unit = info(throwableMessage(msg, throwable))

def trace(msg: String, obj: Object): Unit = trace(s"$msg, $obj")
def trace(msg: String, obj1: Object, obj2: Object): Unit = trace(s"$msg, $obj1, $obj2")
def trace(msg: String, objs: Array[? <: Object]): Unit = trace(s"$msg ${objs.mkString(", ")}")
def trace(msg: String, throwable: Throwable): Unit = trace(throwableMessage(msg, throwable))
def trace(x$0: Marker, msg: String): Unit = trace(msg)
def trace(x$0: Marker, msg: String, obj: Object): Unit = trace(s"$msg, $obj")
def trace(x$0: Marker, msg: String, obj1: Object, obj2: Object): Unit = trace(s"$msg, $obj1, $obj2")
def trace(x$0: Marker, msg: String, objs: Array[? <: Object]): Unit = trace(s"$msg ${objs.mkString(", ")}")
def trace(x$0: Marker, msg: String, throwable: Throwable): Unit = trace(throwableMessage(msg, throwable))

def warn(msg: String, obj: Object): Unit = warn(s"$msg, $obj")
def warn(msg: String, obj1: Object, obj2: Object): Unit = warn(s"$msg, $obj1, $obj2")
def warn(msg: String, objs: Array[? <: Object]): Unit = warn(s"$msg ${objs.mkString(", ")}")
def warn(msg: String, throwable: Throwable): Unit = warn(throwableMessage(msg, throwable))
def warn(x$0: Marker, msg: String): Unit = warn(msg)
def warn(x$0: Marker, msg: String, obj: Object): Unit = warn(s"$msg, $obj")
def warn(x$0: Marker, msg: String, obj1: Object, obj2: Object): Unit = warn(s"$msg, $obj1, $obj2")
def warn(x$0: Marker, msg: String, objs: Array[? <: Object]): Unit = warn(s"$msg ${objs.mkString(", ")}")
def warn(x$0: Marker, msg: String, throwable: Throwable): Unit = warn(throwableMessage(msg, throwable))

private def testLevel(logLevel: LogLevel): Boolean = true

def isDebugEnabled(): Boolean = testLevel(LogLevel.Debug)
def isErrorEnabled(): Boolean = testLevel(LogLevel.Error)
def isInfoEnabled(): Boolean = testLevel(LogLevel.Info)
def isTraceEnabled(): Boolean = testLevel(LogLevel.Trace)
def isWarnEnabled(): Boolean = testLevel(LogLevel.Warn)

def isInfoEnabled(x$0: Marker): Boolean = isInfoEnabled()
def isTraceEnabled(x$0: Marker): Boolean = isTraceEnabled()
def isDebugEnabled(x$0: Marker): Boolean = isDebugEnabled()
def isErrorEnabled(x$0: Marker): Boolean = isErrorEnabled()
def isWarnEnabled(x$0: Marker): Boolean = isWarnEnabled()

end Slf4jWoofLoggerImpl
108 changes: 12 additions & 96 deletions modules/slf4j/src/main/scala/org/legogroup/woof/slf4j/WoofLogger.scala
Original file line number Diff line number Diff line change
@@ -2,111 +2,27 @@ package org.legogroup.woof.slf4j

import cats.Id
import cats.effect.IO
import cats.effect.std.Dispatcher
import cats.effect.unsafe.IORuntime
import cats.effect.unsafe.IORuntime.global
import org.legogroup.woof.{EnclosingClass, LogInfo, LogLevel, LogLine, Logger as WLogger}
import org.slf4j.Logger
import org.slf4j.{Logger, Marker}

import java.io.File
import scala.util.Try
class WoofLogger(name: String) extends Logger:

import WoofLogger.{given_IORuntime, logger}

private def getLogInfo() =
val stacktraceElements = Thread.currentThread().getStackTrace()
val lastIndex = stacktraceElements.reverse.indexWhere(s =>
s.getClassName == this.getClass.getName
) // after last mention of this class
val callingMethodIndex = stacktraceElements.size - lastIndex
val callingMethod: StackTraceElement = stacktraceElements(callingMethodIndex)
val enclosingClassName = EnclosingClass(callingMethod.getClassName)
val fileName = (enclosingClassName.fullName.replace('.', '/') + ".scala").split("\\/").takeRight(1).mkString
val lineNumber = callingMethod.getLineNumber - 1
LogInfo(enclosingClassName, fileName, lineNumber)
end getLogInfo

def getName(): String = name

private def log(level: LogLevel, msg: String) =
logger.foreach(_.doLog(level, msg)(using getLogInfo()).unsafeRunSync())
def info(msg: String): Unit = log(LogLevel.Info, msg)
def debug(msg: String): Unit = log(LogLevel.Debug, msg)
def error(msg: String): Unit = log(LogLevel.Error, msg)
def trace(msg: String): Unit = log(LogLevel.Trace, msg)
def warn(msg: String): Unit = log(LogLevel.Warn, msg)

private def throwableMessage(msg: String, throwable: Throwable) = s"$msg " + Try(throwable.getMessage).getOrElse("")

def debug(msg: String, obj: Object): Unit = debug(s"$msg $obj")
def debug(msg: String, obj1: Object, obj2: Object): Unit = debug(s"$msg $obj1, $obj2")
def debug(msg: String, objs: Array[? <: Object]): Unit = debug(s"$msg ${objs.mkString(", ")}")
def debug(msg: String, throwable: Throwable): Unit = debug(throwableMessage(msg, throwable))
def debug(x$0: org.slf4j.Marker, msg: String): Unit = debug(msg)
def debug(x$0: org.slf4j.Marker, msg: String, obj: Object): Unit = debug(s"$msg, $obj")
def debug(x$0: org.slf4j.Marker, msg: String, obj1: Object, obj2: Object): Unit = debug(s"$msg, $obj1, $obj2")
def debug(x$0: org.slf4j.Marker, msg: String, objs: Array[? <: Object]): Unit = debug(s"$msg ${objs.mkString(", ")}")
def debug(x$0: org.slf4j.Marker, msg: String, throwable: Throwable): Unit = debug(throwableMessage(msg, throwable))

def error(msg: String, obj: Object): Unit = error(s"$msg $obj")
def error(msg: String, obj1: Object, obj2: Object): Unit = error(s"$msg $obj1, $obj2")
def error(msg: String, objs: Array[? <: Object]): Unit = error(s"$msg ${objs.mkString(", ")}")
def error(msg: String, throwable: Throwable): Unit = error(throwableMessage(msg, throwable))
def error(x$0: org.slf4j.Marker, msg: String): Unit = error(msg)
def error(x$0: org.slf4j.Marker, msg: String, obj: Object): Unit = error(s"$msg $obj")
def error(x$0: org.slf4j.Marker, msg: String, obj1: Object, obj2: Object): Unit = error(s"$msg $obj1, $obj2")
def error(x$0: org.slf4j.Marker, msg: String, objs: Array[? <: Object]): Unit = error(s"$msg ${objs.mkString(", ")}")
def error(x$0: org.slf4j.Marker, msg: String, throwable: Throwable): Unit = error(throwableMessage(msg, throwable))

def info(msg: String, obj: Object): Unit = info(s"$msg, $obj")
def info(msg: String, obj1: Object, obj2: Object): Unit = info(s"$msg, $obj1, $obj2")
def info(msg: String, objs: Array[? <: Object]): Unit = info(s"$msg ${objs.mkString(", ")}")
def info(msg: String, throwable: Throwable): Unit = info(throwableMessage(msg, throwable))
def info(x$0: org.slf4j.Marker, msg: String): Unit = info(msg)
def info(x$0: org.slf4j.Marker, msg: String, obj: Object): Unit = info(s"$msg, $obj")
def info(x$0: org.slf4j.Marker, msg: String, obj1: Object, obj2: Object): Unit = info(s"$msg, $obj1, $obj2")
def info(x$0: org.slf4j.Marker, msg: String, objs: Array[? <: Object]): Unit = info(s"$msg ${objs.mkString(", ")}")
def info(x$0: org.slf4j.Marker, msg: String, throwable: Throwable): Unit = info(throwableMessage(msg, throwable))

def trace(msg: String, obj: Object): Unit = trace(s"$msg, $obj")
def trace(msg: String, obj1: Object, obj2: Object): Unit = trace(s"$msg, $obj1, $obj2")
def trace(msg: String, objs: Array[? <: Object]): Unit = trace(s"$msg ${objs.mkString(", ")}")
def trace(msg: String, throwable: Throwable): Unit = trace(throwableMessage(msg, throwable))
def trace(x$0: org.slf4j.Marker, msg: String): Unit = trace(msg)
def trace(x$0: org.slf4j.Marker, msg: String, obj: Object): Unit = trace(s"$msg, $obj")
def trace(x$0: org.slf4j.Marker, msg: String, obj1: Object, obj2: Object): Unit = trace(s"$msg, $obj1, $obj2")
def trace(x$0: org.slf4j.Marker, msg: String, objs: Array[? <: Object]): Unit = trace(s"$msg ${objs.mkString(", ")}")
def trace(x$0: org.slf4j.Marker, msg: String, throwable: Throwable): Unit = trace(throwableMessage(msg, throwable))

def warn(msg: String, obj: Object): Unit = warn(s"$msg, $obj")
def warn(msg: String, obj1: Object, obj2: Object): Unit = warn(s"$msg, $obj1, $obj2")
def warn(msg: String, objs: Array[? <: Object]): Unit = warn(s"$msg ${objs.mkString(", ")}")
def warn(msg: String, throwable: Throwable): Unit = warn(throwableMessage(msg, throwable))
def warn(x$0: org.slf4j.Marker, msg: String): Unit = warn(msg)
def warn(x$0: org.slf4j.Marker, msg: String, obj: Object): Unit = warn(s"$msg, $obj")
def warn(x$0: org.slf4j.Marker, msg: String, obj1: Object, obj2: Object): Unit = warn(s"$msg, $obj1, $obj2")
def warn(x$0: org.slf4j.Marker, msg: String, objs: Array[? <: Object]): Unit = warn(s"$msg ${objs.mkString(", ")}")
def warn(x$0: org.slf4j.Marker, msg: String, throwable: Throwable): Unit = warn(throwableMessage(msg, throwable))

private def testLevel(logLevel: LogLevel): Boolean = true

def isDebugEnabled(): Boolean = testLevel(LogLevel.Debug)
def isErrorEnabled(): Boolean = testLevel(LogLevel.Error)
def isInfoEnabled(): Boolean = testLevel(LogLevel.Info)
def isTraceEnabled(): Boolean = testLevel(LogLevel.Trace)
def isWarnEnabled(): Boolean = testLevel(LogLevel.Warn)

def isInfoEnabled(x$0: org.slf4j.Marker): Boolean = isInfoEnabled()
def isTraceEnabled(x$0: org.slf4j.Marker): Boolean = isTraceEnabled()
def isDebugEnabled(x$0: org.slf4j.Marker): Boolean = isDebugEnabled()
def isErrorEnabled(x$0: org.slf4j.Marker): Boolean = isErrorEnabled()
def isWarnEnabled(x$0: org.slf4j.Marker): Boolean = isWarnEnabled()

class WoofLogger(name: String) extends Logger with Slf4jWoofLoggerImpl[IO, Marker](name):
override def logger: Option[WLogger[IO]] = WoofLogger.logger
override def dispatcher: Option[Dispatcher[IO]] = WoofLogger.dispatcher
end WoofLogger

object WoofLogger:
private given IORuntime = global
var logger: Option[WLogger[IO]] = None
var logger: Option[WLogger[IO]] = None
var dispatcher: Option[Dispatcher[IO]] = None
end WoofLogger

extension (w: WLogger[IO]) def registerSlf4j: IO[Unit] = IO.delay(WoofLogger.logger = Some(w))
extension (w: WLogger[IO])
def registerSlf4j(using d: Dispatcher[IO]): IO[Unit] = IO.delay {
WoofLogger.logger = Some(w)
WoofLogger.dispatcher = Some(d)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.slf4j.impl

import cats.effect.IO
import cats.effect.std.Dispatcher
import org.legogroup.woof.slf4j.WoofLogger
import org.slf4j.spi.LoggerFactoryBinder
import org.slf4j.{ILoggerFactory, Logger, LoggerFactory}
@@ -18,7 +20,7 @@ class StaticLoggerBinder extends LoggerFactoryBinder:
end StaticLoggerBinder

object StaticLoggerBinder:
@static val REQUESTED_API_VERSION: String = "1.7.35"
@static val REQUESTED_API_VERSION: String = "1.7.36"
@static val SINGLETON: StaticLoggerBinder = StaticLoggerBinder()
@static def getSingleton: StaticLoggerBinder = SINGLETON
lazy val factory = WoofLoggerFactory()
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package org.legogroup.woof.slf4j
import cats.Id
import cats.effect.IO
import cats.effect.kernel.Clock
import cats.effect.std.Dispatcher
import org.legogroup.woof.*
import org.slf4j.LoggerFactory
import org.slf4j.impl.StaticLoggerBinder
@@ -11,9 +12,11 @@ import scala.concurrent.duration.*

class Slf4jSuite extends munit.CatsEffectSuite:

override def munitTimeout = 10.minutes
override def munitIOTimeout = 10.minutes

test("should log stuff") {
val dispatcher = ResourceFunFixture(Dispatcher.sequential[IO](true))

dispatcher.test("should log stuff") { implicit dispatcher =>
given Printer = NoColorPrinter(testFormatTime)
given Filter = Filter.everything
given Clock[IO] = leetClock
@@ -26,12 +29,12 @@ class Slf4jSuite extends munit.CatsEffectSuite:
result <- stringOutput.get
yield assertEquals(
result,
"1987-05-31 13:37:00 [INFO ] org.legogroup.woof.slf4j.Slf4jSuite: HELLO, SLF4J! (Slf4jSuite.scala:25)\n",
"1987-05-31 13:37:00 [INFO ] org.legogroup.woof.slf4j.Slf4jSuite: HELLO, SLF4J! (Slf4jSuite.scala:28)\n",
)
end for
}

test("should log arrays of objects") {
dispatcher.test("should log arrays of objects") { implicit dispatcher =>
given Printer = NoColorPrinter(testFormatTime)
given Filter = Filter.everything
given Clock[IO] = leetClock
@@ -44,12 +47,12 @@ class Slf4jSuite extends munit.CatsEffectSuite:
result <- stringOutput.get
yield assertEquals(
result,
"1987-05-31 13:37:00 [INFO ] org.legogroup.woof.slf4j.Slf4jSuite: HELLO, ARRAYS! 1, Some(42), List(1337) (Slf4jSuite.scala:43)\n",
"1987-05-31 13:37:00 [INFO ] org.legogroup.woof.slf4j.Slf4jSuite: HELLO, ARRAYS! 1, Some(42), List(1337) (Slf4jSuite.scala:46)\n",
)
end for
}

test("should respect log levels") {
dispatcher.test("should respect log levels") { implicit dispatcher =>
given Printer = NoColorPrinter(testFormatTime)
given Filter = Filter.exactLevel(LogLevel.Warn)
given Clock[IO] = leetClock
@@ -65,12 +68,12 @@ class Slf4jSuite extends munit.CatsEffectSuite:
result <- stringWriter.get
yield assertEquals(
result,
"1987-05-31 13:37:00 [WARN ] org.legogroup.woof.slf4j.Slf4jSuite: WARN, SLF4J! (Slf4jSuite.scala:63)\n",
"1987-05-31 13:37:00 [WARN ] org.legogroup.woof.slf4j.Slf4jSuite: WARN, SLF4J! (Slf4jSuite.scala:66)\n",
)
end for
}

test("should not fail on null throwable") {
dispatcher.test("should not fail on null throwable") { implicit dispatcher =>
given Printer = NoColorPrinter(testFormatTime)
given Filter = Filter.everything
given Clock[IO] = leetClock
@@ -84,7 +87,7 @@ class Slf4jSuite extends munit.CatsEffectSuite:
result <- stringWriter.get
yield assertEquals(
result,
"1987-05-31 13:37:00 [DEBUG] org.legogroup.woof.slf4j.Slf4jSuite: null exception (Slf4jSuite.scala:83)\n",
"1987-05-31 13:37:00 [DEBUG] org.legogroup.woof.slf4j.Slf4jSuite: null exception (Slf4jSuite.scala:86)\n",
)
end for
}
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.6.2
sbt.version=1.10.2
15 changes: 10 additions & 5 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.0")
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.0")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.6.1")
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.7.0")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")
addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7")

addDependencyTreePlugin