Skip to content

Commit db98aad

Browse files
committed
use local semantics in TracedHandler and deprecate the explicit use of Trace.ioTrace
1 parent 0bb80f3 commit db98aad

7 files changed

Lines changed: 213 additions & 52 deletions

File tree

examples/shared/src/main/scala/feral/examples/Http4sLambda.scala renamed to examples/shared/src/main/scala-2/feral/examples/Http4sHandler.scala

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616

1717
package feral.examples
1818

19-
import cats.effect._
2019
import cats.effect.std.Random
20+
import cats.effect.{Trace => _, _}
21+
import cats.mtl._
2122
import feral.lambda._
2223
import feral.lambda.events._
2324
import feral.lambda.http4s._
24-
import natchez.Trace
25+
import natchez._
2526
import natchez.http4s.NatchezMiddleware
2627
import natchez.xray.XRay
2728
import org.http4s.HttpApp
@@ -43,6 +44,9 @@ import org.http4s.syntax.all._
4344
object http4sHandler
4445
extends IOLambda[ApiGatewayProxyEventV2, ApiGatewayProxyStructuredResultV2] {
4546

47+
private type Handler =
48+
Invocation[IO, ApiGatewayProxyEventV2] => IO[Option[ApiGatewayProxyStructuredResultV2]]
49+
4650
/**
4751
* Actually, this is a `Resource` that builds your handler. The handler is acquired exactly
4852
* once when your Lambda starts and is permanently installed to process all incoming events.
@@ -52,29 +56,31 @@ object http4sHandler
5256
* event come from? Because accessing the event via `Invocation` is now also an effect in
5357
* `IO`, it becomes a step in your program.
5458
*/
55-
def handler = for {
56-
entrypoint <- Resource
57-
.eval(Random.scalaUtilRandom[IO])
58-
.flatMap(implicit r => XRay.entryPoint[IO]())
59-
client <- EmberClientBuilder.default[IO].build
60-
} yield { implicit inv => // the Invocation provides access to the event and context
59+
def handler: Resource[IO, Handler] =
60+
for {
61+
entrypoint <- Resource
62+
.eval(Random.scalaUtilRandom[IO])
63+
.flatMap(implicit r => XRay.entryPoint[IO]())
64+
client <- EmberClientBuilder.default[IO].build
65+
implicit0(local: Local[IO, Span[IO]]) <- IO.local(Span.noop[IO]).toResource
66+
} yield { implicit inv => // the Invocation provides access to the event and context
6167

62-
// a middleware to add tracing to any handler
63-
// it extracts the kernel from the event and adds tags derived from the context
64-
TracedHandler(entrypoint) { implicit trace =>
65-
val tracedClient = NatchezMiddleware.client(client)
68+
// a middleware to add tracing to any handler
69+
// it extracts the kernel from the event and adds tags derived from the context
70+
TracedHandler(entrypoint) { implicit trace =>
71+
val tracedClient = NatchezMiddleware.client(client)
6672

67-
// a "middleware" that converts an HttpApp into a ApiGatewayProxyHandler
68-
ApiGatewayProxyHandlerV2(myApp[IO](tracedClient))
73+
// a "middleware" that converts an HttpApp into a ApiGatewayProxyHandler
74+
ApiGatewayProxyHandlerV2(myApp[IO](tracedClient))
75+
}
6976
}
70-
}
7177

7278
/**
7379
* Nothing special about this method, including its existence, just an example :)
7480
*/
7581
def myApp[F[_]: Concurrent: Trace](client: Client[F]): HttpApp[F] = {
7682
implicit val dsl = Http4sDsl[F]
77-
import dsl._
83+
import dsl.*
7884

7985
val routes = HttpRoutes.of[F] {
8086
case GET -> Root / "foo" => Ok("bar")
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2021 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package feral.examples
18+
19+
import cats.effect.std.Random
20+
import cats.effect.{Trace as _, *}
21+
import cats.mtl.*
22+
import feral.lambda.*
23+
import feral.lambda.events.*
24+
import feral.lambda.http4s.*
25+
import natchez.*
26+
import natchez.http4s.NatchezMiddleware
27+
import natchez.xray.XRay
28+
import org.http4s.HttpApp
29+
import org.http4s.HttpRoutes
30+
import org.http4s.client.Client
31+
import org.http4s.dsl.Http4sDsl
32+
import org.http4s.ember.client.EmberClientBuilder
33+
import org.http4s.syntax.all.*
34+
35+
/**
36+
* For a gentle introduction, please look at the `KinesisLambda` first which uses
37+
* `IOLambda.Simple`.
38+
*
39+
* The `IOLambda` uses a slightly more complicated encoding by introducing an effect
40+
* `Invocation[F]` which provides access to the event and context in `F`. This allows you to
41+
* compose your handler as a stack of "middlewares", making it easy to e.g. add tracing to your
42+
* Lambda.
43+
*/
44+
object http4sHandler
45+
extends IOLambda[ApiGatewayProxyEventV2, ApiGatewayProxyStructuredResultV2]:
46+
47+
private type Handler =
48+
Invocation[IO, ApiGatewayProxyEventV2] => IO[Option[ApiGatewayProxyStructuredResultV2]]
49+
50+
/**
51+
* Actually, this is a `Resource` that builds your handler. The handler is acquired exactly
52+
* once when your Lambda starts and is permanently installed to process all incoming events.
53+
*
54+
* The handler itself is a program expressed as `IO[Option[Result]]`, which is run every time
55+
* that your Lambda is triggered. This may seem counter-intuitive at first: where does the
56+
* event come from? Because accessing the event via `Invocation` is now also an effect in
57+
* `IO`, it becomes a step in your program.
58+
*/
59+
def handler: Resource[IO, Handler] =
60+
for
61+
entrypoint <- Resource
62+
.eval(Random.scalaUtilRandom[IO])
63+
.flatMap(implicit r => XRay.entryPoint[IO]())
64+
client <- EmberClientBuilder.default[IO].build
65+
given Local[IO, Span[IO]] <- IO.local(Span.noop[IO]).toResource
66+
yield implicit inv => // the Invocation provides access to the event and context
67+
68+
// a middleware to add tracing to any handler
69+
// it extracts the kernel from the event and adds tags derived from the context
70+
TracedHandler(entrypoint):
71+
val tracedClient = NatchezMiddleware.client(client)
72+
73+
// a "middleware" that converts an HttpApp into a ApiGatewayProxyHandler
74+
ApiGatewayProxyHandlerV2(myApp[IO](tracedClient))
75+
76+
/**
77+
* Nothing special about this method, including its existence, just an example :)
78+
*/
79+
def myApp[F[_]: Concurrent: Trace](client: Client[F]): HttpApp[F] =
80+
implicit val dsl = Http4sDsl[F]
81+
import dsl.*
82+
83+
val routes = HttpRoutes.of[F]:
84+
case GET -> Root / "foo" => Ok("bar")
85+
case GET -> Root / "joke" => Ok(client.expect[String](uri"icanhazdadjoke.com"))
86+
87+
NatchezMiddleware.server(routes).orNotFound

lambda/shared/src/main/scala-2/feral/lambda/TracedHandlerPlatform.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ import natchez._
2323
import natchez.mtl._
2424

2525
trait TracedHandlerPlatform {
26-
def local[F[_] : MonadCancelThrow, Event, Result](entryPoint: EntryPoint[F])
27-
(handler: Trace[F] => F[Option[Result]])
28-
(implicit inv: Invocation[F, Event],
29-
KS: KernelSource[Event],
30-
L: Local[F, Span[F]]): F[Option[Result]] =
26+
def apply[F[_]: MonadCancelThrow, Event, Result](entryPoint: EntryPoint[F])(
27+
handler: Trace[F] => F[Option[Result]])(
28+
implicit inv: Invocation[F, Event],
29+
KS: KernelSource[Event],
30+
L: Local[F, Span[F]]): F[Option[Result]] =
3131
for {
3232
event <- inv.event
3333
context <- inv.context
@@ -36,7 +36,7 @@ trait TracedHandlerPlatform {
3636
Local[F, Span[F]].scope {
3737
Trace[F].put(
3838
AwsTags.arn(context.invokedFunctionArn),
39-
AwsTags.requestId(context.awsRequestId),
39+
AwsTags.requestId(context.awsRequestId)
4040
) >> handler(Trace[F])
4141
}
4242
}

lambda/shared/src/main/scala-3/feral/lambda/TracedHandlerPlatform.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import natchez.*
2323
import natchez.mtl.*
2424

2525
trait TracedHandlerPlatform:
26-
def local[F[_]: MonadCancelThrow, Event, Result](entryPoint: EntryPoint[F])(
26+
def apply[F[_]: MonadCancelThrow, Event, Result](entryPoint: EntryPoint[F])(
2727
handler: Trace[F] ?=> F[Option[Result]])(
2828
using Invocation[F, Event],
2929
KernelSource[Event],

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

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,44 +18,66 @@ package feral.lambda
1818

1919
import cats.data.Kleisli
2020
import cats.effect.IO
21-
import cats.effect.kernel.MonadCancelThrow
21+
import cats.effect.{Trace => _, _}
22+
import cats.mtl.Local
2223
import cats.syntax.all._
23-
import natchez.EntryPoint
24-
import natchez.Span
25-
import natchez.Trace
24+
import fs2.compat.NotGiven
25+
import natchez._
2626

2727
object TracedHandler extends TracedHandlerPlatform {
2828

29-
def apply[Event, Result](entryPoint: EntryPoint[IO])(
29+
def apply[Event, Result](
30+
entryPoint: EntryPoint[IO],
3031
handler: Trace[IO] => IO[Option[Result]])(
3132
implicit inv: Invocation[IO, Event],
32-
KS: KernelSource[Event]): IO[Option[Result]] = for {
33-
event <- inv.event
34-
context <- inv.context
35-
kernel = KS.extract(event)
36-
result <- entryPoint.continueOrElseRoot(context.functionName, kernel).use { span =>
37-
span.put(
38-
AwsTags.arn(context.invokedFunctionArn),
39-
AwsTags.requestId(context.awsRequestId)
40-
) >> Trace.ioTrace(span) >>= handler
41-
}
42-
} yield result
33+
KS: KernelSource[Event],
34+
@annotation.unused NotLocal: NotGiven[Local[IO, Span[IO]]]): IO[Option[Result]] =
35+
for {
36+
event <- inv.event
37+
context <- inv.context
38+
kernel = KS.extract(event)
39+
result <- entryPoint.continueOrElseRoot(context.functionName, kernel).use { span =>
40+
span.put(
41+
AwsTags.arn(context.invokedFunctionArn),
42+
AwsTags.requestId(context.awsRequestId)
43+
) >> Trace.ioTrace(span) >>= handler
44+
}
45+
} yield result
4346

4447
def apply[F[_]: MonadCancelThrow, Event, Result](
4548
entryPoint: EntryPoint[F],
4649
handler: Kleisli[F, Span[F], Option[Result]])(
47-
// inv first helps bind Event for KernelSource. h/t @bpholt
4850
implicit inv: Invocation[F, Event],
49-
KS: KernelSource[Event]): F[Option[Result]] = for {
50-
event <- inv.event
51-
context <- inv.context
52-
kernel = KS.extract(event)
53-
result <- entryPoint.continueOrElseRoot(context.functionName, kernel).use { span =>
54-
span.put(
55-
AwsTags.arn(context.invokedFunctionArn),
56-
AwsTags.requestId(context.awsRequestId)
57-
) >> handler(span)
58-
}
59-
} yield result
51+
KS: KernelSource[Event],
52+
@annotation.unused NotLocal: NotGiven[Local[IO, Span[IO]]]): F[Option[Result]] =
53+
for {
54+
event <- inv.event
55+
context <- inv.context
56+
kernel = KS.extract(event)
57+
result <- entryPoint.continueOrElseRoot(context.functionName, kernel).use { span =>
58+
span.put(
59+
AwsTags.arn(context.invokedFunctionArn),
60+
AwsTags.requestId(context.awsRequestId)
61+
) >> handler(span)
62+
}
63+
} yield result
64+
65+
@deprecated("use variant with Local tracing semantics", "0.3.2")
66+
def apply[Event, Result](
67+
entryPoint: EntryPoint[IO],
68+
handler: Trace[IO] => IO[Option[Result]],
69+
inv: Invocation[IO, Event],
70+
KS: KernelSource[Event]): IO[Option[Result]] =
71+
apply(entryPoint, handler)(inv, KS, implicitly)
72+
73+
@deprecated("use variant with Local tracing semantics", "0.3.2")
74+
def apply[F[_], Event, Result](
75+
entryPoint: EntryPoint[F],
76+
handler: Kleisli[F, Span[F], Option[Result]],
77+
M: MonadCancel[F, Throwable],
78+
inv: Invocation[F, Event],
79+
KS: KernelSource[Event]): F[Option[Result]] = {
80+
apply(entryPoint, handler)(M, inv, KS, implicitly)
81+
}
6082

6183
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2021 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package feral.lambda
18+
19+
import cats.data.Kleisli
20+
import cats.effect.IO
21+
import feral.lambda.events.KinesisStreamEvent
22+
import natchez.EntryPoint
23+
import natchez.Span
24+
import natchez.Trace
25+
26+
import scala.annotation.nowarn
27+
28+
class TracedLambdaSuite {
29+
30+
@nowarn
31+
def syntaxTest = { // Checking for compilation, nothing more
32+
33+
implicit def inv: Invocation[IO, KinesisStreamEvent] = ???
34+
def ioEntryPoint: EntryPoint[IO] = ???
35+
def needsTrace[F[_]: Trace]: F[Option[INothing]] = ???
36+
37+
IO.local(Span.noop[IO]).flatMap { implicit local =>
38+
TracedHandler(ioEntryPoint) { implicit trace => needsTrace[IO] }
39+
}
40+
41+
TracedHandler(ioEntryPoint, Kleisli[IO, Span[IO], Option[INothing]](???))
42+
}
43+
44+
}

lambda/shared/src/test/scala/feral/lambda/TracedHandlerSuite.scala renamed to lambda/shared/src/test/scala-3/feral/lambda/TracedLambdaSuite.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ class TracedLambdaSuite {
3434
def ioEntryPoint: EntryPoint[IO] = ???
3535
def needsTrace[F[_]: Trace]: F[Option[INothing]] = ???
3636

37-
TracedHandler(ioEntryPoint) { implicit trace => needsTrace[IO] }
37+
IO.local(Span.noop[IO]).flatMap { implicit local =>
38+
TracedHandler(ioEntryPoint) { needsTrace[IO] }
39+
}
3840

3941
TracedHandler(ioEntryPoint, Kleisli[IO, Span[IO], Option[INothing]](???))
4042
}

0 commit comments

Comments
 (0)