Skip to content

Commit

Permalink
add zio2 documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivan Malyshev committed Dec 19, 2022
1 parent 00f0bf6 commit 1a8d6c3
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 29 deletions.
41 changes: 35 additions & 6 deletions docs/tofu.logging.layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" % "<latest version in the badge in README>"
```

* **TofuLoggingProvider** provides JSON logs for logstash-logback-encoder. See [README on github](https://github.com/logstash/logstash-logback-encoder) for more details.

```xml
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<pattern>
<pattern>
{ "a": 1 }
</pattern>
</pattern>
<timestamp/>
<logLevel/>
<message/>
<provider class="tofu.logging.TofuLoggingProvider"/>
</providers>
</encoder>
</appender>
```

In addition, tofu has two own special Layouts:

```sbt
libraryDependencies += "tf.tofu" %% "tofu-logging-layout" % "<latest version in the badge in README>"
```

* **ELKLayout**
that outputs structured logs in JSON format. Example appender looks like that:

```xml
Expand All @@ -33,7 +62,7 @@ Luckily for us, tofu has two special Layouts:
</appender>
```

* [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
Expand All @@ -45,4 +74,4 @@ Luckily for us, tofu has two special Layouts:
</layout>
</encoder>
</appender>
```
```
213 changes: 213 additions & 0 deletions docs/tofu.logging.recipes.zio2.md
Original file line number Diff line number Diff line change
@@ -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" % "<latest version in the badge in README>"
```

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.
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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,
Expand Down Expand Up @@ -103,7 +106,6 @@ object ZLogsSpec extends ZIOSpecDefault {
}
}.provide(
ZLogging.Make.layerPlain,
contextProvider,
ZLayer(ZIO.serviceWith[ZLogging.Make](_.byName(loggerName))),
LogAppender.layer(loggerName)
)
Expand Down
3 changes: 2 additions & 1 deletion website/sidebars.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
},
{
Expand Down

0 comments on commit 1a8d6c3

Please sign in to comment.