|
import cats.data.Kleisli |
|
import cats.effect.{IO, Resource} |
|
import org.http4s.dsl.io.* |
|
import org.http4s.* |
|
import org.http4s.server.Server |
|
import org.http4s.ember.server.EmberServerBuilder |
|
import org.typelevel.ci.CIString |
|
import org.typelevel.otel4s.Attribute |
|
import org.typelevel.otel4s.context.propagation.TextMapGetter |
|
import org.typelevel.otel4s.semconv.attributes.HttpAttributes |
|
import org.typelevel.otel4s.trace.* |
|
|
|
class HttpServer(forecastModule: ForecastModule)(using Tracer[IO]) { |
|
import HttpServer.given |
|
|
|
def start: Resource[IO, Server] = |
|
EmberServerBuilder.default[IO].withHttpApp(httpApp).build |
|
|
|
def httpApp: HttpApp[IO] = |
|
tracingMiddleware(routes.orNotFound) |
|
|
|
private def routes: HttpRoutes[IO] = HttpRoutes.of { case req @ GET -> Root / "weather" / location => |
|
for { |
|
forecast <- forecastModule.checkWeather(location) |
|
} yield Response[IO]().withEntity(forecast) |
|
} |
|
|
|
private def tracingMiddleware(httpApp: HttpApp[IO]): HttpApp[IO] = |
|
Kleisli { (req: Request[IO]) => |
|
IO.uncancelable { poll => |
|
Tracer[IO].joinOrRoot(req.headers) { |
|
Tracer[IO] |
|
.spanBuilder(s"${req.method} ${req.uri}") |
|
.withSpanKind(SpanKind.Server) |
|
.build |
|
.use { span => |
|
poll(httpApp.run(req)).flatTap { response => |
|
span.addAttribute(HttpAttributes.HttpResponseStatusCode(response.status.code)) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
} |
|
|
|
object HttpServer { |
|
def create(forecastModule: ForecastModule)(using TracerProvider[IO]): IO[HttpServer] = |
|
for { |
|
given Tracer[IO] <- TracerProvider[IO].get("org.http4s.server") |
|
} yield new HttpServer(forecastModule) |
|
|
|
private given TextMapGetter[Headers] with { |
|
def get(carrier: Headers, key: String): Option[String] = |
|
carrier.get(CIString(key)).map(_.head.value) |
|
|
|
def keys(carrier: Headers): Iterable[String] = |
|
carrier.headers.view.map(_.name).distinct.map(_.toString).toSeq |
|
} |
|
|
|
} |