Structured logging context with cats-effect

JFTR, for now I’m trying to make do with some sugar on top of plain Kleisli. With this, code may look like this…

class Program[F[_] : CtxLogAware : MonadCancelThrow](client: Client[F]) {

  private def parseBody(body: String): F[String] =
    if (body.startsWith("!"))
      body.tail.pure[F]
    else
      new IllegalStateException("body parse error").raiseError[F, String]

  private def handleResp(status: Int, body: String): F[String] = {
    val res =
      if (status == 200) {
        parseBody(body)
      } else
        new IllegalStateException("unexpected status").raiseError[F, String]
    res
      .logOnFail("error handling request")
      .addLogCtx("status" -> status)
      .addLogCtx("body" -> body)
  }

  def run(url: String): F[String] =
    client
      .runRequest(url)
      .use((handleResp _).tupled)
      .logAround("running request")
      .addLogCtx("url" -> url)
}

override def run: IO[Unit] = {
  val client: Client[IO] = (_: String) => Resource.eval[IO, (Int, String)]((200, "body").pure[IO])
  val prog =
    new Program[StructLog[IO, *]](client.mapK(liftToStructLogFunctionK))
      .run("http://foo.test/")
      .addLogCtx("user" -> "usr-xyz")
  prog(Map.empty)
    .flatMap(IO.println)
}

Logging output like this:

{"level":"INFO","ctx":{"user":"usr-xyz","url":"http://foo.test/"},"message":"started: running request"}
{"level":"WARN","ctx":{"user":"usr-xyz","url":"http://foo.test/","body":"body","status":200},"message":"error handling request","stack_trace":"java.lang.IllegalStateException: body parse error [...]"}
{"level":"WARN","ctx":{"user":"usr-xyz","url":"http://foo.test/"},"message":"failed: running request","stack_trace":"java.lang.IllegalStateException: body parse error [...]"}

Not sure how happy I am with this… :thinking:

1 Like