Skip to content

Commit

Permalink
sdk-trace: add SpanExporter
Browse files Browse the repository at this point in the history
  • Loading branch information
iRevive committed Nov 21, 2023
1 parent 56e3809 commit fafb79c
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2023 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.otel4s.sdk.trace
package exporter

import cats.Applicative
import cats.Monoid
import cats.Parallel
import cats.data.NonEmptyList
import cats.syntax.foldable._
import cats.syntax.parallel._
import org.typelevel.otel4s.sdk.trace.data.SpanData

/** An interface that allows different tracing services to export recorded data
* for sampled spans in their own format.
*
* @see
* [[https://opentelemetry.io/docs/specs/otel/trace/sdk/#span-exporter]]
*
* @tparam F
* the higher-kinded type of a polymorphic effect
*/
trait SpanExporter[F[_]] {

/** Called to export sampled
* [[org.typelevel.otel4s.sdk.trace.data.SpanData SpanData]].
*
* '''Note''': the export operations can be performed simultaneously
* depending on the type of span processor being used. However, the batch
* span exporter will ensure that only one export can occur at a time.
*
* @param span
* the collection of sampled Spans to be exported
*/
def exportSpans(span: List[SpanData]): F[Unit]

/** Exports the collection of sampled
* [[org.typelevel.otel4s.sdk.trace.data.SpanData SpanData]] that have not
* yet been exported.
*
* '''Note''': the export operations can be performed simultaneously
* depending on the type of span processor being used. However, the batch
* span exporter will ensure that only one export can occur at a time.
*/
def flush: F[Unit]
}

object SpanExporter {

/** Creates a [[SpanExporter]] which delegates all exports to the exporters in
* order.
*/
def of[F[_]: Applicative: Parallel](
exporters: SpanExporter[F]*
): SpanExporter[F] =
if (exporters.sizeIs == 1) exporters.head
else exporters.combineAll

/** Creates a no-op implementation of the [[SpanExporter]].
*
* All export operations are no-op.
*/
def noop[F[_]: Applicative]: SpanExporter[F] =
new Noop

implicit def spanExporterMonoid[F[_]: Applicative: Parallel]
: Monoid[SpanExporter[F]] =
new Monoid[SpanExporter[F]] {
val empty: SpanExporter[F] =
noop[F]

def combine(x: SpanExporter[F], y: SpanExporter[F]): SpanExporter[F] =
(x, y) match {
case (that, _: Noop[F]) =>
that
case (_: Noop[F], other) =>
other
case (that: Multi[F], other: Multi[F]) =>
Multi(that.exporters.concatNel(other.exporters))
case (that: Multi[F], other) =>
Multi(that.exporters :+ other)
case (that, other: Multi[F]) =>
Multi(that :: other.exporters)
case (that, other) =>
Multi(NonEmptyList.of(that, other))
}
}

private final class Noop[F[_]: Applicative] extends SpanExporter[F] {
def exportSpans(span: List[SpanData]): F[Unit] = Applicative[F].unit
def flush: F[Unit] = Applicative[F].unit

override def toString: String = "SpanExporter.Noop"
}

private final case class Multi[F[_]: Parallel](
exporters: NonEmptyList[SpanExporter[F]]
) extends SpanExporter[F] {
def exportSpans(span: List[SpanData]): F[Unit] =
exporters.parTraverse_(_.exportSpans(span))

def flush: F[Unit] =
exporters.parTraverse_(_.flush)

override def toString: String =
s"SpanExporter.Multi(${exporters.map(_.toString).mkString_(", ")})"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import munit.CatsEffectSuite
import org.typelevel.otel4s.trace.SpanContext

class IdGeneratorSuite extends CatsEffectSuite {
private val Attempts = 1_000_000
private val Attempts = 100_000

generatorTest("generate a valid trace id") { generator =>
generator.generateTraceId
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2023 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.otel4s.sdk.trace.exporter

import cats.Applicative
import cats.data.Chain
import cats.effect.Concurrent
import cats.effect.Ref
import cats.syntax.functor._
import org.typelevel.otel4s.sdk.trace.data.SpanData

// todo: should be in the testkit package
final class InMemorySpanExporter[F[_]: Applicative] private (
storage: Ref[F, Chain[SpanData]]
) extends SpanExporter[F] {
def exportSpans(span: List[SpanData]): F[Unit] =
storage.update(_.concat(Chain.fromSeq(span)))

def flush: F[Unit] = Applicative[F].unit

def finishedSpans: F[Chain[SpanData]] =
storage.get

def reset: F[Unit] =
storage.set(Chain.empty)

override def toString: String =
"InMemorySpanExporter"
}

object InMemorySpanExporter {

def create[F[_]: Concurrent]: F[InMemorySpanExporter[F]] =
for {
storage <- Ref.of(Chain.empty[SpanData])
} yield new InMemorySpanExporter[F](storage)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2023 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.otel4s.sdk.trace
package exporter

import cats.effect.IO
import munit.CatsEffectSuite
import org.typelevel.otel4s.sdk.trace.data.SpanData

class SpanExporterSuite extends CatsEffectSuite {

test("create a no-op instance") {
val exporter = SpanExporter.noop[IO]

assertEquals(exporter.toString, "SpanExporter.Noop")
}

test("of (empty input) - use noop") {
val exporter = SpanExporter.of[IO]()

assertEquals(exporter.toString, "SpanExporter.Noop")
}

test("of (single input) - use this input") {
val data: SpanData = getSpanData

for {
inMemory <- InMemorySpanExporter.create[IO]
exporter <- IO.pure(SpanExporter.of(inMemory))
_ <- exporter.exportSpans(List(data))
spans <- inMemory.finishedSpans
} yield {
assertEquals(exporter.toString, "InMemorySpanExporter")
assertEquals(spans.toList, List(data))
}
}

test("of (multiple) - create a multi instance") {
val data: SpanData = getSpanData

for {
inMemoryA <- InMemorySpanExporter.create[IO]
inMemoryB <- InMemorySpanExporter.create[IO]
exporter <- IO.pure(SpanExporter.of(inMemoryA, inMemoryB))
_ <- exporter.exportSpans(List(data))
spansA <- inMemoryA.finishedSpans
spansB <- inMemoryB.finishedSpans
} yield {
assertEquals(spansA.toList, List(data))
assertEquals(spansB.toList, List(data))

assertEquals(
exporter.toString,
"SpanExporter.Multi(InMemorySpanExporter, InMemorySpanExporter)"
)
}
}

test("of (multiple) - flatten out nested multi instances") {
val data: SpanData = getSpanData

for {
inMemoryA <- InMemorySpanExporter.create[IO]
inMemoryB <- InMemorySpanExporter.create[IO]

multi1 <- IO.pure(SpanExporter.of(inMemoryA, inMemoryB))
multi2 <- IO.pure(SpanExporter.of(inMemoryA, inMemoryB))

exporter <- IO.pure(SpanExporter.of(multi1, multi2))

_ <- exporter.exportSpans(List(data))
spansA <- inMemoryA.finishedSpans
spansB <- inMemoryB.finishedSpans
} yield {
assertEquals(spansA.toList, List(data, data))
assertEquals(spansB.toList, List(data, data))

assertEquals(
exporter.toString,
"SpanExporter.Multi(InMemorySpanExporter, InMemorySpanExporter, InMemorySpanExporter, InMemorySpanExporter)"
)
}
}

private def getSpanData: SpanData =
Gens.spanData.sample.getOrElse(getSpanData)

}

0 comments on commit fafb79c

Please sign in to comment.