From 1a8d6c3c4083c35b0a15c812e756f1225602d279 Mon Sep 17 00:00:00 2001 From: Ivan Malyshev Date: Fri, 16 Dec 2022 20:35:27 +0500 Subject: [PATCH] add zio2 documentation --- docs/tofu.logging.layouts.md | 41 +++- docs/tofu.logging.recipes.zio2.md | 213 ++++++++++++++++++ .../zlogs/{zlogs.scala => ZLogs.scala} | 0 .../tofu/logging/zlogs/ZLogsSpec.scala | 46 ++-- website/sidebars.json | 3 +- 5 files changed, 274 insertions(+), 29 deletions(-) create mode 100644 docs/tofu.logging.recipes.zio2.md rename modules/interop/zio2/logging/src/main/scala-2/tofu/logging/zlogs/{zlogs.scala => ZLogs.scala} (100%) diff --git a/docs/tofu.logging.layouts.md b/docs/tofu.logging.layouts.md index 77ea8e74c..67374c5bb 100644 --- a/docs/tofu.logging.layouts.md +++ b/docs/tofu.logging.layouts.md @@ -7,12 +7,41 @@ title: Logback Layouts # Layouts Tofu is built upon [Logback](http://logback.qos.ch/) so it needs a custom `logback.xml` file with contextual logging -support. Tofu uses mechanism called markers to store context in logs, so it won't work with existing Layouts e.g. -with [Logstash-encoder](https://github.com/logstash/logstash-logback-encoder). +support. Tofu uses mechanism called markers to store context in logs, so it won't work with existing Layouts. -Luckily for us, tofu has two special Layouts: +Luckily for us, tofu brings a logging provider for _Logstash-encoder_: -* [ELKLayout](https://github.com/tofu-tf/tofu/blob/master/logging/layout/src/main/scala/tofu/logging/ELKLayout.scala) +```sbt +libraryDependencies += "tf.tofu" %% "tofu-logging-logstash-logback" % "" +``` + +* **TofuLoggingProvider** provides JSON logs for logstash-logback-encoder. See [README on github](https://github.com/logstash/logstash-logback-encoder) for more details. + +```xml + + + + + + { "a": 1 } + + + + + + + + + +``` + +In addition, tofu has two own special Layouts: + +```sbt +libraryDependencies += "tf.tofu" %% "tofu-logging-layout" % "" +``` + +* **ELKLayout** that outputs structured logs in JSON format. Example appender looks like that: ```xml @@ -33,7 +62,7 @@ Luckily for us, tofu has two special Layouts: ``` -* [ConsoleContextLayout](https://github.com/tofu-tf/tofu/blob/master/logging/layout/src/main/scala/tofu/logging/ConsoleContextLayout.scala) +* **ConsoleContextLayout** that outputs simple text logs. Example appender looks like that: ```xml @@ -45,4 +74,4 @@ Luckily for us, tofu has two special Layouts: -``` \ No newline at end of file +``` diff --git a/docs/tofu.logging.recipes.zio2.md b/docs/tofu.logging.recipes.zio2.md new file mode 100644 index 000000000..8069c571b --- /dev/null +++ b/docs/tofu.logging.recipes.zio2.md @@ -0,0 +1,213 @@ +--- +id: tofu.logging.recipes.zio2 +title: ZIO2 Logging +--- + +Tofu provides an implementation of `zio.ZLogger` and special annotations called `ZLogAnnotation` +for [ZIO logging facade](https://zio.dev/guides/tutorials/enable-logging-in-a-zio-application). +If you feel more confident with [Tofu Logging](./tofu.logging.main.entities.md#Logging) interface, `ULogging` +, `ZLogging.Make`, `ZLogs` are at your service. + +First add the following dependency: + +```sbt +libraryDependencies += "tf.tofu" %% "tofu-zio2-logging" % "" +``` + +Then import the package: + +```scala +import tofu.logging.zlogs._ +``` + +## ZIO 2 logging facade + +To use Tofu with ZIO logging facade just add `TofuZLogger` to your app runtime: + +```scala +object Main extends ZIOAppDefault { + + val program: UIO[Unit] = ZIO.log("Hello, ZIO logging!") + + override def run = { + program.logSpan("full_app") @@ ZIOAspect.annotated("foo", "bar") + }.provide( + Runtime.removeDefaultLoggers, + TofuZLogger.addToRuntime + ) + +} +``` + +The log will be: + +```json +{ + "level": "INFO", + "logger_name": "tofu.logging.zlogs.Main", + "message": "Hello, ZIO logging!", + "zSpans": { + "full_app": 440 + }, + "zAnnotations": { + "foo": "bar" + } +} +``` + +* __logger_name__ is parsed from `zio.Trace` which contains the location where log method is called +* all `zio.LogSpan` are collected in the json object named __zSpans__ +* all `zio.LogAnnotation` are collected in the json object named __zAnnotations__ (to avoid conflicts with Tofu + annotations) + +### ZLogAnnotation + +A specialized version of [LogAnnotation](./tofu.logging.annotation.md) allows you to add a context via ZIO aspects: + +```scala +import tofu.logging.zlogs.ZLogAnnotation._ + +val httpCode: ZLogAnnotation[Int] = make("httpCode") + +override def run = { + program @@ httpCode(204) @@ loggerName("MyLogger") +}.provide( + Runtime.removeDefaultLoggers, + TofuZLogger.addToRuntime +) +``` + +will produce: + +```json +{ + "level": "INFO", + "logger_name": "MyLogger", + "message": "Hello, ZIO logging!", + "httpCode": 204 +} +``` + +You can change the logger name via `ZLogAnnotation.loggerName`. + +`ZLogAnnotation.make[A]` implicitly requires a `Loggable[A]` instance, see more +in [Loggable](./tofu.logging.loggable.md) section. + +### TofuDefaultContext + +Using this service you can look up an element from the context added via `ZLogAnnotation`: + +```scala +val httpCode: ZLogAnnotation[Int] = make("httpCode") + +val program = { + for { + maybeCode <- ZIO.serviceWithZIO[TofuDefaultContext](_.getValue(httpCode)) // Some(204) + //... + } yield () +} @@ httpCode(204) +``` + +## ZIO implementation of Tofu Logging + +If you want more flexible Tofu Logging, `tofu-zio2-logging` provides some useful stuff: + +* `ULogging` - is a type alias for `Logging[UIO]`, logging methods of this logger look + like `def info(message: String, values: LoggedValue*): UIO[Unit]` + +* Logger factory type aliases: + - `ZLogging.Make` - is a type alias for `Logs[Id, UIO]`, produces plain instances of `ULogging`. + - `ULogs` - is a type alias for `Logs[UIO, UIO]`, produces instances of `ULogging` inside `UIO` effect. + +* `ZLogging.Make` and `ZLogs` objects provide corresponding factory instances + - `layerPlain` creates layer contains simple implementation of `ZLogging.Make` (or `ULogs`) + - `layerPlainWithContext` creates layer with an implementation of `ZLogging.Make` (or `ULogs`) producing loggers + which add the context to your logs from the `ContextProvider`. + This one is supposed to be provided at the app creation point via ZLayer-s. + - `layerContextual[R: Loggable]` makes a factory `ZMake[R]` (or `ZLogs[R]`) of contextual `ZLogging[R]` retrieving a + context from + the ZIO environment of the logging methods. This legacy approach is contrary to + ZIO [Service Pattern](https://zio.dev/reference/service-pattern/), so we won't cover it here. + +### ContextProvider + +```scala +trait ContextProvider { + def getCtx: UIO[LoggedValue] +} +``` + +Is required by `layerPlainWithContext` factory, every logger will log the provided `LoggedValue`. +To implement your own instance it can be convenient to use `ValuedContextProvider`: + +```scala +abstract class ValueContextProvider[A](implicit L: Loggable[A]) extends ContextProvider { + protected def getA: UIO[A] +} +``` + +Or you can use `TofuDefaultContext` (implements `ContextProvider`) which provides all tofu annotations added +via `ZLogAnnotation`: + +* `TofuDefaultContext.layerZioContextOff` — logs just tofu annotations +* `TofuDefaultContext.layerZioContextOn` — includes `LogSpan`-s and zio annotations + +## Example + +Let's write a simple service which logs a current date. + +```scala +import tofu.logging.zlogs._ +import zio._ + +val currentDate: ZLogAnnotation[LocalDate] = make("today") + +class FooBarService(logger: ULogging) { + def doLogs: UIO[Unit] = for { + now <- Clock.localDateTime + _ <- logger.info("Got current date {}", currentDate -> now.toLocalDate) + } yield {} +} + +object FooBarService { + val layer: URLayer[ULogs, FooBarService] = ZLayer( + ZIO.serviceWithZIO[ULogs](_.forService[FooBarService]) + .map(new FooBarService(_)) + ) +} +``` + +Then look at the main app: + +```scala +object Main extends ZIOAppDefault { + def run = { + for { + fooBar <- ZIO.service[FooBarService] + _ <- fooBar.doLogs + //... + } yield () + }.provide( + FooBarService.layer, + ZLogs.layerPlainWithContext, + TofuDefaultContext.layerZioContextOn + ) @@ ZIOAspect.annotated("foo", "bar") @@ httpCode(204) +} +``` + +The output of this program will be: + +```json +{ + "level": "INFO", + "logger_name": "tofu.logging.zlogs.FooBarService", + "message": "Got current date 2022-09-20", + "today": "2022-09-20", + "httpCode": 204, + "zAnnotations": { + "foo": "bar" + } +} +``` + +If `TofuDefaultContext.layerZioContextOff` was used instead of `layerZioContextOn`, `zAnnotations` wouldn't be logged. diff --git a/modules/interop/zio2/logging/src/main/scala-2/tofu/logging/zlogs/zlogs.scala b/modules/interop/zio2/logging/src/main/scala-2/tofu/logging/zlogs/ZLogs.scala similarity index 100% rename from modules/interop/zio2/logging/src/main/scala-2/tofu/logging/zlogs/zlogs.scala rename to modules/interop/zio2/logging/src/main/scala-2/tofu/logging/zlogs/ZLogs.scala diff --git a/modules/interop/zio2/logging/src/test/scala-2/tofu/logging/zlogs/ZLogsSpec.scala b/modules/interop/zio2/logging/src/test/scala-2/tofu/logging/zlogs/ZLogsSpec.scala index 1c464a34d..d34fc7d0d 100644 --- a/modules/interop/zio2/logging/src/test/scala-2/tofu/logging/zlogs/ZLogsSpec.scala +++ b/modules/interop/zio2/logging/src/test/scala-2/tofu/logging/zlogs/ZLogsSpec.scala @@ -1,17 +1,17 @@ package tofu.logging.zlogs -import zio._ -import zio.test._ -import TestStuff._ import ch.qos.logback.classic.Level import io.circe.{Json, JsonObject} -import tofu.logging.{LogTree, LoggedValue} import tofu.logging.impl.ContextMarker +import tofu.logging.zlogs.TestStuff._ import tofu.logging.zlogs.TofuDefaultContextSpec.{testCount, testStatus, testUser} +import tofu.logging.{LogTree, LoggedValue} +import zio._ +import zio.test._ object ZLogsSpec extends ZIOSpecDefault { - val loggerName: String = classOf[ZLogsSpec.type].getName.replace("$", "") + val loggerName: String = this.getClass.getName.replace("$", "") val myLoggerName = "myLogger" @@ -24,26 +24,29 @@ object ZLogsSpec extends ZIOSpecDefault { override def spec: Spec[TestEnvironment with Scope, Any] = suite("Tofu ZIO2 Logging")( test("TofuZLogger parses default logger name") { + val expectedArgs: Set[Json] = + Set[LoggedValue](LogKey.status -> testStatus, LogKey.count -> testCount).map(LogTree(_)) addLogSpan("testSpan")( - for { - _ <- TestClock.adjust(5.seconds) - _ <- ZIO.log("Some message") - events <- LogAppender.events - } yield { - val e = events.head - val ctx = e.getMarker.asInstanceOf[ContextMarker].ctx - assertTrue(LogTree(ctx) == TofuDefaultContextSpec.justZIOContextJson) && - assertTrue( - e.getArgumentArray.toSet - .asInstanceOf[Set[LoggedValue]] - .map(LogTree(_)) == - Set(LogKey.status -> testStatus, LogKey.count -> testCount).map(LogTree(_)) - ) && - assertTrue(e.getLevel == Level.WARN) + LogLevel.Warning { + for { + _ <- TestClock.adjust(5.seconds) + _ <- ZIO.log("Some message") + events <- LogAppender.events + } yield { + val e = events.head + val ctx = e.getMarker.asInstanceOf[ContextMarker].ctx + assertTrue(LogTree(ctx) == TofuDefaultContextSpec.justZIOContextJson) && + assertTrue( + e.getArgumentArray.toSet + .asInstanceOf[Set[LoggedValue]] + .map(LogTree(_)) == expectedArgs + ) && + assertTrue(e.getLevel == Level.WARN) + } } ) @@ ZIOAspect.annotated("foo", "bar") @@ LogKey.status(testStatus) @@ LogKey.count( testCount - ) @@ LogLevel.Warning + ) }.provide( Runtime.removeDefaultLoggers, TofuZLogger.addToRuntime, @@ -103,7 +106,6 @@ object ZLogsSpec extends ZIOSpecDefault { } }.provide( ZLogging.Make.layerPlain, - contextProvider, ZLayer(ZIO.serviceWith[ZLogging.Make](_.byName(loggerName))), LogAppender.layer(loggerName) ) diff --git a/website/sidebars.json b/website/sidebars.json index e3d3b7da4..4bcb3eb77 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -37,7 +37,8 @@ "tofu.logging.recipes.service", "tofu.logging.recipes.context", "tofu.logging.recipes.auto", - "tofu.logging.recipes.zio" + "tofu.logging.recipes.zio", + "tofu.logging.recipes.zio2" ] }, {