Skip to content

Commit

Permalink
build and log our own JSON output for traces instead of using opentel…
Browse files Browse the repository at this point in the history
…emetry-exporter-logging-otlp

opentelemetry-exporter-logging-otlp depends on a Jackson version that is
incompatible with the one used by Finagle, which unfortunately makes it
a nonstarter
  • Loading branch information
bpholt committed Dec 21, 2023
1 parent 6879ccf commit 3e90866
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 17 deletions.
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ lazy val core = project.in(file("core"))
"org.typelevel" %% "cats-effect-kernel" % "3.5.2",
"org.typelevel" %% "cats-effect-std" % "3.5.2",
"org.typelevel" %% "cats-mtl" % "1.4.0",
"org.typelevel" %% "log4cats-core" % "2.6.0",
"io.circe" %% "circe-literal" % "0.14.5",
"org.typelevel" %% "jawn-parser" % "1.5.1" % Provided,
"io.opentelemetry" % "opentelemetry-api" % "1.33.0",
"io.opentelemetry" % "opentelemetry-context" % "1.33.0",
"io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.33.0",
"io.opentelemetry" % "opentelemetry-exporter-logging-otlp" % "1.33.0",
"io.opentelemetry" % "opentelemetry-extension-trace-propagators" % "1.33.0",
"io.opentelemetry" % "opentelemetry-sdk" % "1.33.0",
"io.opentelemetry" % "opentelemetry-sdk-common" % "1.33.0",
Expand Down
115 changes: 115 additions & 0 deletions core/src/main/scala/com/dwolla/tracing/LoggingSpanExporter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.dwolla.tracing

import cats.*
import cats.syntax.all.*
import cats.effect.std.*
import com.dwolla.tracing.LoggingSpanExporter.*
import io.opentelemetry.sdk.common.CompletableResultCode
import io.opentelemetry.sdk.trace.`export`.SpanExporter
import io.opentelemetry.sdk.trace.data.{EventData, LinkData, SpanData, StatusData}
import org.typelevel.log4cats.Logger
import io.circe.literal.*
import io.circe.syntax.*
import io.circe.*
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.trace.{SpanContext, SpanKind, StatusCode}
import io.opentelemetry.proto.trace.v1.internal.{Span, Status}

import java.util
import scala.jdk.CollectionConverters.*

private[tracing] class LoggingSpanExporter[F[_] : Applicative : Logger](dispatcher: Dispatcher[F]) extends SpanExporter {
override def `export`(spans: util.Collection[SpanData]): CompletableResultCode =
dispatcher.unsafeRunSync {
Logger[F].info {
spans
.asScala
.toList
.map(_.asJson)
.map(_.noSpaces)
.mkString("\n")
}
.as(CompletableResultCode.ofSuccess())
}

override def flush(): CompletableResultCode =
CompletableResultCode.ofSuccess()

override def shutdown(): CompletableResultCode =
CompletableResultCode.ofSuccess()
}

