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

io.kaizensolutions.trace4cats.zio.extras.http4s.server.Http4sServerTracer.scala Maven / Gradle / Ivy

The newest version!
package io.kaizensolutions.trace4cats.zio.extras.http4s.server

import cats.data.{Kleisli, OptionT}
import trace4cats.ErrorHandler
import trace4cats.http4s.common.{Http4sHeaders, Http4sSpanNamer, Http4sStatusMapping, Request_, Response_}
import trace4cats.model.{AttributeValue, SampleDecision, SpanKind, SpanStatus}
import io.kaizensolutions.trace4cats.zio.extras.{ZSpan, ZTracer}
import org.http4s.{Headers, HttpApp, HttpRoutes, Request, Response}
import org.typelevel.ci.CIString
import zio.*

object Http4sServerTracer {

  /**
   * This has been adapted from
   * https://github.com/trace4cats/trace4cats-http4s/blob/master/modules/http4s-server/src/main/scala/io/janstenpickle/trace4cats/http4s/server/Http4sResourceKleislis.scala
   * &
   * https://github.com/trace4cats/trace4cats-http4s/blob/master/modules/http4s-server/src/main/scala/io/janstenpickle/trace4cats/http4s/server/ServerTracer.scala
   * in order to preserve the same semantics as the original implementation
   * found in trace4cats-http4s-server but to allow flexibility to vary the ZIO
   * Environment.
   *
   * @param tracer
   *   is the ZTracer to use for tracing
   * @param spanNamer
   *   is the SpanNamer to use for naming the spans
   * @param dropHeadersWhen
   *   is the predicate to use for determining whether to drop the headers
   * @param routes
   *   is the underlying HttpRoutes to trace
   * @tparam R
   *   is the ZIO Environment
   * @return
   *   an HttpRoutes that traces the underlying HttpRoutes
   */
  def traceRoutes[R, E](
    tracer: ZTracer,
    routes: HttpRoutes[ZIO[R, E, *]],
    spanNamer: Http4sSpanNamer = Http4sSpanNamer.methodWithPath,
    dropHeadersWhen: CIString => Boolean = Headers.SensitiveHeaders.contains,
    errorHandler: ErrorHandler = ErrorHandler.empty
  ): HttpRoutes[ZIO[R, E, *]] =
    Kleisli[
      OptionT[ZIO[R, E, *], *],
      Request[ZIO[R, E, *]],
      Response[ZIO[R, E, *]]
    ] { request =>
      val traceHeaders = Http4sHeaders.converter.from(request.headers)
      val nameOfSpan   = spanNamer(request)

      val tracedResponse =
        tracer.fromHeaders(
          headers = traceHeaders,
          kind = SpanKind.Server,
          name = nameOfSpan,
          errorHandler = errorHandler
        ) { span =>
          enrichRequest(request, dropHeadersWhen, span) *>
            routes.run(request).value.onExit {
              case Exit.Success(Some(response)) => enrichResponse(response, dropHeadersWhen, span)
              case Exit.Success(None)           => span.setStatus(SpanStatus.NotFound)
              case Exit.Failure(cause)          => enrichCause(cause, span)
            }
        }
      OptionT(tracedResponse)
    }

  /**
   * This has been adapted from
   * https://github.com/trace4cats/trace4cats-http4s/blob/master/modules/http4s-server/src/main/scala/io/janstenpickle/trace4cats/http4s/server/Http4sResourceKleislis.scala
   * &
   * https://github.com/trace4cats/trace4cats-http4s/blob/master/modules/http4s-server/src/main/scala/io/janstenpickle/trace4cats/http4s/server/ServerTracer.scala
   * in order to preserve the same semantics as the original implementation
   * found in trace4cats-http4s-server but to allow flexibility to vary the ZIO
   * Environment.
   *
   * @param tracer
   *   is the ZTracer to use for tracing
   * @param spanNamer
   *   is the SpanNamer to use for naming the spans
   * @param dropHeadersWhen
   *   is the predicate to use for determining whether to drop the headers
   * @param app
   *   is the underlying HttpApp to trace
   * @tparam R
   *   is the ZIO Environment
   * @return
   *   an HttpApp that traces the underlying HttpRoutes
   */
  def traceApp[R, E](
    tracer: ZTracer,
    app: HttpApp[ZIO[R, E, *]],
    spanNamer: Http4sSpanNamer = Http4sSpanNamer.methodWithPath,
    dropHeadersWhen: CIString => Boolean = _ => false,
    errorHandler: ErrorHandler = ErrorHandler.empty
  ): HttpApp[ZIO[R, E, *]] =
    Kleisli[
      ZIO[R, E, *],
      Request[ZIO[R, E, *]],
      Response[ZIO[R, E, *]]
    ] { request =>
      val traceHeaders = Http4sHeaders.converter.from(request.headers)
      val nameOfSpan   = spanNamer(request)

      tracer.fromHeaders(
        headers = traceHeaders,
        kind = SpanKind.Server,
        name = nameOfSpan,
        errorHandler = errorHandler
      ) { span =>
        enrichRequest(request, dropHeadersWhen, span) *>
          app.run(request).onExit {
            case Exit.Success(response) => enrichResponse(response, dropHeadersWhen, span)
            case Exit.Failure(cause)    => enrichCause(cause, span)
          }
      }
    }

  private def enrichRequest[R, E](
    request: Request[ZIO[R, E, *]],
    dropHeadersWhen: CIString => Boolean,
    span: ZSpan
  ): UIO[Unit] = {
    val spanSampled = span.context.traceFlags.sampled == SampleDecision.Include
    val reqFields   = Http4sHeaders.requestFields(request: Request_, dropHeadersWhen)

    if (spanSampled) span.putAll(reqFields*)
    else ZIO.unit
  }

  private def enrichResponse[R, E](
    response: Response[ZIO[R, E, *]],
    dropHeadersWhen: CIString => Boolean,
    span: ZSpan
  ): UIO[Unit] = {
    val spanSampled = span.context.traceFlags.sampled == SampleDecision.Include
    val enrichRespAttribs =
      if (spanSampled) span.putAll(Http4sHeaders.responseFields(response: Response_, dropHeadersWhen)*)
      else ZIO.unit

    enrichRespAttribs *> span.setStatus(Http4sStatusMapping.toSpanStatus(response.status))
  }

  private def enrichCause[E](cause: Cause[E], span: ZSpan): UIO[Unit] = {
    val spanSampled = span.context.traceFlags.sampled == SampleDecision.Include
    if (spanSampled) {
      if (cause.isDie) span.put("error.cause", AttributeValue.StringValue(cause.prettyPrint))
      else if (cause.isFailure) {
        val failure = cause.failureOption.get
        span.put(
          "error.message",
          failure match {
            case t: Throwable => AttributeValue.StringValue(t.getLocalizedMessage)
            case other        => AttributeValue.StringValue(other.toString)
          }
        )
      } else ZIO.unit
    } else ZIO.unit
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy