diff --git a/build.sbt b/build.sbt index e341ea899..3d258c5c1 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import ReleaseTransformations._ name := """izanami""" organization := "fr.maif" -scalaVersion := "2.12.8" +scalaVersion := "2.12.9" lazy val root = (project in file(".")) .aggregate( diff --git a/example/example-play/build.sbt b/example/example-play/build.sbt index d345e27ab..a69fe2a69 100644 --- a/example/example-play/build.sbt +++ b/example/example-play/build.sbt @@ -6,7 +6,7 @@ lazy val `example-play` = (project in file(".")) .enablePlugins(NoPublish) .disablePlugins(BintrayPlugin) -scalaVersion := "2.12.8" +scalaVersion := "2.12.9" val akkaVersion = "2.5.21" diff --git a/example/example-spring/build.sbt b/example/example-spring/build.sbt index ee34483fb..de6095c2b 100644 --- a/example/example-spring/build.sbt +++ b/example/example-spring/build.sbt @@ -1,7 +1,7 @@ organization := "fr.maif" name := "example-spring" -scalaVersion := "2.12.8" +scalaVersion := "2.12.9" mainClass := Some("izanami.example.Application") diff --git a/izanami-clients/jvm/build.sbt b/izanami-clients/jvm/build.sbt index 2cb11af89..3638462d9 100644 --- a/izanami-clients/jvm/build.sbt +++ b/izanami-clients/jvm/build.sbt @@ -7,7 +7,7 @@ val disabledPlugins = if (sys.env.get("TRAVIS_TAG").filterNot(_.isEmpty).isDefin Seq(RevolverPlugin, BintrayPlugin) } -scalaVersion := "2.12.8" +scalaVersion := "2.12.9" val akkaVersion = "2.5.23" val akkaHttpVersion = "10.1.8" diff --git a/izanami-documentation/build.sbt b/izanami-documentation/build.sbt index c7bb6d186..ab9a377bf 100644 --- a/izanami-documentation/build.sbt +++ b/izanami-documentation/build.sbt @@ -1,6 +1,6 @@ import sbt.project -scalaVersion := "2.12.8" +scalaVersion := "2.12.9" lazy val `izanami-documentation` = (project in file(".")) .enablePlugins(ParadoxPlugin, DitaaPlugin) diff --git a/izanami-server/app/IzanamiLoader.scala b/izanami-server/app/IzanamiLoader.scala index fe0d14426..4636c4f0d 100644 --- a/izanami-server/app/IzanamiLoader.scala +++ b/izanami-server/app/IzanamiLoader.scala @@ -53,6 +53,7 @@ class IzanamiLoader extends ApplicationLoader { } package object modules { + import scala.concurrent.Future class IzanamiComponentsInstances(context: Context) extends BuiltInComponentsFromContext(context) @@ -129,6 +130,10 @@ package object modules { *> patchs.run().ignore ) + applicationLifecycle.addStopHook { () => + Future(globalContext.prometheusRegistry.clear()) + } + lazy val homeController: HomeController = wire[HomeController] lazy val globalScripController: GlobalScriptController = wire[GlobalScriptController] lazy val configController: ConfigController = wire[ConfigController] diff --git a/izanami-server/app/controllers/MetricController.scala b/izanami-server/app/controllers/MetricController.scala index cd4cf8b5c..48da6a209 100644 --- a/izanami-server/app/controllers/MetricController.scala +++ b/izanami-server/app/controllers/MetricController.scala @@ -15,7 +15,7 @@ class MetricController(AuthAction: ActionBuilder[AuthContext, AnyContent], cc: C req match { case Accepts.Json => Ok(metrics.jsonExport).withHeaders("Content-Type" -> "application/json") - case Prometheus => + case Prometheus() => Ok(metrics.prometheusExport) case _ => Ok(metrics.defaultHttpFormat) diff --git a/izanami-server/app/domains/feature/feature.scala b/izanami-server/app/domains/feature/feature.scala index 2a50b51af..060ec7566 100644 --- a/izanami-server/app/domains/feature/feature.scala +++ b/izanami-server/app/domains/feature/feature.scala @@ -18,6 +18,8 @@ import store._ import zio.{RIO, ZIO} import domains.{AuthInfo, AuthInfoModule} import java.time.LocalTime +import metrics.Metrics +import metrics.MetricsService sealed trait Strategy @@ -149,6 +151,7 @@ object FeatureService { feature <- jsResultToError(created.validate[Feature]) authInfo <- AuthInfo.authInfo _ <- EventStore.publish(FeatureCreated(id, feature, authInfo = authInfo)) + _ <- MetricsService.incFeatureCreated(id.key) } yield feature def update(oldId: FeatureKey, id: FeatureKey, data: Feature): ZIO[FeatureContext, IzanamiErrors, Feature] = @@ -159,6 +162,7 @@ object FeatureService { feature <- jsResultToError(updated.validate[Feature]) authInfo <- AuthInfo.authInfo _ <- EventStore.publish(FeatureUpdated(id, oldValue, feature, authInfo = authInfo)) + _ <- MetricsService.incFeatureUpdated(id.key) } yield feature def delete(id: FeatureKey): ZIO[FeatureContext, IzanamiErrors, Feature] = @@ -167,10 +171,11 @@ object FeatureService { feature <- jsResultToError(deleted.validate[Feature]) authInfo <- AuthInfo.authInfo _ <- EventStore.publish(FeatureDeleted(id, feature, authInfo = authInfo)) + _ <- MetricsService.incFeatureDeleted(id.key) } yield feature def deleteAll(query: Query): ZIO[FeatureContext, IzanamiErrors, Unit] = - FeatureDataStore.deleteAll(query) + FeatureDataStore.deleteAll(query) <* MetricsService.incFeatureCreated(query.toString()) def getById(id: FeatureKey): RIO[FeatureContext, Option[Feature]] = for { diff --git a/izanami-server/app/domains/feature/instances.scala b/izanami-server/app/domains/feature/instances.scala index bdc4d1aa7..eee6a50cc 100644 --- a/izanami-server/app/domains/feature/instances.scala +++ b/izanami-server/app/domains/feature/instances.scala @@ -16,6 +16,7 @@ import zio.{IO, Task, ZIO} import scala.util.hashing.MurmurHash3 import java.time.LocalTime +import metrics.MetricsService object DefaultFeatureInstances { @@ -320,7 +321,9 @@ object FeatureInstances { case f: ReleaseDateFeature => ReleaseDateFeatureInstances.isActive.isActive(f, context) case f: PercentageFeature => PercentageFeatureInstances.isActive.isActive(f, context) case f: HourRangeFeature => HourRangeFeatureInstances.isActive.isActive(f, context) - }) + }) >>= { checked => + MetricsService.incFeatureCheckCount(feature.id.key, checked) *> ZIO.succeed(checked) + } } implicit val isActive: IsActive[Feature] = @@ -355,7 +358,6 @@ object FeatureInstances { GlobalScriptFeatureInstances.format.reads(o) case o if (o \ "activationStrategy").asOpt[String].contains(PERCENTAGE) => PercentageFeatureInstances.format.reads(o) - GlobalScriptFeatureInstances.format.reads(o) case o if (o \ "activationStrategy").asOpt[String].contains(HOUR_RANGE) => HourRangeFeatureInstances.format.reads(o) case _ => diff --git a/izanami-server/app/filters/IzanamiDefaultFilter.scala b/izanami-server/app/filters/IzanamiDefaultFilter.scala index 05a29e1e4..7d1cd7b77 100644 --- a/izanami-server/app/filters/IzanamiDefaultFilter.scala +++ b/izanami-server/app/filters/IzanamiDefaultFilter.scala @@ -27,20 +27,51 @@ import zio.{DefaultRuntime, Runtime, ZIO} import scala.concurrent.{ExecutionContext, Future} import scala.util._ +import io.prometheus.client.Gauge +import io.prometheus.client.Counter +import metrics.MetricsContext +import metrics.Metrics +import metrics.MetricsService +import io.prometheus.client.Histogram + +object PrometheusMetricsHolder { + + val prometheursRequestCounter = io.prometheus.client.Counter + .build() + .name("request_count") + .labelNames("http_method", "request_path", "request_status") + .help("Count of http request") + .create() + + val prometheursRequestHisto = io.prometheus.client.Histogram + .build() + .name("request_duration_details") + .labelNames("http_method", "request_path") + .help("Duration of http request") + .create() + + prometheursRequestCounter.register() + prometheursRequestHisto.register() +} class IzanamiDefaultFilter[F[_]: Effect](env: Env, izanamiConfig: IzanamiConfig, config: DefaultFilter, apikeyConfig: ApikeyConfig)( implicit ec: ExecutionContext, - runtime: Runtime[ApiKeyContext], + runtime: Runtime[ApiKeyContext with MetricsContext], val mat: Materializer ) extends Filter { import cats.effect.implicits._ + import scala.collection.JavaConverters._ private val logger = Logger("filter") private val decoder = Base64.getDecoder + // private val knownQueryParams = Seq("active", "clientId", "configs", "domains", "experiments", "features", "flat", "name_only", "newLevel", + // "page", "pageSize", "pattern", "patterns", "render", "scripts") + + //private val labelNames = Seq("http_method", "request_path", "query_params").sorted.toArray private val allowedPath: Seq[String] = izanamiConfig.contextPath match { case "/" => config.allowedPaths @@ -70,6 +101,11 @@ class IzanamiDefaultFilter[F[_]: Effect](env: Env, val startTime: Long = System.currentTimeMillis val maybeClaim = Try(requestHeader.cookies.get(config.cookieClaim).get.value).toOption + val histoWithLabels = + PrometheusMetricsHolder.prometheursRequestHisto + .labels(requestHeader.method, requestHeader.path) + .startTimer() + val maybeAuthorization = requestHeader.headers .get("Authorization") .map(_.replace("Basic ", "")) @@ -244,12 +280,21 @@ class IzanamiDefaultFilter[F[_]: Effect](env: Env, timer.stop() timerMethod.foreach(_.stop()) timerMethodPath.stop() + + PrometheusMetricsHolder.prometheursRequestCounter + .labels(requestHeader.method, requestHeader.path, s"${resp.header.status}") + .inc() + histoWithLabels.observeDuration() + case Failure(e) => logger.error(s"Error for request ${requestHeader.method} ${requestHeader.uri}", e) env.metricRegistry.meter(name("request", "500", "rate")).mark() timer.stop() timerMethod.foreach(_.stop()) timerMethodPath.stop() + + PrometheusMetricsHolder.prometheursRequestCounter.labels(requestHeader.method, requestHeader.path, "500").inc() + histoWithLabels.observeDuration() } result } diff --git a/izanami-server/app/metrics/Metrics.scala b/izanami-server/app/metrics/Metrics.scala index 32c454b3e..136865d30 100644 --- a/izanami-server/app/metrics/Metrics.scala +++ b/izanami-server/app/metrics/Metrics.scala @@ -58,10 +58,18 @@ import domains.script.GlobalScriptService import domains.webhook.WebhookService import domains.abtesting.ExperimentService import zio.Schedule +import io.prometheus.client.CollectorRegistry +import java.{util => ju} +import io.prometheus.client.Collector +import zio.Ref +import io.prometheus.client.Counter +import io.prometheus.client.Histogram +import zio.UIO trait MetricsModule extends IzanamiConfigModule { def metricRegistry: MetricRegistry - val prometheus: DropwizardExports = new DropwizardExports(metricRegistry) + val prometheusRegistry: CollectorRegistry = CollectorRegistry.defaultRegistry + val prometheus: DropwizardExports = new DropwizardExports(metricRegistry) } trait MetricsContext @@ -82,13 +90,18 @@ trait MetricsContext with ExperimentContext case class Metrics(metricRegistry: MetricRegistry, + prometheusRegistry: CollectorRegistry, prometheus: DropwizardExports, objectMapper: ObjectMapper, metricsConfig: MetricsConfig) { def prometheusExport: String = { - val writer = new StringWriter() - TextFormat.write004(writer, new SimpleEnum(prometheus.collect())) + val writer = new StringWriter() + val prometheuseMetrics = ju.Collections.list(prometheusRegistry.metricFamilySamples()) + val dropwizardMetrics = prometheus.collect() + dropwizardMetrics.addAll(prometheuseMetrics) + + TextFormat.write004(writer, new SimpleEnum(dropwizardMetrics)) writer.toString } @@ -114,6 +127,39 @@ object MetricsService { objectMapper } + private val featureCheckCount = Counter + .build() + .name("feature_check_count") + .labelNames("key", "active") + .help("Count feature check") + .create() + + private val featureCreatedCount = Counter + .build() + .name("feature_created_count") + .labelNames("key") + .help("Count for feature creations") + .create() + + private val featureUpdatedCount = Counter + .build() + .name("feature_updated_count") + .labelNames("key") + .help("Count for feature updates") + .create() + + private val featureDeletedCount = Counter + .build() + .name("feature_deleted_count") + .labelNames("key") + .help("Count for feature deletions") + .create() + + featureCheckCount.register() + featureCreatedCount.register() + featureUpdatedCount.register() + featureDeletedCount.register() + private def startMetricsLogger(metricsConfig: MetricsConfig, context: MetricsContext): RIO[MetricsContext, Fiber[Throwable, Unit]] = if (metricsConfig.log.enabled) { @@ -228,8 +274,12 @@ object MetricsService { def start: RIO[MetricsContext, Unit] = for { - runtime <- ZIO.runtime[MetricsContext] - context <- ZIO.environment[MetricsContext] + runtime <- ZIO.runtime[MetricsContext] + context <- ZIO.environment[MetricsContext] + // _ <- ZIO(featureCheckCount.register()) + // _ <- ZIO(featureCreatedCount.register()) + // _ <- ZIO(featureUpdatedCount.register()) + // _ <- ZIO(featureDeletedCount.register()) _ = context.metricRegistry.register("jvm.memory", new MemoryUsageGaugeSet()) _ = context.metricRegistry.register("jvm.thread", new ThreadStatesGaugeSet()) metricsConfig = context.izanamiConfig.metrics @@ -260,8 +310,94 @@ object MetricsService { countAndStore(count, GlobalScriptService.count(Query.oneOf("*")), "globalScript", metricRegistry) <&> countAndStore(count, UserService.count(Query.oneOf("*")), "user", metricRegistry) <&> countAndStore(count, WebhookService.count(Query.oneOf("*")), "webhook", metricRegistry)) - } yield Metrics(context.metricRegistry, context.prometheus, objectMapper, context.izanamiConfig.metrics) + } yield + Metrics(context.metricRegistry, + context.prometheusRegistry, + context.prometheus, + objectMapper, + context.izanamiConfig.metrics) + + def incFeatureCheckCount(key: String, active: Boolean): UIO[Unit] = + UIO(featureCheckCount.labels(key, s"$active").inc()) + + def incFeatureCreated(key: String): UIO[Unit] = + UIO(featureCreatedCount.labels(key).inc()) + + def incFeatureUpdated(key: String): UIO[Unit] = + UIO(featureUpdatedCount.labels(key).inc()) + + def incFeatureDeleted(key: String): UIO[Unit] = + UIO(featureDeletedCount.labels(key).inc()) + + // def counter(name: String, + // help: String, + // labels: (String, String)*): RIO[MetricsContext, io.prometheus.client.Counter] = { + + // import cats._ + // import cats.implicits._ + // val sortedLabels = labels.sortBy(_._1) + // val sortedLabelNames = sortedLabels.map(_._1) + // for { + // context <- ZIO.environment[MetricsContext] + // _ <- Logger.info(s"Registering counter $name $sortedLabelNames") + // metric <- context.prometheusMetrics.modify { metrics => + // metrics.collect { + // case PrometheusCounter(n, l, counter) if n === name && l == sortedLabelNames => counter + // }.headOption match { + // case None => + // val metricBuidler = io.prometheus.client.Counter + // .build() + // .name(name) + // .help(help) + + // val metric = if (sortedLabelNames.isEmpty) { + // metricBuidler.create() + // } else { + // metricBuidler + // .labelNames(sortedLabelNames.toArray: _*) + // .create() + // } + + // metric.register(context.prometheusRegistry) + // (metric, metrics :+ PrometheusCounter(name, sortedLabelNames, metric)) + // case Some(metric) => (metric, metrics) + // } + // } + // } yield metric + // } + // def histogram(name: String, + // help: String, + // labels: (String, String)*): RIO[MetricsContext, io.prometheus.client.Histogram] = { + // val sortedLabels = labels.sortBy(_._1) + // val sortedLabelNames = sortedLabels.map(_._1) + // for { + // context <- ZIO.environment[MetricsContext] + // _ <- Logger.info(s"Registering histogram $name $sortedLabelNames") + // metric <- context.prometheusMetrics.modify { metrics => + // metrics.collect { + // case PrometheusHistogram(n, l, counter) if n == name && l == sortedLabelNames => counter + // }.headOption match { + // case None => + // val metricBuidler = io.prometheus.client.Histogram + // .build() + // .name(name) + // .help(help) + + // val metric = if (sortedLabelNames.isEmpty) { + // metricBuidler.create() + // } else { + // metricBuidler + // .labelNames(sortedLabelNames.toArray: _*) + // .create() + // } + // metric.register(context.prometheusRegistry) + // (metric, metrics :+ PrometheusHistogram(name, sortedLabelNames, metric)) + // case Some(metric) => (metric, metrics) + // } + // } + // } yield metric + // } private def countAndStore[Ctx](enabled: Boolean, count: => RIO[Ctx, Long], name: String, diff --git a/izanami-server/build.sbt b/izanami-server/build.sbt index 29f0d0e67..b6dd74fa4 100644 --- a/izanami-server/build.sbt +++ b/izanami-server/build.sbt @@ -7,7 +7,7 @@ packageName in Universal := "izanami" name in Universal := "izanami" -scalaVersion := "2.12.8" +scalaVersion := "2.12.9" lazy val ITest = config("it") extend Test diff --git a/izanami-server/it/specs/memorywithdb/store/InMemoryWithDbStoreTest.scala b/izanami-server/it/specs/memorywithdb/store/InMemoryWithDbStoreTest.scala index e2dff7f08..30fe51dc5 100644 --- a/izanami-server/it/specs/memorywithdb/store/InMemoryWithDbStoreTest.scala +++ b/izanami-server/it/specs/memorywithdb/store/InMemoryWithDbStoreTest.scala @@ -42,7 +42,7 @@ class InMemoryWithDbStoreTest extends PlaySpec with ScalaFutures with Integratio val key1 = Key("key:1") val key2 = Key("key:2") - val feature1 = DefaultFeature(key1, false) + val feature1 = DefaultFeature(key1, false, None) underlyingStore.create(key1, Json.toJson(feature1)).either.unsafeRunSync() val inMemoryWithDb = new InMemoryWithDbStore( @@ -56,7 +56,7 @@ class InMemoryWithDbStoreTest extends PlaySpec with ScalaFutures with Integratio Thread.sleep(500) val feature1Updated = feature1.copy(enabled = true) - val feature2 = DefaultFeature(key2, false) + val feature2 = DefaultFeature(key2, false, None) actorSystem.eventStream.publish(FeatureUpdated(key1, feature1, feature1Updated, authInfo = None)) actorSystem.eventStream.publish(FeatureCreated(key2, feature2, authInfo = None)) @@ -78,7 +78,7 @@ class InMemoryWithDbStoreTest extends PlaySpec with ScalaFutures with Integratio val key1 = Key("key:1") val key2 = Key("key:2") - val feature1 = DefaultFeature(key1, false) + val feature1 = DefaultFeature(key1, false, None) underlyingStore.create(key1, Json.toJson(feature1)).either.unsafeRunSync() val inMemoryWithDb = new InMemoryWithDbStore( diff --git a/perfs-jmh/build.sbt b/perfs-jmh/build.sbt index 7bb1c4dcb..2c99dd1ab 100644 --- a/perfs-jmh/build.sbt +++ b/perfs-jmh/build.sbt @@ -1,4 +1,4 @@ -scalaVersion := "2.12.8" +scalaVersion := "2.12.9" val akkaVersion = "2.5.12" libraryDependencies ++= Seq( diff --git a/simulation/build.sbt b/simulation/build.sbt index e70fe9b88..b1d887b37 100644 --- a/simulation/build.sbt +++ b/simulation/build.sbt @@ -1,7 +1,7 @@ name := """simulation""" organization := "fr.maif" -scalaVersion := "2.12.8" +scalaVersion := "2.12.9" lazy val simulation = (project in file(".")) .enablePlugins(GatlingPlugin)