Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

work around bug in EC2 query serialization when using DescribeInstances #147

Merged
merged 2 commits into from
Dec 7, 2023
Merged
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
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import com.dwolla.aws.sns.*
import feral.lambda.events.SnsEvent
import feral.lambda.{INothing, LambdaEnv}
import fs2.Stream
import natchez.{EntryPoint, Span}
import natchez.{EntryPoint, Span, Trace}
import natchez.mtl.given
import org.typelevel.log4cats.{Logger, LoggerFactory}
import com.dwolla.tracing.syntax.*

Expand All @@ -18,15 +19,19 @@ object LifecycleHookHandler {
(eventBridge: (TopicARN, LifecycleHookNotification) => F[Unit])
(using fs2.Compiler[F, F], Local[F, Span[F]]): LambdaEnv[F, SnsEvent] => F[Option[INothing]] = env =>
entryPoint.runInRoot(hookName) {
Stream.eval(env.event)
.map(_.records)
.flatMap(Stream.emits(_))
.map(_.sns)
.evalMap(ParseLifecycleHookNotification[F])
.unNone
.evalMap(eventBridge.tupled)
.compile
.drain
.as(None)
Trace[F].kernel.flatMap(k => Logger[F].info(s"trace kernel: ${k.toHeaders}")) >>
Stream.eval(env.event)
.map(_.records)
.flatMap(Stream.emits(_))
.map(_.sns)
.evalMap(ParseLifecycleHookNotification[F])
.unNone
.evalMap(eventBridge.tupled)
.compile
.drain
.as(None)
}
.flatTap { _ =>
Logger[F].info("tracing should be complete by now")
}
}
40 changes: 40 additions & 0 deletions core/src/main/scala/com/dwolla/aws/ec2/model.scala
Original file line number Diff line number Diff line change
@@ -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))
}
}
2 changes: 1 addition & 1 deletion core/src/main/scala/com/dwolla/aws/ecs/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down
Original file line number Diff line number Diff line change
@@ -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]
}
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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"))
)
}