diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36174d9c..c3cba126 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,11 +117,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p lambda-cloudformation-custom-resource/.js/target lambda-http4s/.jvm/target unidocs/target lambda-http4s/.js/target lambda/js/target scalafix/rules/target lambda/jvm/target sbt-lambda/target google-cloud-http4s/jvm/target lambda-cloudformation-custom-resource/.jvm/target google-cloud-http4s/js/target project/target + run: mkdir -p lambda-natchez/jvm/target lambda-natchez/js/target lambda-cloudformation-custom-resource/.js/target lambda-http4s/.jvm/target unidocs/target lambda-http4s/.js/target lambda/js/target scalafix/rules/target lambda/jvm/target lambda-otel4s/jvm/target sbt-lambda/target lambda-otel4s/js/target google-cloud-http4s/jvm/target lambda-cloudformation-custom-resource/.jvm/target google-cloud-http4s/js/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar lambda-cloudformation-custom-resource/.js/target lambda-http4s/.jvm/target unidocs/target lambda-http4s/.js/target lambda/js/target scalafix/rules/target lambda/jvm/target sbt-lambda/target google-cloud-http4s/jvm/target lambda-cloudformation-custom-resource/.jvm/target google-cloud-http4s/js/target project/target + run: tar cf targets.tar lambda-natchez/jvm/target lambda-natchez/js/target lambda-cloudformation-custom-resource/.js/target lambda-http4s/.jvm/target unidocs/target lambda-http4s/.js/target lambda/js/target scalafix/rules/target lambda/jvm/target lambda-otel4s/jvm/target sbt-lambda/target lambda-otel4s/js/target google-cloud-http4s/jvm/target lambda-cloudformation-custom-resource/.jvm/target google-cloud-http4s/js/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') diff --git a/.mergify.yml b/.mergify.yml index aa44cd0a..980475a7 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -68,6 +68,22 @@ pull_request_rules: add: - lambda-http4s remove: [] +- name: Label lambda-natchez PRs + conditions: + - files~=^lambda-natchez/ + actions: + label: + add: + - lambda-natchez + remove: [] +- name: Label lambda-otel4s PRs + conditions: + - files~=^lambda-otel4s/ + actions: + label: + add: + - lambda-otel4s + remove: [] - name: Label output PRs conditions: - files~=^scalafix/output/ diff --git a/build.sbt b/build.sbt index c22ab4ae..a94f7144 100644 --- a/build.sbt +++ b/build.sbt @@ -18,7 +18,7 @@ import com.typesafe.tools.mima.core._ name := "feral" -ThisBuild / tlBaseVersion := "0.3" +ThisBuild / tlBaseVersion := "0.4" ThisBuild / startYear := Some(2021) ThisBuild / developers := List( @@ -64,6 +64,7 @@ val natchezVersion = "0.3.8" val munitVersion = "1.2.0" val munitCEVersion = "2.1.0" val scalacheckEffectVersion = "2.1.0-RC1" +val otel4sVersion = "0.14.0" lazy val commonSettings = Seq( crossScalaVersions := Seq(Scala3, Scala213) @@ -75,6 +76,8 @@ lazy val root = lambda, lambdaHttp4s, lambdaCloudFormationCustomResource, + lambdaNatchez, + lambdaOtel4s, googleCloudHttp4s, examples, unidocs @@ -95,7 +98,7 @@ lazy val lambda = crossProject(JSPlatform, JVMPlatform) name := "feral-lambda", libraryDependencies ++= Seq( "org.typelevel" %%% "cats-effect" % catsEffectVersion, - "org.tpolecat" %%% "natchez-core" % natchezVersion, + "org.typelevel" %%% "case-insensitive" % "1.4.0", "io.circe" %%% "circe-scodec" % circeVersion, "io.circe" %%% "circe-jawn" % circeVersion, "com.comcast" %%% "ip4s-core" % "3.7.0", @@ -181,19 +184,55 @@ lazy val lambdaCloudFormationCustomResource = crossProject(JSPlatform, JVMPlatfo .settings(commonSettings) .dependsOn(lambda) +lazy val lambdaNatchez = crossProject(JSPlatform, JVMPlatform) + .in(file("lambda-natchez")) + .settings( + name := "feral-lambda-natchez", + libraryDependencies ++= Seq( + "org.tpolecat" %%% "natchez-core" % natchezVersion, + "org.scalameta" %%% "munit-scalacheck" % munitVersion % Test, + "org.typelevel" %%% "munit-cats-effect" % munitCEVersion % Test + ) + ) + .settings(commonSettings) + .dependsOn(lambda) + +lazy val lambdaOtel4s = crossProject(JSPlatform, JVMPlatform) + .in(file("lambda-otel4s")) + .settings( + name := "feral-lambda-otel4s", + libraryDependencies ++= Seq( + "org.typelevel" %%% "otel4s-core-trace" % otel4sVersion, + "org.typelevel" %%% "otel4s-sdk-trace-testkit" % otel4sVersion % Test, + "org.typelevel" %%% "otel4s-semconv-experimental" % otel4sVersion % Test, + "org.scalameta" %%% "munit-scalacheck" % munitVersion % Test, + "org.typelevel" %%% "munit-cats-effect" % munitCEVersion % Test + ) + ) + .settings(commonSettings) + .dependsOn(lambda) + lazy val examples = crossProject(JSPlatform, JVMPlatform) .in(file("examples")) .settings( libraryDependencies ++= Seq( "org.http4s" %%% "http4s-dsl" % http4sVersion, "org.http4s" %%% "http4s-ember-client" % http4sVersion, + "org.http4s" %%% "http4s-otel4s-middleware-trace-client" % "0.15.0", "org.tpolecat" %%% "natchez-xray" % natchezVersion, "org.tpolecat" %%% "natchez-http4s" % "0.6.1", + "org.tpolecat" %%% "skunk-core" % "0.6.4", + "org.tpolecat" %%% "natchez-http4s" % "0.6.1", "org.tpolecat" %%% "skunk-core" % "0.6.4" ) ) .settings(commonSettings) - .dependsOn(lambda, lambdaHttp4s, googleCloudHttp4s) + .dependsOn(lambda, lambdaHttp4s, lambdaNatchez, lambdaOtel4s, googleCloudHttp4s) + .jvmSettings(libraryDependencies ++= Seq( + "org.typelevel" %%% "otel4s-oteljava" % otel4sVersion, + "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.55.0", + "com.google.cloud.functions.invoker" % "java-function-invoker" % "1.3.3" + )) .jsSettings( scalaJSUseMainModuleInitializer := true, Compile / mainClass := Some("feral.examples.http4sGoogleCloudHandler"), @@ -224,6 +263,7 @@ lazy val unidocs = project ) lazy val scalafix = tlScalafixProject + .in(file("scalafix")) .rulesSettings( name := "feral-scalafix", startYear := Some(2023), @@ -231,7 +271,7 @@ lazy val scalafix = tlScalafixProject ) .inputSettings( crossScalaVersions := Seq(Scala213), - libraryDependencies += "org.typelevel" %%% "feral-lambda-http4s" % "0.2.4", + libraryDependencies += "org.typelevel" %%% "feral-lambda" % "0.3.1", headerSources / excludeFilter := AllPassFilter ) .inputConfigure(_.disablePlugins(ScalafixPlugin)) @@ -239,7 +279,7 @@ lazy val scalafix = tlScalafixProject crossScalaVersions := Seq(Scala213), headerSources / excludeFilter := AllPassFilter ) - .outputConfigure(_.dependsOn(lambdaHttp4s.jvm).disablePlugins(ScalafixPlugin)) + .outputConfigure(_.dependsOn(lambdaNatchez.jvm).disablePlugins(ScalafixPlugin)) .testsSettings( startYear := Some(2023), crossScalaVersions := Seq(Scala212) diff --git a/examples/jvm/src/main/scala/feral/examples/SqsOtelJavaExample.scala b/examples/jvm/src/main/scala/feral/examples/SqsOtelJavaExample.scala new file mode 100644 index 00000000..2222c35b --- /dev/null +++ b/examples/jvm/src/main/scala/feral/examples/SqsOtelJavaExample.scala @@ -0,0 +1,82 @@ +/* + * 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.Monad +import cats.effect.IO +import cats.syntax.all._ +import feral.lambda.INothing +import feral.lambda.IOLambda +import feral.lambda.Invocation +import feral.lambda.events.SqsEvent +import feral.lambda.events.SqsRecord +import feral.lambda.otel4s._ +import org.http4s.client.Client +import org.http4s.ember.client.EmberClientBuilder +import org.http4s.otel4s.middleware.trace.client.ClientMiddleware +import org.http4s.otel4s.middleware.trace.client.ClientSpanDataProvider +import org.http4s.otel4s.middleware.trace.client.UriRedactor +import org.typelevel.otel4s.oteljava.OtelJava +import org.typelevel.otel4s.trace.Tracer +import org.typelevel.otel4s.trace.TracerProvider +import org.typelevel.scalaccompat.annotation.unused + +object SqsOtelExample extends IOLambda[SqsEvent, INothing] { + + def handler = + OtelJava + .autoConfigured[IO]() + .evalMap { otel => + implicit val tp: TracerProvider[IO] = otel.tracerProvider + val otelClientRedactor = new UriRedactor.OnlyRedactUserInfo {} + val spanDataProvider = ClientSpanDataProvider.openTelemetry(otelClientRedactor) + val tracer = tp.get("com.example") + val middleware = ClientMiddleware.builder[IO](spanDataProvider).build + (middleware, tracer).tupled + } + .flatMap { + case (middleware, tracer) => + implicit val t: Tracer[IO] = tracer + + for { + client <- EmberClientBuilder.default[IO].build.map(middleware.wrapClient) + } yield { implicit inv: Invocation[IO, SqsEvent] => + TracedHandler[IO, SqsEvent, INothing]( + handleEvent[IO](client) + ) + } + } + + def handleEvent[F[_]: Monad: Tracer]( + @unused client: Client[F] + )(implicit inv: Invocation[F, SqsEvent]): F[Option[INothing]] = inv.event.flatMap { event => + event + .records + .traverse(record => + Tracer[F].span("handle-record", SqsRecordAttributes(record)).surround { + handleRecord[F](record) + }) + .as(None) + } + + def handleRecord[F[_]: Monad: Tracer](@unused record: SqsRecord): F[Unit] = { + Tracer[F].span("some-operation").surround { + Monad[F].unit + } + } + +} diff --git a/examples/shared/src/main/scala/feral/examples/Http4sLambda.scala b/examples/shared/src/main/scala/feral/examples/Http4sLambda.scala index 6d43807f..3a78ee4f 100644 --- a/examples/shared/src/main/scala/feral/examples/Http4sLambda.scala +++ b/examples/shared/src/main/scala/feral/examples/Http4sLambda.scala @@ -16,14 +16,15 @@ package feral.examples +import _root_.feral.lambda._ +import _root_.feral.lambda.events._ +import _root_.feral.lambda.http4s._ +import _root_.feral.lambda.natchez._ +import _root_.natchez.Trace +import _root_.natchez.http4s.NatchezMiddleware +import _root_.natchez.xray.XRay import cats.effect._ import cats.effect.std.Random -import feral.lambda._ -import feral.lambda.events._ -import feral.lambda.http4s._ -import natchez.Trace -import natchez.http4s.NatchezMiddleware -import natchez.xray.XRay import org.http4s.HttpApp import org.http4s.HttpRoutes import org.http4s.client.Client diff --git a/examples/shared/src/main/scala/feral/examples/KinesisLambda.scala b/examples/shared/src/main/scala/feral/examples/KinesisLambda.scala index 5b9decac..8b6a2baf 100644 --- a/examples/shared/src/main/scala/feral/examples/KinesisLambda.scala +++ b/examples/shared/src/main/scala/feral/examples/KinesisLambda.scala @@ -16,12 +16,12 @@ package feral.examples +import _root_.feral.lambda._ +import _root_.feral.lambda.events.SqsEvent +import _root_.natchez.Trace +import _root_.natchez.xray.XRay import cats.effect._ import cats.effect.std.Random -import feral.lambda._ -import feral.lambda.events.SqsEvent -import natchez.Trace -import natchez.xray.XRay import skunk.Session /** diff --git a/lambda/shared/src/main/scala/feral/lambda/AwsTags.scala b/lambda-natchez/shared/src/main/scala/feral/lambda/natchez/AwsTags.scala similarity index 96% rename from lambda/shared/src/main/scala/feral/lambda/AwsTags.scala rename to lambda-natchez/shared/src/main/scala/feral/lambda/natchez/AwsTags.scala index b75d521b..30c1f74c 100644 --- a/lambda/shared/src/main/scala/feral/lambda/AwsTags.scala +++ b/lambda-natchez/shared/src/main/scala/feral/lambda/natchez/AwsTags.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package feral.lambda +package feral.lambda.natchez import natchez.TraceValue diff --git a/lambda-natchez/shared/src/main/scala/feral/lambda/natchez/KernelSource.scala b/lambda-natchez/shared/src/main/scala/feral/lambda/natchez/KernelSource.scala new file mode 100644 index 00000000..8bf40981 --- /dev/null +++ b/lambda-natchez/shared/src/main/scala/feral/lambda/natchez/KernelSource.scala @@ -0,0 +1,59 @@ +/* + * 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.natchez + +import feral.lambda.events.ApiGatewayProxyEvent +import feral.lambda.events.ApiGatewayProxyEventV2 +import feral.lambda.events.DynamoDbStreamEvent +import feral.lambda.events.KinesisStreamEvent +import feral.lambda.events.S3BatchEvent +import feral.lambda.events.SqsRecordAttributes +import natchez.Kernel +import org.typelevel.ci._ + +trait KernelSource[Event] { + def extract(event: Event): Kernel +} + +object KernelSource { + @inline def apply[E](implicit ev: KernelSource[E]): ev.type = ev + + def emptyKernelSource[E]: KernelSource[E] = _ => Kernel(Map.empty) + + private[this] val `X-Amzn-Trace-Id` = ci"X-Amzn-Trace-Id" + + implicit def apiGatewayProxyEvent: KernelSource[ApiGatewayProxyEvent] = + e => Kernel(e.headers.getOrElse(Map.empty)) + + implicit def apiGatewayProxyEventV2: KernelSource[ApiGatewayProxyEventV2] = + e => Kernel(e.headers) + + implicit def sqsRecordAttributes: KernelSource[SqsRecordAttributes] = + a => Kernel(a.awsTraceHeader.map(`X-Amzn-Trace-Id` -> _).toMap) + + implicit def s3BatchEvent: KernelSource[S3BatchEvent] = KernelSource.emptyKernelSource + + @deprecated( + "See feral.lambda.events.KinesisStreamEvent deprecation", + since = "0.3.0" + ) + implicit def kinesisStreamEvent: KernelSource[KinesisStreamEvent] = + KernelSource.emptyKernelSource + + implicit def dynamoDbStreamEvent: KernelSource[DynamoDbStreamEvent] = + KernelSource.emptyKernelSource +} diff --git a/lambda/shared/src/main/scala/feral/lambda/TracedHandler.scala b/lambda-natchez/shared/src/main/scala/feral/lambda/natchez/TracedHandler.scala similarity index 97% rename from lambda/shared/src/main/scala/feral/lambda/TracedHandler.scala rename to lambda-natchez/shared/src/main/scala/feral/lambda/natchez/TracedHandler.scala index ac8cc28c..57c54645 100644 --- a/lambda/shared/src/main/scala/feral/lambda/TracedHandler.scala +++ b/lambda-natchez/shared/src/main/scala/feral/lambda/natchez/TracedHandler.scala @@ -14,12 +14,13 @@ * limitations under the License. */ -package feral.lambda +package feral.lambda.natchez import cats.data.Kleisli import cats.effect.IO import cats.effect.kernel.MonadCancelThrow import cats.syntax.all._ +import feral.lambda.Invocation import natchez.EntryPoint import natchez.Span import natchez.Trace diff --git a/lambda/shared/src/test/scala/feral/lambda/TracedHandlerSuite.scala b/lambda-natchez/shared/src/test/scala/feral/lambda/natchez/TracedHandlerSuite.scala similarity index 91% rename from lambda/shared/src/test/scala/feral/lambda/TracedHandlerSuite.scala rename to lambda-natchez/shared/src/test/scala/feral/lambda/natchez/TracedHandlerSuite.scala index b02044fe..bd74ff3b 100644 --- a/lambda/shared/src/test/scala/feral/lambda/TracedHandlerSuite.scala +++ b/lambda-natchez/shared/src/test/scala/feral/lambda/natchez/TracedHandlerSuite.scala @@ -14,10 +14,12 @@ * limitations under the License. */ -package feral.lambda +package feral.lambda.natchez import cats.data.Kleisli import cats.effect.IO +import feral.lambda.INothing +import feral.lambda.Invocation import feral.lambda.events.KinesisStreamEvent import natchez.EntryPoint import natchez.Span @@ -25,7 +27,7 @@ import natchez.Trace import scala.annotation.nowarn -class TracedLambdaSuite { +class TracedHandlerSuite { @nowarn def syntaxTest = { // Checking for compilation, nothing more diff --git a/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/EventAttributeSource.scala b/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/EventAttributeSource.scala new file mode 100644 index 00000000..eafacb1a --- /dev/null +++ b/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/EventAttributeSource.scala @@ -0,0 +1,97 @@ +/* + * 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.otel4s + +import feral.lambda.events.ApiGatewayProxyEvent +import feral.lambda.events.ApiGatewayProxyEventV2 +import feral.lambda.events.DynamoDbStreamEvent +import feral.lambda.events.S3BatchEvent +import feral.lambda.events.SqsEvent +import org.typelevel.otel4s.Attributes +import org.typelevel.otel4s.trace.SpanKind + +trait EventAttributeSource[E] { + def contextCarrier(e: E): Map[String, String] + def spanKind: SpanKind + def attributes(e: E): Attributes +} + +object EventAttributeSource { + def empty[E](sk: SpanKind): EventAttributeSource[E] = + new EventAttributeSource[E] { + def contextCarrier(e: E): Map[String, String] = + Map.empty + def spanKind: SpanKind = sk + def attributes(e: E): Attributes = + Attributes.empty + } + + implicit def sqsEvent: EventAttributeSource[SqsEvent] = + new EventAttributeSource[SqsEvent] { + def contextCarrier(e: SqsEvent): Map[String, String] = + Map.empty + + def spanKind: SpanKind = SpanKind.Consumer + + def attributes(e: SqsEvent): Attributes = + SqsEventAttributes() + } + + implicit def dynamoDbStreamEvent: EventAttributeSource[DynamoDbStreamEvent] = + new EventAttributeSource[DynamoDbStreamEvent] { + def contextCarrier(e: DynamoDbStreamEvent): Map[String, String] = + Map.empty + + def spanKind: SpanKind = SpanKind.Consumer + + def attributes(e: DynamoDbStreamEvent): Attributes = + DynamoDbStreamEventAttributes() + } + + implicit def apiGatewayProxyEvent: EventAttributeSource[ApiGatewayProxyEvent] = + new EventAttributeSource[ApiGatewayProxyEvent] { + def contextCarrier(e: ApiGatewayProxyEvent): Map[String, String] = + e.headers.getOrElse(Map.empty).map { case (k, v) => (k.toString, v) } + + def spanKind: SpanKind = SpanKind.Server + + def attributes(e: ApiGatewayProxyEvent): Attributes = + ApiGatewayProxyEventAttributes() + } + + implicit def apiGatewayProxyEventV2: EventAttributeSource[ApiGatewayProxyEventV2] = + new EventAttributeSource[ApiGatewayProxyEventV2] { + def contextCarrier(e: ApiGatewayProxyEventV2): Map[String, String] = + e.headers.map { case (k, v) => (k.toString, v) } + + def spanKind: SpanKind = SpanKind.Server + + def attributes(e: ApiGatewayProxyEventV2): Attributes = + ApiGatewayProxyEventAttributes() + } + + implicit def s3BatchEvent: EventAttributeSource[S3BatchEvent] = + new EventAttributeSource[S3BatchEvent] { + def contextCarrier(e: S3BatchEvent): Map[String, String] = + Map.empty + + def spanKind: SpanKind = SpanKind.Server + + def attributes(e: S3BatchEvent): Attributes = + S3BatchEventAttributes(e) + } +} diff --git a/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/EventAttributes.scala b/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/EventAttributes.scala new file mode 100644 index 00000000..a9779ba1 --- /dev/null +++ b/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/EventAttributes.scala @@ -0,0 +1,75 @@ +/* + * 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.otel4s + +import feral.lambda.events.DynamoDbRecord +import feral.lambda.events.S3BatchEvent +import feral.lambda.events.SqsRecord +import org.typelevel.otel4s.Attributes + +import OtelAttributes._ + +object SqsEventAttributes { + def apply(): Attributes = + Attributes( + FaasTrigger(FaasTriggerValue.Pubsub.value), + MessagingSystem(MessagingSystemValue.AwsSqs.value) + ) +} + +object SqsRecordAttributes { + def apply(e: SqsRecord): Attributes = + Attributes( + FaasTrigger(FaasTriggerValue.Pubsub.value), + MessagingOperationType(MessagingOperationTypeValue.Receive.value), + MessagingMessageId(e.messageId) + ) +} + +object DynamoDbStreamEventAttributes { + def apply(): Attributes = + Attributes( + FaasTrigger(FaasTriggerValue.Datasource.value), + MessagingSystem(MessagingSystemValue.AwsSqs.value) + ) +} + +object DynamoDbRecordAttributes { + def apply(e: DynamoDbRecord): Attributes = { + Attributes( + FaasTrigger(FaasTriggerValue.Datasource.value), + MessagingOperationType(MessagingOperationTypeValue.Receive.value) + ) ++ e.eventId.map(MessagingMessageId(_)) + } +} + +object ApiGatewayProxyEventAttributes { + def apply(): Attributes = + Attributes( + FaasTrigger(FaasTriggerValue.Http.value), + MessagingOperationType(MessagingOperationTypeValue.Receive.value) + ) +} + +object S3BatchEventAttributes { + def apply(e: S3BatchEvent): Attributes = + Attributes( + MessagingMessageId(e.invocationId), + FaasTrigger(FaasTriggerValue.Datasource.value), + MessagingOperationType(MessagingOperationTypeValue.Receive.value) + ) +} diff --git a/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/LambdaContextAttributes.scala b/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/LambdaContextAttributes.scala new file mode 100644 index 00000000..7972a5fb --- /dev/null +++ b/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/LambdaContextAttributes.scala @@ -0,0 +1,36 @@ +/* + * 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.otel4s + +import feral.lambda.Context +import org.typelevel.otel4s.Attributes + +object LambdaContextAttributes { + import OtelAttributes._ + + def apply[F[_]](context: Context[F]): Attributes = { + Attributes( + CloudProvider(CloudProviderValue.Aws.value), + CloudResourceId(context.invokedFunctionArn), + FaasInstance(context.logStreamName), + FaasMaxMemory(context.memoryLimitInMB.toLong * 1024 * 1024), + FaasName(context.functionName), + FaasVersion(context.functionVersion) + ) + } + +} diff --git a/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/OtelAttributes.scala b/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/OtelAttributes.scala new file mode 100644 index 00000000..aa877131 --- /dev/null +++ b/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/OtelAttributes.scala @@ -0,0 +1,66 @@ +/* + * 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.otel4s + +import org.typelevel.otel4s.AttributeKey + +/** + * Temporary aliases for Lambda platform attributes in otel4s-semconv-experimental + */ +object OtelAttributes { + val FaasInvocationId = AttributeKey.string("faas.invocation_id") + val FaasTrigger = AttributeKey.string("faas.trigger") + object FaasTriggerValue { + object Pubsub { + val value = "pubsub" + } + object Datasource { + val value = "datasource" + } + object Http { + val value = "http" + } + } + + // ARN + val CloudResourceId = AttributeKey.string("cloud.resource_id") + val FaasInstance = AttributeKey.string("faas.instance") + val FaasMaxMemory = AttributeKey.long("faas.max_memory") + val FaasName = AttributeKey.string("faas.name") + val FaasVersion = AttributeKey.string("faas.version") + val CloudProvider = AttributeKey.string("cloud.provider") + object CloudProviderValue { + object Aws { + val value = "aws" + } + } + + val MessagingSystem = AttributeKey.string("messaging.system") + object MessagingSystemValue { + object AwsSqs { + val value = "aws_sqs" + } + } + val MessagingOperationType = AttributeKey.string("messaging.operation.type") + object MessagingOperationTypeValue { + object Receive { + val value = "receive" + } + } + val MessagingMessageId = AttributeKey.string("messaging.message.id") + val MessagingDestinationName = AttributeKey.string("messaging.destination.name") +} diff --git a/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/TracedHandler.scala b/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/TracedHandler.scala new file mode 100644 index 00000000..4f92eb5d --- /dev/null +++ b/lambda-otel4s/shared/src/main/scala/feral/lambda/otel4s/TracedHandler.scala @@ -0,0 +1,53 @@ +/* + * 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.otel4s + +import cats.Monad +import cats.syntax.all._ +import feral.lambda.Context +import feral.lambda.Invocation +import org.typelevel.otel4s.trace.SpanOps +import org.typelevel.otel4s.trace.Tracer + +object TracedHandler { + + def apply[F[_]: Monad: Tracer, Event, Result]( + handler: F[Option[Result]] + )( + implicit inv: Invocation[F, Event], + attr: EventAttributeSource[Event] + ): F[Option[Result]] = + for { + event <- inv.event + context <- inv.context + res <- Tracer[F].joinOrRoot(attr.contextCarrier(event)) { + buildSpan(event, context).surround { + handler + } + } + } yield res + + private def buildSpan[F[_]: Tracer, Event](event: Event, context: Context[F])( + implicit attr: EventAttributeSource[Event] + ): SpanOps[F] = + Tracer[F] + .spanBuilder(context.functionName) + .addAttributes(LambdaContextAttributes(context)) + .withSpanKind(attr.spanKind) + .addAttributes(attr.attributes(event)) + .build +} diff --git a/lambda-otel4s/shared/src/test/scala/feral/lambda/otel4s/OtelAttributesTest.scala b/lambda-otel4s/shared/src/test/scala/feral/lambda/otel4s/OtelAttributesTest.scala new file mode 100644 index 00000000..15e73af8 --- /dev/null +++ b/lambda-otel4s/shared/src/test/scala/feral/lambda/otel4s/OtelAttributesTest.scala @@ -0,0 +1,154 @@ +/* + * 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.otel4s + +import munit.FunSuite +import org.typelevel.otel4s.semconv.experimental.attributes.CloudExperimentalAttributes +import org.typelevel.otel4s.semconv.experimental.attributes.FaasExperimentalAttributes +import org.typelevel.otel4s.semconv.experimental.attributes.MessagingExperimentalAttributes + +/** + * Check attributes match experimental ones + */ +class OtelAttributesTest extends FunSuite { + + import OtelAttributes._ + val otel4s = FaasExperimentalAttributes + val otel4sCloud = CloudExperimentalAttributes + val otel4sMessaging = MessagingExperimentalAttributes + + test("FaasInvocationId") { + val value = "value" + val ours = FaasInvocationId(value) + val otel = otel4s.FaasInvocationId(value) + assertEquals(ours, otel) + } + + test("FaasTrigger") { + val value = "value" + val ours = FaasTrigger(value) + val otel = otel4s.FaasTrigger(value) + assertEquals(ours, otel) + } + + test("FaasTriggerValue.PubSub") { + val ours = FaasTriggerValue.Pubsub.value + val otel = otel4s.FaasTriggerValue.Pubsub.value + assertEquals(ours, otel) + } + + test("FaasTriggerValue.Datasource") { + val ours = FaasTriggerValue.Datasource.value + val otel = otel4s.FaasTriggerValue.Datasource.value + assertEquals(ours, otel) + } + + test("FaasTriggerValue.Http") { + val ours = FaasTriggerValue.Http.value + val otel = otel4s.FaasTriggerValue.Http.value + assertEquals(ours, otel) + } + + test("CloudResourceId") { + val value = "value" + val ours = CloudResourceId(value) + val otel = otel4sCloud.CloudResourceId(value) + assertEquals(ours, otel) + } + + test("FaasInstance") { + val value = "value" + val ours = FaasInstance(value) + val otel = otel4s.FaasInstance(value) + assertEquals(ours, otel) + } + + test("FaasMaxMemory") { + val value = 0L + val ours = FaasMaxMemory(value) + val otel = otel4s.FaasMaxMemory(value) + assertEquals(ours, otel) + } + + test("FaasName") { + val value = "value" + val ours = FaasName(value) + val otel = otel4s.FaasName(value) + assertEquals(ours, otel) + } + + test("FaasVersion") { + val value = "value" + val ours = FaasVersion(value) + val otel = otel4s.FaasVersion(value) + assertEquals(ours, otel) + } + + test("CloudProvider") { + val value = "value" + val ours = CloudProvider(value) + val otel = otel4sCloud.CloudProvider(value) + assertEquals(ours, otel) + } + + test("CloudProviderValue.Aws") { + val ours = CloudProviderValue.Aws.value + val otel = otel4sCloud.CloudProviderValue.Aws.value + assertEquals(ours, otel) + } + + test("MessagingSystem") { + val value = "value" + val ours = MessagingSystem(value) + val otel = otel4sMessaging.MessagingSystem(value) + assertEquals(ours, otel) + } + + test("MessagingSystemValue.AwsSqs") { + val ours = MessagingSystemValue.AwsSqs.value + val otel = otel4sMessaging.MessagingSystemValue.AwsSqs.value + assertEquals(ours, otel) + } + + test("MessagingOperationType") { + val value = "value" + val ours = MessagingOperationType(value) + val otel = otel4sMessaging.MessagingOperationType(value) + assertEquals(ours, otel) + } + + test("MessagingOperationValue.Receive") { + val ours = MessagingOperationTypeValue.Receive.value + val otel = otel4sMessaging.MessagingOperationTypeValue.Receive.value + assertEquals(ours, otel) + } + + test("MessagingMessageId") { + val value = "value" + val ours = MessagingMessageId(value) + val otel = otel4sMessaging.MessagingMessageId(value) + assertEquals(ours, otel) + } + + test("MessagingDestinationName") { + val value = "value" + val ours = MessagingDestinationName(value) + val otel = otel4sMessaging.MessagingDestinationName(value) + assertEquals(ours, otel) + } + +} diff --git a/lambda-otel4s/shared/src/test/scala/feral/lambda/otel4s/TracedHandlerSuite.scala b/lambda-otel4s/shared/src/test/scala/feral/lambda/otel4s/TracedHandlerSuite.scala new file mode 100644 index 00000000..895cc123 --- /dev/null +++ b/lambda-otel4s/shared/src/test/scala/feral/lambda/otel4s/TracedHandlerSuite.scala @@ -0,0 +1,166 @@ +/* + * 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 +package otel4s + +import cats.effect.IO +import cats.syntax.all._ +import io.circe.Decoder +import io.circe.Encoder +import munit.CatsEffectSuite +import org.typelevel.otel4s.Attributes +import org.typelevel.otel4s.sdk.testkit.trace.TracesTestkit +import org.typelevel.otel4s.semconv.experimental.attributes.CloudExperimentalAttributes +import org.typelevel.otel4s.semconv.experimental.attributes.CloudExperimentalAttributes.CloudProviderValue +import org.typelevel.otel4s.semconv.experimental.attributes.FaasExperimentalAttributes +import org.typelevel.otel4s.trace.SpanKind + +import java.util.concurrent.atomic.AtomicInteger + +class SharedTracedHandlerSuite extends CatsEffectSuite { + import SharedTracedHandlerSuite._ + + val fixture = ResourceFunFixture(TracesTestkit.inMemory[IO]()) + + fixture.test("single root span is created for single invocation") { traces => + traces.tracerProvider.tracer("test-tracer").get.flatMap { implicit tracer => + val event = TestEvent("1", "body") + + val attributes = Attributes( + FaasExperimentalAttributes.FaasName(functionName), + FaasExperimentalAttributes.FaasVersion(functionVersion), + FaasExperimentalAttributes.FaasInstance(logStreamName), + FaasExperimentalAttributes.FaasMaxMemory(memoryLimitInMB * 1024L * 1024L), + CloudExperimentalAttributes.CloudProvider(CloudProviderValue.Aws.value), + CloudExperimentalAttributes.CloudResourceId(invokedFunctionArn) + ) + + implicit val inv: Invocation[IO, TestEvent] = makeInvocation(event) + + val invokeCounter = new AtomicInteger + + val tracedHandler = TracedHandler[IO, TestEvent, INothing] { + for { + _ <- IO(invokeCounter.getAndIncrement()) + } yield Option.empty[INothing] + } + + for { + _ <- tracedHandler + spans <- traces.finishedSpans + headSpan = spans.headOption + } yield { + assertEquals(spans.length, 1) + assertEquals(headSpan.map(_.name), Some(functionName)) + assertEquals(headSpan.map(_.attributes.elements), Some(attributes)) + assertEquals(invokeCounter.get(), 1) + } + + } + } + + fixture.test("multiple root spans created for multiple invocations") { traces => + traces.tracerProvider.tracer("test-tracer").get.flatMap { implicit tracer => + val event = TestEvent("2", "body") + + val attributes = Attributes( + FaasExperimentalAttributes.FaasName(functionName), + FaasExperimentalAttributes.FaasVersion(functionVersion), + FaasExperimentalAttributes.FaasInstance(logStreamName), + FaasExperimentalAttributes.FaasMaxMemory(memoryLimitInMB * 1024L * 1024L), + CloudExperimentalAttributes.CloudProvider(CloudProviderValue.Aws.value), + CloudExperimentalAttributes.CloudResourceId(invokedFunctionArn) + ) + + implicit val inv: Invocation[IO, TestEvent] = makeInvocation(event) + + val invokeCounter = new AtomicInteger + + val tracedHandler = TracedHandler[IO, TestEvent, INothing] { + for { + _ <- IO(invokeCounter.getAndIncrement()) + } yield Option.empty[INothing] + } + + val handlerIndices = 0.to(4) + val runHandlers = handlerIndices.toList.map(_ => tracedHandler).sequence + + for { + _ <- runHandlers + spans <- traces.finishedSpans + } yield { + assertEquals(spans.length, handlerIndices.size) + spans.map(s => assertEquals(s.name, functionName)) + spans.map(s => assertEquals(s.attributes.elements, attributes)) + assertEquals(invokeCounter.get(), handlerIndices.size) + } + } + } +} + +object SharedTracedHandlerSuite { + + case class TestEvent(traceId: String, payload: String) + + object TestEvent { + + implicit val decoder: Decoder[TestEvent] = + Decoder.forProduct2("traceId", "payload")(TestEvent.apply) + implicit val encoder: Encoder[TestEvent] = + Encoder.forProduct2("traceId", "payload")(ev => (ev.traceId, ev.payload)) + + implicit val attr: EventAttributeSource[TestEvent] = + new EventAttributeSource[TestEvent] { + + override def contextCarrier(e: TestEvent): Map[String, String] = + Map("trace_id" -> e.traceId) + + override def spanKind: SpanKind = SpanKind.Consumer + + override def attributes(e: TestEvent): Attributes = Attributes.empty + + } + } + + val memoryLimitInMB = 1024 + val functionVersion = "1.0.1" + val logStreamNameArn = + "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/my-function:*" + val invokedFunctionArn = + "arn:aws:lambda:us-west-2:123456789012:function:test-function-name:PROD" + val functionName = "test-function-name" + val logStreamName = "log-stream-name" + val logGroupName = "log-group-name" + + val context: Context[IO] = { + Context.apply[IO]( + functionName = functionName, + functionVersion = functionVersion, + invokedFunctionArn = invokedFunctionArn, + memoryLimitInMB = memoryLimitInMB, + awsRequestId = "aws-request-id", + logGroupName = logGroupName, + logStreamName = logStreamName, + identity = None, + clientContext = None, + remainingTime = IO.realTime + ) + } + + def makeInvocation(event: TestEvent): Invocation[IO, TestEvent] = + Invocation.pure[IO, TestEvent](event, context) +} diff --git a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEvent.scala index e7b1fc34..435b4eb0 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEvent.scala @@ -18,7 +18,6 @@ package feral.lambda package events import io.circe.Decoder -import natchez.Kernel import org.typelevel.ci.CIString sealed abstract class ApiGatewayProxyEvent { @@ -78,9 +77,6 @@ object ApiGatewayProxyEvent { "multiValueHeaders" )(ApiGatewayProxyEvent.apply) - implicit def kernelSource: KernelSource[ApiGatewayProxyEvent] = - e => Kernel(e.headers.getOrElse(Map.empty)) - private final case class Impl( body: Option[String], resource: String, diff --git a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEventV2.scala b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEventV2.scala index 83017a91..f0ca01b8 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEventV2.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEventV2.scala @@ -18,7 +18,6 @@ package feral.lambda package events import io.circe.Decoder -import natchez.Kernel import org.typelevel.ci.CIString sealed abstract class Http { @@ -86,9 +85,6 @@ object ApiGatewayProxyEventV2 { "isBase64Encoded" )(ApiGatewayProxyEventV2.apply) - implicit def kernelSource: KernelSource[ApiGatewayProxyEventV2] = - e => Kernel(e.headers) - private final case class Impl( rawPath: String, rawQueryString: String, diff --git a/lambda/shared/src/main/scala/feral/lambda/events/DynamoDbStreamEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/DynamoDbStreamEvent.scala index f9177431..6a298f82 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/DynamoDbStreamEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/DynamoDbStreamEvent.scala @@ -215,8 +215,6 @@ object DynamoDbStreamEvent { implicit val decoder: Decoder[DynamoDbStreamEvent] = Decoder.forProduct1("Records")(DynamoDbStreamEvent.apply) - implicit def kernelSource: KernelSource[DynamoDbStreamEvent] = KernelSource.emptyKernelSource - private final case class Impl( records: List[DynamoDbRecord] ) extends DynamoDbStreamEvent { diff --git a/lambda/shared/src/main/scala/feral/lambda/events/KinesisStreamEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/KinesisStreamEvent.scala index bbd23757..17aea202 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/KinesisStreamEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/KinesisStreamEvent.scala @@ -146,8 +146,6 @@ object KinesisStreamEvent { implicit val decoder: Decoder[KinesisStreamEvent] = Decoder.forProduct1("Records")(KinesisStreamEvent.apply) - implicit def kernelSource: KernelSource[KinesisStreamEvent] = KernelSource.emptyKernelSource - private final case class Impl( records: List[KinesisStreamRecord] ) extends KinesisStreamEvent { diff --git a/lambda/shared/src/main/scala/feral/lambda/events/S3BatchEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/S3BatchEvent.scala index 864889d6..be44098c 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/S3BatchEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/S3BatchEvent.scala @@ -16,7 +16,6 @@ package feral.lambda.events -import feral.lambda.KernelSource import io.circe.Decoder // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/s3-batch.d.ts @@ -41,8 +40,6 @@ object S3BatchEvent { Decoder.forProduct4("invocationSchemaVersion", "invocationId", "job", "tasks")( S3BatchEvent.apply) - implicit def kernelSource: KernelSource[S3BatchEvent] = KernelSource.emptyKernelSource - private final case class Impl( invocationSchemaVersion: String, invocationId: String, diff --git a/lambda/shared/src/main/scala/feral/lambda/events/SqsEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/SqsEvent.scala index cc5271f5..d2f05067 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/SqsEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/SqsEvent.scala @@ -19,8 +19,6 @@ package events import io.circe.Decoder import io.circe.scodec._ -import natchez.Kernel -import org.typelevel.ci._ import scodec.bits.ByteVector import java.time.Instant @@ -176,9 +174,6 @@ object SqsRecordAttributes { ) } - implicit def kernelSource: KernelSource[SqsRecordAttributes] = a => - Kernel(a.awsTraceHeader.map(`X-Amzn-Trace-Id` -> _).toMap) - private final case class Impl( awsTraceHeader: Option[String], approximateReceiveCount: String, @@ -192,7 +187,6 @@ object SqsRecordAttributes { override def productPrefix = "SqsRecordAttributes" } - private[this] val `X-Amzn-Trace-Id` = ci"X-Amzn-Trace-Id" } sealed abstract class SqsMessageAttribute diff --git a/scalafix/input/src/main/scala/example/V0_3_0Rewrites.scala b/scalafix/input/src/main/scala/example/V0_3_0Rewrites.scala deleted file mode 100644 index 0b60baa2..00000000 --- a/scalafix/input/src/main/scala/example/V0_3_0Rewrites.scala +++ /dev/null @@ -1,36 +0,0 @@ -/* rule=V0_3_0Rewrites */ - -package example - -// format: off -import cats.effect.Concurrent -import feral.lambda.LambdaEnv -import feral.lambda.ApiGatewayProxyLambdaEnv -import feral.lambda.DynamoDbStreamLambdaEnv -import feral.lambda.S3BatchLambdaEnv -import feral.lambda.SnsLambdaEnv -import feral.lambda.SqsLambdaEnv -import feral.lambda.events.APIGatewayProxyRequestEvent -import feral.lambda.events.APIGatewayProxyResponseEvent -import feral.lambda.events.ApiGatewayProxyStructuredResultV2 -import feral.lambda.http4s.ApiGatewayProxyHandler -import org.http4s.HttpApp -// format: on - -class Foo[F[_], E] { - - def bar(implicit env: LambdaEnv[F, E]): Unit = ??? - -} - -object Handlers { - def handler1[F[_]: Concurrent]( - implicit env: ApiGatewayProxyLambdaEnv[F] - ): F[Option[ApiGatewayProxyStructuredResultV2]] = - ApiGatewayProxyHandler.httpApp(HttpApp.notFound) - def handler2[F[_]](implicit env: DynamoDbStreamLambdaEnv[F]): Unit = ??? - def handler3[F[_]](implicit env: S3BatchLambdaEnv[F]): Unit = ??? - def handler4[F[_]](implicit env: SnsLambdaEnv[F]): Unit = ??? - def handler5[F[_]](implicit env: SqsLambdaEnv[F]): Unit = ??? - def handler6(event: APIGatewayProxyRequestEvent): APIGatewayProxyResponseEvent = ??? -} diff --git a/scalafix/input/src/main/scala/example/V0_4_0Rewrites.scala b/scalafix/input/src/main/scala/example/V0_4_0Rewrites.scala new file mode 100644 index 00000000..d74cda51 --- /dev/null +++ b/scalafix/input/src/main/scala/example/V0_4_0Rewrites.scala @@ -0,0 +1,26 @@ +/* rule=V0_4_0Rewrites */ + +package example + +// format: off +import cats.effect.IO +import feral.lambda.AwsTags +import feral.lambda.KernelSource +import feral.lambda.TracedHandler +import feral.lambda.Invocation +import natchez.Trace +import natchez.EntryPoint +// format: on + +object V0_4_0RewritesInput { + + def useTags = AwsTags.arn("") + def source = KernelSource.emptyKernelSource[String] + def tracedHandler[Event, Result](entrypoint: EntryPoint[IO])( + handler: Trace[IO] => IO[Option[Result]] + )( + implicit env: Invocation[IO, Event], + KS: KernelSource[Event] + ): IO[Option[Result]] = TracedHandler(entrypoint)(handler) + +} diff --git a/scalafix/output/src/main/scala/example/V0_3_0Rewrites.scala b/scalafix/output/src/main/scala/example/V0_3_0Rewrites.scala deleted file mode 100644 index 99b10faa..00000000 --- a/scalafix/output/src/main/scala/example/V0_3_0Rewrites.scala +++ /dev/null @@ -1,28 +0,0 @@ -package example - -// format: off -import cats.effect.Concurrent -import feral.lambda.events.ApiGatewayProxyStructuredResultV2 -import org.http4s.HttpApp -import feral.lambda.{ ApiGatewayProxyInvocationV2, DynamoDbStreamInvocation, Invocation, S3BatchInvocation, SnsInvocation, SqsInvocation } -import feral.lambda.events.{ ApiGatewayProxyEvent, ApiGatewayProxyResult } -import feral.lambda.http4s.ApiGatewayProxyHandlerV2 -// format: on - -class Foo[F[_], E] { - - def bar(implicit env: Invocation[F, E]): Unit = ??? - -} - -object Handlers { - def handler1[F[_]: Concurrent]( - implicit env: ApiGatewayProxyInvocationV2[F] - ): F[Option[ApiGatewayProxyStructuredResultV2]] = - ApiGatewayProxyHandlerV2.apply(HttpApp.notFound) - def handler2[F[_]](implicit env: DynamoDbStreamInvocation[F]): Unit = ??? - def handler3[F[_]](implicit env: S3BatchInvocation[F]): Unit = ??? - def handler4[F[_]](implicit env: SnsInvocation[F]): Unit = ??? - def handler5[F[_]](implicit env: SqsInvocation[F]): Unit = ??? - def handler6(event: ApiGatewayProxyEvent): ApiGatewayProxyResult = ??? -} diff --git a/scalafix/output/src/main/scala/example/V0_4_0Rewrites.scala b/scalafix/output/src/main/scala/example/V0_4_0Rewrites.scala new file mode 100644 index 00000000..28398d74 --- /dev/null +++ b/scalafix/output/src/main/scala/example/V0_4_0Rewrites.scala @@ -0,0 +1,22 @@ +package example + +// format: off +import cats.effect.IO +import feral.lambda.Invocation +import natchez.Trace +import natchez.EntryPoint +import feral.lambda.natchez.{ AwsTags, KernelSource, TracedHandler } +// format: on + +object V0_4_0RewritesInput { + + def useTags = AwsTags.arn("") + def source = KernelSource.emptyKernelSource[String] + def tracedHandler[Event, Result](entrypoint: EntryPoint[IO])( + handler: Trace[IO] => IO[Option[Result]] + )( + implicit env: Invocation[IO, Event], + KS: KernelSource[Event] + ): IO[Option[Result]] = TracedHandler(entrypoint)(handler) + +} diff --git a/scalafix/rules/src/main/resources/META-INF/services/scalafix.v1.Rule b/scalafix/rules/src/main/resources/META-INF/services/scalafix.v1.Rule index 505e297f..5356dae0 100644 --- a/scalafix/rules/src/main/resources/META-INF/services/scalafix.v1.Rule +++ b/scalafix/rules/src/main/resources/META-INF/services/scalafix.v1.Rule @@ -1 +1 @@ -feral.scalafix.V0_3_0Rewrites +feral.scalafix.V0_4_0Rewrites diff --git a/scalafix/rules/src/main/scala/feral/scalafix/V0_3_0Rewrites.scala b/scalafix/rules/src/main/scala/feral/scalafix/V0_3_0Rewrites.scala deleted file mode 100644 index 9d320d1f..00000000 --- a/scalafix/rules/src/main/scala/feral/scalafix/V0_3_0Rewrites.scala +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2023 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.scalafix - -import scalafix.v1._ - -class V0_3_0Rewrites extends SemanticRule("V0_3_0Rewrites") { - override def fix(implicit doc: SemanticDocument): Patch = - Patch.replaceSymbols( - "feral.lambda.LambdaEnv" -> "feral.lambda.Invocation", - "feral.lambda.ApiGatewayProxyLambdaEnv" -> "feral.lambda.ApiGatewayProxyInvocationV2", - "feral.lambda.DynamoDbStreamLambdaEnv" -> "feral.lambda.DynamoDbStreamInvocation", - "feral.lambda.S3BatchLambdaEnv" -> "feral.lambda.S3BatchInvocation", - "feral.lambda.SnsLambdaEnv" -> "feral.lambda.SnsInvocation", - "feral.lambda.SqsLambdaEnv" -> "feral.lambda.SqsInvocation", - "feral.lambda.events.APIGatewayProxyRequestEvent" -> "feral.lambda.events.ApiGatewayProxyEvent", - "feral.lambda.events.APIGatewayProxyResponseEvent" -> "feral.lambda.events.ApiGatewayProxyResult", - "feral.lambda.http4s.ApiGatewayProxyHandler.httpApp" -> "feral.lambda.http4s.ApiGatewayProxyHandlerV2.apply", - "feral.lambda.http4s.ApiGatewayProxyHandler.apply" -> "feral.lambda.http4s.ApiGatewayProxyHandlerV2.httpRoutes", - "feral.lambda.http4s.ApiGatewayProxyHandler.httpRoutes" -> "feral.lambda.http4s.ApiGatewayProxyHandlerV2.httpRoutes" - ) + Patch.removeGlobalImport(Symbol("feral/lambda/http4s/ApiGatewayProxyHandler.")) -} diff --git a/lambda/shared/src/main/scala/feral/lambda/KernelSource.scala b/scalafix/rules/src/main/scala/feral/scalafix/V0_4_0Rewrites.scala similarity index 56% rename from lambda/shared/src/main/scala/feral/lambda/KernelSource.scala rename to scalafix/rules/src/main/scala/feral/scalafix/V0_4_0Rewrites.scala index 6b0346a1..683673aa 100644 --- a/lambda/shared/src/main/scala/feral/lambda/KernelSource.scala +++ b/scalafix/rules/src/main/scala/feral/scalafix/V0_4_0Rewrites.scala @@ -1,5 +1,5 @@ /* - * Copyright 2021 Typelevel + * Copyright 2023 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,15 @@ * limitations under the License. */ -package feral.lambda +package feral.scalafix -import natchez.Kernel +import scalafix.v1._ -trait KernelSource[Event] { - def extract(event: Event): Kernel -} - -object KernelSource { - @inline def apply[E](implicit ev: KernelSource[E]): ev.type = ev - - def emptyKernelSource[E]: KernelSource[E] = _ => Kernel(Map.empty) +class V0_4_0Rewrites extends SemanticRule("V0_4_0Rewrites") { + override def fix(implicit doc: SemanticDocument): Patch = + Patch.replaceSymbols( + "feral.lambda.AwsTags" -> "feral.lambda.natchez.AwsTags", + "feral.lambda.KernelSource" -> "feral.lambda.natchez.KernelSource", + "feral.lambda.TracedHandler" -> "feral.lambda.natchez.TracedHandler" + ) }