diff --git a/build.sbt b/build.sbt index 86096ef4..d0782249 100644 --- a/build.sbt +++ b/build.sbt @@ -71,7 +71,10 @@ 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-core" % "0.1.5+47-0f7e4bf4-SNAPSHOT", + "org.tpolecat" %%% "natchez-xray" % "0.1.5+47-0f7e4bf4-SNAPSHOT", + "org.tpolecat" %%% "natchez-noop" % "0.1.5+47-0f7e4bf4-SNAPSHOT", ) ) .jsSettings( diff --git a/lambda-api-gateway-proxy-http4s/src/main/scala/feral/lambda/ApiGatewayProxyHttp4sLambda.scala b/lambda-api-gateway-proxy-http4s/src/main/scala/feral/lambda/ApiGatewayProxyHttp4sLambda.scala index 06d1bce9..7e0da358 100644 --- a/lambda-api-gateway-proxy-http4s/src/main/scala/feral/lambda/ApiGatewayProxyHttp4sLambda.scala +++ b/lambda-api-gateway-proxy-http4s/src/main/scala/feral/lambda/ApiGatewayProxyHttp4sLambda.scala @@ -16,9 +16,10 @@ package feral.lambda -import cats.effect.IO import cats.effect.Resource -import cats.effect.SyncIO +import cats.effect.Sync +import cats.effect.syntax.all._ +import cats.syntax.all._ import feral.lambda.events.ApiGatewayProxyEventV2 import feral.lambda.events.ApiGatewayProxyStructuredResultV2 import fs2.Stream @@ -33,56 +34,52 @@ import org.http4s.Uri import org.typelevel.vault.Key import org.typelevel.vault.Vault -abstract class ApiGatewayProxyHttp4sLambda - extends IOLambda[ApiGatewayProxyEventV2, ApiGatewayProxyStructuredResultV2] { +object ApiGatewayProxyHttp4sLambda { - val ContextKey = Key.newKey[SyncIO, Context].unsafeRunSync() - val EventKey = Key.newKey[SyncIO, ApiGatewayProxyEventV2].unsafeRunSync() - - def routes: Resource[IO, HttpRoutes[IO]] - - protected type Setup = HttpRoutes[IO] - protected override final val setup: Resource[IO, HttpRoutes[IO]] = routes - - override final def apply( - event: ApiGatewayProxyEventV2, - context: Context, - routes: HttpRoutes[IO]): IO[Some[ApiGatewayProxyStructuredResultV2]] = + def apply[F[_]: Sync]( + f: (Key[Context[F]], Key[ApiGatewayProxyEventV2]) => Resource[F, HttpRoutes[F]]) + : Resource[F, Lambda[F, ApiGatewayProxyEventV2, ApiGatewayProxyStructuredResultV2]] = for { - method <- IO.fromEither(Method.fromString(event.requestContext.http.method)) - uri <- IO.fromEither(Uri.fromString(event.rawPath)) - headers = Headers(event.headers.toList) - requestBody = - if (event.isBase64Encoded) - Stream.fromOption[IO](event.body).through(fs2.text.base64.decode) - else - Stream.fromOption[IO](event.body).through(fs2.text.utf8.encode) - request = Request( - method, - uri, - headers = headers, - body = requestBody, - attributes = Vault.empty.insert(ContextKey, context).insert(EventKey, event)) - response <- routes(request).getOrElse(Response.notFound[IO]) - isBase64Encoded = !response.charset.contains(Charset.`UTF-8`) - responseBody <- (if (isBase64Encoded) - response.body.through(fs2.text.base64.encode) - else - response.body.through(fs2.text.utf8.decode)).compile.foldMonoid - } yield Some( - ApiGatewayProxyStructuredResultV2( - response.status.code, - response - .headers - .headers - .map { - case Header.Raw(name, value) => - name.toString -> value - } - .toMap, - responseBody, - isBase64Encoded + contextKey <- Key.newKey[F, Context[F]].toResource + eventKey <- Key.newKey[F, ApiGatewayProxyEventV2].toResource + routes <- f(contextKey, eventKey) + } yield { (event: ApiGatewayProxyEventV2, context: Context[F]) => + for { + method <- Method.fromString(event.requestContext.http.method).liftTo[F] + uri <- Uri.fromString(event.rawPath).liftTo[F] + headers = Headers(event.headers.toList) + requestBody = + if (event.isBase64Encoded) + Stream.fromOption[F](event.body).through(fs2.text.base64.decode) + else + Stream.fromOption[F](event.body).through(fs2.text.utf8.encode) + request = Request( + method, + uri, + headers = headers, + body = requestBody, + attributes = Vault.empty.insert(contextKey, context).insert(eventKey, event)) + response <- routes(request).getOrElse(Response.notFound[F]) + isBase64Encoded = !response.charset.contains(Charset.`UTF-8`) + responseBody <- (if (isBase64Encoded) + response.body.through(fs2.text.base64.encode) + else + response.body.through(fs2.text.utf8.decode)).compile.foldMonoid + } yield Some( + ApiGatewayProxyStructuredResultV2( + response.status.code, + response + .headers + .headers + .map { + case Header.Raw(name, value) => + name.toString -> value + } + .toMap, + responseBody, + isBase64Encoded + ) ) - ) + } } diff --git a/lambda-cloudformation-custom-resource/src/main/scala/feral/lambda/cloudformation/CloudFormationCustomResource.scala b/lambda-cloudformation-custom-resource/src/main/scala/feral/lambda/cloudformation/CloudFormationCustomResource.scala index 04cda301..0bdad394 100644 --- a/lambda-cloudformation-custom-resource/src/main/scala/feral/lambda/cloudformation/CloudFormationCustomResource.scala +++ b/lambda-cloudformation-custom-resource/src/main/scala/feral/lambda/cloudformation/CloudFormationCustomResource.scala @@ -14,67 +14,61 @@ * limitations under the License. */ -package feral.lambda.cloudformation +package feral.lambda +package cloudformation -import cats.effect._ +import cats.ApplicativeThrow +import cats.MonadThrow import cats.effect.kernel.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 org.http4s.Method.POST -import org.http4s.client.Client import org.http4s.circe._ -import org.http4s.client.dsl.io._ -import org.http4s.ember.client.EmberClientBuilder +import org.http4s.client.Client +import org.http4s.client.dsl.Http4sClientDsl -import java.io.{PrintWriter, StringWriter} +import java.io.PrintWriter +import java.io.StringWriter trait CloudFormationCustomResource[F[_], Input, Output] { def createResource( event: CloudFormationCustomResourceRequest[Input], - context: Context): F[HandlerResponse[Output]] + context: Context[F]): F[HandlerResponse[Output]] def updateResource( event: CloudFormationCustomResourceRequest[Input], - context: Context): F[HandlerResponse[Output]] + context: Context[F]): F[HandlerResponse[Output]] def deleteResource( event: CloudFormationCustomResourceRequest[Input], - context: Context): F[HandlerResponse[Output]] + context: Context[F]): F[HandlerResponse[Output]] } -abstract class CloudFormationCustomResourceHandler[Input: Decoder, Output: Encoder] - extends IOLambda[CloudFormationCustomResourceRequest[Input], Unit] { - type Setup = (Client[IO], CloudFormationCustomResource[IO, Input, Output]) - - override final def setup: Resource[IO, Setup] = - client.mproduct(handler) - - protected def client: Resource[IO, Client[IO]] = - EmberClientBuilder.default[IO].build +object CloudFormationCustomResource { - def handler(client: Client[IO]): Resource[IO, CloudFormationCustomResource[IO, Input, Output]] + def apply[F[_]: MonadThrow, Input, Output: Encoder](client: Client[F])( + handler: Resource[F, CloudFormationCustomResource[F, Input, Output]]) + : Resource[F, Lambda[F, CloudFormationCustomResourceRequest[Input], Unit]] = + handler.map { handler => (event, context) => + val http4sClientDsl = new Http4sClientDsl[F] {} + import http4sClientDsl._ - override def apply( - event: CloudFormationCustomResourceRequest[Input], - context: Context, - setup: Setup): IO[Option[Unit]] = - (event.RequestType match { - case CreateRequest => setup._2.createResource(event, context) - case UpdateRequest => setup._2.updateResource(event, context) - case DeleteRequest => setup._2.deleteResource(event, context) - case OtherRequestType(other) => illegalRequestType(other) - }).attempt - .map(_.fold(exceptionResponse(event)(_), successResponse(event)(_))) - .flatMap { resp => setup._1.successful(POST(resp.asJson, event.ResponseURL)) } - .as(None) + (event.RequestType match { + case CreateRequest => handler.createResource(event, context) + case UpdateRequest => handler.updateResource(event, context) + case DeleteRequest => handler.deleteResource(event, context) + case OtherRequestType(other) => illegalRequestType(other) + }).attempt + .map(_.fold(exceptionResponse(event)(_), successResponse(event)(_))) + .flatMap { resp => client.successful(POST(resp.asJson, event.ResponseURL)) } + .as(None) + } - private def illegalRequestType[A](other: String): IO[A] = + private def illegalRequestType[F[_]: ApplicativeThrow, A](other: String): F[A] = (new IllegalArgumentException( - s"unexpected CloudFormation request type `$other``"): Throwable).raiseError[IO, A] + s"unexpected CloudFormation request type `$other``"): Throwable).raiseError[F, A] - private def exceptionResponse(req: CloudFormationCustomResourceRequest[Input])( + private def exceptionResponse[Input](req: CloudFormationCustomResourceRequest[Input])( ex: Throwable): CloudFormationCustomResourceResponse = CloudFormationCustomResourceResponse( Status = RequestResponseStatus.Failed, @@ -87,7 +81,8 @@ abstract class CloudFormationCustomResourceHandler[Input: Decoder, Output: Encod "StackTrace" -> Json.arr(stackTraceLines(ex).map(Json.fromString): _*)).asJson ) - private def successResponse(req: CloudFormationCustomResourceRequest[Input])( + private def successResponse[Input, Output: Encoder]( + req: CloudFormationCustomResourceRequest[Input])( res: HandlerResponse[Output]): CloudFormationCustomResourceResponse = CloudFormationCustomResourceResponse( Status = RequestResponseStatus.Success, @@ -99,12 +94,10 @@ abstract class CloudFormationCustomResourceHandler[Input: Decoder, Output: Encod Data = res.data.asJson ) -} - -object CloudFormationCustomResourceHandler { def stackTraceLines(throwable: Throwable): List[String] = { val writer = new StringWriter() throwable.printStackTrace(new PrintWriter(writer)) writer.toString.linesIterator.toList } + } diff --git a/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala b/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala index 8d167b01..ba19a2f6 100644 --- a/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala +++ b/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala @@ -16,13 +16,13 @@ package feral.lambda -import cats.effect.IO +import cats.effect.Sync import scala.concurrent.duration._ private[lambda] trait ContextCompanionPlatform { - private[lambda] def fromJS(context: facade.Context): Context = + private[lambda] def fromJS[F[_]: Sync](context: facade.Context): Context[F] = Context( context.functionName, context.functionVersion, @@ -55,6 +55,6 @@ private[lambda] trait ContextCompanionPlatform { ) ) }, - IO(context.getRemainingTimeInMillis().millis) + Sync[F].delay(context.getRemainingTimeInMillis().millis) ) } diff --git a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala index d71bbeb9..53bb17ad 100644 --- a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala +++ b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala @@ -29,8 +29,8 @@ private[lambda] trait IOLambdaPlatform[Event, Result] { // @JSExportTopLevel("handler") // TODO final def handler(event: js.Any, context: facade.Context): js.Promise[js.Any | Unit] = (for { - setup <- setupMemo + lambda <- setupMemo event <- IO.fromEither(decodeJs[Event](event)) - result <- apply(event, Context.fromJS(context), setup) + result <- lambda(event, Context.fromJS(context)) } yield result.map(_.asJsAny).orUndefined).unsafeToPromise()(runtime) } diff --git a/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala b/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala index 77aa1118..ca8f8d1b 100644 --- a/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala +++ b/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala @@ -16,14 +16,14 @@ package feral.lambda -import cats.effect.IO +import cats.effect.Sync import com.amazonaws.services.lambda.runtime import scala.concurrent.duration._ private[lambda] trait ContextCompanionPlatform { - private[lambda] def fromJava(context: runtime.Context): Context = + private[lambda] def fromJava[F[_]: Sync](context: runtime.Context): Context[F] = Context( context.getFunctionName(), context.getFunctionVersion(), @@ -53,7 +53,7 @@ private[lambda] trait ContextCompanionPlatform { ) ) }, - IO(context.getRemainingTimeInMillis().millis) + Sync[F].delay(context.getRemainingTimeInMillis().millis) ) } diff --git a/lambda/jvm/src/main/scala/feral/lambda/IOLambdaPlatform.scala b/lambda/jvm/src/main/scala/feral/lambda/IOLambdaPlatform.scala index 73a7fdcd..e3c6742b 100644 --- a/lambda/jvm/src/main/scala/feral/lambda/IOLambdaPlatform.scala +++ b/lambda/jvm/src/main/scala/feral/lambda/IOLambdaPlatform.scala @@ -36,7 +36,7 @@ private[lambda] abstract class IOLambdaPlatform[Event, Result] Resource .eval { for { - setup <- setupMemo + lambda <- setupMemo event <- fs2 .io .readInputStream(IO.pure(input), 8192, closeAfterUse = false) @@ -45,8 +45,8 @@ private[lambda] abstract class IOLambdaPlatform[Event, Result] .head .compile .lastOrError - context <- IO(Context.fromJava(context)) - _ <- OptionT(apply(event, context, setup)).foldF(IO.unit) { result => + context <- IO(Context.fromJava[IO](context)) + _ <- OptionT(lambda(event, context)).foldF(IO.unit) { result => // TODO can circe write directly to output? IO(output.write(encoder(result).noSpaces.getBytes(StandardCharsets.UTF_8))) } diff --git a/lambda/shared/src/main/scala/feral/lambda/Context.scala b/lambda/shared/src/main/scala/feral/lambda/Context.scala index bbbccb6b..dae59d23 100644 --- a/lambda/shared/src/main/scala/feral/lambda/Context.scala +++ b/lambda/shared/src/main/scala/feral/lambda/Context.scala @@ -16,11 +16,11 @@ package feral.lambda -import cats.effect.IO +import cats.~> import scala.concurrent.duration.FiniteDuration -final case class Context( +final case class Context[F[_]]( functionName: String, functionVersion: String, invokedFunctionArn: String, @@ -30,8 +30,11 @@ final case class Context( logStreamName: String, identity: Option[CognitoIdentity], clientContext: Option[ClientContext], - remainingTime: IO[FiniteDuration] -) + remainingTime: F[FiniteDuration] +) { + def mapK[G[_]](fk: F ~> G): Context[G] = + this.copy(remainingTime = fk(this.remainingTime)) +} object Context extends ContextCompanionPlatform diff --git a/lambda/shared/src/main/scala/feral/lambda/IOLambda.scala b/lambda/shared/src/main/scala/feral/lambda/IOLambda.scala index 81d22e81..4a480384 100644 --- a/lambda/shared/src/main/scala/feral/lambda/IOLambda.scala +++ b/lambda/shared/src/main/scala/feral/lambda/IOLambda.scala @@ -20,6 +20,7 @@ package lambda import cats.effect.IO import io.circe.Decoder import io.circe.Encoder +import cats.effect.kernel.Resource abstract class IOLambda[Event, Result]( implicit private[lambda] val decoder: Decoder[Event], @@ -27,5 +28,10 @@ abstract class IOLambda[Event, Result]( ) extends IOLambdaPlatform[Event, Result] with IOSetup { - def apply(event: Event, context: Context, setup: Setup): IO[Option[Result]] + final type Setup = Lambda[IO, Event, Result] + + final override protected def setup: Resource[IO, Setup] = run + + def run: Resource[IO, Lambda[IO, Event, Result]] + } diff --git a/lambda/shared/src/main/scala/feral/lambda/package.scala b/lambda/shared/src/main/scala/feral/lambda/package.scala new file mode 100644 index 00000000..0a544b10 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/package.scala @@ -0,0 +1,21 @@ +/* + * 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 + +package object lambda { + type Lambda[F[_], Event, Result] = (Event, Context[F]) => F[Option[Result]] +} diff --git a/lambda/shared/src/main/scala/feral/lambda/tracing/LiftTrace.scala b/lambda/shared/src/main/scala/feral/lambda/tracing/LiftTrace.scala new file mode 100644 index 00000000..56be87e3 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/tracing/LiftTrace.scala @@ -0,0 +1,49 @@ +/* + * 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 remove and replace with version in Natchez +package feral.lambda.tracing + +import cats.arrow.FunctionK +import cats.data.Kleisli +import cats.effect.IO +import cats.effect.kernel.MonadCancelThrow +import cats.~> +import natchez._ + +trait LiftTrace[F[_], G[_]] { + def run[A](span: Span[F])(f: Trace[G] => G[A]): F[A] + def liftK: F ~> G +} + +object LiftTrace extends LiftTraceLowPriority { + def apply[F[_], G[_]](implicit LT: LiftTrace[F, G]): LiftTrace[F, G] = LT + + implicit val ioInstance: LiftTrace[IO, IO] = new LiftTrace[IO, IO] { + def run[A](span: Span[IO])(f: Trace[IO] => IO[A]): IO[A] = + Trace.ioTrace(span).flatMap(f) + def liftK: IO ~> IO = FunctionK.id + } +} + +private[tracing] trait LiftTraceLowPriority { + implicit def kleisliInstance[F[_]: MonadCancelThrow]: LiftTrace[F, Kleisli[F, Span[F], *]] = + new LiftTrace[F, Kleisli[F, Span[F], *]] { + def run[A](span: Span[F])(f: Trace[Kleisli[F, Span[F], *]] => Kleisli[F, Span[F], A]): F[A] = + f(Trace.kleisliInstance).run(span) + def liftK: F ~> Kleisli[F, Span[F], *] = Kleisli.liftK + } +} diff --git a/lambda/shared/src/main/scala/feral/lambda/tracing/TracedIO.scala b/lambda/shared/src/main/scala/feral/lambda/tracing/TracedIO.scala new file mode 100644 index 00000000..e2b8a295 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/tracing/TracedIO.scala @@ -0,0 +1,32 @@ +/* + * 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.tracing + +import cats.effect.IO +import natchez.{Span, Trace} + +// TODO remove and replace with version in Natchez +sealed abstract class TracedIO[A] { + def run(span: Span[IO]): IO[A] +} + +object TracedIO { + def apply[A](f: Trace[IO] => IO[A]): TracedIO[A] = + new TracedIO[A] { + def run(span: Span[IO]): IO[A] = Trace.ioTrace(span).flatMap(f) + } +} diff --git a/lambda/shared/src/main/scala/feral/lambda/tracing/TracedLambda.scala b/lambda/shared/src/main/scala/feral/lambda/tracing/TracedLambda.scala new file mode 100644 index 00000000..d0b50edf --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/tracing/TracedLambda.scala @@ -0,0 +1,40 @@ +/* + * 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.tracing + +import cats.data.Kleisli +import cats.effect.{Trace => _, _} +import feral.lambda._ +import natchez._ + +object TracedLambda { + def evalKernel[Event] = new PartiallyAppliedEvalKernel[Event]() + + def apply[F[_] : MonadCancelThrow, G[_], Event, Result](entryPoint: EntryPoint[F], + extractKernel: Kleisli[F, (Event, Context[F]), Kernel]) + (lambda: Lambda[G, Event, Result]) + (implicit LT: LiftTrace[F, G]): Lambda[F, Event, Result] = + (event, context) => + Resource + .eval(extractKernel((event, context))) + .flatMap { kernel => entryPoint.continueOrElseRoot(context.functionName, kernel) } + .use(LiftTrace[F, G].run(_)((_: Trace[G]) => lambda(event, context.mapK(LiftTrace[F, G].liftK)))) +} + +class PartiallyAppliedEvalKernel[Event](private val dummy: Unit = ()) extends AnyVal { + def apply[F[_]](fa: F[Kernel]): Kleisli[F, (Event, Context[F]), Kernel] = Kleisli.liftF(fa) +} diff --git a/lambda/shared/src/main/scala/feral/lambda/tracing/XRayTracedLambda.scala b/lambda/shared/src/main/scala/feral/lambda/tracing/XRayTracedLambda.scala new file mode 100644 index 00000000..8a85b9b2 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/tracing/XRayTracedLambda.scala @@ -0,0 +1,52 @@ +/* + * 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.tracing + +import cats.data._ +import cats.effect.std.Random +import cats.effect.{Trace => _, _} +import feral.lambda._ +import feral.lambda.tracing.TracedLambda.evalKernel +import natchez._ +import natchez.noop.NoopSpan +import natchez.xray.{XRay, XRayEnvironment} + +object XRayTracedLambda { + def apply[F[_] : Async, Event, Result](installer: Resource[Kleisli[F, Span[F], *], Lambda[Kleisli[F, Span[F], *], Event, Result]]) + (implicit LT: LiftTrace[F, Kleisli[F, Span[F], *]]): Resource[F, Lambda[F, Event, Result]] = + installer + .mapK(Kleisli.applyK(new NoopSpan[F]())) + .flatMap(XRayTracedLambda.usingEnvironment[F, Kleisli[F, Span[F], *], Event, Result](_)) + + def usingEnvironment[F[_] : Async, G[_], Event, Result](lambda: Lambda[G, Event, Result]) + (implicit LT: LiftTrace[F, G]): Resource[F, Lambda[F, Event, Result]] = + XRayTracedLambda(evalKernel[Event](XRayEnvironment[F].kernelFromEnvironment))(lambda) + + def apply[F[_] : Async, G[_], Event, Result](extractKernel: Kleisli[F, (Event, Context[F]), Kernel]) + (lambda: Lambda[G, Event, Result]) + (implicit LT: LiftTrace[F, G]): Resource[F, Lambda[F, Event, Result]] = { + Resource.eval(Random.scalaUtilRandom[F]).flatMap { implicit random => + Resource + .eval(XRayEnvironment[F].daemonAddress) + .flatMap { + case Some(addr) => XRay.entryPoint[F](addr) + case None => XRay.entryPoint[F]() + } + .map(TracedLambda(_, extractKernel)(lambda)) + } + } +}