zio/zio-logging

Adding logging environmental effect

Closed this issue ยท 16 comments

Here's a quick sketch of a logging API.

trait Logging {
  def logging: Logging.Service[Any]
}
object Logging {
  trait Service[-R] {
    // TODO: Define methods here
    def log(line: => String): ZIO[R, Nothing, Unit]
    def info(line: => String): ZIO[R, Nothing, Unit]
    def warning(line: => String): ZIO[R, Nothing, Unit]
    def error(line: => String): ZIO[R, Nothing, Unit]
    def errorCause(cause: Cause[Any]): ZIO[R, Nothing, Unit]
  }
}

package object logging extends Logging.Service[Logging] {
  ...
}

I don't think this is quite sufficient. We probably need to think about FiberRef integration and making sure we have a small set of operations that can be implemented by many different logging libraries (e.g. LogStage, all the ZIO loggers, slf4j, etc.).

This is a good idea. Are we trying to get this done in time for release 1.0?

This wouldn't work for our structured logging solution. I would propose this as a starting point:

trait Logging[F[_], Message] {
 def info(message: Message): F[Unit] 
  //... 
}

trait UnstructuredLogging[F[_]] extends Logging[F, String] 


// `Message` is a data structure with an implicit materializer from LogStage

trait StructuredLogging[F[_]] extends Logging[F, Message] {

}

Also we may wish to add withCustomContext method - logstage users found it a very useful feature.

Some references to the code which we may wish to reuse:

  1. The structure extraction macro
  2. The structured logger message
  3. Context API usage example

@NeQuissimus yes it is very similar. we would like to have very simple interface that can be back up with slf4j and other implementations. Moreover we would like to avoid dependencies to scalaz if possible.

Oh, I know... I meant to point out that you are welcome to take whatever I have in that repo.
It would come down to implementations of Logger.Service on how each framework handles the actual logging, of course.

@pshirshov why we need F[_] ? I was thinking that ZIO would be enough in this case. We try to keep this interface very simple.

It's not neccessary to make it polymorphic, though it may be very useful at least for those who wish to reuse the same logger for any code outside of ZIO so they may put identity there. We have our own typeclass wrapping ZIO for basic usecases and in general it's very useful. Though I don't insist (but I still think it may be a very good idea to build a hierarchy of typeclasses)

I'd prefer not to make the effect type polymorphic. At that point we're just trying to be a better log4cats, but it's far easier to own new markets than compete with existing ones.

I'd propose a slight modification on the above proposal:

trait AbstractLogging[Message] {
  def logging: AbstractLogging.Service[Message]
}
object AbstractLogging {
  trait Service[-R, -Message] {
    def info(message: Message): ZIO[R, Nothing, Unit]
    ...
  }
}
trait Logging extends AbstractLogging[String]
object Logging { type Service[-R] = AbstractLogging.Service[R, String] }

// In log-stage module:
trait StructuredLogging extends Logging[Message]
object StructuredLogging { type Service[-R] = AbstractLogging.Service[R, Message] }

Possibly we should add something like MDC:

  trait Service[-R, Message] {
    def context: FiberRef[Map[String, Message]]
    def info(message: Message): ZIO[R, Nothing, Unit]
    ...
  }

Then the context can be adjusted, although mapping that to MDC is obviously a lot of work and the subject of a pending PR.

Then the last point is whether or not to support the creation of loggers based on classes, etc., or whether "global loggers" are sufficient.

Then the last point is whether or not to support the creation of loggers based on classes, etc., or whether "global loggers" are sufficient.

A macro can do it. Also we may provide a classic way to define location specifically for Logging

Then the context can be adjusted, although mapping that to MDC is obviously a lot of work and the subject of a pending PR.

I don't think that MDC-like approach is a good thing. Just custom-provided contexts are very efficient (and very easy to handle and debug). def apply(userContext: (String, String)*): AbstractLogging should be just fine for 95% of the usecases (IMO) - user can always pass a logger bound to custom context while spawning a fork.

@pshirshov how would you handle things like request-id/correlation-id without MDC-like approach?

In our case we create a logger with custom context containing requestid and passing it further explicitly. Though I'm not trying to say that it's the best option possible, just easiest.

I don't think it hurts to have context, since we can always "compile that away" for logging implementations that don't support it (that is, embed context, if present, into the strings / messages passed to underlying implementation). Context, however, strikes me as an 80% solution: simple, good enough for the common case, and easily understandable.

Having the Logging service explicitly support context instead of passing it around separately seems much better to me, not only for reduced boilerplate, but also (and probably more importantly) because a Logging implementation could then also integrate that context with the MDC capabilities of the underlying logging backend, so that not only our own ZIO-aware application code benefits from it, but all that low-level code that we call into and that in turn logs via SLF4J directly would also pick up our "correlation id" and include it in their logging output.

That FiberRef <-> MDC <-> ThreadLocal mapping magic would be the most important feature for me.

@thiloplanz As mentioned by @jdegoes, there is a pending PR that allows FiberRefs to be read unsafely from side effecting code - basically allowing the "mapping magic" you mentioned above. A proof of concept implementation of fiber aware MDC logging for log4j2 based on a slightly outdated version of the aforementioned PR can be found here.

I have a logging library https://github.com/leigh-perry/log4zio that might be a useful starting point for zio-logging. Its main selling point is contravariant-functor-style composition of loggers. It is ZIO-based without dependencies on Cats or Scalaz.

It is in line with @jdegoes initial sketch at the top of this issue.