object LoggingSpanExporter {
implicit val AttributesEncoder: Encoder[Attributes] =
Encoder[Map[String, String]].contramap {
_.asMap().asScala.toMap.map { case (k, v) =>
k.getKey -> v.toString
}
}

implicit val SpanKindEncoder: Encoder[SpanKind] =
Encoder[Int].contramap {
case SpanKind.INTERNAL => Span.SpanKind.SPAN_KIND_INTERNAL.getEnumNumber
case SpanKind.SERVER => Span.SpanKind.SPAN_KIND_SERVER.getEnumNumber
case SpanKind.CLIENT => Span.SpanKind.SPAN_KIND_CLIENT.getEnumNumber
case SpanKind.PRODUCER => Span.SpanKind.SPAN_KIND_PRODUCER.getEnumNumber
case SpanKind.CONSUMER => Span.SpanKind.SPAN_KIND_CONSUMER.getEnumNumber
}

implicit val EventDataEncoder: Encoder[EventData] = // TODO implement a real encoder
Encoder.AsObject.instance(_ => JsonObject.empty)

implicit val SpanContextEncoder: Encoder[SpanContext] = // TODO implement a real encoder
Encoder.AsObject.instance(_ => JsonObject.empty)

implicit val LinkDataEncoder: Encoder[LinkData] = Encoder.instance { ld =>
json"""{
"spanContext": ${ld.getSpanContext},
"attributes": ${ld.getAttributes}
}"""
}

implicit val StatusCodeEncoder: Encoder[StatusCode] = Encoder[Int].contramap {
case StatusCode.OK => Status.StatusCode.STATUS_CODE_OK.getEnumNumber
case StatusCode.ERROR => Status.StatusCode.STATUS_CODE_ERROR.getEnumNumber
case StatusCode.UNSET => Status.StatusCode.STATUS_CODE_UNSET.getEnumNumber
}

implicit val StatusDataEncoder: Encoder[StatusData] = Encoder.instance { sd =>
json"""{
"code": ${sd.getStatusCode},
"description": ${sd.getDescription}
}"""
}

implicit val SpanDataEncoder: Encoder[SpanData] = Encoder.instance { sp =>
json"""
{
"resource": {
"attributes": []
},
"scopeSpans": [
{
"scope": {
"name": ${sp.getInstrumentationScopeInfo.getName},
"attributes": ${sp.getInstrumentationScopeInfo.getAttributes}
},
"spans": [
{
"traceId": ${sp.getTraceId},
"spanId": ${sp.getSpanId},
"name": ${sp.getName},
"kind": ${sp.getKind},
"startTimeUnixNano": ${sp.getStartEpochNanos},
"endTimeUnixNano": ${sp.getEndEpochNanos},
"attributes": ${sp.getAttributes},
"events": ${sp.getEvents.asScala},
"links": ${sp.getLinks.asScala},
"status": ${sp.getStatus}
}
]
}
]
}"""
}
}
45 changes: 29 additions & 16 deletions core/src/main/scala/com/dwolla/tracing/OpenTelemetryAtDwolla.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,55 @@ import io.opentelemetry.context.propagation.{ContextPropagators, TextMapPropagat
import io.opentelemetry.contrib.aws.resource.{Ec2Resource, EcsResource}
import io.opentelemetry.contrib.awsxray.AwsXrayIdGenerator
import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator
import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingSpanExporter
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter
import io.opentelemetry.extension.trace.propagation.B3Propagator
import io.opentelemetry.sdk.resources.Resource as OTResource
import io.opentelemetry.sdk.trace.{SdkTracerProvider, SdkTracerProviderBuilder}
import io.opentelemetry.sdk.trace.{SdkTracerProvider, SdkTracerProviderBuilder, SpanProcessor}
import io.opentelemetry.sdk.trace.`export`.{BatchSpanProcessor, SimpleSpanProcessor}
import io.opentelemetry.semconv.ResourceAttributes
import natchez.*
import natchez.opentelemetry.OpenTelemetry
import org.typelevel.log4cats.LoggerFactory

object OpenTelemetryAtDwolla {
def apply[F[_] : Sync : Env](serviceName: String,
env: DwollaEnvironment): Resource[F, EntryPoint[F]] =
OpenTelemetryAtDwolla[F](serviceName, env, logTraces = false)
buildOtel(serviceName, env, None)

def apply[F[_] : Sync : Env](serviceName: String,
env: DwollaEnvironment,
logTraces: Boolean): Resource[F, EntryPoint[F]] =
def apply[F[_] : Async : Env : LoggerFactory](serviceName: String,
env: DwollaEnvironment,
logTraces: Boolean): Resource[F, EntryPoint[F]] =
logTraces
.guard[Option]
.traverse { _ =>
LoggerFactory[F]
.create
.toResource
.flatMap { implicit logger =>
Dispatcher.sequential(true)
.map(new LoggingSpanExporter(_))
.map(SimpleSpanProcessor.create)
}
}
.flatMap(buildOtel(serviceName, env, _))

private def buildOtel[F[_] : Sync : Env](serviceName: String,
env: DwollaEnvironment,
loggingProcessor: Option[SpanProcessor],
): Resource[F, EntryPoint[F]] =
OpenTelemetry.entryPoint(globallyRegister = true) { sdkBuilder =>
// TODO consider whether to use the OpenTelemetry SDK Autoconfigure module to support all the environment variables https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure
Env[F].get("OTEL_EXPORTER_OTLP_ENDPOINT")
.toResource
.flatMap { endpoint =>
Resource.fromAutoCloseable(Sync[F].delay {
val builder =
endpoint.foldl(OtlpGrpcSpanExporter.builder())(_ setEndpoint _).build()
val builder =
endpoint.foldl(OtlpGrpcSpanExporter.builder())(_ setEndpoint _).build()

BatchSpanProcessor.builder(builder).build()
})
BatchSpanProcessor.builder(builder).build()
})
.map(_.pure[List])
.map {
logTraces
.guard[Option]
.as(SimpleSpanProcessor.create(OtlpJsonLoggingSpanExporter.create()))
.toList ::: _
}
.map(loggingProcessor.toList ::: _)
}
.evalMap { spanProcessors =>
Sync[F].delay {
Expand Down

0 comments on commit 3e90866

Please sign in to comment.