mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-10-31 09:30:12 +00:00 
			
		
		
		
	Add (inofficial) routes to get system information
This commit is contained in:
		| @@ -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) | ||||
|   ) | ||||
|   | ||||
							
								
								
									
										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) | ||||
|  | ||||
|       httpApp = Router( | ||||
|         "/api/info" -> InfoRoutes(), | ||||
|         "/api/info" -> InfoRoutes(cfg), | ||||
|         "/api/v1"   -> JoexRoutes(joexApp) | ||||
|       ).orNotFound | ||||
|  | ||||
|   | ||||
| @@ -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(_)) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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] = { | ||||
|   | ||||
| @@ -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(_)) | ||||
|     } | ||||
|  | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user