|
| 1 | +package feral.lambda |
| 2 | +package otel4s |
| 3 | + |
| 4 | +import cats.effect.IO |
| 5 | +import cats.effect.kernel.Resource |
| 6 | +import cats.syntax.all._ |
| 7 | +import feral.lambda.IOLambda |
| 8 | +import io.circe.Decoder |
| 9 | +import io.circe.Encoder |
| 10 | +import io.circe.scalajs._ |
| 11 | +import munit.CatsEffectSuite |
| 12 | +import org.typelevel.otel4s.Attribute |
| 13 | +import org.typelevel.otel4s.sdk.testkit.trace.TracesTestkit |
| 14 | +import org.typelevel.otel4s.trace.SpanKind |
| 15 | + |
| 16 | +import java.util.concurrent.atomic.AtomicInteger |
| 17 | +import scala.scalajs.js |
| 18 | + |
| 19 | +class TracedHandlerSuite extends CatsEffectSuite { |
| 20 | + import TracedHandlerSuite._ |
| 21 | + |
| 22 | + val fixture = ResourceFixture(TracesTestkit.inMemory[IO]()) |
| 23 | + |
| 24 | + fixture.test("single root span is created for single invocation") { traces => |
| 25 | + traces.tracerProvider.tracer("test-tracer").get.flatMap { implicit tracer => |
| 26 | + val allocationCounter = new AtomicInteger |
| 27 | + val invokeCounter = new AtomicInteger |
| 28 | + |
| 29 | + val lambda = new IOLambda[TestEvent, String] { |
| 30 | + def handler = |
| 31 | + Resource.eval(IO(allocationCounter.getAndIncrement())).as { implicit inv => |
| 32 | + def fn(ev: TestEvent): IO[Option[String]] = |
| 33 | + for { |
| 34 | + _ <- IO(invokeCounter.getAndIncrement()) |
| 35 | + res = Some(ev.payload) |
| 36 | + } yield res |
| 37 | + |
| 38 | + TracedHandler(fn) |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + val event = TestEvent("1", "body") |
| 43 | + |
| 44 | + val functionName = "test-function-name" |
| 45 | + val run = IO.fromPromise(IO(lambda.handlerFn(event.asJsAny, DummyContext(functionName)))) |
| 46 | + |
| 47 | + for { |
| 48 | + res <- run |
| 49 | + spans <- traces.finishedSpans |
| 50 | + _ <- IO { |
| 51 | + assertEquals(res, "body".toString.asInstanceOf[js.UndefOr[js.Any]]) |
| 52 | + assertEquals(spans.length, 1) |
| 53 | + assertEquals(spans.headOption.map(_.name), Some(functionName)) |
| 54 | + } |
| 55 | + } yield () |
| 56 | + } |
| 57 | + |
| 58 | + } |
| 59 | + |
| 60 | + fixture.test("multiple root span per invocation created with function name ") { traces => |
| 61 | + traces.tracerProvider.tracer("test-tracer").get.flatMap { implicit tracer => |
| 62 | + val allocationCounter = new AtomicInteger |
| 63 | + val invokeCounter = new AtomicInteger |
| 64 | + |
| 65 | + val lambda = new IOLambda[TestEvent, String] { |
| 66 | + def handler = |
| 67 | + Resource.eval(IO(allocationCounter.getAndIncrement())).as { implicit inv => |
| 68 | + def fn(ev: TestEvent): IO[Option[String]] = |
| 69 | + for { |
| 70 | + _ <- IO(invokeCounter.getAndIncrement()) |
| 71 | + res = Some(ev.payload) |
| 72 | + } yield res |
| 73 | + |
| 74 | + TracedHandler(fn) |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + val functionName = "test-function-name" |
| 79 | + val chars = 'A'.to('C').toList |
| 80 | + val run = |
| 81 | + chars.zipWithIndex.map { case (c, i) => TestEvent(i.toString, c.toString) }.traverse { |
| 82 | + e => IO.fromPromise(IO(lambda.handlerFn(e.asJsAny, DummyContext(functionName)))) |
| 83 | + } |
| 84 | + |
| 85 | + val expectedSpanNames = List.fill(3)(functionName) |
| 86 | + |
| 87 | + for { |
| 88 | + res <- run |
| 89 | + spans <- traces.finishedSpans |
| 90 | + _ <- IO { |
| 91 | + assertEquals(res.length, chars.length) |
| 92 | + assertEquals(spans.length, chars.length) |
| 93 | + assertEquals(spans.map(_.name), expectedSpanNames) |
| 94 | + } |
| 95 | + } yield () |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + object DummyContext { |
| 100 | + def apply(fnName: String): facade.Context = new facade.Context { |
| 101 | + def functionName = fnName |
| 102 | + def functionVersion = "" |
| 103 | + def invokedFunctionArn = "" |
| 104 | + def memoryLimitInMB = "0" |
| 105 | + def awsRequestId = "" |
| 106 | + def logGroupName = "" |
| 107 | + def logStreamName = "" |
| 108 | + def identity = js.undefined |
| 109 | + def clientContext = js.undefined |
| 110 | + def getRemainingTimeInMillis(): Double = 0 |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | +} |
| 115 | + |
| 116 | +object TracedHandlerSuite { |
| 117 | + |
| 118 | + case class TestEvent(traceId: String, payload: String) |
| 119 | + |
| 120 | + object TestEvent { |
| 121 | + |
| 122 | + implicit val decoder: Decoder[TestEvent] = |
| 123 | + Decoder.forProduct2("traceId", "payload")(TestEvent.apply) |
| 124 | + implicit val encoder: Encoder[TestEvent] = |
| 125 | + Encoder.forProduct2("traceId", "payload")(ev => (ev.traceId, ev.payload)) |
| 126 | + |
| 127 | + implicit val attr: EventSpanAttributes[TestEvent] = |
| 128 | + new EventSpanAttributes[TestEvent] { |
| 129 | + |
| 130 | + override def contextCarrier(e: TestEvent): Map[String, String] = |
| 131 | + Map("trace_id" -> e.traceId) |
| 132 | + |
| 133 | + override def spanKind: SpanKind = SpanKind.Consumer |
| 134 | + |
| 135 | + override def attributes(e: TestEvent): List[Attribute[_]] = List.empty |
| 136 | + |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + def tracedLambda(allocationCounter: AtomicInteger, invokeCounter: AtomicInteger) = {} |
| 141 | + |
| 142 | +} |
0 commit comments