Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add metrics endpoint (DEV-1555) #2331

Merged
merged 9 commits into from Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sbt
Expand Up @@ -232,7 +232,7 @@ lazy val webapi: Project = Project(id = "webapi", base = file("webapi"))
dockerUpdateLatest := true,
dockerBaseImage := "eclipse-temurin:17-jre-focal",
Docker / maintainer := "support@dasch.swiss",
Docker / dockerExposedPorts ++= Seq(3333),
Docker / dockerExposedPorts ++= Seq(3333, 3339),
Docker / defaultLinuxInstallLocation := "/opt/docker",
// use filterNot to return all items that do NOT meet the criteria
dockerCommands := dockerCommands.value.filterNot {
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Expand Up @@ -53,6 +53,7 @@ services:
image: daschswiss/knora-api:latest
ports:
- "3333:3333"
- "3339:3339"
volumes:
- /tmp:/tmp
networks:
Expand Down
21 changes: 11 additions & 10 deletions webapi/src/it/scala/org/knora/webapi/CoreSpec.scala
Expand Up @@ -9,21 +9,21 @@ import akka.actor
import akka.testkit.ImplicitSender
import akka.testkit.TestKitBase
import com.typesafe.scalalogging.Logger
import org.scalatest.BeforeAndAfterAll
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import zio._
import zio.logging.backend.SLF4J

import scala.concurrent.ExecutionContext

import org.knora.webapi.config.AppConfig
import org.knora.webapi.core.AppRouter
import org.knora.webapi.core.AppServer
import org.knora.webapi.core.TestStartupUtils
import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject
import org.knora.webapi.messages.util.ResponderData
import org.knora.webapi.store.cache.settings.CacheServiceSettings
import org.knora.webapi.util.LogAspect
import org.scalatest.BeforeAndAfterAll
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import zio._
import zio.logging.backend.SLF4J

import scala.concurrent.ExecutionContext

abstract class CoreSpec
extends AnyWordSpec
Expand Down Expand Up @@ -71,7 +71,7 @@ abstract class CoreSpec
/**
* Create router and config by unsafe running them.
*/
private val (router, config) =
private val (router: AppRouter, config: AppConfig) =
Unsafe.unsafe { implicit u =>
runtime.unsafe
.run(
Expand All @@ -97,8 +97,9 @@ abstract class CoreSpec
runtime.unsafe
.run(
(for {
_ <- AppServer.testWithoutSipi
_ <- prepareRepository(rdfDataObjects) @@ LogAspect.logSpan("prepare-repo")
} yield ()).provideSomeLayer(AppServer.testWithoutSipi)
} yield ())
)
.getOrThrow()

Expand Down
9 changes: 5 additions & 4 deletions webapi/src/it/scala/org/knora/webapi/E2ESpec.scala
Expand Up @@ -28,9 +28,9 @@ import scala.concurrent.Await
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration

import dsp.errors.FileWriteException
import org.knora.webapi.config.AppConfig
import org.knora.webapi.core.AppRouter
import org.knora.webapi.core.AppServer
import org.knora.webapi.core.TestStartupUtils
import org.knora.webapi.messages.store.sipimessages.SipiUploadResponse
Expand Down Expand Up @@ -95,7 +95,7 @@ abstract class E2ESpec
/**
* Create router and config by unsafe running them.
*/
val (router, config) =
val (router: AppRouter, config: AppConfig) =
Unsafe.unsafe { implicit u =>
runtime.unsafe
.run(
Expand All @@ -120,9 +120,10 @@ abstract class E2ESpec
Unsafe.unsafe { implicit u =>
runtime.unsafe
.run(
(for {
for {
_ <- AppServer.testWithoutSipi
_ <- prepareRepository(rdfDataObjects) @@ LogAspect.logSpan("prepare-repo")
} yield ()).provideSomeLayer(AppServer.testWithoutSipi)
} yield ()
)
.getOrThrow()
}
Expand Down
9 changes: 5 additions & 4 deletions webapi/src/it/scala/org/knora/webapi/ITKnoraLiveSpec.scala
Expand Up @@ -22,8 +22,8 @@ import scala.concurrent.Await
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration

import org.knora.webapi.config.AppConfig
import org.knora.webapi.core.AppRouter
import org.knora.webapi.core.AppServer
import org.knora.webapi.core.TestStartupUtils
import org.knora.webapi.messages.store.sipimessages._
Expand Down Expand Up @@ -87,7 +87,7 @@ abstract class ITKnoraLiveSpec
/**
* Create router and config by unsafe running them.
*/
val (router, config) =
val (router: AppRouter, config: AppConfig) =
Unsafe.unsafe { implicit u =>
runtime.unsafe
.run(
Expand All @@ -112,9 +112,10 @@ abstract class ITKnoraLiveSpec
Unsafe.unsafe { implicit u =>
runtime.unsafe
.run(
(for {
for {
_ <- AppServer.testWithSipi
_ <- prepareRepository(rdfDataObjects) @@ LogAspect.logSpan("prepare-repo")
} yield ()).provideSomeLayer(AppServer.testWithSipi)
} yield ()
)
.getOrThrow()
}
Expand Down
9 changes: 5 additions & 4 deletions webapi/src/it/scala/org/knora/webapi/R2RSpec.scala
Expand Up @@ -20,8 +20,8 @@ import java.nio.file.Paths
import java.util.concurrent.TimeUnit
import scala.concurrent.Await
import scala.concurrent.Future

import org.knora.webapi.config.AppConfig
import org.knora.webapi.core.AppRouter
import org.knora.webapi.core.AppServer
import org.knora.webapi.core.TestStartupUtils
import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject
Expand Down Expand Up @@ -78,7 +78,7 @@ abstract class R2RSpec
/**
* Create router and config by unsafe running them.
*/
private val (router, config) =
private val (router: AppRouter, config: AppConfig) =
Unsafe.unsafe { implicit u =>
runtime.unsafe
.run(
Expand All @@ -101,9 +101,10 @@ abstract class R2RSpec
Unsafe.unsafe { implicit u =>
runtime.unsafe
.run(
(for {
for {
_ <- AppServer.testWithoutSipi
_ <- prepareRepository(rdfDataObjects) @@ LogAspect.logSpan("prepare-repo")
} yield ()).provideSomeLayer(AppServer.testWithoutSipi)
} yield ()
)
.getOrThrow()
}
Expand Down
@@ -1,7 +1,6 @@
package org.knora.webapi.core

import zio.ZLayer

import org.knora.webapi.auth.JWTService
import org.knora.webapi.config.AppConfigForTestContainers
import org.knora.webapi.routing.ApiRoutes
Expand Down
5 changes: 5 additions & 0 deletions webapi/src/main/resources/application.conf
Expand Up @@ -501,6 +501,11 @@ app {
collect-client-test-data = false
}

prometheus-server-config {
port = 3339
interval = 5 seconds
}

monitoring {
prometheus-endpoint: false
prometheus-endpoint: ${?KNORA_WEBAPI_PROMETHEUS_ENDPOINT}
Expand Down
9 changes: 7 additions & 2 deletions webapi/src/main/scala/org/knora/webapi/Main.scala
Expand Up @@ -9,6 +9,7 @@ import zio._
import zio.logging.backend.SLF4J

import org.knora.webapi.core._
import org.knora.webapi.instrumentation.metrics.PrometheusServer

object Main extends ZIOApp {

Expand All @@ -30,6 +31,10 @@ object Main extends ZIOApp {
] = ZLayer.empty ++ Runtime.removeDefaultLoggers ++ SLF4J.slf4j ++ LayersLive.dspLayersLive

/* Here we start our Application */
override def run = AppServer.live.launch

override def run = for {
f1 <- PrometheusServer.make.forkDaemon
f2 <- (AppServer.live *> ZIO.never).forkDaemon

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to leave AppServer.live.launch instead of AppServer.live *> ZIO.never?

Copy link
Collaborator Author

@subotic subotic Dec 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.launch is a method on ZLayer. It does exactly the same.

_ <- f1.join
_ <- f2.join
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f1 <- PrometheusServer.make.forkDaemon
f2 <- (AppServer.live *> ZIO.never).forkDaemon
_ <- f1.join
_ <- f2.join
_ <- PrometheusServer.make.forkDaemon.join
_ <- (AppServer.live *> ZIO.never).forkDaemon.join

How about join in the same lines?

} yield ()
}
Expand Up @@ -12,6 +12,7 @@ import zio.config._
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Duration
import scala.concurrent.duration
import scala.util.Failure
import scala.util.Success
Expand Down Expand Up @@ -55,7 +56,8 @@ final case class AppConfig(
triplestore: Triplestore,
shacl: Shacl,
cacheService: CacheService,
clientTestDataService: ClientTestDataService
clientTestDataService: ClientTestDataService,
prometheusServerConfig: PrometheusServerConfig
) {
val jwtLongevityAsDuration = scala.concurrent.duration.Duration(jwtLongevity)
val defaultTimeoutAsDuration =
Expand Down Expand Up @@ -237,6 +239,11 @@ final case class ClientTestDataService(
collectClientTestData: Boolean
)

final case class PrometheusServerConfig(
port: Int,
interval: Duration
)

/**
* Loads the applicaton configuration using ZIO-Config. ZIO-Config is capable of loading
* the Typesafe-Config format.
Expand Down
42 changes: 18 additions & 24 deletions webapi/src/main/scala/org/knora/webapi/core/AppServer.scala
Expand Up @@ -179,37 +179,31 @@ object AppServer {
} yield appServer

/**
* The AppServer live layer
* The live AppServer
*/
val live: ZLayer[AppServerEnvironment, Nothing, Unit] =
ZLayer {
for {
appServer <- AppServer.init()
_ <- appServer.start(requiresAdditionalRepositoryChecks = true, requiresIIIFService = true)
} yield ()
}
val live: ZIO[AppServerEnvironment, Nothing, Unit] =
for {
appServer <- AppServer.init()
_ <- appServer.start(requiresAdditionalRepositoryChecks = true, requiresIIIFService = true)
} yield ()

/**
* The AppServer test layer with Sipi, which initiates the startup checks. Before this layer does what it does,
* The test AppServer with Sipi, which initiates the startup checks. Before this effect does what it does,
* the complete server should have already been started.
*/
val testWithSipi: ZLayer[AppServerEnvironment, Nothing, Unit] =
ZLayer {
for {
appServer <- AppServer.init()
_ <- appServer.start(requiresAdditionalRepositoryChecks = false, requiresIIIFService = true)
} yield ()
}
val testWithSipi: ZIO[AppServerEnvironment, Nothing, Unit] =
for {
appServer <- AppServer.init()
_ <- appServer.start(requiresAdditionalRepositoryChecks = false, requiresIIIFService = true)
} yield ()

/**
* The AppServer test layer without Sipi, which initiates the startup checks. Before this layer does what it does,
* The test AppServer without Sipi, which initiates the startup checks. Before this effect does what it does,
* the complete server should have already been started.
*/
val testWithoutSipi: ZLayer[AppServerEnvironment, Nothing, Unit] =
ZLayer {
for {
appServer <- AppServer.init()
_ <- appServer.start(requiresAdditionalRepositoryChecks = false, requiresIIIFService = false)
} yield ()
}
val testWithoutSipi: ZIO[AppServerEnvironment, Nothing, Unit] =
for {
appServer <- AppServer.init()
_ <- appServer.start(requiresAdditionalRepositoryChecks = false, requiresIIIFService = false)
} yield ()
}
Expand Up @@ -10,7 +10,6 @@ import zio.ZLayer
import zio._

import org.knora.webapi.config.AppConfig
import org.knora.webapi.core._
import org.knora.webapi.routing.ApiRoutesWithZIOHttp

object HttpServerWithZIOHttp {
Expand Down
@@ -0,0 +1,70 @@
package org.knora.webapi.instrumentation.metrics

import zhttp.html._
import zhttp.http._
import zhttp.service.EventLoopGroup
import zhttp.service.Server
import zhttp.service.server.ServerChannelFactory
import zio.Runtime
import zio.ZIO
import zio.ZLayer
import zio.metrics.connectors.MetricsConfig
import zio.metrics.connectors.prometheus
import zio.metrics.connectors.prometheus.PrometheusPublisher
import zio.metrics.jvm.DefaultJvmMetrics

import org.knora.webapi.config.AppConfig

object PrometheusServer {

private val nThreads = 5
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this not coming from the config?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what it does exactly. I still need to figure that out first. Maybe we don't need it.


private lazy val indexPage =
"""<html>
|<title>Simple Server</title>
|<body>
|<p><a href="/metrics">Prometheus Metrics</a></p>
|</body
|</html>""".stripMargin

private lazy val static =
Http.collect[Request] { case Method.GET -> !! => Response.html(Html.fromString(indexPage)) }

private lazy val prometheusRouter =
Http
.collectZIO[Request] { case Method.GET -> !! / "metrics" =>
ZIO.serviceWithZIO[PrometheusPublisher](_.get.map(Response.text))
}

private def server(config: AppConfig): Server[PrometheusPublisher, Throwable] =
Server.port(config.prometheusServerConfig.port) ++ Server.app(static ++ prometheusRouter)

private def runHttpServer(config: AppConfig) =
ZIO.logInfo("starting prometheus server in a separate root fiber.") *>
server(config).start

val make: ZIO[AppConfig, Throwable, Nothing] =
ZIO
.service[AppConfig]
.flatMap(config =>
runHttpServer(config)
.provide(
ServerChannelFactory.auto,
EventLoopGroup.auto(nThreads),

// Metrics config
ZLayer.succeed(MetricsConfig(config.prometheusServerConfig.interval)),

// The prometheus reporting layer
prometheus.publisherLayer,
prometheus.prometheusLayer,

// Enable the ZIO internal metrics and the default JVM metricsConfig
// Do NOT forget the .unit for the JVM metrics layer
Runtime.enableRuntimeMetrics,
Runtime.enableFiberRoots,
DefaultJvmMetrics.live.unit.orDie
)
)

}
Expand Up @@ -24,7 +24,10 @@ object AppConfigZSpec extends ZIOSpecDefault {
assertTrue(appConfig.printExtendedConfig == false) &&
assertTrue(appConfig.jwtLongevityAsDuration == FiniteDuration(30L, TimeUnit.DAYS)) &&
assertTrue(appConfig.sipi.timeoutInSeconds == FiniteDuration(120L, TimeUnit.SECONDS)) &&
assertTrue(appConfig.bcryptPasswordStrength == User.PasswordStrength(12))
assertTrue(appConfig.bcryptPasswordStrength == User.PasswordStrength(12)) &&
assertTrue(
appConfig.prometheusServerConfig.interval == java.time.Duration.ofSeconds(5)
)
}
}.provideLayer(AppConfig.live)
)
Expand Down