Skip to content
Closed
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
11 changes: 8 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ val catsEffectVersion = "3.2.9"
val circeVersion = "0.14.1"
val fs2Version = "3.2.2"
val http4sVersion = "0.23.6"
val natchezVersion = "0.1.5"

lazy val root =
project
Expand All @@ -62,7 +63,9 @@ lazy val core = crossProject(JSPlatform, JVMPlatform)
.settings(
name := "feral-core",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-effect" % catsEffectVersion
"org.typelevel" %%% "cats-effect" % catsEffectVersion,
"org.tpolecat" %%% "natchez-core" % natchezVersion,
"org.tpolecat" %%% "natchez-noop" % natchezVersion, // TODO Rob needs to publish this for SJS
)
)

Expand All @@ -71,7 +74,8 @@ lazy val lambda = crossProject(JSPlatform, JVMPlatform)
.settings(
name := "feral-lambda",
libraryDependencies ++= Seq(
"io.circe" %%% "circe-core" % circeVersion
"io.circe" %%% "circe-core" % circeVersion,
// "org.tpolecat" %%% "natchez-xray" % natchezVersion, // TODO uncomment when this is published
)
)
.jsSettings(
Expand Down Expand Up @@ -104,7 +108,8 @@ lazy val lambdaApiGatewayProxyHttp4s = crossProject(JSPlatform, JVMPlatform)
.settings(
name := "feral-lambda-api-gateway-proxy-http4s",
libraryDependencies ++= Seq(
"org.http4s" %%% "http4s-core" % http4sVersion
"org.http4s" %%% "http4s-core" % http4sVersion,
"org.tpolecat" %% "natchez-http4s" % "0.1.3",
)
)
.dependsOn(lambda, lambdaEvents)
Expand Down
12 changes: 9 additions & 3 deletions core/src/main/scala/feral/IOSetup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
package feral

import cats.effect.unsafe.IORuntime
import cats.effect.kernel.Resource
import cats.effect.IO
import cats.effect.kernel.Deferred
import cats.effect.{Deferred, IO, Resource}
import cats.syntax.all._
import natchez.Span
import natchez.noop.NoopSpan

import scala.annotation.nowarn

private[feral] trait IOSetup {

Expand All @@ -29,6 +31,10 @@ private[feral] trait IOSetup {
protected type Setup
protected def setup: Resource[IO, Setup] = Resource.pure(null.asInstanceOf[Setup])

@nowarn(value = "msg=parameter value name in method traceRootSpan is never used")
protected def traceRootSpan(name: String): Resource[IO, Span[IO]] =
Resource.pure(NoopSpan[IO]())

private[feral] final lazy val setupMemo: IO[Setup] = {
val deferred = Deferred.unsafe[IO, Either[Throwable, Setup]]
setup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,13 @@

package feral.lambda

import cats.effect.IO
import cats.effect.Resource
import cats.effect.SyncIO
import feral.lambda.events.ApiGatewayProxyEventV2
import feral.lambda.events.ApiGatewayProxyStructuredResultV2
import cats.effect.{IO, Resource, SyncIO}
import feral.lambda.events.{ApiGatewayProxyEventV2, ApiGatewayProxyStructuredResultV2}
import fs2.Stream
import org.http4s.Charset
import org.http4s.Header
import org.http4s.Headers
import org.http4s.HttpRoutes
import org.http4s.Method
import org.http4s.Request
import org.http4s.Response
import org.http4s.Uri
import org.typelevel.vault.Key
import org.typelevel.vault.Vault
import natchez.http4s.NatchezMiddleware
import natchez.{Span, Trace}
import org.http4s.{Charset, Header, Headers, HttpRoutes, Method, Request, Response, Uri}
import org.typelevel.vault.{Key, Vault}

abstract class ApiGatewayProxyHttp4sLambda
extends IOLambda[ApiGatewayProxyEventV2, ApiGatewayProxyStructuredResultV2] {
Expand All @@ -44,10 +35,13 @@ abstract class ApiGatewayProxyHttp4sLambda
protected type Setup = HttpRoutes[IO]
protected override final val setup: Resource[IO, HttpRoutes[IO]] = routes

override protected def traceRootSpan(name: String): Resource[IO, Span[IO]] = super.traceRootSpan(name)

override final def apply(
event: ApiGatewayProxyEventV2,
context: Context,
routes: HttpRoutes[IO]): IO[Some[ApiGatewayProxyStructuredResultV2]] =
routes: HttpRoutes[IO])(
implicit T: Trace[IO]): IO[Some[ApiGatewayProxyStructuredResultV2]] =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How does this Trace propagate to userland code? Apologies if I'm missing something.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No, good catch. I've only made changes to IOLambda and the IOLambdaPlatforms to make sure it worked at that level. We'd need to add the implicit Trace to the other abstract methods if we want to do this consistently.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Aha, I thought it might be using some IOLocal magic :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I need to think a little more on how the ApiGatewayProxyHttp4sLambda fits in here. (Open to suggestions if you see a way to do it with the current structure.)

We may need to change routes to be def routes: Resource[IO, HttpRoutes[Kleisli[IO, Span[IO], *]]]? But even that doesn't fit perfectly if all we have here is the implicit Trace[IO]. Not sure yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I kind of punted and added the Natchez server middleware to ApiGatewayProxyHttp4sLambda. The main problem I see is that doesn't make the Trace[IO] capability available to anything defined in def routes: Resource[IO, HttpRoutes[IO]], meaning I'm not sure how application code could open its own spans or propagate the trace onwards if it makes calls to other services.

@armanbilge it looked like you started a new branch that (from the commit messages) might be reworking the structure a little bit to help with this. I'm going to pause here to see what you come up with. 🙂

for {
method <- IO.fromEither(Method.fromString(event.requestContext.http.method))
uri <- IO.fromEither(Uri.fromString(event.rawPath))
Expand All @@ -63,7 +57,7 @@ abstract class ApiGatewayProxyHttp4sLambda
headers = headers,
body = requestBody,
attributes = Vault.empty.insert(ContextKey, context).insert(EventKey, event))
response <- routes(request).getOrElse(Response.notFound[IO])
response <- NatchezMiddleware.server(routes).run(request).getOrElse(Response.notFound[IO])
isBase64Encoded = !response.charset.contains(Charset.`UTF-8`)
responseBody <- (if (isBase64Encoded)
response.body.through(fs2.text.base64.encode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,29 @@

package feral.lambda.cloudformation

import cats.effect._
import cats.effect.kernel.Resource
import cats.effect.{IO, Resource}
import cats.syntax.all._
import feral.lambda.cloudformation.CloudFormationCustomResourceHandler.stackTraceLines
import feral.lambda.cloudformation.CloudFormationRequestType._
import feral.lambda.{Context, IOLambda}
import io.circe._
import io.circe.syntax._
import natchez.Trace
import org.http4s.Method.POST
import org.http4s.client.Client
import org.http4s.circe._
import org.http4s.client.Client
import org.http4s.client.dsl.io._
import org.http4s.ember.client.EmberClientBuilder

import java.io.{PrintWriter, StringWriter}

trait CloudFormationCustomResource[F[_], Input, Output] {
def createResource(
event: CloudFormationCustomResourceRequest[Input],
context: Context): F[HandlerResponse[Output]]
def updateResource(
event: CloudFormationCustomResourceRequest[Input],
context: Context): F[HandlerResponse[Output]]
def deleteResource(
event: CloudFormationCustomResourceRequest[Input],
context: Context): F[HandlerResponse[Output]]
def createResource(event: CloudFormationCustomResourceRequest[Input], context: Context)(
implicit T: Trace[F]): F[HandlerResponse[Output]]
def updateResource(event: CloudFormationCustomResourceRequest[Input], context: Context)(
implicit T: Trace[F]): F[HandlerResponse[Output]]
def deleteResource(event: CloudFormationCustomResourceRequest[Input], context: Context)(
implicit T: Trace[F]): F[HandlerResponse[Output]]
}

abstract class CloudFormationCustomResourceHandler[Input: Decoder, Output: Encoder]
Expand All @@ -59,7 +56,7 @@ abstract class CloudFormationCustomResourceHandler[Input: Decoder, Output: Encod
override def apply(
event: CloudFormationCustomResourceRequest[Input],
context: Context,
setup: Setup): IO[Option[Unit]] =
setup: Setup)(implicit T: Trace[IO]): IO[Option[Unit]] =
Comment thread
bpholt marked this conversation as resolved.
(event.RequestType match {
case CreateRequest => setup._2.createResource(event, context)
case UpdateRequest => setup._2.updateResource(event, context)
Expand Down
23 changes: 17 additions & 6 deletions lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

package feral.lambda

import cats.effect.IO
import cats.effect.{IO, Resource}
import cats.syntax.all._
import io.circe.scalajs._
import natchez.Trace.ioTrace

import scala.scalajs.js
import scala.scalajs.js.JSConverters._
Expand All @@ -26,11 +28,20 @@ import scala.scalajs.js.|
private[lambda] trait IOLambdaPlatform[Event, Result] {
this: IOLambda[Event, Result] =>

private def ioHandler(event: js.Any, context: facade.Context): IO[js.Any | Unit] =
Resource
.pure[IO, Context](Context.fromJS(context))
.mproduct(context => traceRootSpan(context.functionName).evalMap(ioTrace))
.use {
case (context, trace) =>
for {
setup <- setupMemo
event <- IO.fromEither(decodeJs[Event](event))
result <- apply(event, context, setup)(trace)
} yield result.map(_.asJsAny).orUndefined
}

// @JSExportTopLevel("handler") // TODO
final def handler(event: js.Any, context: facade.Context): js.Promise[js.Any | Unit] =
(for {
setup <- setupMemo
event <- IO.fromEither(decodeJs[Event](event))
result <- apply(event, Context.fromJS(context), setup)
} yield result.map(_.asJsAny).orUndefined).unsafeToPromise()(runtime)
ioHandler(event, context).unsafeToPromise()(runtime)
}
37 changes: 20 additions & 17 deletions lambda/jvm/src/main/scala/feral/lambda/IOLambdaPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import cats.effect.IO
import cats.effect.kernel.Resource
import com.amazonaws.services.lambda.{runtime => lambdaRuntime}
import io.circe
import natchez.Trace.ioTrace

import java.io.InputStream
import java.io.OutputStream
Expand All @@ -34,23 +35,25 @@ private[lambda] abstract class IOLambdaPlatform[Event, Result]
output: OutputStream,
context: lambdaRuntime.Context): Unit =
Resource
.eval {
for {
setup <- setupMemo
event <- fs2
.io
.readInputStream(IO.pure(input), 8192, closeAfterUse = false)
.through(circe.fs2.byteStreamParser)
.through(circe.fs2.decoder[IO, Event])
.head
.compile
.lastOrError
context <- IO(Context.fromJava(context))
_ <- OptionT(apply(event, context, setup)).foldF(IO.unit) { result =>
// TODO can circe write directly to output?
IO(output.write(encoder(result).noSpaces.getBytes(StandardCharsets.UTF_8)))
}
} yield ()
.eval(IO(Context.fromJava(context)))
.flatMap { (context: Context) =>
traceRootSpan(context.functionName).evalMap(ioTrace).evalMap { implicit trace =>
for {
setup <- setupMemo
event <- fs2
.io
.readInputStream(IO.pure(input), 8192, closeAfterUse = false)
.through(circe.fs2.byteStreamParser)
.through(circe.fs2.decoder[IO, Event])
.head
.compile
.lastOrError
_ <- OptionT(apply(event, context, setup)).foldF(IO.unit) { result =>
// TODO can circe write directly to output?
IO(output.write(encoder(result).noSpaces.getBytes(StandardCharsets.UTF_8)))
}
} yield ()
}
}
.onFinalize(IO(input.close()))
.onFinalize(IO(output.close()))
Expand Down
7 changes: 4 additions & 3 deletions lambda/shared/src/main/scala/feral/lambda/IOLambda.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ package feral
package lambda

import cats.effect.IO
import io.circe.Decoder
import io.circe.Encoder
import io.circe.{Decoder, Encoder}
import natchez.Trace

abstract class IOLambda[Event, Result](
implicit private[lambda] val decoder: Decoder[Event],
private[lambda] val encoder: Encoder[Result]
) extends IOLambdaPlatform[Event, Result]
with IOSetup {

def apply(event: Event, context: Context, setup: Setup): IO[Option[Result]]
def apply(event: Event, context: Context, setup: Setup)(
implicit T: Trace[IO]): IO[Option[Result]]
}
43 changes: 43 additions & 0 deletions lambda/shared/src/main/scala/feral/lambda/XRayTracing.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.
*/

/* TODO this file won't compile until these PRs are merged & published:
* https://github.com/christiankjaer/natchez/pull/1
* https://github.com/tpolecat/natchez/pull/427
*/

package feral.lambda

import cats.effect.std.Random
import cats.effect.{IO, Resource}
import feral.IOSetup
import natchez.xray.{XRay, XRayEnvironment}
import natchez.{EntryPoint, Span}

trait XRayTracing extends IOSetup {
private def traceEntryPoint: Resource[IO, EntryPoint[IO]] =
Resource.eval(Random.scalaUtilRandom[IO]).flatMap { implicit random =>
Resource.eval(XRayEnvironment[IO].daemonAddress).flatMap {
case Some(addr) => XRay.entryPoint[IO](addr)
case None => XRay.entryPoint[IO]()
}
}

override protected def traceRootSpan(name: String): Resource[IO, Span[IO]] =
Resource.eval(XRayEnvironment[IO].kernelFromEnvironment).flatMap { kernel =>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This strategy for extracting the kernel might only apply to normal AWS Lambdas—e.g. for ApiGatewayProxyHttp4sLambda the kernel should probably be extracted from the incoming HTTP request headers, not the Lambda's environment. 🤔

traceEntryPoint.flatMap(_.continueOrElseRoot(name, kernel))
}
}