diff --git a/README.md b/README.md index ae17be4ce..3f41175f0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Scala Server Toolkit [![Build Status](https://travis-ci.org/avast/scala-server-toolkit.svg?branch=master)](https://travis-ci.org/avast/scala-server-toolkit) -[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-http4s-blaze-server_2.12)](https://repo1.maven.org/maven2/com/avast/sst-http4s-blaze-server_2.12/) +[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-bundle-zio-http4s-blaze_2.12)](https://repo1.maven.org/maven2/com/avast/sst-bundle-zio-http4s-blaze_2.12/) [![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-brightgreen.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org) This project is a culmination of years of Scala development at Avast and tries to represent the best practices of Scala server development @@ -10,7 +10,7 @@ together well and allow you to build reliable server applications. ## [Documentation](./docs/index.md) -Or you can [deep dive into example code](example/src/main/scala/com/avast/server/toolkit/example/Main.scala) if you like that more. +Or you can [deep dive into example code](example/src/main/scala/com/avast/sst/example/Main.scala) if you like that more. ## Design diff --git a/build.sbt b/build.sbt index 596552519..6811fc41e 100644 --- a/build.sbt +++ b/build.sbt @@ -17,14 +17,52 @@ lazy val commonSettings = BuildSettings.common ++ Seq( lazy val root = project .in(file(".")) - .aggregate(example, http4sBlazeClient, http4sBlazeServer, jvmExecution, jvmSsl, jvmSystem, pureconfig) + .aggregate( + bundleMonixHttp4sBlaze, + bundleZioHttp4sBlaze, + example, + http4sClientBlaze, + http4sClientBlazePureConfig, + http4sServer, + http4sServerBlaze, + http4sServerBlazePureConfig, + http4sServerMicrometer, + jvm, + jvmMicrometer, + jvmPureConfig, + micrometerJmx, + micrometerJmxPureConfig, + pureConfig + ) .settings( name := "scala-server-toolkit", publish / skip := true ) +lazy val bundleMonixHttp4sBlaze = project + .in(file("bundle-monix-http4s-blaze")) + .dependsOn(http4sClientBlaze, http4sServerBlaze, http4sServerBlazePureConfig, http4sServerMicrometer, jvmMicrometer, jvmPureConfig) + .settings(commonSettings) + .settings( + name := "sst-bundle-monix-http4s-blaze", + libraryDependencies += Dependencies.monixEval + ) + +lazy val bundleZioHttp4sBlaze = project + .in(file("bundle-zio-http4s-blaze")) + .dependsOn(http4sClientBlaze, http4sServerBlaze, http4sServerBlazePureConfig, http4sServerMicrometer, jvmMicrometer, jvmPureConfig) + .settings(commonSettings) + .settings( + name := "sst-bundle-zio-http4s-blaze", + libraryDependencies ++= Seq( + Dependencies.zio, + Dependencies.zioInteropCats + ) + ) + lazy val example = project - .dependsOn(jvmExecution, http4sBlazeClient, http4sBlazeServer, jvmSsl, jvmSystem, pureconfig) + .in(file("example")) + .dependsOn(bundleZioHttp4sBlaze, micrometerJmxPureConfig) .enablePlugins(MdocPlugin) .settings(commonSettings) .settings( @@ -34,27 +72,43 @@ lazy val example = project Global / cancelable := true, mdocIn := baseDirectory.value / "src" / "main" / "mdoc", mdocOut := baseDirectory.value / ".." / "docs", - libraryDependencies ++= Seq( - Dependencies.zio, - Dependencies.zioInteropCats - ) + libraryDependencies += Dependencies.logbackClassic ) -lazy val http4sBlazeClient = project - .in(file("http4s-blaze-client")) - .dependsOn(jvmSsl) +lazy val http4sClientBlaze = project + .in(file("http4s-client-blaze")) + .dependsOn(jvm) .settings(commonSettings) .settings( - name := "sst-http4s-blaze-client", + name := "sst-http4s-client-blaze", libraryDependencies += Dependencies.http4sBlazeClient ) -lazy val http4sBlazeServer = project - .in(file("http4s-blaze-server")) - .dependsOn(http4sBlazeClient % Test) +lazy val http4sClientBlazePureConfig = project + .in(file("http4s-client-blaze-pureconfig")) + .dependsOn(http4sClientBlaze, jvmPureConfig) + .settings(commonSettings) + .settings(name := "sst-http4s-client-blaze-pureconfig") + +lazy val http4sServer = project + .in(file("http4s-server")) + .settings(commonSettings) + .settings( + name := "sst-http4s-server", + libraryDependencies ++= Seq( + Dependencies.http4sServer, + Dependencies.http4sBlazeClient % Test, + Dependencies.http4sBlazeServer % Test, + Dependencies.http4sDsl % Test + ) + ) + +lazy val http4sServerBlaze = project + .in(file("http4s-server-blaze")) + .dependsOn(http4sServer, http4sClientBlaze % Test) .settings(commonSettings) .settings( - name := "sst-http4s-blaze-server", + name := "sst-http4s-server-blaze", libraryDependencies ++= Seq( Dependencies.http4sBlazeServer, Dependencies.http4sDsl, @@ -62,29 +116,63 @@ lazy val http4sBlazeServer = project ) ) -lazy val jvmExecution = project - .in(file("jvm-execution")) +lazy val http4sServerBlazePureConfig = project + .in(file("http4s-server-blaze-pureconfig")) + .dependsOn(http4sServerBlaze, pureConfig) + .settings(commonSettings) + .settings(name := "sst-http4s-server-blaze-pureconfig") + +lazy val http4sServerMicrometer = project + .in(file("http4s-server-micrometer")) + .dependsOn(http4sServer) + .settings(commonSettings) + .settings( + name := "sst-http4s-server-micrometer", + libraryDependencies += Dependencies.micrometerCore + ) + +lazy val jvm = project + .in(file("jvm")) + .settings(commonSettings) .settings( - commonSettings, - name := "sst-jvm-execution", + name := "sst-jvm", libraryDependencies += Dependencies.slf4jApi ) -lazy val jvmSsl = project - .in(file("jvm-ssl")) +lazy val jvmMicrometer = project + .in(file("jvm-micrometer")) + .dependsOn(jvm) + .settings(commonSettings) .settings( - commonSettings, - name := "sst-jvm-ssl" + name := "sst-jvm-micrometer", + libraryDependencies += Dependencies.micrometerCore ) -lazy val jvmSystem = project - .in(file("jvm-system")) +lazy val jvmPureConfig = project + .in(file("jvm-pureconfig")) + .dependsOn(jvm, pureConfig) + .settings(commonSettings) + .settings(name := "sst-jvm-pureconfig") + +lazy val micrometerJmx = project + .in(file("micrometer-jmx")) + .settings(commonSettings) .settings( - commonSettings, - name := "sst-jvm-system" + name := "sst-micrometer-jmx", + libraryDependencies ++= Seq( + Dependencies.micrometerJmx, + Dependencies.jsr305 // required because of Scala compiler + ) ) -lazy val pureconfig = project +lazy val micrometerJmxPureConfig = project + .in(file("micrometer-jmx-pureconfig")) + .dependsOn(micrometerJmx, pureConfig) + .settings(commonSettings) + .settings(name := "sst-micrometer-jmx-pureconfig") + +lazy val pureConfig = project + .in(file("pureconfig")) .settings(commonSettings) .settings( name := "sst-pureconfig", diff --git a/bundle-monix-http4s-blaze/src/main/scala/com/avast/sst/bundle/MonixServerApp.scala b/bundle-monix-http4s-blaze/src/main/scala/com/avast/sst/bundle/MonixServerApp.scala new file mode 100644 index 000000000..5189db87c --- /dev/null +++ b/bundle-monix-http4s-blaze/src/main/scala/com/avast/sst/bundle/MonixServerApp.scala @@ -0,0 +1,36 @@ +package com.avast.sst.bundle + +import cats.effect.{ExitCode, Resource} +import monix.eval.{Task, TaskApp} +import org.http4s.server.Server +import org.slf4j.LoggerFactory + +/** Extend this `trait` if you want to implement server application using [[monix.eval.Task]] effect data type. + * + * Implement method `program` with initialization and business logic of your application. It will be automatically run until JVM is shut + * down in which case all the resources are cleaned up because the whole `program` is a [[cats.effect.Resource]]. + */ +trait MonixServerApp extends TaskApp { + + private val logger = LoggerFactory.getLogger(this.getClass) + + def program: Resource[Task, Server[Task]] + + override def run(args: List[String]): Task[ExitCode] = { + program + .use { server => + for { + _ <- Task.delay(logger.info(s"Server started @ ${server.address.getHostString}:${server.address.getPort}")) + _ <- Task.never[Unit] + } yield server + } + .redeem( + ex => { + logger.error("Server initialization failed!", ex) + ExitCode.Error + }, + _ => ExitCode.Success + ) + } + +} diff --git a/bundle-zio-http4s-blaze/src/main/scala/com/avast/sst/bundle/ZioServerApp.scala b/bundle-zio-http4s-blaze/src/main/scala/com/avast/sst/bundle/ZioServerApp.scala new file mode 100644 index 000000000..f62af619b --- /dev/null +++ b/bundle-zio-http4s-blaze/src/main/scala/com/avast/sst/bundle/ZioServerApp.scala @@ -0,0 +1,39 @@ +package com.avast.sst.bundle + +import cats.effect.Resource +import com.github.ghik.silencer.silent +import org.http4s.server.Server +import org.slf4j.LoggerFactory +import zio.interop.catz._ +import zio.{Task, UIO, ZIO} + +/** Extend this `trait` if you want to implement server application using [[zio.ZIO]] effect data type. + * + * Implement method `program` with initialization and business logic of your application. It will be automatically run until JVM is shut + * down in which case all the resources are cleaned up because the whole `program` is a [[cats.effect.Resource]]. + */ +trait ZioServerApp extends CatsApp { + + private val logger = LoggerFactory.getLogger(this.getClass) + + def program: Resource[Task, Server[Task]] + + @silent("dead code") + override def run(args: List[String]): ZIO[Environment, Nothing, Int] = { + program + .use { server => + for { + _ <- UIO.effectTotal(logger.info(s"Server started @ ${server.address.getHostString}:${server.address.getPort}")) + _ <- Task.never + } yield server + } + .fold( + ex => { + logger.error("Server initialization failed!", ex) + 1 + }, + _ => 0 + ) + } + +} diff --git a/docs/http4s.md b/docs/http4s.md index 74862a485..24dfa0757 100644 --- a/docs/http4s.md +++ b/docs/http4s.md @@ -1,8 +1,8 @@ # Module http4s -[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-http4s-blaze-server_2.12)](https://repo1.maven.org/maven2/com/avast/sst-http4s-blaze-server_2.12/) +[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-http4s-server-blaze_2.12)](https://repo1.maven.org/maven2/com/avast/sst-http4s-server-blaze_2.12/) -`libraryDependencies += "com.avast" %% "sst-http4s-blaze-server" % ""` +`libraryDependencies += "com.avast" %% "sst-http4s-server-blaze" % ""` There are `http4s-*` modules that provide easy initialization of a server and a client. Http4s is an interface with multiple possible implementations - for now we provide only implementations based on [Blaze](https://github.com/http4s/blaze). @@ -11,9 +11,10 @@ Both server and client are configured via configuration `case class` which conta ```scala import cats.effect._ -import com.avast.sst.execution.ExecutorModule -import com.avast.sst.http4s._ -import com.avast.sst.system.console.ConsoleModule +import com.avast.sst.http4s.client._ +import com.avast.sst.http4s.server._ +import com.avast.sst.jvm.execution.ExecutorModule +import com.avast.sst.jvm.system.console.ConsoleModule import org.http4s.dsl.Http4sDsl import org.http4s.HttpRoutes import zio.DefaultRuntime @@ -59,9 +60,9 @@ runtime.unsafeRun(program) ```scala import cats.effect._ -import com.avast.sst.execution.ExecutorModule -import com.avast.sst.http4s._ -import com.avast.sst.http4s.middleware.CorrelationIdMiddleware +import com.avast.sst.http4s.server._ +import com.avast.sst.http4s.server.middleware.CorrelationIdMiddleware +import com.avast.sst.jvm.execution.ExecutorModule import org.http4s.dsl.Http4sDsl import org.http4s.HttpRoutes import zio.DefaultRuntime diff --git a/docs/index.md b/docs/index.md index 39c2d2f25..ed08dc1cd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,16 +2,90 @@ * [Getting Started](#getting-started) * [Rationale](rationale.md) +* [Module Structure](#module-structure) +* [Bundles](#bundles) * [Modules http4s](http4s.md) -* [Modules JVM](jvm.md) +* [Module JVM](jvm.md) +* [Modules Micrometer](micrometer.md) * [Module PureConfig](pureconfig.md) ## Getting Started -Creating a simple HTTP server is as easy as this: +Creating a simple HTTP server using [http4s](https://http4s.org) and [ZIO](https://zio.dev) is as easy as this: #### build.sbt -[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-http4s-blaze-server_2.12)](https://repo1.maven.org/maven2/com/avast/sst-http4s-blaze-server_2.12/) +[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-bundle-zio-http4s-blaze_2.12)](https://repo1.maven.org/maven2/com/avast/sst-bundle-zio-http4s-blaze_2.12/) -`libraryDependencies += "com.avast" %% "sst-http4s-blaze-server" % ""` +`libraryDependencies += "com.avast" %% "sst-bundle-zio-http4s-blaze" % ""` + +#### Main + +```scala +import cats.effect._ +import com.avast.sst.http4s.client._ +import com.avast.sst.http4s.server._ +import com.avast.sst.jvm.execution.ExecutorModule +import com.avast.sst.jvm.system.console.ConsoleModule +import org.http4s.dsl.Http4sDsl +import org.http4s.HttpRoutes +import zio.DefaultRuntime +import zio.interop.catz._ +import zio.interop.catz.implicits._ +import zio.Task + +implicit val runtime = new DefaultRuntime {} // this is just needed in example + +val dsl = Http4sDsl[Task] // this is just needed in example +import dsl._ + +val routes = Http4sRouting.make { + HttpRoutes.of[Task] { + case GET -> Root / "hello" => Ok("Hello World!") + } +} + +val resource = for { + executorModule <- ExecutorModule.makeDefault[Task] + console = ConsoleModule.make[Task] + server <- Http4sBlazeServerModule.make[Task](Http4sBlazeServerConfig("127.0.0.1", 0), routes, executorModule.executionContext) + client <- Http4sBlazeClient.make[Task](Http4sBlazeClientConfig(), executorModule.executionContext) +} yield (server, client, console) + +val program = resource + .use { + case (server, client, console) => + client + .expect[String](s"http://127.0.0.1:${server.address.getPort}/hello") + .flatMap(console.printLine) + } +``` + +```scala +runtime.unsafeRun(program) +// Hello World! +``` + +## Module Structure + +The project is split into many small modules based on dependencies. For example code related to loading of configuration files via +[PureConfig](https://pureconfig.github.io) lives in module named `sst-pureconfig` and code related to http4s server implemented using +[Blaze](https://github.com/http4s/blaze) lives in module named `sst-http4s-server-blaze`. + +There are also modules that implement interoperability between usually two dependencies. For example we want to configure our HTTP server +using PureConfig so definition of `implicit` `ConfigReader` instances lives in module named `sst-http4s-server-blaze-pureconfig`. Or to give +another example, monitoring of HTTP server using [Micrometer](https://micrometer.io) lives in module named `sst-http4s-server-micrometer`. +Note that such module depends on APIs of both http4s server and Micrometer but it does not depend on concrete implementation which allows +you to choose any http4s implementation (Blaze, ...) and any Micrometer implementation (JMX, StatsD, ...). + +## Bundles + +Having many small and independent modules is great but in practice everyone wants to use certain combination of dependencies and does not +want to worry about many small dependencies. There are "bundles" for such use case - either the ones provided by this project or custom +ones created by the user. + +One of the main decisions (dependency-wise) is to choose the effect data type. This project does not force you into specific data type and +supports both [ZIO](https://zio.dev) and [Monix](https://monix.io) out-of-the-box. So there are two main bundles one for each effect data +type that also bring in http4s server/client (Blaze), PureConfig and Micrometer. + +Unless you have specific needs take one of these bundles and write your server application using them - it will be the simplest way. diff --git a/docs/jvm.md b/docs/jvm.md index e83b6de82..420519a7b 100644 --- a/docs/jvm.md +++ b/docs/jvm.md @@ -1,20 +1,21 @@ -# Modules JVM +# Module JVM -![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-jvm-system_2.12) +![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-jvm_2.12) -`libraryDependencies += "com.avast" %% "sst-jvm-system" % ""` +`libraryDependencies += "com.avast" %% "sst-jvm" % ""` -There is a set of `sst-jvm-*` modules that provide pure implementations of different JVM-related utilities: +Module `sst-jvm` provides pure implementations of different JVM-related utilities: -* `sst-jvm-execution` - creation of thread pools, -* `sst-jvm-ssl` - initialization of SSL context, -* `sst-jvm-system` - standard in/out/err, random number generation. +* creation of thread pools, +* initialization of SSL context, +* standard in/out/err, random number generation, +* and more. ```scala -import com.avast.sst.system.console.ConsoleModule -import com.avast.sst.system.random.RandomModule -import zio.interop.catz._ +import com.avast.sst.jvm.system.console.ConsoleModule +import com.avast.sst.jvm.system.random.RandomModule import zio.DefaultRuntime +import zio.interop.catz._ import zio.Task val program = for { @@ -23,11 +24,11 @@ val program = for { console = ConsoleModule.make[Task] _ <- console.printLine(s"Random number: $randomNumber") } yield () -// program: zio.ZIO[Any, Throwable, Unit] = zio.ZIO$FlatMap@2ce47652 +// program: zio.ZIO[Any, Throwable, Unit] = zio.ZIO$FlatMap@159314ca val runtime = new DefaultRuntime {} // this is just needed in example -// runtime: AnyRef with DefaultRuntime = repl.Session$App$$anon$1@35becbd4 // this is just needed in example +// runtime: AnyRef with DefaultRuntime = repl.Session$App$$anon$1@bafc754 // this is just needed in example runtime.unsafeRun(program) -// Random number: 1821017404 +// Random number: 235841973 ``` diff --git a/docs/micrometer.md b/docs/micrometer.md new file mode 100644 index 000000000..eec7ee791 --- /dev/null +++ b/docs/micrometer.md @@ -0,0 +1,59 @@ +# Modules Micrometer + +[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-micrometer-jmx_2.12)](https://repo1.maven.org/maven2/com/avast/sst-micrometer-jmx_2.12/) + +`libraryDependencies += "com.avast" %% "sst-micrometer-jmx" % ""` + +This module allows you to monitor your applications using [Micrometer](https://micrometer.io). There are many actual implementations of +the Micrometer API one of which is JMX. Module `sst-micrometer-jmx` implements the initialization of Micrometer for JMX. There are also +interop modules such as `sst-http4s-server-micrometer` which implement monitoring of HTTP server and individual routes using Micrometer. + +```scala +import cats.effect.{Clock, Resource} +import com.avast.sst.http4s.server._ +import com.avast.sst.http4s.server.micrometer.MicrometerHttp4sServerMetricsModule +import com.avast.sst.jvm.execution.ExecutorModule +import com.avast.sst.jvm.micrometer.MicrometerJvmModule +import com.avast.sst.micrometer.jmx._ +import org.http4s.dsl.Http4sDsl +import org.http4s.HttpRoutes +import org.http4s.server.Server +import zio.DefaultRuntime +import zio.interop.catz._ +import zio.interop.catz.implicits._ +import zio.Task + +implicit val runtime = new DefaultRuntime {} // this is just needed in example +// runtime: AnyRef with DefaultRuntime = repl.Session$App$$anon$1@1c5cd2ea // this is just needed in example + +val dsl = Http4sDsl[Task] // this is just needed in example +// dsl: Http4sDsl[Task] = org.http4s.dsl.Http4sDsl$$anon$1@1f7fcec2 // this is just needed in example +import dsl._ + +for { + executorModule <- ExecutorModule.makeFromExecutionContext[Task](runtime.Platform.executor.asEC) + clock = Clock.create[Task] + jmxMeterRegistry <- MicrometerJmxModule.make[Task](MicrometerJmxConfig("com.avast")) + _ <- Resource.liftF(MicrometerJvmModule.make[Task](jmxMeterRegistry)) + serverMetricsModule <- Resource.liftF(MicrometerHttp4sServerMetricsModule.make[Task](jmxMeterRegistry, clock)) + routes = Http4sRouting.make { + serverMetricsModule.serverMetrics { + HttpRoutes.of[Task] { + case GET -> Root / "hello" => Ok("Hello World!") + } + } + } + server <- Http4sBlazeServerModule.make[Task](Http4sBlazeServerConfig("127.0.0.1", 0), routes, executorModule.executionContext) +} yield server +// res0: Resource[Task, Server[Task]] = Bind( +// Bind( +// Bind( +// Suspend(zio.ZIO$FlatMap@77e1dacd), +// com.avast.sst.jvm.execution.ExecutorModule$$$Lambda$1652/2118457232@34ede267 +// ), +// cats.effect.Resource$$Lambda$1653/622274963@7a522157 +// ), +// +// ) +``` + diff --git a/docs/pureconfig.md b/docs/pureconfig.md index e23de4a28..6ab28ea17 100644 --- a/docs/pureconfig.md +++ b/docs/pureconfig.md @@ -4,14 +4,14 @@ `libraryDependencies += "com.avast" %% "sst-pureconfig" % ""` -This module allows you to load your application's configuration file according to a case class provided to it. It uses -[PureConfig](https://pureconfig.github.io) library to do so which uses [Lightbend Config](https://github.com/lightbend/config) which means -that your application's configuration will be in [HOCON](https://github.com/lightbend/config/blob/master/HOCON.md) format. +This module allows you to load your application's configuration file into a case class. It uses [PureConfig](https://pureconfig.github.io) +library to do so which uses [Lightbend Config](https://github.com/lightbend/config) which means that your application's configuration +will be in [HOCON](https://github.com/lightbend/config/blob/master/HOCON.md) format. Loading of configuration is side-effectful so it is wrapped in `F` which is `Sync`. This module also tweaks the error messages a little. ```scala -import com.avast.sst.pureconfig._ +import com.avast.sst.pureconfig.PureConfigModule import pureconfig.ConfigReader import pureconfig.generic.semiauto.deriveReader import zio.interop.catz._ @@ -24,3 +24,9 @@ implicit val serverConfigurationReader: ConfigReader[ServerConfiguration] = deri val maybeConfiguration = PureConfigModule.make[Task, ServerConfiguration] ``` +Look for `sst-*-pureconfig` modules to get `implicit` instances of `ConfigReader` for specific libraries, e.g.: + +```scala +import com.avast.sst.http4s.server.pureconfig.implicits._ +``` + diff --git a/example/src/main/mdoc/http4s.md b/example/src/main/mdoc/http4s.md index 72c953e6e..196f2c7bf 100644 --- a/example/src/main/mdoc/http4s.md +++ b/example/src/main/mdoc/http4s.md @@ -1,8 +1,8 @@ # Module http4s -[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-http4s-blaze-server_2.12)](https://repo1.maven.org/maven2/com/avast/sst-http4s-blaze-server_2.12/) +[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-http4s-server-blaze_2.12)](https://repo1.maven.org/maven2/com/avast/sst-http4s-server-blaze_2.12/) -`libraryDependencies += "com.avast" %% "sst-http4s-blaze-server" % ""` +`libraryDependencies += "com.avast" %% "sst-http4s-server-blaze" % ""` There are `http4s-*` modules that provide easy initialization of a server and a client. Http4s is an interface with multiple possible implementations - for now we provide only implementations based on [Blaze](https://github.com/http4s/blaze). @@ -11,9 +11,10 @@ Both server and client are configured via configuration `case class` which conta ```scala mdoc:silent:reset-class import cats.effect._ -import com.avast.sst.execution.ExecutorModule -import com.avast.sst.http4s._ -import com.avast.sst.system.console.ConsoleModule +import com.avast.sst.http4s.client._ +import com.avast.sst.http4s.server._ +import com.avast.sst.jvm.execution.ExecutorModule +import com.avast.sst.jvm.system.console.ConsoleModule import org.http4s.dsl.Http4sDsl import org.http4s.HttpRoutes import zio.DefaultRuntime @@ -58,9 +59,9 @@ runtime.unsafeRun(program) ```scala mdoc:silent:reset import cats.effect._ -import com.avast.sst.execution.ExecutorModule -import com.avast.sst.http4s._ -import com.avast.sst.http4s.middleware.CorrelationIdMiddleware +import com.avast.sst.http4s.server._ +import com.avast.sst.http4s.server.middleware.CorrelationIdMiddleware +import com.avast.sst.jvm.execution.ExecutorModule import org.http4s.dsl.Http4sDsl import org.http4s.HttpRoutes import zio.DefaultRuntime diff --git a/example/src/main/mdoc/index.md b/example/src/main/mdoc/index.md index 39c2d2f25..73b8a3b33 100644 --- a/example/src/main/mdoc/index.md +++ b/example/src/main/mdoc/index.md @@ -2,16 +2,89 @@ * [Getting Started](#getting-started) * [Rationale](rationale.md) +* [Module Structure](#module-structure) +* [Bundles](#bundles) * [Modules http4s](http4s.md) -* [Modules JVM](jvm.md) +* [Module JVM](jvm.md) +* [Modules Micrometer](micrometer.md) * [Module PureConfig](pureconfig.md) ## Getting Started -Creating a simple HTTP server is as easy as this: +Creating a simple HTTP server using [http4s](https://http4s.org) and [ZIO](https://zio.dev) is as easy as this: #### build.sbt -[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-http4s-blaze-server_2.12)](https://repo1.maven.org/maven2/com/avast/sst-http4s-blaze-server_2.12/) +[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-bundle-zio-http4s-blaze_2.12)](https://repo1.maven.org/maven2/com/avast/sst-bundle-zio-http4s-blaze_2.12/) -`libraryDependencies += "com.avast" %% "sst-http4s-blaze-server" % ""` +`libraryDependencies += "com.avast" %% "sst-bundle-zio-http4s-blaze" % ""` + +#### Main + +```scala mdoc:silent:reset-class +import cats.effect._ +import com.avast.sst.http4s.client._ +import com.avast.sst.http4s.server._ +import com.avast.sst.jvm.execution.ExecutorModule +import com.avast.sst.jvm.system.console.ConsoleModule +import org.http4s.dsl.Http4sDsl +import org.http4s.HttpRoutes +import zio.DefaultRuntime +import zio.interop.catz._ +import zio.interop.catz.implicits._ +import zio.Task + +implicit val runtime = new DefaultRuntime {} // this is just needed in example + +val dsl = Http4sDsl[Task] // this is just needed in example +import dsl._ + +val routes = Http4sRouting.make { + HttpRoutes.of[Task] { + case GET -> Root / "hello" => Ok("Hello World!") + } +} + +val resource = for { + executorModule <- ExecutorModule.makeDefault[Task] + console = ConsoleModule.make[Task] + server <- Http4sBlazeServerModule.make[Task](Http4sBlazeServerConfig("127.0.0.1", 0), routes, executorModule.executionContext) + client <- Http4sBlazeClient.make[Task](Http4sBlazeClientConfig(), executorModule.executionContext) +} yield (server, client, console) + +val program = resource + .use { + case (server, client, console) => + client + .expect[String](s"http://127.0.0.1:${server.address.getPort}/hello") + .flatMap(console.printLine) + } +``` + +```scala mdoc +runtime.unsafeRun(program) +``` + +## Module Structure + +The project is split into many small modules based on dependencies. For example code related to loading of configuration files via +[PureConfig](https://pureconfig.github.io) lives in module named `sst-pureconfig` and code related to http4s server implemented using +[Blaze](https://github.com/http4s/blaze) lives in module named `sst-http4s-server-blaze`. + +There are also modules that implement interoperability between usually two dependencies. For example we want to configure our HTTP server +using PureConfig so definition of `implicit` `ConfigReader` instances lives in module named `sst-http4s-server-blaze-pureconfig`. Or to give +another example, monitoring of HTTP server using [Micrometer](https://micrometer.io) lives in module named `sst-http4s-server-micrometer`. +Note that such module depends on APIs of both http4s server and Micrometer but it does not depend on concrete implementation which allows +you to choose any http4s implementation (Blaze, ...) and any Micrometer implementation (JMX, StatsD, ...). + +## Bundles + +Having many small and independent modules is great but in practice everyone wants to use certain combination of dependencies and does not +want to worry about many small dependencies. There are "bundles" for such use case - either the ones provided by this project or custom +ones created by the user. + +One of the main decisions (dependency-wise) is to choose the effect data type. This project does not force you into specific data type and +supports both [ZIO](https://zio.dev) and [Monix](https://monix.io) out-of-the-box. So there are two main bundles one for each effect data +type that also bring in http4s server/client (Blaze), PureConfig and Micrometer. + +Unless you have specific needs take one of these bundles and write your server application using them - it will be the simplest way. diff --git a/example/src/main/mdoc/jvm.md b/example/src/main/mdoc/jvm.md index db10d0379..4180d5dbb 100644 --- a/example/src/main/mdoc/jvm.md +++ b/example/src/main/mdoc/jvm.md @@ -1,20 +1,21 @@ -# Modules JVM +# Module JVM -![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-jvm-system_2.12) +![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-jvm_2.12) -`libraryDependencies += "com.avast" %% "sst-jvm-system" % ""` +`libraryDependencies += "com.avast" %% "sst-jvm" % ""` -There is a set of `sst-jvm-*` modules that provide pure implementations of different JVM-related utilities: +Module `sst-jvm` provides pure implementations of different JVM-related utilities: - * `sst-jvm-execution` - creation of thread pools, - * `sst-jvm-ssl` - initialization of SSL context, - * `sst-jvm-system` - standard in/out/err, random number generation. + * creation of thread pools, + * initialization of SSL context, + * standard in/out/err, random number generation, + * and more. ```scala mdoc -import com.avast.sst.system.console.ConsoleModule -import com.avast.sst.system.random.RandomModule -import zio.interop.catz._ +import com.avast.sst.jvm.system.console.ConsoleModule +import com.avast.sst.jvm.system.random.RandomModule import zio.DefaultRuntime +import zio.interop.catz._ import zio.Task val program = for { diff --git a/example/src/main/mdoc/micrometer.md b/example/src/main/mdoc/micrometer.md new file mode 100644 index 000000000..81671db6e --- /dev/null +++ b/example/src/main/mdoc/micrometer.md @@ -0,0 +1,46 @@ +# Modules Micrometer + +[![Maven Central](https://img.shields.io/maven-central/v/com.avast/sst-micrometer-jmx_2.12)](https://repo1.maven.org/maven2/com/avast/sst-micrometer-jmx_2.12/) + +`libraryDependencies += "com.avast" %% "sst-micrometer-jmx" % ""` + +This module allows you to monitor your applications using [Micrometer](https://micrometer.io). There are many actual implementations of +the Micrometer API one of which is JMX. Module `sst-micrometer-jmx` implements the initialization of Micrometer for JMX. There are also +interop modules such as `sst-http4s-server-micrometer` which implement monitoring of HTTP server and individual routes using Micrometer. + +```scala mdoc +import cats.effect.{Clock, Resource} +import com.avast.sst.http4s.server._ +import com.avast.sst.http4s.server.micrometer.MicrometerHttp4sServerMetricsModule +import com.avast.sst.jvm.execution.ExecutorModule +import com.avast.sst.jvm.micrometer.MicrometerJvmModule +import com.avast.sst.micrometer.jmx._ +import org.http4s.dsl.Http4sDsl +import org.http4s.HttpRoutes +import org.http4s.server.Server +import zio.DefaultRuntime +import zio.interop.catz._ +import zio.interop.catz.implicits._ +import zio.Task + +implicit val runtime = new DefaultRuntime {} // this is just needed in example + +val dsl = Http4sDsl[Task] // this is just needed in example +import dsl._ + +for { + executorModule <- ExecutorModule.makeFromExecutionContext[Task](runtime.Platform.executor.asEC) + clock = Clock.create[Task] + jmxMeterRegistry <- MicrometerJmxModule.make[Task](MicrometerJmxConfig("com.avast")) + _ <- Resource.liftF(MicrometerJvmModule.make[Task](jmxMeterRegistry)) + serverMetricsModule <- Resource.liftF(MicrometerHttp4sServerMetricsModule.make[Task](jmxMeterRegistry, clock)) + routes = Http4sRouting.make { + serverMetricsModule.serverMetrics { + HttpRoutes.of[Task] { + case GET -> Root / "hello" => Ok("Hello World!") + } + } + } + server <- Http4sBlazeServerModule.make[Task](Http4sBlazeServerConfig("127.0.0.1", 0), routes, executorModule.executionContext) +} yield server +``` diff --git a/example/src/main/mdoc/pureconfig.md b/example/src/main/mdoc/pureconfig.md index 5c742929e..6e80440c8 100644 --- a/example/src/main/mdoc/pureconfig.md +++ b/example/src/main/mdoc/pureconfig.md @@ -4,14 +4,14 @@ `libraryDependencies += "com.avast" %% "sst-pureconfig" % ""` -This module allows you to load your application's configuration file according to a case class provided to it. It uses -[PureConfig](https://pureconfig.github.io) library to do so which uses [Lightbend Config](https://github.com/lightbend/config) which means -that your application's configuration will be in [HOCON](https://github.com/lightbend/config/blob/master/HOCON.md) format. +This module allows you to load your application's configuration file into a case class. It uses [PureConfig](https://pureconfig.github.io) +library to do so which uses [Lightbend Config](https://github.com/lightbend/config) which means that your application's configuration +will be in [HOCON](https://github.com/lightbend/config/blob/master/HOCON.md) format. Loading of configuration is side-effectful so it is wrapped in `F` which is `Sync`. This module also tweaks the error messages a little. ```scala mdoc:silent -import com.avast.sst.pureconfig._ +import com.avast.sst.pureconfig.PureConfigModule import pureconfig.ConfigReader import pureconfig.generic.semiauto.deriveReader import zio.interop.catz._ @@ -23,3 +23,9 @@ implicit val serverConfigurationReader: ConfigReader[ServerConfiguration] = deri val maybeConfiguration = PureConfigModule.make[Task, ServerConfiguration] ``` + +Look for `sst-*-pureconfig` modules to get `implicit` instances of `ConfigReader` for specific libraries, e.g.: + +```scala +import com.avast.sst.http4s.server.pureconfig.implicits._ +``` diff --git a/example/src/main/resources/reference.conf b/example/src/main/resources/reference.conf index e69de29bb..6b1a6172f 100644 --- a/example/src/main/resources/reference.conf +++ b/example/src/main/resources/reference.conf @@ -0,0 +1,8 @@ +server { + listen-address = "0.0.0.0" + listen-port = 8080 +} + +jmx { + domain = "com.avast.sst.example" +} \ No newline at end of file diff --git a/example/src/main/scala/com/avast/sst/example/Main.scala b/example/src/main/scala/com/avast/sst/example/Main.scala index 9cc4704d4..c86cfa90d 100644 --- a/example/src/main/scala/com/avast/sst/example/Main.scala +++ b/example/src/main/scala/com/avast/sst/example/Main.scala @@ -3,20 +3,26 @@ package com.avast.sst.example import java.util.concurrent.TimeUnit import cats.effect.{Clock, Resource} +import com.avast.sst.bundle.ZioServerApp import com.avast.sst.example.config.Configuration -import com.avast.sst.execution.ExecutorModule +import com.avast.sst.example.module.Http4sRoutingModule +import com.avast.sst.http4s.server.Http4sBlazeServerModule +import com.avast.sst.http4s.server.micrometer.MicrometerHttp4sServerMetricsModule +import com.avast.sst.jvm.execution.ExecutorModule +import com.avast.sst.jvm.micrometer.MicrometerJvmModule +import com.avast.sst.jvm.system.console.{Console, ConsoleModule} +import com.avast.sst.micrometer.jmx.MicrometerJmxModule import com.avast.sst.pureconfig.PureConfigModule -import com.avast.sst.system.console.{Console, ConsoleModule} -import com.github.ghik.silencer.silent +import org.http4s.server.Server +import zio.Task import zio.interop.catz._ -import zio.{Task, ZIO} +import zio.interop.catz.implicits._ -object Main extends CatsApp { - - def program: Resource[Task, Unit] = { +object Main extends ZioServerApp { + def program: Resource[Task, Server[Task]] = { for { - _ <- Resource.liftF(PureConfigModule.makeOrRaise[Task, Configuration]) + configuration <- Resource.liftF(PureConfigModule.makeOrRaise[Task, Configuration]) executorModule <- ExecutorModule.makeFromExecutionContext[Task](runtime.Platform.executor.asEC) clock = Clock.create[Task] currentTime <- Resource.liftF(clock.realTime(TimeUnit.MILLISECONDS)) @@ -24,17 +30,12 @@ object Main extends CatsApp { _ <- Resource.liftF( console.printLine(s"The current Unix epoch time is $currentTime. This system has ${executorModule.numOfCpus} CPUs.") ) - } yield () - } - - @silent("dead code") // false positive - override def run(args: List[String]): ZIO[Environment, Nothing, Int] = { - program - .use(_ => Task.never) - .fold( - _ => 1, - _ => 0 - ) + meterRegistry <- MicrometerJmxModule.make[Task](configuration.jmx) + _ <- Resource.liftF(MicrometerJvmModule.make[Task](meterRegistry)) + serverMetricsModule <- Resource.liftF(MicrometerHttp4sServerMetricsModule.make[Task](meterRegistry, clock)) + routingModule = new Http4sRoutingModule(serverMetricsModule) + server <- Http4sBlazeServerModule.make[Task](configuration.server, routingModule.router, executorModule.executionContext) + } yield server } } diff --git a/example/src/main/scala/com/avast/sst/example/config/Configuration.scala b/example/src/main/scala/com/avast/sst/example/config/Configuration.scala index 9cb62fed3..293a13c7f 100644 --- a/example/src/main/scala/com/avast/sst/example/config/Configuration.scala +++ b/example/src/main/scala/com/avast/sst/example/config/Configuration.scala @@ -1,9 +1,13 @@ package com.avast.sst.example.config +import com.avast.sst.http4s.server.Http4sBlazeServerConfig +import com.avast.sst.micrometer.jmx.MicrometerJmxConfig +import com.avast.sst.http4s.server.pureconfig.implicits._ +import com.avast.sst.micrometer.jmx.pureconfig.implicits._ import pureconfig.ConfigReader -import pureconfig.generic.semiauto._ +import pureconfig.generic.semiauto.deriveReader -final case class Configuration() +final case class Configuration(server: Http4sBlazeServerConfig, jmx: MicrometerJmxConfig) object Configuration { diff --git a/example/src/main/scala/com/avast/sst/example/module/Http4sRoutingModule.scala b/example/src/main/scala/com/avast/sst/example/module/Http4sRoutingModule.scala new file mode 100644 index 000000000..0652e0975 --- /dev/null +++ b/example/src/main/scala/com/avast/sst/example/module/Http4sRoutingModule.scala @@ -0,0 +1,26 @@ +package com.avast.sst.example.module + +import com.avast.sst.http4s.server.Http4sRouting +import com.avast.sst.http4s.server.micrometer.MicrometerHttp4sServerMetricsModule +import org.http4s.dsl.Http4sDsl +import org.http4s.{HttpApp, HttpRoutes} +import zio.Task +import zio.interop.catz._ + +class Http4sRoutingModule(serverMetricsModule: MicrometerHttp4sServerMetricsModule[Task]) extends Http4sDsl[Task] { + + import serverMetricsModule._ + + private val helloWorldRoute = routeMetrics.wrap("hello")(Ok("Hello World!")) + + private val routes = HttpRoutes.of[Task] { + case GET -> Root / "hello" => helloWorldRoute + } + + val router: HttpApp[Task] = Http4sRouting.make { + serverMetrics { + routes + } + } + +} diff --git a/http4s-client-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/client/pureconfig/ConfigReaders.scala b/http4s-client-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/client/pureconfig/ConfigReaders.scala new file mode 100644 index 000000000..de50144f4 --- /dev/null +++ b/http4s-client-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/client/pureconfig/ConfigReaders.scala @@ -0,0 +1,24 @@ +package com.avast.sst.http4s.client.pureconfig + +import cats.syntax.either._ +import com.avast.sst.http4s.client.Http4sBlazeClientConfig +import com.avast.sst.jvm.pureconfig.implicits._ +import org.http4s.client.blaze.ParserMode +import org.http4s.headers.`User-Agent` +import pureconfig.ConfigReader +import pureconfig.error.CannotConvert +import pureconfig.generic.semiauto.{deriveEnumerationReader, deriveReader} + +trait ConfigReaders { + + implicit val userAgentReader: ConfigReader[`User-Agent`] = ConfigReader[String].emap { value => + `User-Agent`.parse(value).leftMap { parseFailure => + CannotConvert(value, "User-Agent HTTP header", parseFailure.message) + } + } + + implicit val parserModeReader: ConfigReader[ParserMode] = deriveEnumerationReader + + implicit val http4sBlazeClientConfigReader: ConfigReader[Http4sBlazeClientConfig] = deriveReader + +} diff --git a/http4s-client-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/client/pureconfig/implicits.scala b/http4s-client-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/client/pureconfig/implicits.scala new file mode 100644 index 000000000..d535661a4 --- /dev/null +++ b/http4s-client-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/client/pureconfig/implicits.scala @@ -0,0 +1,3 @@ +package com.avast.sst.http4s.client.pureconfig + +object implicits extends ConfigReaders diff --git a/http4s-blaze-client/src/main/scala/com/avast/sst/http4s/Http4sBlazeClient.scala b/http4s-client-blaze/src/main/scala/com/avast/sst/http4s/client/Http4sBlazeClient.scala similarity index 95% rename from http4s-blaze-client/src/main/scala/com/avast/sst/http4s/Http4sBlazeClient.scala rename to http4s-client-blaze/src/main/scala/com/avast/sst/http4s/client/Http4sBlazeClient.scala index 38c5bd10b..4de5fd9ed 100644 --- a/http4s-blaze-client/src/main/scala/com/avast/sst/http4s/Http4sBlazeClient.scala +++ b/http4s-client-blaze/src/main/scala/com/avast/sst/http4s/client/Http4sBlazeClient.scala @@ -1,9 +1,9 @@ -package com.avast.sst.http4s +package com.avast.sst.http4s.client import cats.Traverse import cats.effect.{ConcurrentEffect, Resource, Sync} import cats.implicits._ -import com.avast.sst.ssl.{SslContextConfig, SslContextModule} +import com.avast.sst.jvm.ssl.{SslContextConfig, SslContextModule} import javax.net.ssl.SSLContext import org.http4s.client.Client import org.http4s.client.blaze.BlazeClientBuilder diff --git a/http4s-blaze-client/src/main/scala/com/avast/sst/http4s/Http4sBlazeClientConfig.scala b/http4s-client-blaze/src/main/scala/com/avast/sst/http4s/client/Http4sBlazeClientConfig.scala similarity index 93% rename from http4s-blaze-client/src/main/scala/com/avast/sst/http4s/Http4sBlazeClientConfig.scala rename to http4s-client-blaze/src/main/scala/com/avast/sst/http4s/client/Http4sBlazeClientConfig.scala index f98d46d30..faa397d96 100644 --- a/http4s-blaze-client/src/main/scala/com/avast/sst/http4s/Http4sBlazeClientConfig.scala +++ b/http4s-client-blaze/src/main/scala/com/avast/sst/http4s/client/Http4sBlazeClientConfig.scala @@ -1,8 +1,8 @@ -package com.avast.sst.http4s +package com.avast.sst.http4s.client import java.util.concurrent.TimeUnit -import com.avast.sst.ssl.SslContextConfig +import com.avast.sst.jvm.ssl.SslContextConfig import org.http4s.BuildInfo import org.http4s.client.blaze.ParserMode import org.http4s.client.defaults diff --git a/http4s-blaze-client/src/test/scala/com/avast/sst/http4s/Http4SBlazeClientTest.scala b/http4s-client-blaze/src/test/scala/com/avast/sst/http4s/client/Http4SBlazeClientTest.scala similarity index 96% rename from http4s-blaze-client/src/test/scala/com/avast/sst/http4s/Http4SBlazeClientTest.scala rename to http4s-client-blaze/src/test/scala/com/avast/sst/http4s/client/Http4SBlazeClientTest.scala index bc9646b2f..f38b31aed 100644 --- a/http4s-blaze-client/src/test/scala/com/avast/sst/http4s/Http4SBlazeClientTest.scala +++ b/http4s-client-blaze/src/test/scala/com/avast/sst/http4s/client/Http4SBlazeClientTest.scala @@ -1,4 +1,4 @@ -package com.avast.sst.http4s +package com.avast.sst.http4s.client import cats.effect._ import org.http4s.headers.{`User-Agent`, AgentComment, AgentProduct} diff --git a/http4s-server-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/server/pureconfig/ConfigReaders.scala b/http4s-server-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/server/pureconfig/ConfigReaders.scala new file mode 100644 index 000000000..d41dcff47 --- /dev/null +++ b/http4s-server-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/server/pureconfig/ConfigReaders.scala @@ -0,0 +1,14 @@ +package com.avast.sst.http4s.server.pureconfig + +import com.avast.sst.http4s.server.Http4sBlazeServerConfig +import com.avast.sst.http4s.server.Http4sBlazeServerConfig.SocketOptions +import pureconfig.ConfigReader +import pureconfig.generic.semiauto.deriveReader + +trait ConfigReaders { + + implicit val socketOptionsReader: ConfigReader[SocketOptions] = deriveReader + + implicit val http4sBlazeServerConfigReader: ConfigReader[Http4sBlazeServerConfig] = deriveReader + +} diff --git a/http4s-server-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/server/pureconfig/implicits.scala b/http4s-server-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/server/pureconfig/implicits.scala new file mode 100644 index 000000000..af3719357 --- /dev/null +++ b/http4s-server-blaze-pureconfig/src/main/scala/com/avast/sst/http4s/server/pureconfig/implicits.scala @@ -0,0 +1,3 @@ +package com.avast.sst.http4s.server.pureconfig + +object implicits extends ConfigReaders diff --git a/http4s-blaze-server/src/main/scala/com/avast/sst/http4s/Http4sBlazeServerConfig.scala b/http4s-server-blaze/src/main/scala/com/avast/sst/http4s/server/Http4sBlazeServerConfig.scala similarity index 90% rename from http4s-blaze-server/src/main/scala/com/avast/sst/http4s/Http4sBlazeServerConfig.scala rename to http4s-server-blaze/src/main/scala/com/avast/sst/http4s/server/Http4sBlazeServerConfig.scala index 461bb4a19..b557c52fd 100644 --- a/http4s-blaze-server/src/main/scala/com/avast/sst/http4s/Http4sBlazeServerConfig.scala +++ b/http4s-server-blaze/src/main/scala/com/avast/sst/http4s/server/Http4sBlazeServerConfig.scala @@ -1,8 +1,8 @@ -package com.avast.sst.http4s +package com.avast.sst.http4s.server import java.util.concurrent.TimeUnit -import com.avast.sst.http4s.Http4sBlazeServerConfig.SocketOptions +import com.avast.sst.http4s.server.Http4sBlazeServerConfig.SocketOptions import org.http4s.blaze.channel import org.http4s.server.defaults diff --git a/http4s-blaze-server/src/main/scala/com/avast/sst/http4s/Http4sBlazeServerModule.scala b/http4s-server-blaze/src/main/scala/com/avast/sst/http4s/server/Http4sBlazeServerModule.scala similarity index 97% rename from http4s-blaze-server/src/main/scala/com/avast/sst/http4s/Http4sBlazeServerModule.scala rename to http4s-server-blaze/src/main/scala/com/avast/sst/http4s/server/Http4sBlazeServerModule.scala index 14e61cad7..4e532e8fb 100644 --- a/http4s-blaze-server/src/main/scala/com/avast/sst/http4s/Http4sBlazeServerModule.scala +++ b/http4s-server-blaze/src/main/scala/com/avast/sst/http4s/server/Http4sBlazeServerModule.scala @@ -1,4 +1,4 @@ -package com.avast.sst.http4s +package com.avast.sst.http4s.server import java.net.{InetSocketAddress, StandardSocketOptions} diff --git a/http4s-blaze-server/src/test/scala/com/avast/sst/http4s/Http4SBlazeServerModuleTest.scala b/http4s-server-blaze/src/test/scala/com/avast/sst/http4s/server/Http4sBlazeServerModuleTest.scala similarity index 85% rename from http4s-blaze-server/src/test/scala/com/avast/sst/http4s/Http4SBlazeServerModuleTest.scala rename to http4s-server-blaze/src/test/scala/com/avast/sst/http4s/server/Http4sBlazeServerModuleTest.scala index 0d2e08669..3f6a5b0d5 100644 --- a/http4s-blaze-server/src/test/scala/com/avast/sst/http4s/Http4SBlazeServerModuleTest.scala +++ b/http4s-server-blaze/src/test/scala/com/avast/sst/http4s/server/Http4sBlazeServerModuleTest.scala @@ -1,13 +1,14 @@ -package com.avast.sst.http4s +package com.avast.sst.http4s.server import cats.effect.{ContextShift, IO, Timer} +import com.avast.sst.http4s.client.{Http4sBlazeClient, Http4sBlazeClientConfig} import org.http4s.HttpRoutes import org.http4s.dsl.Http4sDsl import org.scalatest.AsyncFunSuite import scala.concurrent.ExecutionContext -class Http4SBlazeServerModuleTest extends AsyncFunSuite with Http4sDsl[IO] { +class Http4sBlazeServerModuleTest extends AsyncFunSuite with Http4sDsl[IO] { implicit private val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) implicit private val timer: Timer[IO] = IO.timer(ExecutionContext.global) diff --git a/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/HttpStatusMetrics.scala b/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/HttpStatusMetrics.scala new file mode 100644 index 000000000..a8292389f --- /dev/null +++ b/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/HttpStatusMetrics.scala @@ -0,0 +1,25 @@ +package com.avast.sst.http4s.server.micrometer + +import io.micrometer.core.instrument.{Counter, MeterRegistry} +import org.http4s.Status + +import scala.collection.concurrent.TrieMap + +/** Records counts of HTTP statuses in [[io.micrometer.core.instrument.MeterRegistry]]. */ +private[micrometer] class HttpStatusMetrics(prefix: String, meterRegistry: MeterRegistry) { + + private val meters = TrieMap[Int, Counter]( + 1 -> meterRegistry.counter(s"$prefix.status.1xx"), + 2 -> meterRegistry.counter(s"$prefix.status.2xx"), + 3 -> meterRegistry.counter(s"$prefix.status.3xx"), + 4 -> meterRegistry.counter(s"$prefix.status.4xx"), + 5 -> meterRegistry.counter(s"$prefix.status.5xx") + ) + + def recordHttpStatus(status: Status): Unit = { + val code = status.code + meters(code / 100).increment() + meters.getOrElseUpdate(code, meterRegistry.counter(s"$prefix.status.$code")).increment() + } + +} diff --git a/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sMetricsOpsModule.scala b/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sMetricsOpsModule.scala new file mode 100644 index 000000000..2f9861324 --- /dev/null +++ b/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sMetricsOpsModule.scala @@ -0,0 +1,50 @@ +package com.avast.sst.http4s.server.micrometer + +import java.util.concurrent.TimeUnit + +import cats.effect.Sync +import cats.syntax.flatMap._ +import cats.syntax.functor._ +import io.micrometer.core.instrument.MeterRegistry +import org.http4s.metrics.{MetricsOps, TerminationType} +import org.http4s.{Method, Status} + +object MicrometerHttp4sMetricsOpsModule { + + /** Makes [[org.http4s.metrics.MetricsOps]] to record the usual HTTP server metrics. */ + def make[F[_]: Sync](meterRegistry: MeterRegistry): F[MetricsOps[F]] = { + val F = Sync[F] + + F.delay { + new MetricsOps[F] { + private val prefix = "http.global" + private val activeRequests = meterRegistry.counter(s"$prefix.active-requests") + private val headersTime = meterRegistry.timer(s"$prefix.headers-time") + private val totalTime = meterRegistry.timer(s"$prefix.total-time") + private val failureTime = meterRegistry.timer(s"$prefix.failure-time") + private val httpStatusCodes = new HttpStatusMetrics(prefix, meterRegistry) + + override def increaseActiveRequests(classifier: Option[String]): F[Unit] = F.delay(activeRequests.increment()) + + override def decreaseActiveRequests(classifier: Option[String]): F[Unit] = F.delay(activeRequests.increment(-1)) + + override def recordHeadersTime(method: Method, elapsed: Long, classifier: Option[String]): F[Unit] = { + F.delay(headersTime.record(elapsed, TimeUnit.NANOSECONDS)) + } + + override def recordTotalTime(method: Method, status: Status, elapsed: Long, classifier: Option[String]): F[Unit] = { + for { + _ <- F.delay(totalTime.record(elapsed, TimeUnit.NANOSECONDS)) + _ <- F.delay(httpStatusCodes.recordHttpStatus(status)) + } yield () + } + + override def recordAbnormalTermination(elapsed: Long, terminationType: TerminationType, classifier: Option[String]): F[Unit] = { + F.delay(failureTime.record(elapsed, TimeUnit.NANOSECONDS)) + } + + } + } + } + +} diff --git a/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sServerMetricsModule.scala b/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sServerMetricsModule.scala new file mode 100644 index 000000000..f2b895a19 --- /dev/null +++ b/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sServerMetricsModule.scala @@ -0,0 +1,27 @@ +package com.avast.sst.http4s.server.micrometer + +import cats.effect.{Clock, Effect, Sync} +import cats.syntax.flatMap._ +import cats.syntax.functor._ +import io.micrometer.core.instrument.MeterRegistry +import org.http4s.HttpRoutes +import org.http4s.server.middleware.Metrics + +import scala.language.higherKinds + +class MicrometerHttp4sServerMetricsModule[F[_]: Sync](val serverMetrics: HttpRoutes[F] => HttpRoutes[F], val routeMetrics: RouteMetrics[F]) + +object MicrometerHttp4sServerMetricsModule { + + /** Makes [[com.avast.sst.http4s.server.micrometer.MicrometerHttp4sServerMetricsModule]] that can be used to setup monitoring + * of the whole HTTP server and individual routes. */ + def make[F[_]: Effect](meterRegistry: MeterRegistry, clock: Clock[F]): F[MicrometerHttp4sServerMetricsModule[F]] = { + implicit val c: Clock[F] = clock + + for { + metricsOps <- MicrometerHttp4sMetricsOpsModule.make[F](meterRegistry) + routeMetrics <- Sync[F].delay(new RouteMetrics[F](meterRegistry, clock)) + } yield new MicrometerHttp4sServerMetricsModule[F](Metrics(metricsOps), routeMetrics) + } + +} diff --git a/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/RouteMetrics.scala b/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/RouteMetrics.scala new file mode 100644 index 000000000..f6aac4259 --- /dev/null +++ b/http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/RouteMetrics.scala @@ -0,0 +1,45 @@ +package com.avast.sst.http4s.server.micrometer + +import java.util.concurrent.TimeUnit + +import cats.effect.syntax.bracket._ +import cats.effect.{Clock, Sync} +import cats.syntax.flatMap._ +import cats.syntax.functor._ +import io.micrometer.core.instrument.MeterRegistry +import org.http4s.Response + +import scala.language.higherKinds + +/** Provides the usual metrics for single HTTP route. */ +class RouteMetrics[F[_]: Sync](meterRegistry: MeterRegistry, clock: Clock[F]) { + + private val F = Sync[F] + + /** Wraps a single route with the usual metrics (requests in-flight, timer, HTTP status counts). + * + * @param name will be used in metric name + */ + def wrap(name: String)(route: => F[Response[F]]): F[Response[F]] = { + val prefix = s"http.$name" + val activeRequests = meterRegistry.counter(s"$prefix.active-requests") + val timer = meterRegistry.timer(s"$prefix.total-time") + val httpStatusCodes = new HttpStatusMetrics(prefix, meterRegistry) + for { + start <- clock.monotonic(TimeUnit.NANOSECONDS) + response <- F.delay(activeRequests.increment()) + .bracket { _ => + route.flatTap(response => F.delay(httpStatusCodes.recordHttpStatus(response.status))) + } { _ => + for { + time <- computeTime(start) + _ <- F.delay(activeRequests.increment(-1)) + _ <- F.delay(timer.record(time, TimeUnit.NANOSECONDS)) + } yield () + } + } yield response + } + + private def computeTime(start: Long): F[Long] = clock.monotonic(TimeUnit.NANOSECONDS).map(_ - start) + +} diff --git a/http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/HttpStatusMetricsTest.scala b/http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/HttpStatusMetricsTest.scala new file mode 100644 index 000000000..251d09417 --- /dev/null +++ b/http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/HttpStatusMetricsTest.scala @@ -0,0 +1,25 @@ +package com.avast.sst.http4s.server.micrometer + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry +import org.http4s.Status +import org.scalatest.FunSuite + +class HttpStatusMetricsTest extends FunSuite { + + test("HTTP status monitoring") { + val simpleMeterRegistry = new SimpleMeterRegistry() + val target = new HttpStatusMetrics("test", simpleMeterRegistry) + + target.recordHttpStatus(Status.Ok) + target.recordHttpStatus(Status.NoContent) + target.recordHttpStatus(Status.BadRequest) + target.recordHttpStatus(Status.ServiceUnavailable) + + assert(simpleMeterRegistry.get("test.status.2xx").counter().count() === 2.0) + assert(simpleMeterRegistry.get("test.status.200").counter().count() === 1.0) + assert(simpleMeterRegistry.get("test.status.204").counter().count() === 1.0) + assert(simpleMeterRegistry.get("test.status.4xx").counter().count() === 1.0) + assert(simpleMeterRegistry.get("test.status.5xx").counter().count() === 1.0) + } + +} diff --git a/http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sMetricsOpsModuleTest.scala b/http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sMetricsOpsModuleTest.scala new file mode 100644 index 000000000..f57f84c2e --- /dev/null +++ b/http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sMetricsOpsModuleTest.scala @@ -0,0 +1,25 @@ +package com.avast.sst.http4s.server.micrometer + +import java.util.concurrent.TimeUnit + +import cats.effect.SyncIO +import io.micrometer.core.instrument.simple.SimpleMeterRegistry +import org.http4s.{Method, Status} +import org.scalatest.FunSuite + +class MicrometerHttp4sMetricsOpsModuleTest extends FunSuite { + + test("http4s MetricsOps for Micrometer") { + val registry = new SimpleMeterRegistry() + val metricsOps = MicrometerHttp4sMetricsOpsModule.make[SyncIO](registry).unsafeRunSync() + + metricsOps.increaseActiveRequests(None).unsafeRunSync() + metricsOps.recordTotalTime(Method.GET, Status.Ok, 2500, None).unsafeRunSync() + + assert(registry.get("http.global.active-requests").counter().count() === 1) + assert(registry.get("http.global.total-time").timer().count() === 1) + assert(registry.get("http.global.total-time").timer().totalTime(TimeUnit.NANOSECONDS) > 2499) + assert(registry.get("http.global.status.200").counter().count() === 1) + } + +} diff --git a/http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/RouteMetricsTest.scala b/http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/RouteMetricsTest.scala new file mode 100644 index 000000000..d5a003dfc --- /dev/null +++ b/http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/RouteMetricsTest.scala @@ -0,0 +1,23 @@ +package com.avast.sst.http4s.server.micrometer + +import java.util.concurrent.TimeUnit + +import cats.effect.{Clock, SyncIO} +import io.micrometer.core.instrument.simple.SimpleMeterRegistry +import org.http4s.Response +import org.scalatest.FunSuite + +class RouteMetricsTest extends FunSuite { + + test("Single route metrics") { + val registry = new SimpleMeterRegistry() + val target = new RouteMetrics[SyncIO](registry, Clock.create[SyncIO]) + + target.wrap("test")(SyncIO.pure(Response.notFound[SyncIO])).unsafeRunSync() + assert(registry.get("http.test.active-requests").counter().count() === 0) + assert(registry.get("http.test.total-time").timer().count() === 1) + assert(registry.get("http.test.total-time").timer().totalTime(TimeUnit.MILLISECONDS) > 0) + assert(registry.get("http.test.status.404").counter().count() === 1) + } + +} diff --git a/http4s-blaze-server/src/main/scala/com/avast/sst/http4s/Http4sRouting.scala b/http4s-server/src/main/scala/com/avast/sst/http4s/server/Http4sRouting.scala similarity index 92% rename from http4s-blaze-server/src/main/scala/com/avast/sst/http4s/Http4sRouting.scala rename to http4s-server/src/main/scala/com/avast/sst/http4s/server/Http4sRouting.scala index 8c04212af..23fc8902a 100644 --- a/http4s-blaze-server/src/main/scala/com/avast/sst/http4s/Http4sRouting.scala +++ b/http4s-server/src/main/scala/com/avast/sst/http4s/server/Http4sRouting.scala @@ -1,4 +1,4 @@ -package com.avast.sst.http4s +package com.avast.sst.http4s.server import cats.Monad import cats.syntax.all._ @@ -11,7 +11,6 @@ object Http4sRouting { /** Makes [[org.http4s.HttpApp]] from [[org.http4s.HttpRoutes]] */ def make[F[_]: Monad](routes: HttpRoutes[F], more: HttpRoutes[F]*): HttpApp[F] = { - more .foldLeft[HttpRoutes[F]](routes)(_.combineK(_)) .orNotFound diff --git a/http4s-blaze-server/src/main/scala/com/avast/sst/http4s/middleware/CorrelationIdMiddleware.scala b/http4s-server/src/main/scala/com/avast/sst/http4s/server/middleware/CorrelationIdMiddleware.scala similarity index 94% rename from http4s-blaze-server/src/main/scala/com/avast/sst/http4s/middleware/CorrelationIdMiddleware.scala rename to http4s-server/src/main/scala/com/avast/sst/http4s/server/middleware/CorrelationIdMiddleware.scala index f269e76a8..2b6bc1928 100644 --- a/http4s-blaze-server/src/main/scala/com/avast/sst/http4s/middleware/CorrelationIdMiddleware.scala +++ b/http4s-server/src/main/scala/com/avast/sst/http4s/server/middleware/CorrelationIdMiddleware.scala @@ -1,11 +1,11 @@ -package com.avast.sst.http4s.middleware +package com.avast.sst.http4s.server.middleware import java.util.UUID import cats.data.{Kleisli, OptionT} import cats.effect.Sync import cats.syntax.functor._ -import com.avast.sst.http4s.middleware.CorrelationIdMiddleware.CorrelationId +import com.avast.sst.http4s.server.middleware.CorrelationIdMiddleware.CorrelationId import io.chrisdavenport.vault.Key import org.http4s.util.CaseInsensitiveString import org.http4s.{Header, HttpRoutes, Request, Response} diff --git a/http4s-blaze-server/src/test/scala/com/avast/sst/http4s/middleware/CorrelationIdMiddlewareTest.scala b/http4s-server/src/test/scala/com/avast/sst/http4s/server/middleware/CorrelationIdMiddlewareTest.scala similarity index 75% rename from http4s-blaze-server/src/test/scala/com/avast/sst/http4s/middleware/CorrelationIdMiddlewareTest.scala rename to http4s-server/src/test/scala/com/avast/sst/http4s/server/middleware/CorrelationIdMiddlewareTest.scala index cec929e87..a69a21ecc 100644 --- a/http4s-blaze-server/src/test/scala/com/avast/sst/http4s/middleware/CorrelationIdMiddlewareTest.scala +++ b/http4s-server/src/test/scala/com/avast/sst/http4s/server/middleware/CorrelationIdMiddlewareTest.scala @@ -1,8 +1,12 @@ -package com.avast.sst.http4s.middleware +package com.avast.sst.http4s.server.middleware + +import java.net.InetSocketAddress import cats.effect.{ContextShift, IO, Resource, Timer} -import com.avast.sst.http4s.{Http4sBlazeClient, Http4sBlazeClientConfig, Http4sBlazeServerConfig, Http4sBlazeServerModule, Http4sRouting} +import com.avast.sst.http4s.server.Http4sRouting +import org.http4s.client.blaze.BlazeClientBuilder import org.http4s.dsl.Http4sDsl +import org.http4s.server.blaze.BlazeServerBuilder import org.http4s.util.CaseInsensitiveString import org.http4s.{Header, HttpRoutes, Request, Uri} import org.scalatest.AsyncFunSuite @@ -26,8 +30,12 @@ class CorrelationIdMiddlewareTest extends AsyncFunSuite with Http4sDsl[IO] { } } } - server <- Http4sBlazeServerModule.make[IO](Http4sBlazeServerConfig("127.0.0.1", 0), routes, ExecutionContext.global) - client <- Http4sBlazeClient.make[IO](Http4sBlazeClientConfig(), ExecutionContext.global) + server <- BlazeServerBuilder[IO] + .bindSocketAddress(InetSocketAddress.createUnresolved("127.0.0.1", 0)) + .withExecutionContext(ExecutionContext.global) + .withHttpApp(routes) + .resource + client <- BlazeClientBuilder[IO](ExecutionContext.global).resource } yield (server, client) test diff --git a/jvm-micrometer/src/main/scala/com/avast/sst/jvm/micrometer/MicrometerJvmModule.scala b/jvm-micrometer/src/main/scala/com/avast/sst/jvm/micrometer/MicrometerJvmModule.scala new file mode 100644 index 000000000..2a948c04e --- /dev/null +++ b/jvm-micrometer/src/main/scala/com/avast/sst/jvm/micrometer/MicrometerJvmModule.scala @@ -0,0 +1,21 @@ +package com.avast.sst.jvm.micrometer + +import cats.effect.Sync +import io.micrometer.core.instrument.MeterRegistry +import io.micrometer.core.instrument.binder.jvm.{ClassLoaderMetrics, JvmGcMetrics, JvmMemoryMetrics, JvmThreadMetrics} +import io.micrometer.core.instrument.binder.system.ProcessorMetrics + +object MicrometerJvmModule { + + /** Sets up publishing of JVM metrics (class loading, GC, memory, CPU, ...) into the given [[io.micrometer.core.instrument.MeterRegistry]] */ + def make[F[_]: Sync](registry: MeterRegistry): F[Unit] = { + Sync[F].delay { + new ClassLoaderMetrics().bindTo(registry) + new JvmMemoryMetrics().bindTo(registry) + new JvmGcMetrics().bindTo(registry) + new ProcessorMetrics().bindTo(registry) + new JvmThreadMetrics().bindTo(registry) + } + } + +} diff --git a/jvm-pureconfig/src/main/scala/com/avast/sst/jvm/pureconfig/ConfigReaders.scala b/jvm-pureconfig/src/main/scala/com/avast/sst/jvm/pureconfig/ConfigReaders.scala new file mode 100644 index 000000000..22dba38de --- /dev/null +++ b/jvm-pureconfig/src/main/scala/com/avast/sst/jvm/pureconfig/ConfigReaders.scala @@ -0,0 +1,25 @@ +package com.avast.sst.jvm.pureconfig + +import com.avast.sst.jvm.execution.ForkJoinPoolConfig.TaskPeekingMode +import com.avast.sst.jvm.execution.{ForkJoinPoolConfig, ThreadPoolExecutorConfig} +import com.avast.sst.jvm.ssl.{KeyStoreConfig, KeyStoreType, Protocol, SslContextConfig} +import pureconfig.ConfigReader +import pureconfig.generic.semiauto.{deriveEnumerationReader, deriveReader} + +trait ConfigReaders { + + implicit val threadPoolExecutorConfigReader: ConfigReader[ThreadPoolExecutorConfig] = deriveReader + + implicit val taskPeekingModeReader: ConfigReader[TaskPeekingMode] = deriveEnumerationReader + + implicit val forkJoinPoolConfigReader: ConfigReader[ForkJoinPoolConfig] = deriveReader + + implicit val sslProtocolReader: ConfigReader[Protocol] = deriveEnumerationReader + + implicit val keyStoreTypeReader: ConfigReader[KeyStoreType] = deriveEnumerationReader + + implicit val keyStoreConfigReader: ConfigReader[KeyStoreConfig] = deriveReader + + implicit val sslContextConfigReader: ConfigReader[SslContextConfig] = deriveReader + +} diff --git a/jvm-pureconfig/src/main/scala/com/avast/sst/jvm/pureconfig/implicits.scala b/jvm-pureconfig/src/main/scala/com/avast/sst/jvm/pureconfig/implicits.scala new file mode 100644 index 000000000..22982ce30 --- /dev/null +++ b/jvm-pureconfig/src/main/scala/com/avast/sst/jvm/pureconfig/implicits.scala @@ -0,0 +1,3 @@ +package com.avast.sst.jvm.pureconfig + +object implicits extends ConfigReaders diff --git a/jvm-execution/src/main/scala/com/avast/sst/execution/ConfigurableThreadFactory.scala b/jvm/src/main/scala/com/avast/sst/jvm/execution/ConfigurableThreadFactory.scala similarity index 90% rename from jvm-execution/src/main/scala/com/avast/sst/execution/ConfigurableThreadFactory.scala rename to jvm/src/main/scala/com/avast/sst/jvm/execution/ConfigurableThreadFactory.scala index fb8d28611..b6a460c52 100644 --- a/jvm-execution/src/main/scala/com/avast/sst/execution/ConfigurableThreadFactory.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/execution/ConfigurableThreadFactory.scala @@ -1,14 +1,14 @@ -package com.avast.sst.execution +package com.avast.sst.jvm.execution import java.lang.Thread.UncaughtExceptionHandler import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.{ForkJoinPool, ForkJoinWorkerThread, ThreadFactory} -import com.avast.sst.execution.ConfigurableThreadFactory.Config +import com.avast.sst.jvm.execution.ConfigurableThreadFactory.Config /** Thread factory (both [[java.util.concurrent.ThreadFactory]] and [[java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory]]) - * that creates new threads according to the provided [[com.avast.sst.execution.ConfigurableThreadFactory.Config]]. + * that creates new threads according to the provided [[com.avast.sst.jvm.execution.ConfigurableThreadFactory.Config]]. */ class ConfigurableThreadFactory(config: Config) extends ThreadFactory with ForkJoinWorkerThreadFactory { diff --git a/jvm-execution/src/main/scala/com/avast/sst/execution/ExecutorModule.scala b/jvm/src/main/scala/com/avast/sst/jvm/execution/ExecutorModule.scala similarity index 90% rename from jvm-execution/src/main/scala/com/avast/sst/execution/ExecutorModule.scala rename to jvm/src/main/scala/com/avast/sst/jvm/execution/ExecutorModule.scala index 37f2c7deb..f695f05e2 100644 --- a/jvm-execution/src/main/scala/com/avast/sst/execution/ExecutorModule.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/execution/ExecutorModule.scala @@ -1,4 +1,4 @@ -package com.avast.sst.execution +package com.avast.sst.jvm.execution import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory import java.util.concurrent.{ @@ -13,7 +13,7 @@ import java.util.concurrent.{ } import cats.effect.{Blocker, Resource, Sync} -import com.avast.sst.execution.ConfigurableThreadFactory.Config +import com.avast.sst.jvm.execution.ConfigurableThreadFactory.Config import scala.concurrent.{ExecutionContext, ExecutionContextExecutorService} import scala.language.higherKinds @@ -53,7 +53,7 @@ class ExecutorModule[F[_]: Sync](val numOfCpus: Int, object ExecutorModule { - /** Makes [[com.avast.sst.execution.ExecutorModule]] with default callback executor and extra [[cats.effect.Blocker]] executor + /** Makes [[com.avast.sst.jvm.execution.ExecutorModule]] with default callback executor and extra [[cats.effect.Blocker]] executor * for blocking operations. */ def makeDefault[F[_]: Sync]: Resource[F, ExecutorModule[F]] = { @@ -66,7 +66,7 @@ object ExecutorModule { } yield new ExecutorModule[F](numOfCpus, executor, blockingExecutor) } - /** Makes [[com.avast.sst.execution.ExecutorModule]] with the provided callback executor and extra [[cats.effect.Blocker]] + /** Makes [[com.avast.sst.jvm.execution.ExecutorModule]] with the provided callback executor and extra [[cats.effect.Blocker]] * executor for blocking operations. */ def makeFromExecutionContext[F[_]: Sync](executor: ExecutionContext): Resource[F, ExecutorModule[F]] = { @@ -76,7 +76,7 @@ object ExecutorModule { } yield new ExecutorModule[F](numOfCpus, executor, blockingExecutor) } - /** Makes [[com.avast.sst.execution.ExecutorModule]] with executor and extra [[cats.effect.Blocker]] executor + /** Makes [[com.avast.sst.jvm.execution.ExecutorModule]] with executor and extra [[cats.effect.Blocker]] executor * for blocking operations. */ def makeFromConfig[F[_]: Sync](executorConfig: ThreadPoolExecutorConfig): Resource[F, ExecutorModule[F]] = { @@ -88,7 +88,7 @@ object ExecutorModule { } yield new ExecutorModule[F](numOfCpus, executor, blockingExecutor) } - /** Makes [[com.avast.sst.execution.ExecutorModule]] with fork-join executor and extra [[cats.effect.Blocker]] executor + /** Makes [[com.avast.sst.jvm.execution.ExecutorModule]] with fork-join executor and extra [[cats.effect.Blocker]] executor * for blocking operations. */ def makeForkJoinFromConfig[F[_]: Sync](executorConfig: ForkJoinPoolConfig): Resource[F, ExecutorModule[F]] = { diff --git a/jvm-execution/src/main/scala/com/avast/sst/execution/ForkJoinPoolConfig.scala b/jvm/src/main/scala/com/avast/sst/jvm/execution/ForkJoinPoolConfig.scala similarity index 81% rename from jvm-execution/src/main/scala/com/avast/sst/execution/ForkJoinPoolConfig.scala rename to jvm/src/main/scala/com/avast/sst/jvm/execution/ForkJoinPoolConfig.scala index bef556bf7..bed4135cc 100644 --- a/jvm-execution/src/main/scala/com/avast/sst/execution/ForkJoinPoolConfig.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/execution/ForkJoinPoolConfig.scala @@ -1,7 +1,7 @@ -package com.avast.sst.execution +package com.avast.sst.jvm.execution -import com.avast.sst.execution.ForkJoinPoolConfig.TaskPeekingMode -import com.avast.sst.execution.ForkJoinPoolConfig.TaskPeekingMode.{FIFO, LIFO} +import com.avast.sst.jvm.execution.ForkJoinPoolConfig.TaskPeekingMode +import com.avast.sst.jvm.execution.ForkJoinPoolConfig.TaskPeekingMode.{FIFO, LIFO} final case class ForkJoinPoolConfig(parallelismMin: Int = 8, parallelismFactor: Double = 1.0, diff --git a/jvm-execution/src/main/scala/com/avast/sst/execution/LoggingUncaughtExceptionHandler.scala b/jvm/src/main/scala/com/avast/sst/jvm/execution/LoggingUncaughtExceptionHandler.scala similarity index 91% rename from jvm-execution/src/main/scala/com/avast/sst/execution/LoggingUncaughtExceptionHandler.scala rename to jvm/src/main/scala/com/avast/sst/jvm/execution/LoggingUncaughtExceptionHandler.scala index f83e2bc4d..5b7313698 100644 --- a/jvm-execution/src/main/scala/com/avast/sst/execution/LoggingUncaughtExceptionHandler.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/execution/LoggingUncaughtExceptionHandler.scala @@ -1,4 +1,4 @@ -package com.avast.sst.execution +package com.avast.sst.jvm.execution import java.lang.Thread.UncaughtExceptionHandler diff --git a/jvm-execution/src/main/scala/com/avast/sst/execution/ThreadPoolExecutorConfig.scala b/jvm/src/main/scala/com/avast/sst/jvm/execution/ThreadPoolExecutorConfig.scala similarity index 91% rename from jvm-execution/src/main/scala/com/avast/sst/execution/ThreadPoolExecutorConfig.scala rename to jvm/src/main/scala/com/avast/sst/jvm/execution/ThreadPoolExecutorConfig.scala index 891eb1d8c..9e8ebc7aa 100644 --- a/jvm-execution/src/main/scala/com/avast/sst/execution/ThreadPoolExecutorConfig.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/execution/ThreadPoolExecutorConfig.scala @@ -1,4 +1,4 @@ -package com.avast.sst.execution +package com.avast.sst.jvm.execution import java.util.concurrent.TimeUnit diff --git a/jvm-ssl/src/main/scala/com/avast/sst/ssl/KeyStoreConfig.scala b/jvm/src/main/scala/com/avast/sst/jvm/ssl/KeyStoreConfig.scala similarity index 83% rename from jvm-ssl/src/main/scala/com/avast/sst/ssl/KeyStoreConfig.scala rename to jvm/src/main/scala/com/avast/sst/jvm/ssl/KeyStoreConfig.scala index 1b293100e..e9db0c18c 100644 --- a/jvm-ssl/src/main/scala/com/avast/sst/ssl/KeyStoreConfig.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/ssl/KeyStoreConfig.scala @@ -1,4 +1,4 @@ -package com.avast.sst.ssl +package com.avast.sst.jvm.ssl import java.nio.file.Path diff --git a/jvm-ssl/src/main/scala/com/avast/sst/ssl/KeyStoreType.scala b/jvm/src/main/scala/com/avast/sst/jvm/ssl/KeyStoreType.scala similarity index 89% rename from jvm-ssl/src/main/scala/com/avast/sst/ssl/KeyStoreType.scala rename to jvm/src/main/scala/com/avast/sst/jvm/ssl/KeyStoreType.scala index a2e8de779..b6a0f91f0 100644 --- a/jvm-ssl/src/main/scala/com/avast/sst/ssl/KeyStoreType.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/ssl/KeyStoreType.scala @@ -1,4 +1,4 @@ -package com.avast.sst.ssl +package com.avast.sst.jvm.ssl import cats.Show diff --git a/jvm-ssl/src/main/scala/com/avast/sst/ssl/Protocol.scala b/jvm/src/main/scala/com/avast/sst/jvm/ssl/Protocol.scala similarity index 88% rename from jvm-ssl/src/main/scala/com/avast/sst/ssl/Protocol.scala rename to jvm/src/main/scala/com/avast/sst/jvm/ssl/Protocol.scala index 751fd28ee..748a8b683 100644 --- a/jvm-ssl/src/main/scala/com/avast/sst/ssl/Protocol.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/ssl/Protocol.scala @@ -1,4 +1,4 @@ -package com.avast.sst.ssl +package com.avast.sst.jvm.ssl import cats.Show diff --git a/jvm-ssl/src/main/scala/com/avast/sst/ssl/SslContextConfig.scala b/jvm/src/main/scala/com/avast/sst/jvm/ssl/SslContextConfig.scala similarity index 76% rename from jvm-ssl/src/main/scala/com/avast/sst/ssl/SslContextConfig.scala rename to jvm/src/main/scala/com/avast/sst/jvm/ssl/SslContextConfig.scala index 7d8525162..73c685418 100644 --- a/jvm-ssl/src/main/scala/com/avast/sst/ssl/SslContextConfig.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/ssl/SslContextConfig.scala @@ -1,7 +1,7 @@ -package com.avast.sst.ssl +package com.avast.sst.jvm.ssl -import com.avast.sst.ssl.Protocol.TLS -import com.avast.sst.ssl.Protocol.TLS +import com.avast.sst.jvm.ssl.Protocol.TLS +import com.avast.sst.jvm.ssl.Protocol.TLS final case class SslContextConfig(protocol: Protocol = TLS, keystore: Option[KeyStoreConfig] = None, diff --git a/jvm-ssl/src/main/scala/com/avast/sst/ssl/SslContextModule.scala b/jvm/src/main/scala/com/avast/sst/jvm/ssl/SslContextModule.scala similarity index 96% rename from jvm-ssl/src/main/scala/com/avast/sst/ssl/SslContextModule.scala rename to jvm/src/main/scala/com/avast/sst/jvm/ssl/SslContextModule.scala index fa5ee319f..02dfbcad4 100644 --- a/jvm-ssl/src/main/scala/com/avast/sst/ssl/SslContextModule.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/ssl/SslContextModule.scala @@ -1,4 +1,4 @@ -package com.avast.sst.ssl +package com.avast.sst.jvm.ssl import java.nio.file.Files import java.security.KeyStore @@ -11,7 +11,7 @@ import javax.net.ssl.{KeyManager, KeyManagerFactory, SSLContext, TrustManager, T import scala.language.higherKinds -@SuppressWarnings(Array("org.wartremover.warts.Null")) +@SuppressWarnings(Array("org.wartremover.warts.Null")) // JVM-interop code that must use nulls object SslContextModule { /** Loads [[javax.net.ssl.SSLContext]] and fills it with key/trust managers from the provided config. */ diff --git a/jvm-system/src/main/scala/com/avast/sst/system/console/Console.scala b/jvm/src/main/scala/com/avast/sst/jvm/system/console/Console.scala similarity index 95% rename from jvm-system/src/main/scala/com/avast/sst/system/console/Console.scala rename to jvm/src/main/scala/com/avast/sst/jvm/system/console/Console.scala index b1a8c79c9..323c5dc17 100644 --- a/jvm-system/src/main/scala/com/avast/sst/system/console/Console.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/system/console/Console.scala @@ -1,4 +1,4 @@ -package com.avast.sst.system.console +package com.avast.sst.jvm.system.console import java.io.{OutputStream, Reader} diff --git a/jvm-system/src/main/scala/com/avast/sst/system/console/ConsoleModule.scala b/jvm/src/main/scala/com/avast/sst/jvm/system/console/ConsoleModule.scala similarity index 66% rename from jvm-system/src/main/scala/com/avast/sst/system/console/ConsoleModule.scala rename to jvm/src/main/scala/com/avast/sst/jvm/system/console/ConsoleModule.scala index c5b92428f..ef5f298f7 100644 --- a/jvm-system/src/main/scala/com/avast/sst/system/console/ConsoleModule.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/system/console/ConsoleModule.scala @@ -1,4 +1,4 @@ -package com.avast.sst.system.console +package com.avast.sst.jvm.system.console import cats.effect.Sync @@ -8,7 +8,7 @@ import scala.{Console => SConsole} /** Provides console - standard in/out/err. */ object ConsoleModule { - /** Makes [[com.avast.sst.system.console.Console]] with standard in/out/err. */ + /** Makes [[com.avast.sst.jvm.system.console.Console]] with standard in/out/err. */ def make[F[_]: Sync]: Console[F] = Console(SConsole.in, SConsole.out, SConsole.err) } diff --git a/jvm-system/src/main/scala/com/avast/sst/system/random/Random.scala b/jvm/src/main/scala/com/avast/sst/jvm/system/random/Random.scala similarity index 96% rename from jvm-system/src/main/scala/com/avast/sst/system/random/Random.scala rename to jvm/src/main/scala/com/avast/sst/jvm/system/random/Random.scala index c073e22e7..caad843b6 100644 --- a/jvm-system/src/main/scala/com/avast/sst/system/random/Random.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/system/random/Random.scala @@ -1,4 +1,4 @@ -package com.avast.sst.system.random +package com.avast.sst.jvm.system.random import cats.effect.Sync diff --git a/jvm-system/src/main/scala/com/avast/sst/system/random/RandomModule.scala b/jvm/src/main/scala/com/avast/sst/jvm/system/random/RandomModule.scala similarity index 55% rename from jvm-system/src/main/scala/com/avast/sst/system/random/RandomModule.scala rename to jvm/src/main/scala/com/avast/sst/jvm/system/random/RandomModule.scala index fc32378bd..9c2ee25c6 100644 --- a/jvm-system/src/main/scala/com/avast/sst/system/random/RandomModule.scala +++ b/jvm/src/main/scala/com/avast/sst/jvm/system/random/RandomModule.scala @@ -1,4 +1,4 @@ -package com.avast.sst.system.random +package com.avast.sst.jvm.system.random import java.security.SecureRandom @@ -9,16 +9,16 @@ import scala.language.higherKinds /** Provides random number generators. */ object RandomModule { - /** Makes [[com.avast.sst.system.random.Random]] with default random seed. */ + /** Makes [[com.avast.sst.jvm.system.random.Random]] with default random seed. */ def makeRandom[F[_]: Sync]: F[Random[F]] = Sync[F].delay(Random(new scala.util.Random())) - /** Makes [[com.avast.sst.system.random.Random]] with the provided `seed`. */ + /** Makes [[com.avast.sst.jvm.system.random.Random]] with the provided `seed`. */ def makeRandom[F[_]: Sync](seed: Long): F[Random[F]] = Sync[F].delay(Random(new scala.util.Random(seed))) - /** Makes [[com.avast.sst.system.random.Random]] based on [[java.security.SecureRandom]] with default random seed. */ + /** Makes [[com.avast.sst.jvm.system.random.Random]] based on [[java.security.SecureRandom]] with default random seed. */ def makeSecureRandom[F[_]: Sync]: F[Random[F]] = Sync[F].delay(Random(new SecureRandom())) - /** Makes [[com.avast.sst.system.random.Random]] based on [[java.security.SecureRandom]] with the provided `seed`. */ + /** Makes [[com.avast.sst.jvm.system.random.Random]] based on [[java.security.SecureRandom]] with the provided `seed`. */ def makeSecureRandom[F[_]: Sync](seed: Array[Byte]): F[Random[F]] = Sync[F].delay(Random(new SecureRandom(seed))) } diff --git a/jvm-ssl/src/test/resources/truststore.jks b/jvm/src/test/resources/truststore.jks similarity index 100% rename from jvm-ssl/src/test/resources/truststore.jks rename to jvm/src/test/resources/truststore.jks diff --git a/jvm-execution/src/test/scala/com/avast/sst/execution/ExecutorModuleTest.scala b/jvm/src/test/scala/com/avast/sst/jvm/execution/ExecutorModuleTest.scala similarity index 91% rename from jvm-execution/src/test/scala/com/avast/sst/execution/ExecutorModuleTest.scala rename to jvm/src/test/scala/com/avast/sst/jvm/execution/ExecutorModuleTest.scala index 1815940ed..81db843ff 100644 --- a/jvm-execution/src/test/scala/com/avast/sst/execution/ExecutorModuleTest.scala +++ b/jvm/src/test/scala/com/avast/sst/jvm/execution/ExecutorModuleTest.scala @@ -1,4 +1,4 @@ -package com.avast.sst.execution +package com.avast.sst.jvm.execution import cats.effect.SyncIO import org.scalatest.FunSuite diff --git a/jvm-ssl/src/test/scala/com/avast/sst/ssl/SslContextModuleTest.scala b/jvm/src/test/scala/com/avast/sst/jvm/ssl/SslContextModuleTest.scala similarity index 94% rename from jvm-ssl/src/test/scala/com/avast/sst/ssl/SslContextModuleTest.scala rename to jvm/src/test/scala/com/avast/sst/jvm/ssl/SslContextModuleTest.scala index b1bd8f358..bf7b2ad26 100644 --- a/jvm-ssl/src/test/scala/com/avast/sst/ssl/SslContextModuleTest.scala +++ b/jvm/src/test/scala/com/avast/sst/jvm/ssl/SslContextModuleTest.scala @@ -1,4 +1,4 @@ -package com.avast.sst.ssl +package com.avast.sst.jvm.ssl import java.nio.file.Paths diff --git a/jvm-system/src/test/scala/com/avast/sst/system/console/ConsoleModuleTest.scala b/jvm/src/test/scala/com/avast/sst/jvm/system/console/ConsoleModuleTest.scala similarity index 96% rename from jvm-system/src/test/scala/com/avast/sst/system/console/ConsoleModuleTest.scala rename to jvm/src/test/scala/com/avast/sst/jvm/system/console/ConsoleModuleTest.scala index 15bb65b1c..4a2e2a890 100644 --- a/jvm-system/src/test/scala/com/avast/sst/system/console/ConsoleModuleTest.scala +++ b/jvm/src/test/scala/com/avast/sst/jvm/system/console/ConsoleModuleTest.scala @@ -1,4 +1,4 @@ -package com.avast.sst.system.console +package com.avast.sst.jvm.system.console import java.io.{ByteArrayInputStream, ByteArrayOutputStream} diff --git a/jvm-system/src/test/scala/com/avast/sst/system/random/RandomModuleTest.scala b/jvm/src/test/scala/com/avast/sst/jvm/system/random/RandomModuleTest.scala similarity index 89% rename from jvm-system/src/test/scala/com/avast/sst/system/random/RandomModuleTest.scala rename to jvm/src/test/scala/com/avast/sst/jvm/system/random/RandomModuleTest.scala index 31e98c834..e08424fd9 100644 --- a/jvm-system/src/test/scala/com/avast/sst/system/random/RandomModuleTest.scala +++ b/jvm/src/test/scala/com/avast/sst/jvm/system/random/RandomModuleTest.scala @@ -1,4 +1,4 @@ -package com.avast.sst.system.random +package com.avast.sst.jvm.system.random import cats.effect.SyncIO import org.scalatest.FunSuite diff --git a/micrometer-jmx-pureconfig/src/main/scala/com/avast/sst/micrometer/jmx/pureconfig/ConfigReaders.scala b/micrometer-jmx-pureconfig/src/main/scala/com/avast/sst/micrometer/jmx/pureconfig/ConfigReaders.scala new file mode 100644 index 000000000..b3d7839ed --- /dev/null +++ b/micrometer-jmx-pureconfig/src/main/scala/com/avast/sst/micrometer/jmx/pureconfig/ConfigReaders.scala @@ -0,0 +1,11 @@ +package com.avast.sst.micrometer.jmx.pureconfig + +import com.avast.sst.micrometer.jmx.MicrometerJmxConfig +import pureconfig.ConfigReader +import pureconfig.generic.semiauto.deriveReader + +trait ConfigReaders { + + implicit val micrometerJmxConfigReader: ConfigReader[MicrometerJmxConfig] = deriveReader + +} diff --git a/micrometer-jmx-pureconfig/src/main/scala/com/avast/sst/micrometer/jmx/pureconfig/implicits.scala b/micrometer-jmx-pureconfig/src/main/scala/com/avast/sst/micrometer/jmx/pureconfig/implicits.scala new file mode 100644 index 000000000..c987f96e3 --- /dev/null +++ b/micrometer-jmx-pureconfig/src/main/scala/com/avast/sst/micrometer/jmx/pureconfig/implicits.scala @@ -0,0 +1,3 @@ +package com.avast.sst.micrometer.jmx.pureconfig + +object implicits extends ConfigReaders diff --git a/micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/MicrometerJmxConfig.scala b/micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/MicrometerJmxConfig.scala new file mode 100644 index 000000000..cfcccb0bc --- /dev/null +++ b/micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/MicrometerJmxConfig.scala @@ -0,0 +1,3 @@ +package com.avast.sst.micrometer.jmx + +final case class MicrometerJmxConfig(domain: String, enableTypeScopeNameHierarchy: Boolean = false) diff --git a/micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/MicrometerJmxModule.scala b/micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/MicrometerJmxModule.scala new file mode 100644 index 000000000..3ecd145c8 --- /dev/null +++ b/micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/MicrometerJmxModule.scala @@ -0,0 +1,55 @@ +package com.avast.sst.micrometer.jmx + +import cats.effect.{Resource, Sync} +import com.codahale.metrics.MetricRegistry +import com.codahale.metrics.jmx.JmxReporter +import io.micrometer.core.instrument.Clock +import io.micrometer.core.instrument.config.NamingConvention +import io.micrometer.core.instrument.util.HierarchicalNameMapper +import io.micrometer.jmx.{JmxConfig, JmxMeterRegistry} + +import scala.language.higherKinds + +object MicrometerJmxModule { + + /** Makes configured [[io.micrometer.jmx.JmxMeterRegistry]]. */ + @SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements")) + def make[F[_]: Sync](config: MicrometerJmxConfig): Resource[F, JmxMeterRegistry] = { + Resource + .make { + Sync[F].delay { + if (config.enableTypeScopeNameHierarchy) { + val dropwizardRegistry = new MetricRegistry + val registry = new JmxMeterRegistry( + new DomainJmxConfig(config.domain), + Clock.SYSTEM, + HierarchicalNameMapper.DEFAULT, + dropwizardRegistry, + makeJmxReporter(dropwizardRegistry, config.domain) + ) + registry.config.namingConvention(NamingConvention.dot) + registry + } else { + new JmxMeterRegistry(new DomainJmxConfig(config.domain), Clock.SYSTEM) + } + } + }(registry => Sync[F].delay(registry.close())) + } + + private def makeJmxReporter(metricRegistry: MetricRegistry, domain: String) = { + JmxReporter + .forRegistry(metricRegistry) + .inDomain(domain) + .createsObjectNamesWith(new TypeScopeNameObjectNameFactory()) + .build + } + + private class DomainJmxConfig(override val domain: String) extends JmxConfig { + + // implements MeterRegistryConfig.get which can return null according to JavaDoc and @Nullable annotation + @SuppressWarnings(Array("org.wartremover.warts.Null")) + override def get(key: String): String = null + + } + +} diff --git a/micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/TypeScopeNameObjectNameFactory.scala b/micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/TypeScopeNameObjectNameFactory.scala new file mode 100644 index 000000000..7e7169b18 --- /dev/null +++ b/micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/TypeScopeNameObjectNameFactory.scala @@ -0,0 +1,50 @@ +package com.avast.sst.micrometer.jmx + +import java.util +import java.util.regex.Pattern + +import cats.syntax.either._ +import com.codahale.metrics.jmx.{DefaultObjectNameFactory, ObjectNameFactory} +import javax.management.ObjectName; + +/** This is custom [[com.codahale.metrics.jmx.ObjectNameFactory]] which uses "type-scope-name" hierarchy of resulting + * [[javax.management.ObjectName]] (levels 3-N are glued together). + */ +private[jmx] class TypeScopeNameObjectNameFactory(separator: String = ".") extends ObjectNameFactory { + + private val quotedSeparator = Pattern.quote(separator) + + private val defaultFactory = new DefaultObjectNameFactory() + + private val partNames = Vector("type", "scope", "name") + + override def createName(`type`: String, domain: String, name: String): ObjectName = { + val parsedName = parseName(domain, name) + parsedName.getOrElse(defaultFactory.createName(`type`, domain, name)) + } + + @SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements")) + private def parseName(domain: String, name: String) = Either.catchNonFatal { + val parts = name.split(quotedSeparator, partNames.length) + + /* The following block of code is a little hack. The problem is that ObjectName requires HashTable as parameter but HashTable + is unsorted and thus unusable for us. We hack it by raping the HashTable and in-fact using LinkedHashMap which is + much more suitable for our needs. */ + val map = new java.util.LinkedHashMap[String, String](parts.length) + val properties = new java.util.Hashtable[String, String](parts.length) { + override def entrySet(): util.Set[util.Map.Entry[String, String]] = map.entrySet() + } + + parts.zip(partNames).foreach { + case (part, partName) => + val quoted = quote(part) + properties.put(partName, quoted) + map.put(partName, quoted) + } + + new ObjectName(domain, properties) + } + + private def quote(objectName: String) = objectName.replaceAll("[\\Q.?*\"\\E]", "_") + +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c85ea0c49..514d7c174 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -6,8 +6,13 @@ object Dependencies { val http4sBlazeClient = "org.http4s" %% "http4s-blaze-client" % Versions.http4s val http4sBlazeServer = "org.http4s" %% "http4s-blaze-server" % Versions.http4s val http4sDsl = "org.http4s" %% "http4s-dsl" % Versions.http4s + val http4sServer = "org.http4s" %% "http4s-server" % Versions.http4s + val jsr305 = "com.google.code.findbugs" % "jsr305" % "3.0.2" val kindProjector = "org.typelevel" %% "kind-projector" % "0.10.3" val logbackClassic = "ch.qos.logback" % "logback-classic" % "1.2.3" + val micrometerCore = "io.micrometer" % "micrometer-core" % Versions.micrometer + val micrometerJmx = "io.micrometer" % "micrometer-registry-jmx" % Versions.micrometer + val monixEval = "io.monix" %% "monix-eval" % "3.0.0" val pureConfig = "com.github.pureconfig" %% "pureconfig" % "0.12.1" val scalaTest = "org.scalatest" %% "scalatest" % "3.0.8" val scalazzi = "com.github.vovapolu" %% "scaluzzi" % "0.1.3" @@ -20,6 +25,7 @@ object Dependencies { object Versions { val http4s = "0.20.11" + val micrometer = "1.3.0" val silencer = "1.4.4" }