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

trace4cats.stackdriver.StackdriverGrpcSpanExporter.scala Maven / Gradle / Ivy

package trace4cats.stackdriver

import java.time.Instant

import cats.Foldable
import cats.data.NonEmptyList
import cats.effect.kernel.{Async, Resource, Sync}
import cats.syntax.flatMap._
import cats.syntax.foldable._
import cats.syntax.functor._
import cats.syntax.show._
import com.google.api.core.{ApiFuture, ApiFutureCallback, ApiFutures}
import com.google.api.gax.core.FixedCredentialsProvider
import com.google.auth.Credentials
import com.google.auth.oauth2.GoogleCredentials
import com.google.cloud.trace.v2.{TraceServiceClient, TraceServiceSettings}
import com.google.devtools.cloudtrace.v2.Span.Attributes
import com.google.devtools.cloudtrace.v2.{AttributeValue => GAttributeValue, TruncatableString => GTruncatableString, _}
import com.google.protobuf.{BoolValue, Timestamp}
import com.google.rpc.Status
import trace4cats.kernel.SpanExporter
import trace4cats.model._
import trace4cats.stackdriver.StackdriverConstants._

import scala.concurrent.duration._
import scala.jdk.CollectionConverters._

object StackdriverGrpcSpanExporter {
  def apply[F[_]: Async, G[_]: Foldable](
    projectId: String,
    credentials: Option[Credentials] = None,
    requestTimeout: FiniteDuration = 5.seconds
  ): Resource[F, SpanExporter[F, G]] = {
    val projectName = ProjectName.of(projectId)

    val traceClient: F[TraceServiceClient] = Sync[F].delay {
      val creds = credentials.getOrElse(GoogleCredentials.getApplicationDefault())

      val clientBuilder = TraceServiceSettings.newBuilder
        .setCredentialsProvider(FixedCredentialsProvider.create(creds))

      clientBuilder
        .batchWriteSpansSettings()
        .setSimpleTimeoutNoRetries(org.threeten.bp.Duration.ofMillis(requestTimeout.toMillis))

      TraceServiceClient.create(clientBuilder.build())
    }

    def toTruncatableStringProto(string: String) = {
      val truncatableString = TruncatableString(string)
      GTruncatableString.newBuilder
        .setValue(truncatableString.value)
        .setTruncatedByteCount(truncatableString.truncatedByteCount)
        .build
    }

    def toTimestampProto(timestamp: Instant): Timestamp =
      Timestamp.newBuilder.setSeconds(timestamp.getEpochSecond).setNanos(timestamp.getNano).build

    def toDisplayName(spanName: String, spanKind: SpanKind) =
      spanKind match {
        case SpanKind.Server if !spanName.startsWith(ServerPrefix) => ServerPrefix + spanName
        case SpanKind.Client if !spanName.startsWith(ClientPrefix) => ClientPrefix + spanName
        case SpanKind.Consumer if !spanName.startsWith(ServerPrefix) => ServerPrefix + spanName
        case SpanKind.Producer if !spanName.startsWith(ClientPrefix) => ClientPrefix + spanName
        case _ => spanName
      }

    def toAttributesProto(attributes: Map[String, AttributeValue]): Attributes =
      attributes.toList
        .foldLeft(Attributes.newBuilder()) { case (acc, (k, v)) =>
          acc.putAttributeMap(
            k,
            (v match {
              case AttributeValue.StringValue(value) =>
                GAttributeValue.newBuilder().setStringValue(toTruncatableStringProto(value.value))
              case AttributeValue.BooleanValue(value) => GAttributeValue.newBuilder().setBoolValue(value.value)
              case AttributeValue.DoubleValue(value) =>
                GAttributeValue.newBuilder().setStringValue(toTruncatableStringProto(value.value.show))
              case AttributeValue.LongValue(value) => GAttributeValue.newBuilder().setIntValue(value.value)
              case vs: AttributeValue.AttributeList =>
                GAttributeValue.newBuilder().setStringValue(toTruncatableStringProto(vs.show))
            }).build()
          )

        }
        .build()

    def toStatusProto(status: SpanStatus) =
      Status
        .newBuilder()
        .setCode(status.canonicalCode)
        .build()

    def toLinksProto(links: Option[NonEmptyList[Link]]): Span.Links =
      links
        .fold(Span.Links.newBuilder())(_.foldLeft(Span.Links.newBuilder()) { (builder, link) =>
          builder.addLink(
            Span.Link
              .newBuilder()
              .setType(Span.Link.Type.PARENT_LINKED_SPAN)
              .setTraceId(link.traceId.show)
              .setSpanId(link.spanId.show)
          )
        })
        .build()

    def convert(completedSpan: CompletedSpan): Span = {
      val spanIdHex = completedSpan.context.spanId.show

      val spanName =
        SpanName.newBuilder.setProject(projectId).setTrace(completedSpan.context.traceId.show).setSpan(spanIdHex).build

      val spanBuilder =
        Span
          .newBuilder()
          .setName(spanName.toString)
          .setSpanId(spanIdHex)
          .setDisplayName(toTruncatableStringProto(toDisplayName(completedSpan.name, completedSpan.kind)))
          .setStartTime(toTimestampProto(completedSpan.start))
          .setEndTime(toTimestampProto(completedSpan.end))
          .setAttributes(toAttributesProto(completedSpan.allAttributes))
          .setStatus(toStatusProto(completedSpan.status))
          .setLinks(toLinksProto(completedSpan.links))

      val builder = completedSpan.context.parent.fold(spanBuilder) { parent =>
        spanBuilder.setParentSpanId(parent.spanId.show).setSameProcessAsParentSpan(BoolValue.of(!parent.isRemote))
      }

      builder.build()
    }

    def liftApiFuture[A](ffa: F[ApiFuture[A]]): F[A] = {
      for {
        fut <- ffa
        ec <- Async[F].executionContext
        a <- Async[F].async_[A] { cb =>
          ApiFutures.addCallback(
            fut,
            new ApiFutureCallback[A] {
              def onFailure(t: Throwable): Unit = cb(Left(t))
              def onSuccess(result: A): Unit = cb(Right(result))
            },
            ec.execute _
          )
        }
      } yield a
    }

    def write(client: TraceServiceClient, spans: G[CompletedSpan]): F[Unit] =
      for {
        request <- Sync[F].delay(
          BatchWriteSpansRequest
            .newBuilder()
            .setName(projectName.toString)
            .addAllSpans(
              spans
                .foldLeft(scala.collection.mutable.ListBuffer.empty[Span]) { (buf, span) =>
                  buf += convert(span)
                }
                .asJava
            )
            .build()
        )
        _ <- liftApiFuture(Sync[F].delay(client.batchWriteSpansCallable().futureCall(request)))
      } yield ()

    Resource.make(traceClient)(client => Sync[F].delay(client.shutdown())).map { client =>
      new SpanExporter[F, G] {
        override def exportBatch(batch: Batch[G]): F[Unit] = write(client, batch.spans)
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy