{"id":13566472,"url":"https://github.com/LEGO/woof","last_synced_at":"2025-04-04T00:30:49.061Z","repository":{"id":39848830,"uuid":"415810838","full_name":"LEGO/woof","owner":"LEGO","description":"A pure Scala 3 logging library with no reflection","archived":false,"fork":false,"pushed_at":"2024-04-02T06:01:07.000Z","size":326,"stargazers_count":427,"open_issues_count":5,"forks_count":16,"subscribers_count":9,"default_branch":"main","last_synced_at":"2024-04-03T06:43:13.989Z","etag":null,"topics":["logger","logging","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/LEGO.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2021-10-11T06:53:34.000Z","updated_at":"2024-04-15T07:08:15.590Z","dependencies_parsed_at":"2023-02-14T10:00:52.851Z","dependency_job_id":"56184c57-867d-4469-a8ea-5e180cbbd40d","html_url":"https://github.com/LEGO/woof","commit_stats":{"total_commits":202,"total_committers":16,"mean_commits":12.625,"dds":0.6732673267326732,"last_synced_commit":"24e2f7d2e00ac557c1eb2c1dfc81e2943a26858a"},"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LEGO%2Fwoof","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LEGO%2Fwoof/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LEGO%2Fwoof/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LEGO%2Fwoof/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LEGO","download_url":"https://codeload.github.com/LEGO/woof/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247103306,"owners_count":20884023,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["logger","logging","scala"],"created_at":"2024-08-01T13:02:10.420Z","updated_at":"2025-04-04T00:30:46.135Z","avatar_url":"https://github.com/LEGO.png","language":"Scala","funding_links":[],"categories":["Scala","日志库"],"sub_categories":[],"readme":"# Woof\n\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.legogroup/woof-core_3/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.legogroup/woof-core_3)\n[![Scala CI](https://github.com/LEGO/woof/actions/workflows/scala.yml/badge.svg?branch=main)](https://github.com/LEGO/woof/actions/workflows/scala.yml)\n\nA **pure** _(in both senses of the word!)_ **Scala 3** logging library with **no runtime reflection**.\n\n![logo](dog-svgrepo-com.svg)\n\n# Table of Contents\n  - [Highlights](#highlights)\n    - [Cross platform](#cross-platform)\n  - [Installation](#installation)\n  - [Example](#example)\n  - [Can I use `SLF4J`?](#can-i-use-slf4j)\n    - [Limitations of SLF4J bindings](#limitations-of-slf4j-bindings)\n  - [Can I use `http4s`?](#can-i-use-http4s)\n  - [Structured Logging](#structured-logging)\n\n## Highlights\n\n* Pure **Scala 3** library\n* Made with _Cats Effect_\n* Macro based (_no runtime reflection_)\n* Configured with plain Scala code\n\n### Cross platform\n\n| Module  | JVM   | scala.js  | native |\n|---------|-------|-----------|--------|\n| core    | ✅    | ✅        | ✅     |\n| http4s  | ✅    | ✅        | ✅     |\n| slf4j   | ✅    | 🚫        | 🚫     |\n| slf4j-2 | ✅    | 🚫        | 🚫     |\n\n## Installation\n\n\u003e build.sbt\n\n```scala\nlibraryDependencies ++= Seq(\n  \"org.legogroup\" %% \"woof-core\"    % \"$VERSION\",\n  \"org.legogroup\" %% \"woof-slf4j\"   % \"$VERSION\", // only if you need to use Woof via slf4j 1.x.x\n  \"org.legogroup\" %% \"woof-slf4j-2\" % \"$VERSION\", // only if you need to use Woof via slf4j 2.x.x\n  \"org.legogroup\" %% \"woof-http4s\"  % \"$VERSION\", // only if you need to add correlation IDs in http4s \n)\n```\n\nYou 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`:\n\n```\nsbt:root\u003e examples/run\n\nMultiple main classes detected. Select one to run:\n [1] examples.AtLeastLevel\n [2] examples.CustomPrinter\n [3] examples.CustomTheme\n [4] examples.ExactLevel\n [5] examples.FileOutput\n [6] examples.HelloWorld\n [7] examples.LogLevelFromEnv\n [8] examples.RegexFilter\n [9] examples.TaglessFinal\n\nEnter number:\n```\n\nit 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)\n\n## Example \n\n```scala\nimport cats.effect.IO\nimport org.legogroup.woof.{given, *}\n\nval consoleOutput: Output[IO] = new Output[IO]:\n  def output(str: String)      = IO.delay(println(str))\n  def outputError(str: String) = output(str) // MDOC ignores stderr\n\ngiven Filter = Filter.everything\ngiven Printer = NoColorPrinter()\n\ndef program(using Logger[IO]): IO[Unit] = \n  for\n    _ \u003c- Logger[IO].debug(\"This is some debug\")\n    _ \u003c- Logger[IO].info(\"HEY!\")\n    _ \u003c- Logger[IO].warn(\"I'm warning you\")\n    _ \u003c- Logger[IO].error(\"I give up\")\n  yield ()\n\nval main: IO[Unit] = \n  for\n    given Logger[IO]  \u003c- DefaultLogger.makeIo(consoleOutput)\n    _                 \u003c- program\n  yield ()\n```\n\nand running it yields:\n\n```scala\nimport cats.effect.unsafe.implicits.global\nmain.unsafeRunSync()\n// 2023-03-13 09:00:42 [DEBUG] repl.MdocSession$.MdocApp: This is some debug (README.md:27)\n// 2023-03-13 09:00:42 [INFO ] repl.MdocSession$.MdocApp: HEY! (README.md:28)\n// 2023-03-13 09:00:42 [WARN ] repl.MdocSession$.MdocApp: I'm warning you (README.md:29)\n// 2023-03-13 09:00:42 [ERROR] repl.MdocSession$.MdocApp: I give up (README.md:30)\n```\n\nWe can also re-use the program and add context to our logger:\n\n```scala\nimport Logger.*\nval mainWithContext: IO[Unit] = \n  for\n    given Logger[IO]  \u003c- DefaultLogger.makeIo(consoleOutput)\n    _                 \u003c- program.withLogContext(\"trace-id\", \"4d334544-6462-43fa-b0b1-12846f871573\")\n    _                 \u003c- Logger[IO].info(\"Now the context is gone\")\n  yield ()\n```\n\nAnd running with context yields:\n\n```scala\nmainWithContext.unsafeRunSync()\n// 2023-03-13 09:00:42 [DEBUG] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.MdocApp: This is some debug (README.md:27)\n// 2023-03-13 09:00:42 [INFO ] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.MdocApp: HEY! (README.md:28)\n// 2023-03-13 09:00:42 [WARN ] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.MdocApp: I'm warning you (README.md:29)\n// 2023-03-13 09:00:42 [ERROR] trace-id=4d334544-6462-43fa-b0b1-12846f871573 repl.MdocSession$.MdocApp: I give up (README.md:30)\n// 2023-03-13 09:00:42 [INFO ] repl.MdocSession$.MdocApp: Now the context is gone (README.md:61)\n```\n\n## Can I use `SLF4J`?\n\nYes, 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\n`SLF4J` API, e.g. the filtering functionality in `woof` is much more flexible and thus does not map directly to, e.g., `isDebugEnabled`.\n\n\u003e NOTE: This is about implementing the `SLF4J` API for `woof`, **not** about sending `woof` logs INTO existing SLF4J implementations\n\nConsider this program which logs using the `SLF4J` API\n\n```scala\nimport org.slf4j.LoggerFactory\ndef programWithSlf4j: IO[Unit] = \n  for\n    slf4jLogger \u003c- IO.delay(LoggerFactory.getLogger(this.getClass))\n    _           \u003c- IO.delay(slf4jLogger.info(\"Hello from SLF4j!\"))\n    _           \u003c- IO.delay(slf4jLogger.warn(\"This is not the pure woof.\"))\n  yield ()\n```\n\nTo use this program with woof\n\n1. add `woof-slf4j` as a dependency to our program\n1. instantiate a `woof.Logger[F[_]]` as per usual\n1. register the _woof logger_ to the static log binder to allow the slf4j `LoggerFactory` to find it.\n\n\u003e Note that any logs that happen before registration are lost!\n\n```scala\nimport org.legogroup.woof.slf4j.*\nimport cats.effect.std.Dispatcher\nval mainSlf4j: IO[Unit] = \n  Dispatcher.sequential[IO].use{ implicit dispatcher =\u003e\n    for\n      woofLogger  \u003c- DefaultLogger.makeIo(consoleOutput)\n      _           \u003c- woofLogger.registerSlf4j\n      _           \u003c- programWithSlf4j\n    yield ()\n  }\n```\n\nand running it:\n\n```scala\nmainSlf4j.unsafeRunSync()\n```\n\n### Limitations of SLF4J bindings\n\nCurrently, markers do nothing. You can get the same behaviour easily with context when using the direct `woof` api with filters and printers.\n\n## Can I use `http4s`?\n\nYes 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.\n\n\u003e NOTE: The correlation ID is also added to the response header when using this middleware\n\nConsider the following `http4s` route:\n\n```scala\nimport org.http4s.{HttpRoutes, Response}\nimport cats.data.{Kleisli, OptionT}\nimport cats.syntax.functor.given\n\ndef routes(using Logger[IO]): HttpRoutes[IO] =\n  Kleisli(request =\u003e\n    OptionT\n      .liftF(Logger[IO].info(\"I got a request with trace id! :D\"))\n      .as(Response[IO]()),\n  )\n```\n\nWe create a tracing middleware from the above routes and call the resulting\nroute with an empty request.\n\n```scala\nimport org.http4s.Request\nimport org.legogroup.woof.http4s.CorrelationIdMiddleware\nimport cats.syntax.option.given\n\nval mainHttp4s: IO[Unit] = \n  for\n    given Logger[IO]  \u003c- DefaultLogger.makeIo(consoleOutput)\n    maybeResponse     \u003c- CorrelationIdMiddleware.middleware[IO]()(routes).run(Request[IO]()).value\n    responseHeaders   =  maybeResponse.map(_.headers).orEmpty\n    _                 \u003c- Logger[IO].info(s\"Got response headers: $responseHeaders\")\n  yield ()\n```\n\nFinally, running it, we see that the correlation ID is added to the log message inside the routes (transparently), and that\nthe correlation ID is also returned in the header of the response.\n\n\u003e NOTE: The correlation ID is _not_ present outside the routes, i.e. we have scoped it only to the service part of our code.\n\n```scala\nmainHttp4s.unsafeRunSync()\n// 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)\n// 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)\n```\n\n## Structured Logging\n\nStructured logging is useful when your logs are collected and inspected by a monitoring system. Having a well structured log output can save you\nhours of reg-ex'ing your way towards the root cause of a burning issue.\n\n`Woof` supports printing as `Json`:\n\n```scala\nimport Logger.*\nval contextAsJson: IO[Unit] = \n  given Printer = JsonPrinter()\n  for\n    given Logger[IO]  \u003c- DefaultLogger.makeIo(consoleOutput)\n    _                 \u003c- program.withLogContext(\"foo\", \"42\").withLogContext(\"bar\", \"1337\")\n    _                 \u003c- Logger[IO].info(\"Now the context is gone\")\n  yield ()\n```\n\nAnd running with context yields:\n\n```scala\ncontextAsJson.unsafeRunSync()\n// {\"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\"}}\n// {\"level\":\"Info\",\"epochMillis\":1678694443159,\"timeStamp\":\"2023-03-13T08:00:43Z\",\"enclosingClass\":\"repl.MdocSession$.MdocApp\",\"lineNumber\":27,\"message\":\"HEY!\",\"context\":{\"bar\":\"1337\",\"foo\":\"42\"}}\n// {\"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\"}}\n// {\"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\"}}\n// {\"level\":\"Info\",\"epochMillis\":1678694443159,\"timeStamp\":\"2023-03-13T08:00:43Z\",\"enclosingClass\":\"repl.MdocSession$.MdocApp\",\"lineNumber\":168,\"message\":\"Now the context is gone\",\"context\":{}}\n```\n\n\u003e 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.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FLEGO%2Fwoof","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FLEGO%2Fwoof","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FLEGO%2Fwoof/lists"}