Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ val Scala213 = "2.13.17"
val Scala3 = "3.3.6"
ThisBuild / crossScalaVersions := Seq(Scala212, Scala3, Scala213)

val catsEffectVersion = "3.5.7"
val catsEffectVersion = "3.6.3"
val circeVersion = "0.14.15"
val fs2Version = "3.12.2"
val http4sVersion = "0.23.32"
Expand Down Expand Up @@ -89,7 +89,9 @@ lazy val lambda = crossProject(JSPlatform, JVMPlatform)
name := "feral-lambda",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-effect" % catsEffectVersion,
"org.typelevel" %%% "cats-mtl" % "1.6.0",
"org.tpolecat" %%% "natchez-core" % natchezVersion,
"org.tpolecat" %%% "natchez-mtl" % natchezVersion,
"io.circe" %%% "circe-scodec" % circeVersion,
"io.circe" %%% "circe-jawn" % circeVersion,
"com.comcast" %%% "ip4s-core" % "3.7.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@

package feral.examples

import cats.effect._
import cats.effect.std.Random
import cats.effect.{Trace => _, _}
import cats.mtl._
import feral.lambda._
import feral.lambda.events._
import feral.lambda.http4s._
import natchez.Trace
import natchez._
import natchez.http4s.NatchezMiddleware
import natchez.xray.XRay
import org.http4s.HttpApp
Expand All @@ -43,6 +44,9 @@ import org.http4s.syntax.all._
object http4sHandler
extends IOLambda[ApiGatewayProxyEventV2, ApiGatewayProxyStructuredResultV2] {

private type Handler =
Invocation[IO, ApiGatewayProxyEventV2] => IO[Option[ApiGatewayProxyStructuredResultV2]]

/**
* Actually, this is a `Resource` that builds your handler. The handler is acquired exactly
* once when your Lambda starts and is permanently installed to process all incoming events.
Expand All @@ -52,29 +56,31 @@ object http4sHandler
* event come from? Because accessing the event via `Invocation` is now also an effect in
* `IO`, it becomes a step in your program.
*/
def handler = for {
entrypoint <- Resource
.eval(Random.scalaUtilRandom[IO])
.flatMap(implicit r => XRay.entryPoint[IO]())
client <- EmberClientBuilder.default[IO].build
} yield { implicit inv => // the Invocation provides access to the event and context
def handler: Resource[IO, Handler] =
for {
entrypoint <- Resource
.eval(Random.scalaUtilRandom[IO])
.flatMap(implicit r => XRay.entryPoint[IO]())
client <- EmberClientBuilder.default[IO].build
implicit0(local: Local[IO, Span[IO]]) <- IO.local(Span.noop[IO]).toResource
} yield { implicit inv => // the Invocation provides access to the event and context

// a middleware to add tracing to any handler
// it extracts the kernel from the event and adds tags derived from the context
TracedHandler(entrypoint) { implicit trace =>
val tracedClient = NatchezMiddleware.client(client)
// a middleware to add tracing to any handler
// it extracts the kernel from the event and adds tags derived from the context
TracedHandler(entrypoint) { implicit trace =>
val tracedClient = NatchezMiddleware.client(client)

// a "middleware" that converts an HttpApp into a ApiGatewayProxyHandler
ApiGatewayProxyHandlerV2(myApp[IO](tracedClient))
// a "middleware" that converts an HttpApp into a ApiGatewayProxyHandler
ApiGatewayProxyHandlerV2(myApp[IO](tracedClient))
}
}
}

/**
* Nothing special about this method, including its existence, just an example :)
*/
def myApp[F[_]: Concurrent: Trace](client: Client[F]): HttpApp[F] = {
implicit val dsl = Http4sDsl[F]
import dsl._
import dsl.*

val routes = HttpRoutes.of[F] {
case GET -> Root / "foo" => Ok("bar")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2021 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 feral.examples

import cats.effect.std.Random
import cats.effect.{Trace as _, *}
import cats.mtl.*
import feral.lambda.*
import feral.lambda.events.*
import feral.lambda.http4s.*
import natchez.*
import natchez.http4s.NatchezMiddleware
import natchez.xray.XRay
import org.http4s.HttpApp
import org.http4s.HttpRoutes
import org.http4s.client.Client
import org.http4s.dsl.Http4sDsl
import org.http4s.ember.client.EmberClientBuilder
import org.http4s.syntax.all.*

/**
* For a gentle introduction, please look at the `KinesisLambda` first which uses
* `IOLambda.Simple`.
*
* The `IOLambda` uses a slightly more complicated encoding by introducing an effect
* `Invocation[F]` which provides access to the event and context in `F`. This allows you to
* compose your handler as a stack of "middlewares", making it easy to e.g. add tracing to your
* Lambda.
*/
object http4sHandler
extends IOLambda[ApiGatewayProxyEventV2, ApiGatewayProxyStructuredResultV2]:

private type Handler =
Invocation[IO, ApiGatewayProxyEventV2] => IO[Option[ApiGatewayProxyStructuredResultV2]]

/**
* Actually, this is a `Resource` that builds your handler. The handler is acquired exactly
* once when your Lambda starts and is permanently installed to process all incoming events.
*
* The handler itself is a program expressed as `IO[Option[Result]]`, which is run every time
* that your Lambda is triggered. This may seem counter-intuitive at first: where does the
* event come from? Because accessing the event via `Invocation` is now also an effect in
* `IO`, it becomes a step in your program.
*/
def handler: Resource[IO, Handler] =
for
entrypoint <- Resource
.eval(Random.scalaUtilRandom[IO])
.flatMap(implicit r => XRay.entryPoint[IO]())
client <- EmberClientBuilder.default[IO].build
given Local[IO, Span[IO]] <- IO.local(Span.noop[IO]).toResource
yield implicit inv => // the Invocation provides access to the event and context

// a middleware to add tracing to any handler
// it extracts the kernel from the event and adds tags derived from the context
TracedHandler(entrypoint):
val tracedClient = NatchezMiddleware.client(client)

// a "middleware" that converts an HttpApp into a ApiGatewayProxyHandler
ApiGatewayProxyHandlerV2(myApp[IO](tracedClient))

/**
* Nothing special about this method, including its existence, just an example :)
*/
def myApp[F[_]: Concurrent: Trace](client: Client[F]): HttpApp[F] =
implicit val dsl = Http4sDsl[F]
import dsl.*

val routes = HttpRoutes.of[F]:
case GET -> Root / "foo" => Ok("bar")
case GET -> Root / "joke" => Ok(client.expect[String](uri"icanhazdadjoke.com"))

NatchezMiddleware.server(routes).orNotFound
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2021 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 feral.lambda

import cats.effect.{Trace => _, _}
import cats.mtl.Local
import natchez._

trait TracedHandlerPlatform {
def apply[F[_]: MonadCancelThrow, Event, Result](entryPoint: EntryPoint[F])(
handler: Trace[F] => F[Option[Result]])(
implicit inv: Invocation[F, Event],
KS: KernelSource[Event],
L: Local[F, Span[F]]): F[Option[Result]] =
TracedHandlerImpl[F, Event, Result](entryPoint)(handler)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2021 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 feral.lambda

import cats.effect.{Trace as _, *}
import cats.mtl.Local
import natchez.*

trait TracedHandlerPlatform:
def apply[F[_]: MonadCancelThrow, Event, Result](entryPoint: EntryPoint[F])(
handler: Trace[F] ?=> F[Option[Result]])(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Splitting the apply method into Scala version-specific files lets us use a context function here, which improves the experience on Scala 3.

using Invocation[F, Event],
KernelSource[Event],
Local[F, Span[F]]): F[Option[Result]] =
TracedHandlerImpl[F, Event, Result](entryPoint) { implicit t => handler }
103 changes: 74 additions & 29 deletions lambda/shared/src/main/scala/feral/lambda/TracedHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,44 +18,89 @@ package feral.lambda

import cats.data.Kleisli
import cats.effect.IO
import cats.effect.kernel.MonadCancelThrow
import cats.effect.{Trace => _, _}
import cats.mtl.Local
import cats.syntax.all._
import natchez.EntryPoint
import natchez.Span
import natchez.Trace
import fs2.compat.NotGiven
import natchez._
import natchez.mtl._

object TracedHandler {
object TracedHandler extends TracedHandlerPlatform {

def apply[Event, Result](entryPoint: EntryPoint[IO])(
def apply[Event, Result](
entryPoint: EntryPoint[IO],
handler: Trace[IO] => IO[Option[Result]])(
implicit inv: Invocation[IO, Event],
KS: KernelSource[Event]): IO[Option[Result]] = for {
event <- inv.event
context <- inv.context
kernel = KS.extract(event)
result <- entryPoint.continueOrElseRoot(context.functionName, kernel).use { span =>
span.put(
AwsTags.arn(context.invokedFunctionArn),
AwsTags.requestId(context.awsRequestId)
) >> Trace.ioTrace(span) >>= handler
}
} yield result
KS: KernelSource[Event],
@annotation.unused NotLocal: NotGiven[Local[IO, Span[IO]]]): IO[Option[Result]] =
for {
event <- inv.event
context <- inv.context
kernel = KS.extract(event)
result <- entryPoint.continueOrElseRoot(context.functionName, kernel).use { span =>
span.put(
AwsTags.arn(context.invokedFunctionArn),
AwsTags.requestId(context.awsRequestId)
) >> Trace.ioTrace(span) >>= handler
}
} yield result

def apply[F[_]: MonadCancelThrow, Event, Result](
entryPoint: EntryPoint[F],
handler: Kleisli[F, Span[F], Option[Result]])(
// inv first helps bind Event for KernelSource. h/t @bpholt
implicit inv: Invocation[F, Event],
KS: KernelSource[Event]): F[Option[Result]] = for {
event <- inv.event
context <- inv.context
kernel = KS.extract(event)
result <- entryPoint.continueOrElseRoot(context.functionName, kernel).use { span =>
span.put(
AwsTags.arn(context.invokedFunctionArn),
AwsTags.requestId(context.awsRequestId)
) >> handler(span)
}
} yield result
KS: KernelSource[Event],
@annotation.unused NotLocal: NotGiven[Local[IO, Span[IO]]]): F[Option[Result]] =
for {
event <- inv.event
context <- inv.context
kernel = KS.extract(event)
result <- entryPoint.continueOrElseRoot(context.functionName, kernel).use { span =>
span.put(
AwsTags.arn(context.invokedFunctionArn),
AwsTags.requestId(context.awsRequestId)
) >> handler(span)
}
} yield result

@deprecated("use variant with Local tracing semantics", "0.3.2")
def apply[Event, Result](
entryPoint: EntryPoint[IO],
handler: Trace[IO] => IO[Option[Result]],
inv: Invocation[IO, Event],
KS: KernelSource[Event]): IO[Option[Result]] =
apply(entryPoint, handler)(inv, KS, implicitly)

@deprecated("use variant with Local tracing semantics", "0.3.2")
def apply[F[_], Event, Result](
entryPoint: EntryPoint[F],
handler: Kleisli[F, Span[F], Option[Result]],
M: MonadCancel[F, Throwable],
inv: Invocation[F, Event],
KS: KernelSource[Event]): F[Option[Result]] = {
apply(entryPoint, handler)(M, inv, KS, implicitly)
}

}

private[lambda] object TracedHandlerImpl {
def apply[F[_]: MonadCancelThrow, Event, Result](entryPoint: EntryPoint[F])(
handler: Trace[F] => F[Option[Result]])(
implicit inv: Invocation[F, Event],
KS: KernelSource[Event],
L: Local[F, Span[F]]): F[Option[Result]] =
for {
event <- Invocation[F, Event].event
context <- Invocation[F, Event].context
kernel = KernelSource[Event].extract(event)
result <- entryPoint.continueOrElseRoot(context.functionName, kernel).use {
Local[F, Span[F]].scope {
Trace[F].put(
AwsTags.arn(context.invokedFunctionArn),
AwsTags.requestId(context.awsRequestId)
) >> handler(Trace[F])
}
}
} yield result

}
Loading