scoped-log4cats

We can create a wrapper for any log4cats logger that adds the fields we need to the logging context like the following:

import is.ashley.scoped.logging.ScopedLogger
import is.ashley.scoped.{ScopeToString, given}
import org.typelevel.log4cats.*

class TransactionalLogger[F[_]: Monad: Transactional](underlying: SelfAwareStructuredLogger[F])
    extends ScopedLogger[F](underlying):

  val context = Map("transaction_id" -> ScopeToString[F, Option[TransactionId]])

Assuming you are using the logback backend you can emit these fields in the logs with something like

<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
    <encoder>
      <pattern>logtype="application" transaction_id="%X{transaction_id}" req_time="%date{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}" loglevel="%level" threadname="%thread" classname="%logger{10}" linenumber="%file:%line" message="%msg"%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Then wherever we create our logger we can wrap it

given logger: SelfAwareStructuredLogger[IO] = TransactionalLogger(LoggerFactory[IO].getLogger)