From c7d24e9f0231ad464e51b0b9f7122d0ee15ac018 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Sun, 14 Sep 2025 09:05:35 +0200 Subject: [PATCH 01/24] Setting version to 3.2.3-SNAPSHOT --- version.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.sbt b/version.sbt index 14b63d3c..18d870b1 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -ThisBuild / version := "3.2.2" +ThisBuild / version := "3.2.3-SNAPSHOT" From 1f6ca98f8374581db44356e78ecbcc2449ea984e Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Sun, 14 Sep 2025 09:08:34 +0200 Subject: [PATCH 02/24] Fix ingestor-cli's examples After https://github.com/lichess-org/lila-search/pull/568 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ad7adf64..dd18467e 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,10 @@ ingestor-cli/run --help ```sh # index all documents for specific index -sbt 'ingestor/runMain lila.search.ingestor.cli index --index team --since 0' +sbt 'ingestor-cli/run index --index team --since 0' # index all documents for all indexes -sbt 'ingestor/runMain lila.search.ingestor.cli index --all --since 0' +sbt 'ingestor-cli/run index --all --since 0' ``` ### release From 63ad9594f768c74b00c632154d7b8de344963bf6 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Mon, 15 Sep 2025 21:08:48 +0200 Subject: [PATCH 03/24] Make kvstore file path configurable --- modules/ingestor-core/src/main/scala/config.scala | 8 ++++++-- modules/ingestor-core/src/main/scala/kvstore.scala | 2 +- modules/ingestor-core/src/main/scala/resources.scala | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/ingestor-core/src/main/scala/config.scala b/modules/ingestor-core/src/main/scala/config.scala index eec04dca..dc73cf1e 100644 --- a/modules/ingestor-core/src/main/scala/config.scala +++ b/modules/ingestor-core/src/main/scala/config.scala @@ -17,16 +17,20 @@ object AppConfig: def load: IO[AppConfig] = appConfig.load[IO] + private def kvStorePath = env("KV_STORE_PATH").or(prop("kv.store.path")).as[String].default("store.json") + def appConfig = ( MongoConfig.config, ElasticConfig.config, - IngestorConfig.config + IngestorConfig.config, + kvStorePath ).parMapN(AppConfig.apply) case class AppConfig( mongo: MongoConfig, elastic: ElasticConfig, - ingestor: IngestorConfig + ingestor: IngestorConfig, + kvStorePath: String ) case class MongoConfig(uri: String, name: String, studyUri: String, studyName: String) diff --git a/modules/ingestor-core/src/main/scala/kvstore.scala b/modules/ingestor-core/src/main/scala/kvstore.scala index 6163964b..64647f6f 100644 --- a/modules/ingestor-core/src/main/scala/kvstore.scala +++ b/modules/ingestor-core/src/main/scala/kvstore.scala @@ -19,7 +19,7 @@ object KVStore: given JsonValueCodec[Map[String, Long]] = JsonCodecMaker.make type State = Map[String, Long] - def apply(): IO[KVStore] = + def apply(file: String): IO[KVStore] = Mutex .apply[IO] .map: mutex => diff --git a/modules/ingestor-core/src/main/scala/resources.scala b/modules/ingestor-core/src/main/scala/resources.scala index 1799c342..ce8cc87e 100644 --- a/modules/ingestor-core/src/main/scala/resources.scala +++ b/modules/ingestor-core/src/main/scala/resources.scala @@ -25,7 +25,7 @@ object AppResources: makeStudyMongoClient(conf.mongo), makeStudyOplogClient(conf.mongo), makeElasticClient(conf.elastic), - KVStore.apply().toResource + KVStore(conf.kvStorePath).toResource ).parMapN(AppResources.apply) private def makeElasticClient(conf: ElasticConfig)(using Meter[IO]): Resource[IO, ESClient[IO]] = From e5925f360679199ff498dfd5167a98b8c87fd573 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Mon, 15 Sep 2025 21:10:35 +0200 Subject: [PATCH 04/24] Revert me test docker --- .github/workflows/docker.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d5701f4e..df8fd2a7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,6 +3,8 @@ on: push: tags: - v* + pull_request: + branches: ['**'] env: REGISTRY: ghcr.io From 535d06e88bb37cd61fbf44e1de04bc4b651dd623 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Mon, 15 Sep 2025 21:36:57 +0200 Subject: [PATCH 05/24] Revert "Revert me test docker" This reverts commit e5925f360679199ff498dfd5167a98b8c87fd573. --- .github/workflows/docker.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index df8fd2a7..d5701f4e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,8 +3,6 @@ on: push: tags: - v* - pull_request: - branches: ['**'] env: REGISTRY: ghcr.io From d284cbf7a77b9afb4fff164a0427113efd7f9fc5 Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Wed, 17 Sep 2025 17:11:03 +0000 Subject: [PATCH 06/24] Update scalafmt-core to 3.9.10 --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index d1045b0f..a0bef279 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.9.9" +version = "3.9.10" runner.dialect = scala3 align.preset = none From 17147bfe8689d7fe7487912385c864ca64a9e10f Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Wed, 17 Sep 2025 17:11:11 +0000 Subject: [PATCH 07/24] Reformat with scalafmt 3.9.10 Executed command: scalafmt --non-interactive --- build.sbt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build.sbt b/build.sbt index c02e084e..12ae1cb8 100644 --- a/build.sbt +++ b/build.sbt @@ -12,11 +12,11 @@ inThisBuild( resolvers ++= ourResolvers, Compile / doc / sources := Seq.empty, publishTo := Option(Resolver.file("file", new File(sys.props.getOrElse("publishTo", "")))), - dockerBaseImage := "eclipse-temurin:21-jdk-noble", - dockerUpdateLatest := true, + dockerBaseImage := "eclipse-temurin:21-jdk-noble", + dockerUpdateLatest := true, dockerBuildxPlatforms := Seq("linux/amd64", "linux/arm64"), - Docker / maintainer := "lichess.org", - Docker / dockerRepository := Some("ghcr.io"), + Docker / maintainer := "lichess.org", + Docker / dockerRepository := Some("ghcr.io") ) ) @@ -90,7 +90,7 @@ lazy val `ingestor-app` = project name := "ingestor-app", commonSettings, buildInfoSettings, - Docker / packageName := "lichess-org/lila-search-ingestor-app", + Docker / packageName := "lichess-org/lila-search-ingestor-app", publish := {}, publish / skip := true, libraryDependencies ++= Seq( @@ -112,7 +112,7 @@ lazy val `ingestor-cli` = project name := "ingestor-cli", commonSettings, buildInfoSettings, - Docker / packageName := "lichess-org/lila-search-ingestor-cli", + Docker / packageName := "lichess-org/lila-search-ingestor-cli", publish := {}, publish / skip := true, libraryDependencies ++= Seq( @@ -181,7 +181,7 @@ lazy val app = project name := "lila-search", commonSettings, buildInfoSettings, - Docker / packageName := "lichess-org/lila-search-app", + Docker / packageName := "lichess-org/lila-search-app", publish := {}, publish / skip := true, libraryDependencies ++= Seq( From d5e808104305bd352bc60170b75bb7b3ba2816de Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Wed, 17 Sep 2025 17:11:11 +0000 Subject: [PATCH 08/24] Add 'Reformat with scalafmt 3.9.10' to .git-blame-ignore-revs --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 7f80fb21..27f2ad91 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -21,3 +21,6 @@ adcf303d99abee49f1266dad223bb3e5e34bd73f # Scala Steward: Reformat with scalafmt 3.9.9 6dfb7ed8ffa2f5df0f5c36bee8d3497d29016495 + +# Scala Steward: Reformat with scalafmt 3.9.10 +17147bfe8689d7fe7487912385c864ca64a9e10f From daf9c3a0c0cfcaa71ec352689e5539c93b334d1d Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 18 Sep 2025 20:21:02 +0200 Subject: [PATCH 09/24] Try to setup index's mappings before indexing --- modules/e2e/src/test/scala/CompatSuite.scala | 4 +- modules/elastic/src/main/scala/ESClient.scala | 7 ++ modules/ingestor-cli/src/main/scala/cli.scala | 71 +++++++++++-------- 3 files changed, 53 insertions(+), 29 deletions(-) diff --git a/modules/e2e/src/test/scala/CompatSuite.scala b/modules/e2e/src/test/scala/CompatSuite.scala index 071942c7..31d80fa2 100644 --- a/modules/e2e/src/test/scala/CompatSuite.scala +++ b/modules/e2e/src/test/scala/CompatSuite.scala @@ -86,7 +86,9 @@ object CompatSuite extends weaver.IOSuite: override def search[A](query: A, from: From, size: Size)(using Queryable[A]) = IO.pure(Nil) - override def status = IO.pure("yellow") + override def status = IO("yellow") + + override def indexExists(index: Index) = IO(true) given system: ActorSystem = ActorSystem() diff --git a/modules/elastic/src/main/scala/ESClient.scala b/modules/elastic/src/main/scala/ESClient.scala index 06c00318..76ee3711 100644 --- a/modules/elastic/src/main/scala/ESClient.scala +++ b/modules/elastic/src/main/scala/ESClient.scala @@ -26,6 +26,7 @@ trait ESClient[F[_]]: def putMapping(index: Index): RaiseF[Unit] def refreshIndex(index: Index): RaiseF[Unit] def status: RaiseF[String] + def indexExists(index: Index): RaiseF[Boolean] object ESClient: @@ -164,6 +165,12 @@ object ESClient: .execute(ElasticDsl.refreshIndex(index.value)) .flatMap(_.unitOrFail) + def indexExists(index: Index): RaiseF[Boolean] = + client + .execute(ElasticDsl.indexExists(index.value)) + .flatMap(_.toResult) + .map(_.exists) + private def dropIndex(index: Index) = client.execute(deleteIndex(index.value)) diff --git a/modules/ingestor-cli/src/main/scala/cli.scala b/modules/ingestor-cli/src/main/scala/cli.scala index 44c7049a..ce19ac46 100644 --- a/modules/ingestor-cli/src/main/scala/cli.scala +++ b/modules/ingestor-cli/src/main/scala/cli.scala @@ -3,6 +3,7 @@ package ingestor import cats.data.Validated import cats.effect.* +import cats.mtl.Handle import cats.syntax.all.* import com.monovore.decline.* import com.monovore.decline.effect.* @@ -26,13 +27,13 @@ object cli override def main: Opts[IO[ExitCode]] = opts.parse.map: opts => - makeIngestor.use(_.execute(opts).as(ExitCode.Success)) + makeIngestor.use(execute(opts)).as(ExitCode.Success) - def makeIngestor: Resource[IO, Ingestors] = + private def makeIngestor = for config <- AppConfig.load.toResource res <- AppResources.instance(config) - ingestor <- Ingestors( + ingestors <- Ingestors( res.lichess, res.study, res.studyLocal, @@ -40,16 +41,16 @@ object cli res.elastic, config.ingestor ).toResource - yield ingestor + yield (ingestors, res.elastic) - extension (ingestor: Ingestors) - def execute(opts: IndexOpts | WatchOpts): IO[Unit] = - opts match - case opts: IndexOpts => index(opts) - case opts: WatchOpts => watch(opts) + def execute(opts: IndexOpts | WatchOpts)(ingestor: Ingestors, elastic: ESClient[IO]): IO[Unit] = + opts match + case opts: IndexOpts => index(ingestor, elastic)(opts) + case opts: WatchOpts => watch(ingestor)(opts) - def index(opts: IndexOpts): IO[Unit] = - opts.index match + def index(ingestor: Ingestors, elastic: ESClient[IO])(opts: IndexOpts): IO[Unit] = + putMappingsIfNotExists(elastic, opts.index) *> + opts.index.match case Index.Forum => ingestor.forum.run(opts.since, opts.until, opts.dry) case Index.Ublog => @@ -67,24 +68,38 @@ object cli ingestor.game.run(opts.since, opts.until, opts.dry) *> ingestor.team.run(opts.since, opts.until, opts.dry) - def watch(opts: WatchOpts): IO[Unit] = - opts.index match - case Index.Game => + private def putMappingsIfNotExists(elastic: ESClient[IO], index: Index | Unit): IO[Unit] = + def go(index: Index) = + Handle + .allow: + elastic + .indexExists(index) + .ifM(Logger[IO].info(s"Index ${index.value} exists, start indexing"), elastic.putMapping(index)) + .rescue: e => + Logger[IO].error(e.asException)(s"Failed to check or put mapping for ${index.value}") *> + e.asException.raiseError + index match + case i: Index => go(i) + case _ => Index.values.toList.traverse_(go) + + def watch(ingestor: Ingestors)(opts: WatchOpts): IO[Unit] = + opts.index match + case Index.Game => + ingestor.game.watch(opts.since.some, opts.dry) + case Index.Forum => + ingestor.forum.watch(opts.since.some, opts.dry) + case Index.Ublog => + ingestor.ublog.watch(opts.since.some, opts.dry) + case Index.Team => + ingestor.team.watch(opts.since.some, opts.dry) + case Index.Study => + ingestor.study.watch(opts.since.some, opts.dry) + case _ => + ingestor.forum.watch(opts.since.some, opts.dry) *> + ingestor.ublog.watch(opts.since.some, opts.dry) *> + ingestor.team.watch(opts.since.some, opts.dry) *> + ingestor.study.watch(opts.since.some, opts.dry) *> ingestor.game.watch(opts.since.some, opts.dry) - case Index.Forum => - ingestor.forum.watch(opts.since.some, opts.dry) - case Index.Ublog => - ingestor.ublog.watch(opts.since.some, opts.dry) - case Index.Team => - ingestor.team.watch(opts.since.some, opts.dry) - case Index.Study => - ingestor.study.watch(opts.since.some, opts.dry) - case _ => - ingestor.forum.watch(opts.since.some, opts.dry) *> - ingestor.ublog.watch(opts.since.some, opts.dry) *> - ingestor.team.watch(opts.since.some, opts.dry) *> - ingestor.study.watch(opts.since.some, opts.dry) *> - ingestor.game.watch(opts.since.some, opts.dry) object opts: case class IndexOpts(index: Index | Unit, since: Instant, until: Instant, dry: Boolean) From 2a5b1f557c632f055a2b8c20769489fdece48e66 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 18 Sep 2025 20:21:42 +0200 Subject: [PATCH 10/24] revert me docker pr on publish test --- .github/workflows/docker.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d5701f4e..df8fd2a7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,6 +3,8 @@ on: push: tags: - v* + pull_request: + branches: ['**'] env: REGISTRY: ghcr.io From 3384ae55b0b3da03d8d441ba0febeb61657df690 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 18 Sep 2025 20:56:45 +0200 Subject: [PATCH 11/24] Revert "revert me docker pr on publish test" This reverts commit 2a5b1f557c632f055a2b8c20769489fdece48e66. --- .github/workflows/docker.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index df8fd2a7..d5701f4e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,8 +3,6 @@ on: push: tags: - v* - pull_request: - branches: ['**'] env: REGISTRY: ghcr.io From 88b5313afa9f244e5bba876fb4a7aa24b33e0d42 Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Sun, 21 Sep 2025 15:16:25 +0000 Subject: [PATCH 12/24] Update scalachess to 17.10.1 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index d79b99b8..42f6cfcd 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -10,7 +10,7 @@ object Dependencies { object V { val catsEffect = "3.6.3" val catsMtl = "1.6.0" - val chess = "17.9.6" + val chess = "17.10.1" val ciris = "3.10.0" val decline = "2.5.0" val elastic4s = "9.1.0" From 976976dd644861954d5e9df53ae9e3d26698b33e Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Thu, 25 Sep 2025 15:15:28 +0000 Subject: [PATCH 13/24] Update http4s-client, http4s-ember-client, ... to 0.23.32 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 42f6cfcd..68de2039 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -15,7 +15,7 @@ object Dependencies { val decline = "2.5.0" val elastic4s = "9.1.0" val fs2 = "3.12.2" - val http4s = "0.23.30" + val http4s = "0.23.32" val mongo4cats = "0.7.13" val otel4s = "0.13.1" } From 1e4e2bea2cc24923303c53926ceca8c6b9b27c7e Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Sat, 27 Sep 2025 16:18:31 +0000 Subject: [PATCH 14/24] Update ciris, ciris-http4s to 3.11.0 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 42f6cfcd..470ccd91 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -11,7 +11,7 @@ object Dependencies { val catsEffect = "3.6.3" val catsMtl = "1.6.0" val chess = "17.10.1" - val ciris = "3.10.0" + val ciris = "3.11.0" val decline = "2.5.0" val elastic4s = "9.1.0" val fs2 = "3.12.2" From fc4d205238335588af6c872aae270c439f3ee755 Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Mon, 29 Sep 2025 14:48:49 +0000 Subject: [PATCH 15/24] Update scalachess to 17.12.3 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 42f6cfcd..0c17b380 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -10,7 +10,7 @@ object Dependencies { object V { val catsEffect = "3.6.3" val catsMtl = "1.6.0" - val chess = "17.10.1" + val chess = "17.12.3" val ciris = "3.10.0" val decline = "2.5.0" val elastic4s = "9.1.0" From 077a9d10698e2e0b96e354cd5ba252c0bb8c10f0 Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Wed, 1 Oct 2025 16:10:48 +0000 Subject: [PATCH 16/24] Update logback-classic to 1.5.19 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 42f6cfcd..2a0a3c8e 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -62,7 +62,7 @@ object Dependencies { val otel4sMetrics = "org.typelevel" %% "otel4s-experimental-metrics" % "0.7.0" val log4Cats = "org.typelevel" %% "log4cats-slf4j" % "2.7.1" - val logback = "ch.qos.logback" % "logback-classic" % "1.5.18" + val logback = "ch.qos.logback" % "logback-classic" % "1.5.19" val ducktape = "io.github.arainko" %% "ducktape" % "0.2.10" From 85cf8e8636aeec8bceb8d23f3e813404ce88e017 Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Wed, 1 Oct 2025 16:10:52 +0000 Subject: [PATCH 17/24] Update sbt-native-packager to 1.11.4 --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 31f32462..92f1878b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") addSbtPlugin("com.github.sbt" % "sbt-git" % "2.1.0") -addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.3") +addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.4") addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0") From d4a801c560cbc9646a2fa29ce5bbb4f7ee43cc83 Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Wed, 1 Oct 2025 16:10:57 +0000 Subject: [PATCH 18/24] Update circe-core to 0.14.15 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 42f6cfcd..c26ef03f 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -53,7 +53,7 @@ object Dependencies { val mongo4catsCore = "io.github.kirill5k" %% "mongo4cats-core" % V.mongo4cats val mongo4catsCirce = "io.github.kirill5k" %% "mongo4cats-circe" % V.mongo4cats - val circe = "io.circe" %% "circe-core" % "0.14.14" + val circe = "io.circe" %% "circe-core" % "0.14.15" val otel4sCore = "org.typelevel" %% "otel4s-core" % V.otel4s val otel4sPrometheusExporter = "org.typelevel" %% "otel4s-sdk-exporter-prometheus" % V.otel4s From 7e77cce2f9f1d772d4773711a8d5ada8812f0c46 Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Wed, 1 Oct 2025 16:11:01 +0000 Subject: [PATCH 19/24] Update otel4s-core, ... to 0.13.2 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 42f6cfcd..e22e5321 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -17,7 +17,7 @@ object Dependencies { val fs2 = "3.12.2" val http4s = "0.23.30" val mongo4cats = "0.7.13" - val otel4s = "0.13.1" + val otel4s = "0.13.2" } def http4s(artifact: String) = "org.http4s" %% s"http4s-$artifact" % V.http4s From 472fd3f4087a767b7e39839a0e1ed2e16481951b Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Fri, 19 Sep 2025 22:58:59 +0200 Subject: [PATCH 20/24] Use http4s otel4s middleware for metrics --- build.sbt | 10 +- .../app/src/main/scala/app.resources.scala | 18 ++- modules/app/src/main/scala/app.scala | 23 ++- .../app/src/main/scala/http.middleware.scala | 10 +- .../main/scala/http.routes.prometheus.scala | 2 +- modules/app/src/main/scala/http.routes.scala | 16 +- modules/app/src/main/scala/http.server.scala | 14 +- .../app/src/main/scala/service.search.scala | 79 ++-------- modules/e2e/src/test/scala/CompatSuite.scala | 4 +- .../e2e/src/test/scala/IntegrationSuite.scala | 4 +- modules/elastic/src/main/scala/ESClient.scala | 144 ++++-------------- modules/ingestor-app/src/main/scala/app.scala | 14 +- modules/ingestor-cli/src/main/scala/cli.scala | 4 +- .../src/main/scala/resources.scala | 15 +- project/Dependencies.scala | 11 +- 15 files changed, 121 insertions(+), 247 deletions(-) diff --git a/build.sbt b/build.sbt index 12ae1cb8..c7653388 100644 --- a/build.sbt +++ b/build.sbt @@ -77,8 +77,7 @@ lazy val elastic = project catsEffect, catsMtl, http4sClient, - elastic4sHttp4sClient, - otel4sCore + elastic4sHttp4sClient ) ) .dependsOn(api, core) @@ -151,6 +150,9 @@ lazy val `ingestor-core` = project circe, mongo4catsCore, mongo4catsCirce, + otel4sCore, + otel4sHttp4sCore, + otel4sHttp4sMetrics, http4sEmberClient, log4Cats, weaver, @@ -201,7 +203,9 @@ lazy val app = project otel4sSdk, otel4sMetrics, otel4sPrometheusExporter, - otel4sInstrumentationMetrics + otel4sInstrumentationMetrics, + otel4sHttp4sCore, + otel4sHttp4sMetrics ), Compile / doc / sources := Seq.empty, Compile / run / fork := true diff --git a/modules/app/src/main/scala/app.resources.scala b/modules/app/src/main/scala/app.resources.scala index 95b0f1c8..8d2fc099 100644 --- a/modules/app/src/main/scala/app.resources.scala +++ b/modules/app/src/main/scala/app.resources.scala @@ -2,16 +2,22 @@ package lila.search package app import cats.effect.* +import cats.syntax.all.* +import org.http4s.client.Client import org.http4s.ember.client.EmberClientBuilder -import org.typelevel.otel4s.metrics.Meter +import org.http4s.otel4s.middleware.metrics.OtelMetrics +import org.typelevel.otel4s.metrics.MeterProvider class AppResources(val esClient: ESClient[IO]) object AppResources: - def instance(conf: AppConfig)(using Meter[IO]): Resource[IO, AppResources] = - EmberClientBuilder - .default[IO] - .build - .evalMap(ESClient(conf.elastic.uri)) + def instance(conf: AppConfig)(using MeterProvider[IO]): Resource[IO, AppResources] = + val metrics = OtelMetrics + .clientMetricsOps[IO]() + .map(org.http4s.client.middleware.Metrics[IO](_, _.uri.renderString.some)) + + (metrics.toResource, EmberClientBuilder.default[IO].build) + .mapN(_.apply(_)) + .map(ESClient(conf.elastic.uri)) .map(AppResources.apply) diff --git a/modules/app/src/main/scala/app.scala b/modules/app/src/main/scala/app.scala index 5415aa50..b7b2299a 100644 --- a/modules/app/src/main/scala/app.scala +++ b/modules/app/src/main/scala/app.scala @@ -22,7 +22,13 @@ object App extends IOApp.Simple: def app: Resource[IO, Unit] = for given MetricExporter.Pull[IO] <- PrometheusMetricExporter.builder[IO].build.toResource - given Meter[IO] <- mkMeter + otel4s <- SdkMetrics.autoConfigured[IO]: + _.addMeterProviderCustomizer((b, _) => + b.registerMetricReader(summon[MetricExporter.Pull[IO]].metricReader) + ) + given MeterProvider[IO] = otel4s.meterProvider + _ <- IORuntimeMetrics.register[IO](runtime.metrics, IORuntimeMetrics.Config.default) + given Meter[IO] <- MeterProvider[IO].get("lila-search").toResource _ <- RuntimeMetrics.register[IO] config <- AppConfig.load.toResource _ <- Logger[IO].info(s"Starting lila-search with config: ${config.toString}").toResource @@ -30,20 +36,13 @@ object App extends IOApp.Simple: _ <- mkServer(res, config) yield () - def mkMeter(using exporter: MetricExporter.Pull[IO]) = SdkMetrics - .autoConfigured[IO](_.addMeterProviderCustomizer((b, _) => b.registerMetricReader(exporter.metricReader))) - .flatMap: sdk => - given meterProvider: MeterProvider[IO] = sdk.meterProvider - IORuntimeMetrics.register[IO](runtime.metrics, IORuntimeMetrics.Config.default) *> - meterProvider.get("lila-search").toResource - def mkServer(res: AppResources, config: AppConfig)(using - Meter[IO], - MetricExporter.Pull[IO] + MetricExporter.Pull[IO], + MeterProvider[IO] ): Resource[IO, Unit] = for apiRoutes <- Routes(res, config.server) - httpRoutes = apiRoutes <+> mkPrometheusRoutes - _ <- MkHttpServer().newEmber(config.server, httpRoutes.orNotFound) + httpRoutes = apiRoutes <+> MkPrometheusRoutes + _ <- MkHttpServer.newEmber(config.server, httpRoutes.orNotFound) _ <- Logger[IO].info(s"BuildInfo: ${BuildInfo.toString}").toResource yield () diff --git a/modules/app/src/main/scala/http.middleware.scala b/modules/app/src/main/scala/http.middleware.scala index 585477aa..4abbcafc 100644 --- a/modules/app/src/main/scala/http.middleware.scala +++ b/modules/app/src/main/scala/http.middleware.scala @@ -3,13 +3,15 @@ package app import cats.effect.IO import org.http4s.* +import org.http4s.otel4s.middleware.metrics.OtelMetrics import org.http4s.server.middleware.* import org.typelevel.log4cats.LoggerFactory +import org.typelevel.otel4s.metrics.MeterProvider import scala.concurrent.duration.* type Middleware = HttpRoutes[IO] => HttpRoutes[IO] -def MkMiddleware(config: HttpServerConfig)(using LoggerFactory[IO]): Middleware = +def MkMiddleware(config: HttpServerConfig)(using LoggerFactory[IO], MeterProvider[IO]): IO[Middleware] = def verboseLogger = RequestLogger.httpRoutes[IO](true, true).andThen(ResponseLogger.httpRoutes[IO, Request[IO]](true, true)) @@ -18,4 +20,8 @@ def MkMiddleware(config: HttpServerConfig)(using LoggerFactory[IO]): Middleware if config.apiLogger then verboseLogger else ApiErrorLogger.instance(using LoggerFactory[IO].getLogger) - logger.andThen(AutoSlash(_)).andThen(Timeout(60.seconds)) + OtelMetrics + .serverMetricsOps[IO]() + .map(org.http4s.server.middleware.Metrics[IO](_)) + .map: metrics => + logger.andThen(AutoSlash(_)).andThen(Timeout(60.seconds)).andThen(metrics) diff --git a/modules/app/src/main/scala/http.routes.prometheus.scala b/modules/app/src/main/scala/http.routes.prometheus.scala index 8dd35aa9..6b27d586 100644 --- a/modules/app/src/main/scala/http.routes.prometheus.scala +++ b/modules/app/src/main/scala/http.routes.prometheus.scala @@ -7,7 +7,7 @@ import org.http4s.server.Router import org.typelevel.otel4s.sdk.exporter.prometheus.* import org.typelevel.otel4s.sdk.metrics.exporter.MetricExporter -def mkPrometheusRoutes(using exporter: MetricExporter.Pull[IO]): HttpRoutes[IO] = +def MkPrometheusRoutes(using exporter: MetricExporter.Pull[IO]): HttpRoutes[IO] = val writerConfig = PrometheusWriter.Config.default val prometheusRoutes = PrometheusHttpRoutes.routes[IO](exporter, writerConfig) Router("/metrics" -> prometheusRoutes) diff --git a/modules/app/src/main/scala/http.routes.scala b/modules/app/src/main/scala/http.routes.scala index 45f7311f..024d27ec 100644 --- a/modules/app/src/main/scala/http.routes.scala +++ b/modules/app/src/main/scala/http.routes.scala @@ -7,19 +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 org.typelevel.otel4s.metrics.MeterProvider import smithy4s.http4s.SimpleRestJsonBuilder -def Routes(resources: AppResources, config: HttpServerConfig)(using - LoggerFactory[IO], - Meter[IO] -): Resource[IO, HttpRoutes[IO]] = +def Routes( + resources: AppResources, + config: HttpServerConfig +)(using LoggerFactory[IO], MeterProvider[IO]): Resource[IO, HttpRoutes[IO]] = val healthServiceImpl = HealthServiceImpl(resources.esClient) + val searchServiceImpl = SearchServiceImpl(resources.esClient) val search: Resource[IO, HttpRoutes[IO]] = - SearchServiceImpl(resources.esClient).toResource - .flatMap(SimpleRestJsonBuilder.routes(_).resource) + SimpleRestJsonBuilder.routes(searchServiceImpl).resource val health: Resource[IO, HttpRoutes[IO]] = SimpleRestJsonBuilder.routes(healthServiceImpl).resource @@ -36,4 +36,4 @@ def Routes(resources: AppResources, config: HttpServerConfig)(using if config.enableDocs then apiRoutes.map(_ <+> docs) else apiRoutes - allRoutes.map(MkMiddleware(config)) + allRoutes.evalMap(routes => MkMiddleware(config).map(md => md.apply(routes))) diff --git a/modules/app/src/main/scala/http.server.scala b/modules/app/src/main/scala/http.server.scala index 6c8791b6..4eaff1d1 100644 --- a/modules/app/src/main/scala/http.server.scala +++ b/modules/app/src/main/scala/http.server.scala @@ -10,16 +10,13 @@ import org.typelevel.log4cats.Logger import scala.concurrent.duration.* -trait MkHttpServer: - def newEmber(cfg: HttpServerConfig, httpApp: HttpApp[IO]): Resource[IO, Server] - object MkHttpServer: - def apply()(using server: MkHttpServer): MkHttpServer = server - - given Logger[IO] => MkHttpServer = new: + def newEmber(cfg: HttpServerConfig, httpApp: HttpApp[IO])(using Logger[IO]): Resource[IO, Server] = + def showBanner(s: Server): IO[Unit] = + Logger[IO].info(s"lila-search started at ${s.address.toString}") - def newEmber(cfg: HttpServerConfig, httpApp: HttpApp[IO]): Resource[IO, Server] = EmberServerBuilder + EmberServerBuilder .default[IO] .withHost(cfg.host) .withPort(cfg.port) @@ -27,6 +24,3 @@ object MkHttpServer: .withShutdownTimeout(cfg.shutdownTimeout.seconds) .build .evalTap(showBanner) - - private def showBanner(s: Server): IO[Unit] = - Logger[IO].info(s"lila-search started at ${s.address.toString}") diff --git a/modules/app/src/main/scala/service.search.scala b/modules/app/src/main/scala/service.search.scala index 25fc9a8a..a45c6121 100644 --- a/modules/app/src/main/scala/service.search.scala +++ b/modules/app/src/main/scala/service.search.scala @@ -11,62 +11,31 @@ import lila.search.study.Study import lila.search.team.Team import lila.search.ublog.Ublog 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], metric: Histogram[IO, Double])(using - LoggerFactory[IO] -) extends SearchService[IO]: +class SearchServiceImpl(esClient: ESClient[IO])(using LoggerFactory[IO]) extends SearchService[IO]: - import SearchServiceImpl.{ *, given } + import SearchServiceImpl.given private val 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/search/") - ) - ) - - 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] = - countRecord: - allow: - esClient.count(query) - .rescue: e => - logger.error(e.asException)(s"Error in count: query=${query.toString}") *> - IO.raiseError(InternalServerError("Internal server error")) - .map(CountOutput.apply) + allow: + esClient.count(query) + .rescue: e => + logger.error(e.asException)(s"Error in count: query=${query.toString}") *> + IO.raiseError(InternalServerError("Internal server error")) + .map(CountOutput.apply) override def search(query: Query, from: From, size: Size): IO[SearchOutput] = - searchRecord: - allow: - esClient.search(query, from, size) - .rescue: e => - logger.error(e.asException)(s"Error in search: query=${query.toString}, from=$from, size=$size") *> - IO.raiseError(InternalServerError("Internal server error")) - .map(SearchOutput.apply) + allow: + esClient.search(query, from, size) + .rescue: e => + logger.error(e.asException)(s"Error in search: query=${query.toString}, from=$from, size=$size") *> + IO.raiseError(InternalServerError("Internal server error")) + .map(SearchOutput.apply) object SearchServiceImpl: @@ -103,23 +72,3 @@ 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") diff --git a/modules/e2e/src/test/scala/CompatSuite.scala b/modules/e2e/src/test/scala/CompatSuite.scala index 31d80fa2..660b4f3c 100644 --- a/modules/e2e/src/test/scala/CompatSuite.scala +++ b/modules/e2e/src/test/scala/CompatSuite.scala @@ -11,7 +11,7 @@ import lila.search.spec.{ CountOutput, Query, SearchOutput } import org.http4s.implicits.* import org.typelevel.log4cats.noop.{ NoOpFactory, NoOpLogger } import org.typelevel.log4cats.{ Logger, LoggerFactory } -import org.typelevel.otel4s.metrics.Meter +import org.typelevel.otel4s.metrics.MeterProvider import org.typelevel.otel4s.sdk.exporter.prometheus.PrometheusMetricExporter import org.typelevel.otel4s.sdk.metrics.exporter.MetricExporter import play.api.libs.ws.ahc.* @@ -22,7 +22,7 @@ object CompatSuite extends weaver.IOSuite: given Logger[IO] = NoOpLogger[IO] given LoggerFactory[IO] = NoOpFactory[IO] - given Meter[IO] = Meter.noop[IO] + given MeterProvider[IO] = MeterProvider.noop[IO] override type Res = SearchClient diff --git a/modules/e2e/src/test/scala/IntegrationSuite.scala b/modules/e2e/src/test/scala/IntegrationSuite.scala index 9a19e46f..ed81fef6 100644 --- a/modules/e2e/src/test/scala/IntegrationSuite.scala +++ b/modules/e2e/src/test/scala/IntegrationSuite.scala @@ -13,7 +13,7 @@ import lila.search.spec.* import org.http4s.Uri import org.typelevel.log4cats.noop.{ NoOpFactory, NoOpLogger } import org.typelevel.log4cats.{ Logger, LoggerFactory } -import org.typelevel.otel4s.metrics.Meter +import org.typelevel.otel4s.metrics.MeterProvider import org.typelevel.otel4s.sdk.exporter.prometheus.PrometheusMetricExporter import org.typelevel.otel4s.sdk.metrics.exporter.MetricExporter import smithy4s.Timestamp @@ -25,7 +25,7 @@ object IntegrationSuite extends IOSuite: given Logger[IO] = NoOpLogger[IO] given LoggerFactory[IO] = NoOpFactory[IO] - given Meter[IO] = Meter.noop[IO] + given MeterProvider[IO] = MeterProvider.noop[IO] private given Raise[IO, ElasticError]: def functor: Functor[IO] = Functor[IO] def raise[E <: ElasticError, A](e: E): IO[A] = diff --git a/modules/elastic/src/main/scala/ESClient.scala b/modules/elastic/src/main/scala/ESClient.scala index 76ee3711..1f202bf9 100644 --- a/modules/elastic/src/main/scala/ESClient.scala +++ b/modules/elastic/src/main/scala/ESClient.scala @@ -1,18 +1,15 @@ package lila.search -import cats.effect.* +import cats.MonadThrow +import cats.effect.kernel.Sync import cats.mtl.Raise import cats.syntax.all.* import com.sksamuel.elastic4s.ElasticDsl.* import com.sksamuel.elastic4s.http4s.Http4sClient import com.sksamuel.elastic4s.{ ElasticClient, ElasticDsl, ElasticError, Index as ESIndex, Indexable } -import lila.search.ESClient.MetricKeys.* +import fs2.io.file.Files import org.http4s.Uri import org.http4s.client.Client -import org.typelevel.otel4s.metrics.{ Histogram, Meter } -import org.typelevel.otel4s.{ Attribute, AttributeKey, Attributes } - -import java.util.concurrent.TimeUnit trait ESClient[F[_]]: type RaiseF[A] = Raise[F, ElasticError] ?=> F[A] @@ -30,21 +27,10 @@ trait ESClient[F[_]]: object ESClient: - def apply(uri: Uri)(client: Client[IO])(using Meter[IO]): IO[ESClient[IO]] = - Meter[IO] - .histogram[Double]("db.client.operation.duration") - .withUnit("ms") - .create - .map( - apply( - ElasticClient(new Http4sClient(client, uri)), - Attributes(Attribute("db.system", "elasticsearch"), Attribute("server.address", uri.toString())) - ) - ) - - def apply[F[_]: MonadCancelThrow](client: ElasticClient[F], baseAttributes: Attributes)( - metric: Histogram[F, Double] - ) = new ESClient[F]: + def apply[F[_]: Sync: Files](uri: Uri)(client: Client[F]): ESClient[F] = + apply(ElasticClient(new Http4sClient(client, uri))) + + def apply[F[_]: MonadThrow](client: ElasticClient[F]) = new ESClient[F]: def status: RaiseF[String] = client @@ -53,101 +39,39 @@ object ESClient: .map(_.status) def search[A](query: A, from: From, size: Size)(using Queryable[A]): RaiseF[List[Id]] = - metric - .recordDuration( - TimeUnit.MILLISECONDS, - withErrorType( - baseAttributes - .added(dbOperationName, "search") - .added(dbCollectionName, query.index.value) - ) - ) - .surround: - client - .execute(query.searchDef(from, size)) - .flatMap(_.toResult) - .map(_.hits.hits.toList.map(h => Id(h.id))) + client + .execute(query.searchDef(from, size)) + .flatMap(_.toResult) + .map(_.hits.hits.toList.map(h => Id(h.id))) def count[A](query: A)(using Queryable[A]): RaiseF[Long] = - metric - .recordDuration( - TimeUnit.MILLISECONDS, - withErrorType( - baseAttributes - .added(dbOperationName, "count") - .added(dbCollectionName, query.index.value) - ) - ) - .surround: - client - .execute(query.countDef) - .flatMap(_.toResult) - .map(_.count) + client + .execute(query.countDef) + .flatMap(_.toResult) + .map(_.count) def store[A](index: Index, id: Id, obj: A)(using Indexable[A]): RaiseF[Unit] = - metric - .recordDuration( - TimeUnit.MILLISECONDS, - withErrorType( - baseAttributes - .added(dbOperationName, "store") - .added(dbCollectionName, index.value) - ) - ) - .surround: - client - .execute(indexInto(index.value).source(obj).id(id.value)) - .flatMap(_.unitOrFail) + client + .execute(indexInto(index.value).source(obj).id(id.value)) + .flatMap(_.unitOrFail) def storeBulk[A](index: Index, objs: Seq[SourceWithId[A]])(using Indexable[A]): RaiseF[Unit] = val request = indexInto(index.value) val requests = bulk(objs.map { case (id, source) => request.source(source).id(id) }) - metric - .recordDuration( - TimeUnit.MILLISECONDS, - withErrorType( - baseAttributes - .added(dbOperationName, "store-bulk") - .added(dbCollectionName, index.value) - .added(dbBatchSize, objs.size) - ) - ) - .surround: - client - .execute(requests) - .flatMap(_.unitOrFail) + client + .execute(requests) + .flatMap(_.unitOrFail) .whenA(objs.nonEmpty) def deleteOne(index: Index, id: Id): RaiseF[Unit] = - metric - .recordDuration( - TimeUnit.MILLISECONDS, - withErrorType( - baseAttributes - .added(dbOperationName, "delete-one") - .added(dbCollectionName, index.value) - ) - ) - .surround: - client - .execute(deleteById(index.toES, id.value)) - .flatMap(_.unitOrFail) + client + .execute(deleteById(index.toES, id.value)) + .flatMap(_.unitOrFail) def deleteMany(index: Index, ids: List[Id]): RaiseF[Unit] = - metric - .recordDuration( - TimeUnit.MILLISECONDS, - withErrorType( - baseAttributes - .added(dbOperationName, "delete-bulk") - .added(dbCollectionName, index.value) - .added(dbBatchSize, ids.size) - ) - ) - .surround: - client - .execute(bulk(ids.map(id => deleteById(index.toES, id.value)))) - .flatMap(_.unitOrFail) + client + .execute(bulk(ids.map(id => deleteById(index.toES, id.value)))) + .flatMap(_.unitOrFail) .whenA(ids.nonEmpty) def putMapping(index: Index): RaiseF[Unit] = @@ -173,17 +97,3 @@ object ESClient: private def dropIndex(index: Index) = client.execute(deleteIndex(index.value)) - - object MetricKeys: - val dbCollectionName = AttributeKey.string("db.collection.name") - val dbBatchSize = AttributeKey.long("db.operation.batch.size") - val dbOperationName = AttributeKey.string("db.operation.name") - val errorType = AttributeKey.string("error.type") - - private 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") diff --git a/modules/ingestor-app/src/main/scala/app.scala b/modules/ingestor-app/src/main/scala/app.scala index 8aa6f6eb..96ea5b27 100644 --- a/modules/ingestor-app/src/main/scala/app.scala +++ b/modules/ingestor-app/src/main/scala/app.scala @@ -2,7 +2,6 @@ package lila.search package ingestor import cats.effect.* -import cats.syntax.all.* import org.typelevel.log4cats.slf4j.Slf4jFactory import org.typelevel.log4cats.{ Logger, LoggerFactory } import org.typelevel.otel4s.experimental.metrics.* @@ -20,7 +19,11 @@ object App extends IOApp.Simple: def app: Resource[IO, Unit] = for - given Meter[IO] <- mkMeter + otel4s <- SdkMetrics.autoConfigured[IO]: + _.addExporterConfigurer(PrometheusMetricExporterAutoConfigure[IO]) + given MeterProvider[IO] = otel4s.meterProvider + _ <- IORuntimeMetrics.register[IO](runtime.metrics, IORuntimeMetrics.Config.default) + given Meter[IO] <- MeterProvider[IO].get("lila-search-ingestor").toResource _ <- RuntimeMetrics.register[IO] config <- AppConfig.load.toResource _ <- Logger[IO].info(s"Starting lila-search ingestor with config: ${config.toString}").toResource @@ -29,13 +32,6 @@ object App extends IOApp.Simple: _ <- IngestorApp(res, config).run() yield () - def mkMeter = SdkMetrics - .autoConfigured[IO](_.addExporterConfigurer(PrometheusMetricExporterAutoConfigure[IO])) - .flatMap: sdk => - given meterProvider: MeterProvider[IO] = sdk.meterProvider - IORuntimeMetrics.register[IO](runtime.metrics, IORuntimeMetrics.Config.default) *> - meterProvider.get("lila-search-ingestor").toResource - class IngestorApp(res: AppResources, config: AppConfig)(using Logger[IO], LoggerFactory[IO]): def run(): Resource[IO, Unit] = Ingestors(res.lichess, res.study, res.studyLocal, res.store, res.elastic, config.ingestor) diff --git a/modules/ingestor-cli/src/main/scala/cli.scala b/modules/ingestor-cli/src/main/scala/cli.scala index ce19ac46..473ff03c 100644 --- a/modules/ingestor-cli/src/main/scala/cli.scala +++ b/modules/ingestor-cli/src/main/scala/cli.scala @@ -10,7 +10,7 @@ import com.monovore.decline.effect.* import lila.search.ingestor.opts.{ IndexOpts, WatchOpts } import org.typelevel.log4cats.slf4j.Slf4jFactory import org.typelevel.log4cats.{ Logger, LoggerFactory } -import org.typelevel.otel4s.metrics.Meter +import org.typelevel.otel4s.metrics.MeterProvider import java.time.Instant @@ -23,7 +23,7 @@ object cli given LoggerFactory[IO] = Slf4jFactory.create[IO] given Logger[IO] = LoggerFactory[IO].getLogger - given Meter[IO] = Meter.noop[IO] + given MeterProvider[IO] = MeterProvider.noop[IO] override def main: Opts[IO[ExitCode]] = opts.parse.map: opts => diff --git a/modules/ingestor-core/src/main/scala/resources.scala b/modules/ingestor-core/src/main/scala/resources.scala index ce8cc87e..bc656983 100644 --- a/modules/ingestor-core/src/main/scala/resources.scala +++ b/modules/ingestor-core/src/main/scala/resources.scala @@ -7,7 +7,8 @@ import com.mongodb.ReadPreference import mongo4cats.client.MongoClient import mongo4cats.database.MongoDatabase import org.http4s.ember.client.EmberClientBuilder -import org.typelevel.otel4s.metrics.Meter +import org.http4s.otel4s.middleware.metrics.OtelMetrics +import org.typelevel.otel4s.metrics.MeterProvider class AppResources( val lichess: MongoDatabase[IO], @@ -19,7 +20,7 @@ class AppResources( object AppResources: - def instance(conf: AppConfig)(using Meter[IO]): Resource[IO, AppResources] = + def instance(conf: AppConfig)(using MeterProvider[IO]): Resource[IO, AppResources] = ( makeMongoClient(conf.mongo), makeStudyMongoClient(conf.mongo), @@ -28,8 +29,14 @@ object AppResources: KVStore(conf.kvStorePath).toResource ).parMapN(AppResources.apply) - private def makeElasticClient(conf: ElasticConfig)(using Meter[IO]): Resource[IO, ESClient[IO]] = - EmberClientBuilder.default[IO].build.evalMap(ESClient(conf.uri)) + private def makeElasticClient(conf: ElasticConfig)(using MeterProvider[IO]): Resource[IO, ESClient[IO]] = + val metrics = OtelMetrics + .clientMetricsOps[IO]() + .map(org.http4s.client.middleware.Metrics[IO](_, _.uri.renderString.some)) + + (metrics.toResource, EmberClientBuilder.default[IO].build) + .mapN(_.apply(_)) + .map(ESClient(conf.uri)) private def makeMongoClient(conf: MongoConfig) = MongoClient diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 3fb47ac9..b0d141f3 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -18,6 +18,7 @@ object Dependencies { val http4s = "0.23.32" val mongo4cats = "0.7.13" val otel4s = "0.13.2" + val otel4sHttp4s = "0.14.1" } def http4s(artifact: String) = "org.http4s" %% s"http4s-$artifact" % V.http4s @@ -55,16 +56,18 @@ object Dependencies { val mongo4catsCirce = "io.github.kirill5k" %% "mongo4cats-circe" % V.mongo4cats val circe = "io.circe" %% "circe-core" % "0.14.15" + val log4Cats = "org.typelevel" %% "log4cats-slf4j" % "2.7.1" + val logback = "ch.qos.logback" % "logback-classic" % "1.5.19" + val ducktape = "io.github.arainko" %% "ducktape" % "0.2.10" + val otel4sCore = "org.typelevel" %% "otel4s-core" % V.otel4s val otel4sPrometheusExporter = "org.typelevel" %% "otel4s-sdk-exporter-prometheus" % V.otel4s val otel4sSdk = "org.typelevel" %% "otel4s-sdk" % V.otel4s val otel4sInstrumentationMetrics = "org.typelevel" %% "otel4s-instrumentation-metrics" % V.otel4s val otel4sMetrics = "org.typelevel" %% "otel4s-experimental-metrics" % "0.7.0" - val log4Cats = "org.typelevel" %% "log4cats-slf4j" % "2.7.1" - val logback = "ch.qos.logback" % "logback-classic" % "1.5.19" - - val ducktape = "io.github.arainko" %% "ducktape" % "0.2.10" + val otel4sHttp4sCore = "org.http4s" %% "http4s-otel4s-middleware-core" % V.otel4sHttp4s + val otel4sHttp4sMetrics = "org.http4s" %% "http4s-otel4s-middleware-metrics" % V.otel4sHttp4s val declineCore = "com.monovore" %% "decline" % V.decline val declineCatsEffect = "com.monovore" %% "decline-effect" % V.decline From 6acc10883d06b2d1b24d9776b0a45998131554c2 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 2 Oct 2025 09:28:05 +0200 Subject: [PATCH 21/24] Create registerRuntimeMetrics --- modules/app/src/main/scala/app.scala | 11 ++++++++--- modules/ingestor-app/src/main/scala/app.scala | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/modules/app/src/main/scala/app.scala b/modules/app/src/main/scala/app.scala index b7b2299a..06f7216b 100644 --- a/modules/app/src/main/scala/app.scala +++ b/modules/app/src/main/scala/app.scala @@ -27,9 +27,7 @@ object App extends IOApp.Simple: b.registerMetricReader(summon[MetricExporter.Pull[IO]].metricReader) ) given MeterProvider[IO] = otel4s.meterProvider - _ <- IORuntimeMetrics.register[IO](runtime.metrics, IORuntimeMetrics.Config.default) - given Meter[IO] <- MeterProvider[IO].get("lila-search").toResource - _ <- RuntimeMetrics.register[IO] + _ <- registerRuntimeMetrics config <- AppConfig.load.toResource _ <- Logger[IO].info(s"Starting lila-search with config: ${config.toString}").toResource res <- AppResources.instance(config) @@ -46,3 +44,10 @@ object App extends IOApp.Simple: _ <- MkHttpServer.newEmber(config.server, httpRoutes.orNotFound) _ <- Logger[IO].info(s"BuildInfo: ${BuildInfo.toString}").toResource yield () + + private def registerRuntimeMetrics(using MeterProvider[IO]): Resource[IO, Unit] = + for + _ <- IORuntimeMetrics.register[IO](runtime.metrics, IORuntimeMetrics.Config.default) + given Meter[IO] <- MeterProvider[IO].get("jvm.runtime").toResource + _ <- RuntimeMetrics.register[IO] + yield () diff --git a/modules/ingestor-app/src/main/scala/app.scala b/modules/ingestor-app/src/main/scala/app.scala index 96ea5b27..cec67353 100644 --- a/modules/ingestor-app/src/main/scala/app.scala +++ b/modules/ingestor-app/src/main/scala/app.scala @@ -22,9 +22,7 @@ object App extends IOApp.Simple: otel4s <- SdkMetrics.autoConfigured[IO]: _.addExporterConfigurer(PrometheusMetricExporterAutoConfigure[IO]) given MeterProvider[IO] = otel4s.meterProvider - _ <- IORuntimeMetrics.register[IO](runtime.metrics, IORuntimeMetrics.Config.default) - given Meter[IO] <- MeterProvider[IO].get("lila-search-ingestor").toResource - _ <- RuntimeMetrics.register[IO] + _ <- registerRuntimeMetrics config <- AppConfig.load.toResource _ <- Logger[IO].info(s"Starting lila-search ingestor with config: ${config.toString}").toResource _ <- Logger[IO].info(s"BuildInfo: ${BuildInfo.toString}").toResource @@ -32,6 +30,13 @@ object App extends IOApp.Simple: _ <- IngestorApp(res, config).run() yield () + private def registerRuntimeMetrics(using MeterProvider[IO]): Resource[IO, Unit] = + for + _ <- IORuntimeMetrics.register[IO](runtime.metrics, IORuntimeMetrics.Config.default) + given Meter[IO] <- MeterProvider[IO].get("jvm.runtime").toResource + _ <- RuntimeMetrics.register[IO] + yield () + class IngestorApp(res: AppResources, config: AppConfig)(using Logger[IO], LoggerFactory[IO]): def run(): Resource[IO, Unit] = Ingestors(res.lichess, res.study, res.studyLocal, res.store, res.elastic, config.ingestor) From b34da215cd8192228dd979c8cf82a7454b952d1a Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 2 Oct 2025 09:43:42 +0200 Subject: [PATCH 22/24] Simplify metrics setup --- modules/app/src/main/scala/app.scala | 16 ++++++++++++---- modules/ingestor-app/src/main/scala/app.scala | 13 +++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/modules/app/src/main/scala/app.scala b/modules/app/src/main/scala/app.scala index 06f7216b..006a2a0d 100644 --- a/modules/app/src/main/scala/app.scala +++ b/modules/app/src/main/scala/app.scala @@ -10,6 +10,7 @@ import org.typelevel.otel4s.instrumentation.ce.IORuntimeMetrics import org.typelevel.otel4s.metrics.{ Meter, MeterProvider } import org.typelevel.otel4s.sdk.exporter.prometheus.PrometheusMetricExporter import org.typelevel.otel4s.sdk.metrics.SdkMetrics +import org.typelevel.otel4s.sdk.metrics.SdkMetrics.AutoConfigured.Builder import org.typelevel.otel4s.sdk.metrics.exporter.MetricExporter object App extends IOApp.Simple: @@ -22,10 +23,7 @@ object App extends IOApp.Simple: def app: Resource[IO, Unit] = for given MetricExporter.Pull[IO] <- PrometheusMetricExporter.builder[IO].build.toResource - otel4s <- SdkMetrics.autoConfigured[IO]: - _.addMeterProviderCustomizer((b, _) => - b.registerMetricReader(summon[MetricExporter.Pull[IO]].metricReader) - ) + otel4s <- SdkMetrics.autoConfigured[IO](configBuilder) given MeterProvider[IO] = otel4s.meterProvider _ <- registerRuntimeMetrics config <- AppConfig.load.toResource @@ -51,3 +49,13 @@ object App extends IOApp.Simple: given Meter[IO] <- MeterProvider[IO].get("jvm.runtime").toResource _ <- RuntimeMetrics.register[IO] yield () + + private def configBuilder(builder: Builder[IO])(using exporter: MetricExporter.Pull[IO]) = + builder + .addPropertiesCustomizer(_ => + Map( + "otel.metrics.exporter" -> "none", + "otel.traces.exporter" -> "none" + ) + ) + .addMeterProviderCustomizer((b, _) => b.registerMetricReader(exporter.metricReader)) diff --git a/modules/ingestor-app/src/main/scala/app.scala b/modules/ingestor-app/src/main/scala/app.scala index cec67353..2a953cd4 100644 --- a/modules/ingestor-app/src/main/scala/app.scala +++ b/modules/ingestor-app/src/main/scala/app.scala @@ -19,8 +19,7 @@ object App extends IOApp.Simple: def app: Resource[IO, Unit] = for - otel4s <- SdkMetrics.autoConfigured[IO]: - _.addExporterConfigurer(PrometheusMetricExporterAutoConfigure[IO]) + otel4s <- SdkMetrics.autoConfigured[IO](configBuilder) given MeterProvider[IO] = otel4s.meterProvider _ <- registerRuntimeMetrics config <- AppConfig.load.toResource @@ -37,6 +36,16 @@ object App extends IOApp.Simple: _ <- RuntimeMetrics.register[IO] yield () + private def configBuilder(builder: SdkMetrics.AutoConfigured.Builder[IO]) = + builder + .addPropertiesCustomizer(_ => + Map( + "otel.metrics.exporter" -> "none", + "otel.traces.exporter" -> "none" + ) + ) + .addExporterConfigurer(PrometheusMetricExporterAutoConfigure[IO]) + class IngestorApp(res: AppResources, config: AppConfig)(using Logger[IO], LoggerFactory[IO]): def run(): Resource[IO, Unit] = Ingestors(res.lichess, res.study, res.studyLocal, res.store, res.elastic, config.ingestor) From 36e445b107bc42172e8dbdd57a0592fdbb3a7be5 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 2 Oct 2025 10:04:05 +0200 Subject: [PATCH 23/24] Set prometheus exporter by default for ingestor --- modules/ingestor-app/src/main/scala/app.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ingestor-app/src/main/scala/app.scala b/modules/ingestor-app/src/main/scala/app.scala index 2a953cd4..e4b16cc0 100644 --- a/modules/ingestor-app/src/main/scala/app.scala +++ b/modules/ingestor-app/src/main/scala/app.scala @@ -40,7 +40,7 @@ object App extends IOApp.Simple: builder .addPropertiesCustomizer(_ => Map( - "otel.metrics.exporter" -> "none", + "otel.metrics.exporter" -> "prometheus", "otel.traces.exporter" -> "none" ) ) From 82950090dfbf45e027cf6c9ec74354ccc09583cb Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 2 Oct 2025 10:13:33 +0200 Subject: [PATCH 24/24] Setting version to 3.2.3 --- version.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.sbt b/version.sbt index 18d870b1..f8b02e6d 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -ThisBuild / version := "3.2.3-SNAPSHOT" +ThisBuild / version := "3.2.3"