mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-11-03 18:00:11 +00:00 
			
		
		
		
	Add (inofficial) routes to get system information
This commit is contained in:
		@@ -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(_))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user