All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.kaizensolutions.trace4cats.zio.extras.doobie.TracedTransactor.scala Maven / Gradle / Ivy

package io.kaizensolutions.trace4cats.zio.extras.doobie

import cats.data.NonEmptyList
import doobie.*
import doobie.util.log
import io.kaizensolutions.trace4cats.zio.extras.ZTracer
import trace4cats.model.{AttributeValue, SpanStatus}
import zio.*
import zio.interop.catz.*

import scala.concurrent.duration.FiniteDuration

/**
 * A transactor that will trace all database operations performed by the
 * underlying mechanism.
 *
 * This implementation was heavily inspired by the natchez-doobie integration
 * Special thanks to Eli Kasik (soujiro32167) for his help and ideas
 */
object TracedTransactor {
  val layer: URLayer[ZTracer & Transactor[Task] & LogHandler[Task], Transactor[Task]] =
    ZLayer.fromZIO(
      for {
        tracer    <- ZIO.service[ZTracer]
        xa        <- ZIO.service[Transactor[Task]]
        logHander <- ZIO.service[LogHandler[Task]]
      } yield apply(xa, tracer, logHander)
    )

  val default: URLayer[Transactor[Task] & ZTracer, Transactor[Task]] =
    ZLayer.succeed(LogHandler.noop[Task]) >>> layer

  private def tracingLogHandler(tracer: ZTracer): LogHandler[Task] = (logEvent: log.LogEvent) => {

    def trace(execution: FiniteDuration, processing: Option[FiniteDuration], failure: Option[Throwable]) = {
      tracer.withSpan(logEvent.sql.linesIterator.map(_.trim).mkString) { span =>
        def attributes =
          NonEmptyList
            .fromList(logEvent.args.map(_.toString))
            .map(nel => "query.arguments" -> AttributeValue.StringList(nel))
            .toMap ++
            Map(
              "query.label"      -> AttributeValue.StringValue(logEvent.label),
              "query.execMillis" -> AttributeValue.LongValue(execution.toMillis),
              "query.sql"        -> AttributeValue.StringValue(logEvent.sql)
            ) ++
            processing.map(p => "query.processingMillis" -> AttributeValue.LongValue(p.toMillis))

        ZIO
          .when(span.isSampled)(
            span.putAll(attributes) *>
              ZIO.foreachDiscard(failure)(f => span.setStatus(SpanStatus.Internal(f.getMessage)))
          )
          .unit
      }
    }

    logEvent match {
      case log.Success(_, _, _, exec, processing)                    => trace(exec, Some(processing), None)
      case log.ProcessingFailure(_, _, _, exec, processing, failure) => trace(exec, Some(processing), Some(failure))
      case log.ExecFailure(_, _, _, exec, failure)                   => trace(exec, None, Some(failure))
    }
  }

  implicit class LogHandlerOps(val self: LogHandler[Task]) extends AnyVal {
    def andThen(other: LogHandler[Task]): LogHandler[Task] = (logEvent: log.LogEvent) =>
      self.run(logEvent) *> other.run(logEvent)
  }

  def apply(underlying: Transactor[Task], tracer: ZTracer, logHandler: LogHandler[Task]): Transactor[Task] =
    underlying.copy(interpret0 =
      KleisliInterpreter(logHandler.andThen(tracingLogHandler(tracer))).ConnectionInterpreter
    )
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy