mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-07 07:35:59 +00:00
Add (inofficial) routes to get system information
This commit is contained in:
parent
30df887934
commit
b08e88cd69
@ -446,7 +446,9 @@ val joex = project
|
|||||||
addCompilerPlugin(Dependencies.betterMonadicFor),
|
addCompilerPlugin(Dependencies.betterMonadicFor),
|
||||||
buildInfoPackage := "docspell.joex",
|
buildInfoPackage := "docspell.joex",
|
||||||
reStart / javaOptions ++= Seq(
|
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)
|
Revolver.enableDebugging(port = 5051, suspend = false)
|
||||||
)
|
)
|
||||||
@ -494,7 +496,9 @@ val restserver = project
|
|||||||
(Compile / resourceDirectory).value.getParentFile / "templates"
|
(Compile / resourceDirectory).value.getParentFile / "templates"
|
||||||
),
|
),
|
||||||
reStart / javaOptions ++= Seq(
|
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)
|
Revolver.enableDebugging(port = 5050, suspend = false)
|
||||||
)
|
)
|
||||||
|
69
modules/common/src/main/scala/docspell/common/ByteSize.scala
Normal file
69
modules/common/src/main/scala/docspell/common/ByteSize.scala
Normal file
@ -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)
|
||||||
|
}
|
103
modules/common/src/main/scala/docspell/common/JvmInfo.scala
Normal file
103
modules/common/src/main/scala/docspell/common/JvmInfo.scala
Normal file
@ -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]
|
||||||
|
}
|
@ -35,7 +35,7 @@ object JoexServer {
|
|||||||
.create[F](cfg, signal, pools.connectEC, pools.httpClientEC, pools.blocker)
|
.create[F](cfg, signal, pools.connectEC, pools.httpClientEC, pools.blocker)
|
||||||
|
|
||||||
httpApp = Router(
|
httpApp = Router(
|
||||||
"/api/info" -> InfoRoutes(),
|
"/api/info" -> InfoRoutes(cfg),
|
||||||
"/api/v1" -> JoexRoutes(joexApp)
|
"/api/v1" -> JoexRoutes(joexApp)
|
||||||
).orNotFound
|
).orNotFound
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package docspell.joex.routes
|
package docspell.joex.routes
|
||||||
|
|
||||||
import cats.effect.Sync
|
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 docspell.joexapi.model.VersionInfo
|
||||||
|
|
||||||
import org.http4s.HttpRoutes
|
import org.http4s.HttpRoutes
|
||||||
@ -11,10 +13,11 @@ import org.http4s.dsl.Http4sDsl
|
|||||||
|
|
||||||
object InfoRoutes {
|
object InfoRoutes {
|
||||||
|
|
||||||
def apply[F[_]: Sync](): HttpRoutes[F] = {
|
def apply[F[_]: Sync](cfg: Config): HttpRoutes[F] = {
|
||||||
val dsl = new Http4sDsl[F] {}
|
val dsl = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
HttpRoutes.of[F] { case GET -> (Root / "version") =>
|
HttpRoutes.of[F] {
|
||||||
|
case GET -> Root / "version" =>
|
||||||
Ok(
|
Ok(
|
||||||
VersionInfo(
|
VersionInfo(
|
||||||
BuildInfo.version,
|
BuildInfo.version,
|
||||||
@ -24,6 +27,9 @@ object InfoRoutes {
|
|||||||
BuildInfo.gitDescribedVersion.getOrElse("")
|
BuildInfo.gitDescribedVersion.getOrElse("")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case GET -> Root / "system" =>
|
||||||
|
JvmInfo.create[F](cfg.appId).flatMap(Ok(_))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,8 @@ object RestServer {
|
|||||||
def adminRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
def adminRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||||
Router(
|
Router(
|
||||||
"fts" -> FullTextIndexRoutes.admin(cfg, restApp.backend),
|
"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] = {
|
def redirectTo[F[_]: Effect](path: String): HttpRoutes[F] = {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package docspell.restserver.routes
|
package docspell.restserver.routes
|
||||||
|
|
||||||
import cats.effect.Sync
|
import cats.effect.Sync
|
||||||
|
import cats.implicits._
|
||||||
|
|
||||||
|
import docspell.common._
|
||||||
import docspell.restapi.model.VersionInfo
|
import docspell.restapi.model.VersionInfo
|
||||||
import docspell.restserver.BuildInfo
|
import docspell.restserver.{BuildInfo, Config}
|
||||||
|
|
||||||
import org.http4s.HttpRoutes
|
import org.http4s.HttpRoutes
|
||||||
import org.http4s.circe.CirceEntityEncoder._
|
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(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user