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

Add metrics for search service #376

Merged
merged 2 commits into from
Nov 29, 2024
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
5 changes: 4 additions & 1 deletion modules/app/src/main/scala/app.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ object App extends IOApp.Simple:
.autoConfigured[IO](_.addMeterProviderCustomizer((b, _) => b.registerMetricReader(exporter.metricReader)))
.evalMap(_.meterProvider.get("lila-search"))

def mkServer(res: AppResources, config: AppConfig)(using MetricExporter.Pull[IO]): Resource[IO, Unit] =
def mkServer(res: AppResources, config: AppConfig)(using
Meter[IO],
MetricExporter.Pull[IO]
): Resource[IO, Unit] =
for
apiRoutes <- Routes(res, config.server)
httpRoutes = apiRoutes <+> mkPrometheusRoutes
Expand Down
8 changes: 5 additions & 3 deletions modules/app/src/main/scala/http.routes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ import cats.syntax.all.*
import lila.search.spec.*
import org.http4s.HttpRoutes
import org.typelevel.log4cats.LoggerFactory
import org.typelevel.otel4s.metrics.Meter
import smithy4s.http4s.SimpleRestJsonBuilder

def Routes(resources: AppResources, config: HttpServerConfig)(using
LoggerFactory[IO]
LoggerFactory[IO],
Meter[IO]
): Resource[IO, HttpRoutes[IO]] =

val healthServiceImpl = HealthServiceImpl(resources.esClient)
val searchServiceImpl = SearchServiceImpl(resources.esClient)

val search: Resource[IO, HttpRoutes[IO]] =
SimpleRestJsonBuilder.routes(searchServiceImpl).resource
SearchServiceImpl(resources.esClient).toResource
.flatMap(SimpleRestJsonBuilder.routes(_).resource)

val health: Resource[IO, HttpRoutes[IO]] =
SimpleRestJsonBuilder.routes(healthServiceImpl).resource
Expand Down
79 changes: 65 additions & 14 deletions modules/app/src/main/scala/service.search.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,62 @@ import lila.search.spec.*
import lila.search.study.Study
import lila.search.team.Team
import org.typelevel.log4cats.{ Logger, LoggerFactory }
import org.typelevel.otel4s.metrics.{ Histogram, Meter }
import org.typelevel.otel4s.{ Attribute, AttributeKey, Attributes }
import smithy4s.Timestamp

import java.time.Instant
import java.util.concurrent.TimeUnit

class SearchServiceImpl(esClient: ESClient[IO])(using LoggerFactory[IO]) extends SearchService[IO]:
class SearchServiceImpl(esClient: ESClient[IO], metric: Histogram[IO, Double])(using
LoggerFactory[IO]
) extends SearchService[IO]:

import SearchServiceImpl.given
import SearchServiceImpl.{ *, given }

given logger: Logger[IO] = LoggerFactory[IO].getLogger

private val baseAttributes = Attributes(Attribute("http.request.method", "POST"))
private val countMetric =
metric
.recordDuration(
TimeUnit.MILLISECONDS,
withErrorType(
baseAttributes
.added(MetricKeys.httpRoute, s"/api/count/")
)
)

private val searchMetric =
metric
.recordDuration(
TimeUnit.MILLISECONDS,
withErrorType(
baseAttributes
.added(MetricKeys.httpRoute, s"/api/count/")
)
)

private def countRecord[A](f: IO[A]) = countMetric.surround(f)
private def searchRecord[A](f: IO[A]) = searchMetric.surround(f)

override def count(query: Query): IO[CountOutput] =
esClient
.count(query)
.map(CountOutput.apply)
.handleErrorWith: e =>
logger.error(e)(s"Error in count: query=$query") *>
IO.raiseError(InternalServerError("Internal server error"))
countRecord:
esClient
.count(query)
.map(CountOutput.apply)
.handleErrorWith: e =>
logger.error(e)(s"Error in count: query=$query") *>
IO.raiseError(InternalServerError("Internal server error"))

override def search(query: Query, from: From, size: Size): IO[SearchOutput] =
esClient
.search(query, from, size)
.map(SearchOutput.apply)
.handleErrorWith: e =>
logger.error(e)(s"Error in search: query=$query, from=$from, size=$size") *>
IO.raiseError(InternalServerError("Internal server error"))
searchRecord:
esClient
.search(query, from, size)
.map(SearchOutput.apply)
.handleErrorWith: e =>
logger.error(e)(s"Error in search: query=$query, from=$from, size=$size") *>
IO.raiseError(InternalServerError("Internal server error"))

object SearchServiceImpl:

Expand Down Expand Up @@ -66,3 +97,23 @@ object SearchServiceImpl:
case _: Query.Game => Index.Game
case _: Query.Study => Index.Study
case _: Query.Team => Index.Team

def apply(elastic: ESClient[IO])(using Meter[IO], LoggerFactory[IO]): IO[SearchService[IO]] =
Meter[IO]
.histogram[Double]("http.server.request.duration")
.withUnit("ms")
.create
.map(new SearchServiceImpl(elastic, _))

object MetricKeys:
val httpRoute = AttributeKey.string("http.route")
val errorType = AttributeKey.string("error.type")

import lila.search.ESClient.MetricKeys.*
def withErrorType(static: Attributes)(ec: Resource.ExitCase): Attributes = ec match
case Resource.ExitCase.Succeeded =>
static
case Resource.ExitCase.Errored(e) =>
static.added(errorType, e.getClass.getName)
case Resource.ExitCase.Canceled =>
static.added(errorType, "canceled")