Skip to content

Commit 4242256

Browse files
committed
Add otel4s TracedHandler tests
1 parent 4949c46 commit 4242256

File tree

3 files changed

+156
-9
lines changed

3 files changed

+156
-9
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ lazy val lambdaOtel4s = crossProject(JSPlatform, JVMPlatform)
194194
libraryDependencies ++= Seq(
195195
"org.typelevel" %%% "otel4s-core-trace" % otel4sVersion,
196196
"org.typelevel" %%% "otel4s-semconv" % otel4sVersion,
197+
"org.typelevel" %%% "otel4s-sdk-trace-testkit" % otel4sVersion % Test,
197198
"org.scalameta" %%% "munit-scalacheck" % munitVersion % Test,
198199
"org.typelevel" %%% "munit-cats-effect-3" % munitCEVersion % Test
199200
)
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
}

lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/TracedHandler.scala

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import cats.Monad
2020
import cats.syntax.all._
2121
import feral.lambda.Invocation
2222
import org.typelevel.otel4s.trace.Tracer
23+
import org.typelevel.otel4s.trace.SpanOps
24+
import feral.lambda.Context
2325

2426
object TracedHandler {
2527

@@ -33,19 +35,21 @@ object TracedHandler {
3335
event <- inv.event
3436
context <- inv.context
3537
res <- Tracer[F].joinOrRoot(attr.contextCarrier(event)) {
36-
val spanR =
37-
Tracer[F]
38-
.spanBuilder(context.functionName)
39-
.addAttributes(LambdaContextTraceAttributes(context))
40-
.withSpanKind(attr.spanKind)
41-
.addAttributes(attr.attributes(event))
42-
.build
43-
44-
spanR.surround {
38+
buildSpan(event, context).surround {
4539
for {
4640
res <- handler(event)
4741
} yield res
4842
}
4943
}
5044
} yield res
45+
46+
def buildSpan[F[_]: Tracer, Event](event: Event, context: Context[F])(
47+
implicit attr: EventSpanAttributes[Event]
48+
): SpanOps[F] =
49+
Tracer[F]
50+
.spanBuilder(context.functionName)
51+
.addAttributes(LambdaContextTraceAttributes(context))
52+
.withSpanKind(attr.spanKind)
53+
.addAttributes(attr.attributes(event))
54+
.build
5155
}

0 commit comments

Comments
 (0)