diff --git a/build.sbt b/build.sbt index 52eed4f0..5eb05301 100644 --- a/build.sbt +++ b/build.sbt @@ -446,7 +446,9 @@ val joex = project addCompilerPlugin(Dependencies.betterMonadicFor), buildInfoPackage := "docspell.joex", reStart / javaOptions ++= Seq( - s"-Dconfig.file=${(LocalRootProject / baseDirectory).value / "local" / "dev.conf"}" + s"-Dconfig.file=${(LocalRootProject / baseDirectory).value / "local" / "dev.conf"}", + "-Xmx1596M", + "-XX:+UseG1GC" ), Revolver.enableDebugging(port = 5051, suspend = false) ) @@ -494,7 +496,9 @@ val restserver = project (Compile / resourceDirectory).value.getParentFile / "templates" ), reStart / javaOptions ++= Seq( - s"-Dconfig.file=${(LocalRootProject / baseDirectory).value / "local" / "dev.conf"}" + s"-Dconfig.file=${(LocalRootProject / baseDirectory).value / "local" / "dev.conf"}", + "-Xmx150M", + "-XX:+UseG1GC" ), Revolver.enableDebugging(port = 5050, suspend = false) ) diff --git a/modules/common/src/main/scala/docspell/common/ByteSize.scala b/modules/common/src/main/scala/docspell/common/ByteSize.scala new file mode 100644 index 00000000..3f6e4ccc --- /dev/null +++ b/modules/common/src/main/scala/docspell/common/ByteSize.scala @@ -0,0 +1,69 @@ +package docspell.common + +import io.circe.Decoder +import io.circe.Encoder + +final case class ByteSize(bytes: Long) { + + def toHuman: String = + ByteSize.bytesToHuman(bytes) + + def <=(other: ByteSize) = + bytes <= other.bytes + + def >=(other: ByteSize) = + bytes >= other.bytes + + def >(other: ByteSize) = + bytes > other.bytes + + def -(other: ByteSize) = + ByteSize(bytes - other.bytes) + + def +(other: ByteSize) = + ByteSize(bytes + other.bytes) +} + +object ByteSize { + + val zero = ByteSize(0L) + + def bytesToHuman(bytes: Long): String = + if (math.abs(bytes) < 1024 && bytes != Long.MinValue) s"${bytes}B" + else { + val k = bytes / 1024.0 + if (math.abs(k) < 1024) f"$k%.02fK" + else { + val m = k / 1024.0 + if (math.abs(m) < 1024) f"$m%.02fM" + else f"${m / 1024.0}%.02fG" + } + } + + def parse(str: String): Either[String, ByteSize] = + str.toLongOption + .map(ByteSize.apply) + .toRight(s"Not a valid size string: $str") + .orElse(span(str.toLowerCase) match { + case (num, "k") => + Right(ByteSize(math.round(num.toDouble * 1024))) + case (num, "m") => + Right(ByteSize(math.round(num.toDouble * 1024 * 1024))) + case (num, "g") => + Right(ByteSize(math.round(num.toDouble * 1024 * 1024 * 1024))) + case _ => + Left(s"Invalid byte string: $str") + }) + + private def span(str: String): (String, String) = + if (str.isEmpty) ("", "") + else (str.init, str.last.toString) + + def unsafe(str: String): ByteSize = + parse(str).fold(sys.error, identity) + + implicit val jsonDecoder: Decoder[ByteSize] = + Decoder.decodeLong.map(ByteSize.apply) + implicit val jsonEncoder: Encoder[ByteSize] = + Encoder.encodeLong.contramap(_.bytes) +} diff --git a/modules/common/src/main/scala/docspell/common/JvmInfo.scala b/modules/common/src/main/scala/docspell/common/JvmInfo.scala new file mode 100644 index 00000000..35852c98 --- /dev/null +++ b/modules/common/src/main/scala/docspell/common/JvmInfo.scala @@ -0,0 +1,103 @@ +package docspell.common + +import java.time.Instant + +import scala.jdk.CollectionConverters._ + +import cats.effect._ +import cats.implicits._ + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} + +case class JvmInfo( + id: Ident, + pidHost: String, + ncpu: Int, + inputArgs: List[String], + libraryPath: String, + specVendor: String, + specVersion: String, + startTime: Timestamp, + uptime: Duration, + vmName: String, + vmVendor: String, + vmVersion: String, + heapUsage: JvmInfo.MemoryUsage, + props: Map[String, String] +) + +object JvmInfo { + + def create[F[_]: Sync](id: Ident): F[JvmInfo] = + MemoryUsage.createHeap[F].flatMap { mu => + Sync[F].delay { + val rmb = management.ManagementFactory.getRuntimeMXBean() + val rt = Runtime.getRuntime() + JvmInfo( + id, + pidHost = rmb.getName(), + ncpu = rt.availableProcessors(), + inputArgs = rmb.getInputArguments().asScala.toList, + libraryPath = rmb.getLibraryPath(), + specVendor = rmb.getSpecVendor(), + specVersion = rmb.getSpecVersion(), + startTime = Timestamp(Instant.ofEpochMilli(rmb.getStartTime())), + uptime = Duration.millis(rmb.getUptime()), + vmName = rmb.getVmName(), + vmVendor = rmb.getVmVendor(), + vmVersion = rmb.getVmVersion(), + heapUsage = mu, + props = rmb.getSystemProperties().asScala.toMap + ) + } + } + + case class MemoryUsage( + init: Long, + used: Long, + comitted: Long, + max: Long, + free: Long, + description: String + ) + + object MemoryUsage { + + def apply(init: Long, used: Long, comitted: Long, max: Long): MemoryUsage = { + def str(n: Long) = ByteSize(n).toHuman + + val free = max - used + + val descr = + s"init=${str(init)}, used=${str(used)}, comitted=${str(comitted)}, max=${str(max)}, free=${str(free)}" + MemoryUsage(init, used, comitted, max, free, descr) + } + + val empty = MemoryUsage(0, 0, 0, 0) + + def createHeap[F[_]: Sync]: F[MemoryUsage] = + Sync[F].delay { + val mxb = management.ManagementFactory.getMemoryMXBean() + val heap = mxb.getHeapMemoryUsage() + MemoryUsage( + init = math.max(0, heap.getInit()), + used = math.max(0, heap.getUsed()), + comitted = math.max(0, heap.getCommitted()), + max = math.max(0, heap.getMax()) + ) + } + + implicit val jsonEncoder: Encoder[MemoryUsage] = + deriveEncoder[MemoryUsage] + + implicit val jsonDecoder: Decoder[MemoryUsage] = + deriveDecoder[MemoryUsage] + } + + implicit val jsonEncoder: Encoder[JvmInfo] = + deriveEncoder[JvmInfo] + + implicit val jsonDecoder: Decoder[JvmInfo] = + deriveDecoder[JvmInfo] +} diff --git a/modules/joex/src/main/scala/docspell/joex/JoexServer.scala b/modules/joex/src/main/scala/docspell/joex/JoexServer.scala index f103e76f..9296ee30 100644 --- a/modules/joex/src/main/scala/docspell/joex/JoexServer.scala +++ b/modules/joex/src/main/scala/docspell/joex/JoexServer.scala @@ -35,7 +35,7 @@ object JoexServer { .create[F](cfg, signal, pools.connectEC, pools.httpClientEC, pools.blocker) httpApp = Router( - "/api/info" -> InfoRoutes(), + "/api/info" -> InfoRoutes(cfg), "/api/v1" -> JoexRoutes(joexApp) ).orNotFound diff --git a/modules/joex/src/main/scala/docspell/joex/routes/InfoRoutes.scala b/modules/joex/src/main/scala/docspell/joex/routes/InfoRoutes.scala index 694da0f2..093d9806 100644 --- a/modules/joex/src/main/scala/docspell/joex/routes/InfoRoutes.scala +++ b/modules/joex/src/main/scala/docspell/joex/routes/InfoRoutes.scala @@ -1,8 +1,10 @@ package docspell.joex.routes import cats.effect.Sync +import cats.implicits._ -import docspell.joex.BuildInfo +import docspell.common.JvmInfo +import docspell.joex.{BuildInfo, Config} import docspell.joexapi.model.VersionInfo import org.http4s.HttpRoutes @@ -11,19 +13,23 @@ import org.http4s.dsl.Http4sDsl object InfoRoutes { - def apply[F[_]: Sync](): HttpRoutes[F] = { + def apply[F[_]: Sync](cfg: Config): HttpRoutes[F] = { val dsl = new Http4sDsl[F] {} import dsl._ - HttpRoutes.of[F] { case GET -> (Root / "version") => - Ok( - VersionInfo( - BuildInfo.version, - BuildInfo.builtAtMillis, - BuildInfo.builtAtString, - BuildInfo.gitHeadCommit.getOrElse(""), - BuildInfo.gitDescribedVersion.getOrElse("") + HttpRoutes.of[F] { + case GET -> Root / "version" => + Ok( + VersionInfo( + BuildInfo.version, + BuildInfo.builtAtMillis, + BuildInfo.builtAtString, + BuildInfo.gitHeadCommit.getOrElse(""), + BuildInfo.gitDescribedVersion.getOrElse("") + ) ) - ) + + case GET -> Root / "system" => + JvmInfo.create[F](cfg.appId).flatMap(Ok(_)) } } } diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala index 1f582c47..557bed9e 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala @@ -104,7 +104,8 @@ object RestServer { def adminRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] = Router( "fts" -> FullTextIndexRoutes.admin(cfg, restApp.backend), - "user" -> UserRoutes.admin(restApp.backend) + "user" -> UserRoutes.admin(restApp.backend), + "info" -> InfoRoutes.admin(cfg) ) def redirectTo[F[_]: Effect](path: String): HttpRoutes[F] = { diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/InfoRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/InfoRoutes.scala index c8a01926..9102df6e 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/InfoRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/InfoRoutes.scala @@ -1,9 +1,11 @@ package docspell.restserver.routes import cats.effect.Sync +import cats.implicits._ +import docspell.common._ import docspell.restapi.model.VersionInfo -import docspell.restserver.BuildInfo +import docspell.restserver.{BuildInfo, Config} import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityEncoder._ @@ -26,4 +28,13 @@ object InfoRoutes { ) } } + + def admin[F[_]: Sync](cfg: Config): HttpRoutes[F] = { + val dsl = new Http4sDsl[F] {} + import dsl._ + HttpRoutes.of[F] { case GET -> Root / "system" => + JvmInfo.create[F](cfg.appId).flatMap(Ok(_)) + } + + } }