diff --git a/build.sbt b/build.sbt index 6447873..804d73c 100644 --- a/build.sbt +++ b/build.sbt @@ -162,6 +162,7 @@ lazy val `registrator-health-check-lambda` = project "io.circe" %% "circe-literal" % "0.14.6" % Test, "io.circe" %% "circe-testing" % "0.14.6" % Test, "org.scala-lang.modules" %% "scala-java8-compat" % "1.0.2" % Test, + "com.dwolla" %% "dwolla-otel-natchez" % "0.2.2" % Test, ) }, topLevelDirectory := None, diff --git a/core-tests/src/test/scala/com/dwolla/aws/ec2/Ec2AlgSpec.scala b/core-tests/src/test/scala/com/dwolla/aws/ec2/Ec2AlgSpec.scala index 22188ff..54070ec 100644 --- a/core-tests/src/test/scala/com/dwolla/aws/ec2/Ec2AlgSpec.scala +++ b/core-tests/src/test/scala/com/dwolla/aws/ec2/Ec2AlgSpec.scala @@ -25,7 +25,6 @@ class Ec2AlgSpec .traverse { instanceId => val fakeClient = new EC2.Default[IO](new NotImplementedError().raiseError) { override def describeInstances(dryRun: Boolean, - maxResults: Int, filters: Option[List[Filter]], instanceIds: Option[List[InstanceId]], nextToken: Option[String]): IO[DescribeInstancesResult] = diff --git a/core/src/main/scala/com/dwolla/aws/ec2/model.scala b/core/src/main/scala/com/dwolla/aws/ec2/model.scala new file mode 100644 index 0000000..30d5d12 --- /dev/null +++ b/core/src/main/scala/com/dwolla/aws/ec2/model.scala @@ -0,0 +1,40 @@ +package com.dwolla.aws.ec2 + +import cats.tagless.aop.* +import cats.* +import com.amazonaws.ec2.* +import natchez.* +import com.dwolla.aws.TraceableValueInstances.given + +private def traceableAdvice[A: TraceableValue](name: String, a: A): Aspect.Advice[Eval, TraceableValue] = + Aspect.Advice.byValue[TraceableValue, A](name, a) + +given Aspect[EC2, TraceableValue, TraceableValue] = new Aspect[EC2, TraceableValue, TraceableValue] { + override def weave[F[_]](af: EC2[F]): EC2[[A] =>> Aspect.Weave[F, TraceableValue, TraceableValue, A]] = + new EC2[[A] =>> Aspect.Weave[F, TraceableValue, TraceableValue, A]] { + override def describeInstances(dryRun: Boolean, + filters: Option[List[Filter]], + instanceIds: Option[List[InstanceId]], + nextToken: Option[String]): Aspect.Weave[F, TraceableValue, TraceableValue, DescribeInstancesResult] = + Aspect.Weave[F, TraceableValue, TraceableValue, DescribeInstancesResult]( + "EC2", + List(List( + traceableAdvice("dryRun", dryRun), + traceableAdvice("filters", filters), + traceableAdvice("instanceIds", instanceIds), + traceableAdvice("nextToken", nextToken), + )), + Aspect.Advice("describeInstances", af.describeInstances(dryRun, filters, instanceIds, nextToken)) + ) + } + + override def mapK[F[_], G[_]](af: EC2[F]) + (fk: F ~> G): EC2[G] = + new EC2[G] { + override def describeInstances(dryRun: Boolean, + filters: Option[List[Filter]], + instanceIds: Option[List[InstanceId]], + nextToken: Option[String]): G[DescribeInstancesResult] = + fk(af.describeInstances(dryRun, filters, instanceIds, nextToken)) + } +} diff --git a/core/src/main/scala/com/dwolla/aws/ecs/model.scala b/core/src/main/scala/com/dwolla/aws/ecs/model.scala index 4b0b7fd..724318b 100644 --- a/core/src/main/scala/com/dwolla/aws/ecs/model.scala +++ b/core/src/main/scala/com/dwolla/aws/ecs/model.scala @@ -117,7 +117,7 @@ object TaskStatus { } } -def traceableAdvice[A: TraceableValue](name: String, a: A): Aspect.Advice[Eval, TraceableValue] = +private def traceableAdvice[A: TraceableValue](name: String, a: A): Aspect.Advice[Eval, TraceableValue] = Aspect.Advice.byValue[TraceableValue, A](name, a) given Aspect[ECS, TraceableValue, TraceableValue] = diff --git a/registrator-health-check/src/test/scala/com/dwolla/autoscaling/ecs/registrator/TestApp.scala b/registrator-health-check/src/test/scala/com/dwolla/autoscaling/ecs/registrator/TestApp.scala new file mode 100644 index 0000000..87c2707 --- /dev/null +++ b/registrator-health-check/src/test/scala/com/dwolla/autoscaling/ecs/registrator/TestApp.scala @@ -0,0 +1,75 @@ +package com.dwolla.autoscaling.ecs.registrator + +import cats.* +import cats.effect.* +import cats.effect.std.Console +import cats.mtl.Local +import cats.syntax.all.* +import com.amazonaws.ec2.* +import com.dwolla.aws.ec2.{*, given} +import com.dwolla.tracing.mtl.LocalSpan +import com.dwolla.tracing.syntax.* +import com.dwolla.tracing.{DwollaEnvironment, OpenTelemetryAtDwolla} +import natchez.Span +import natchez.mtl.given +import org.http4s.* +import org.http4s.client.{Client, middleware} +import org.http4s.ember.client.EmberClientBuilder +import org.typelevel.log4cats.* +import smithy4s.aws.* +import smithy4s.aws.kernel.AwsRegion + +object TestApp extends ResourceApp.Simple { + private val id = InstanceId("i-04d30e1ebb1109aa2") + override def run: Resource[IO, Unit] = + OpenTelemetryAtDwolla[IO]("ecs-autoscaling-scale-out-hook", DwollaEnvironment.Local) + .flatMap(_.root("EC2 Test App")) + .evalMap(LocalSpan(_)) + .flatMap { case given Local[IO, Span[IO]] => + given LoggerFactory[IO] = new ConsoleLogger[IO] + + EmberClientBuilder.default[IO].build + .map(middleware.Logger(true, true, logAction = (IO.println(_: String)).some)) + .flatMap(AwsEnvironment.default(_, AwsRegion.US_WEST_2)) + .flatMap(AwsClient(EC2, _)) + .map(_.traceWithInputsAndOutputs) + .map(Ec2Alg(_)) + .evalTap(_ => IO.println("using client via Ec2Alg")) + .evalTap(_.getTagsForInstance(id) >>= IO.println) + .void + } + +} + +class ConsoleLogger[F[_] : Applicative : Console] extends LoggerFactory[F] { + override def getLoggerFromName(name: String): SelfAwareStructuredLogger[F] = + new SelfAwareStructuredLogger[F] { + override def isTraceEnabled: F[Boolean] = true.pure[F] + override def isDebugEnabled: F[Boolean] = true.pure[F] + override def isInfoEnabled: F[Boolean] = true.pure[F] + override def isWarnEnabled: F[Boolean] = true.pure[F] + override def isErrorEnabled: F[Boolean] = true.pure[F] + override def trace(ctx: Map[String, String])(msg: => String): F[Unit] = Console[F].println(msg) + override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = Console[F].println(msg) + override def debug(ctx: Map[String, String])(msg: => String): F[Unit] = Console[F].println(msg) + override def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = Console[F].println(msg) + override def info(ctx: Map[String, String])(msg: => String): F[Unit] = Console[F].println(msg) + override def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = Console[F].println(msg) + override def warn(ctx: Map[String, String])(msg: => String): F[Unit] = Console[F].println(msg) + override def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = Console[F].println(msg) + override def error(ctx: Map[String, String])(msg: => String): F[Unit] = Console[F].println(msg) + override def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = Console[F].println(msg) + override def error(t: Throwable)(message: => String): F[Unit] = Console[F].println(message) + override def warn(t: Throwable)(message: => String): F[Unit] = Console[F].println(message) + override def info(t: Throwable)(message: => String): F[Unit] = Console[F].println(message) + override def debug(t: Throwable)(message: => String): F[Unit] = Console[F].println(message) + override def trace(t: Throwable)(message: => String): F[Unit] = Console[F].println(message) + override def error(message: => String): F[Unit] = Console[F].println(message) + override def warn(message: => String): F[Unit] = Console[F].println(message) + override def info(message: => String): F[Unit] = Console[F].println(message) + override def debug(message: => String): F[Unit] = Console[F].println(message) + override def trace(message: => String): F[Unit] = Console[F].println(message) + } + override def fromName(name: String): F[SelfAwareStructuredLogger[F]] = + getLoggerFromName(name).pure[F] +} diff --git a/smithy4s-preprocessors/src/main/scala/preprocessors/Ec2Preprocessor.scala b/smithy4s-preprocessors/src/main/scala/preprocessors/Ec2Preprocessor.scala index 7fa92c5..cd1678d 100644 --- a/smithy4s-preprocessors/src/main/scala/preprocessors/Ec2Preprocessor.scala +++ b/smithy4s-preprocessors/src/main/scala/preprocessors/Ec2Preprocessor.scala @@ -1,4 +1,7 @@ package preprocessors +import software.amazon.smithy.build.TransformContext +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.ShapeId final class Ec2Preprocessor extends OperationFilteringPreprocessor { override lazy val shapesToKeep: Set[String] = Set( @@ -8,4 +11,18 @@ final class Ec2Preprocessor extends OperationFilteringPreprocessor { override lazy val namespace: Set[String] = Set("com.amazonaws.ec2") override def getName: String = "Ec2Preprocessor" + + override def transform(ctx: TransformContext): Model = + ctx.getTransformer.filterShapes( + super.transform(ctx), + + /* This seems to be a bug in smithy4s; MaxResults and InstanceIds cannot both be set on + * a DescribeInstancesRequest. However, MaxResults is always set on the request, even if + * it's set to the default value. This filter removes the MaxResults attribute from the + * request class so that it can't ever be included. + * + * See https://github.com/disneystreaming/smithy4s/issues/1321 + */ + !_.toShapeId.equals(ShapeId.from("com.amazonaws.ec2#DescribeInstancesRequest$MaxResults")) + ) }