Natchez Doobie

natchez-extras-doobie provides a Transactor that adds spans for database queries to your traces.

Installation

val natchezExtrasVersion = "8.1.0"

libraryDependencies ++= Seq(
  "com.ovoenergy" %% "natchez-extras-doobie" % natchezExtrasVersion
)

Usage

If you want to run this example yourself you can use Docker to spin up a temporary Postgres instance:

docker run -d -p5432:5432 -e"POSTGRES_PASSWORD=password" -e"POSTGRES_USER=postgres" postgres

This example demonstrates connecting to a database with Doobie, wrapping the transactor into a TracedTransactor and then passing that into a tagless final application that queries the database.

import cats.data.Kleisli
import cats.effect._
import cats.syntax.functor._
import com.ovoenergy.natchez.extras.datadog.Datadog
import com.ovoenergy.natchez.extras.doobie.TracedTransactor
import doobie.implicits._
import doobie.util.transactor.Transactor
import natchez.{EntryPoint, Span}
import org.http4s.blaze.client.BlazeClientBuilder

object NatchezDoobie extends IOApp {

  type TracedIO[A] = Kleisli[IO, Span[IO], A]

  /**
   * Create a Natchez entrypoint that will send traces to Datadog
   */
  val datadog: Resource[IO, EntryPoint[IO]] =
    for {
      httpClient <- BlazeClientBuilder[IO].withDefaultSslContext.resource
      entryPoint <- Datadog.entryPoint(httpClient, "example-database", "default-resource")
    } yield entryPoint

  /**
   * Create a Doobie transactor that connects to a preexisting postgres instance
   * and then wrap it in TracedTransactor so it creates spans for queries
   */
  val transactor: Transactor[TracedIO] =
    TracedTransactor(
      service = "my-example-service-db",
      transactor = Transactor.fromDriverManager[IO](
        driver = "org.postgresql.Driver",
        url = "jdbc:postgresql:example",
        user = "postgres",
        password = "password", // of course don't hard code these details in your applications!
        logHandler = None,
      )
    )

  /**
   * Your application code doesn't need to know about the TracedIO type
   */
  def application[F[_]: Sync](db: Transactor[F]): F[ExitCode] =
    sql"SELECT * FROM example"
      .query[String]
      .to[List]
      .transact(db)
      .map(println)
      .as(ExitCode.Success)

  /**
   * To run the application we create a root span
   * and use that to turn the application from a TracedIO into an IO
   */
  def run(args: List[String]): IO[ExitCode] =
    datadog.use { entryPoint =>
      entryPoint.root("root_span").use { root =>
        application(transactor).run(root) 
      }
    }
}

Upon running this code you should see a trace like this show up in Datadog. Note that -db is automatically appended to the service name you provide TracedTransactor.

datadog trace