jobs.reporting.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otoroshi_2.12 Show documentation
Show all versions of otoroshi_2.12 Show documentation
Lightweight api management on top of a modern http reverse proxy
The newest version!
package otoroshi.jobs
import org.joda.time.DateTime
import otoroshi.cluster.{ClusterMode, StatsView}
import otoroshi.env.Env
import otoroshi.jobs.newengine.NewEngine
import otoroshi.models.GlobalConfig
import otoroshi.next.models.NgTlsConfig
import otoroshi.next.plugins.api.NgPluginCategory
import otoroshi.script._
import otoroshi.security.IdGenerator
import otoroshi.utils.http.MtlsConfig
import otoroshi.utils.syntax.implicits._
import play.api.libs.json._
import play.api.libs.ws.{DefaultWSProxyServer, WSProxyServer}
import play.api.{Configuration, Logger}
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
case class AnonymousReportingJobConfig(
enabled: Boolean,
redirect: Boolean,
url: String,
timeout: Duration,
proxy: Option[WSProxyServer],
tlsConfig: NgTlsConfig
)
object AnonymousReportingJobConfig {
val default = AnonymousReportingJobConfig(
enabled = true,
redirect = false,
url = "https://reporting.otoroshi.io/ingest",
timeout = 60.seconds,
proxy = None,
tlsConfig = NgTlsConfig.default
)
def fromEnv(env: Env): AnonymousReportingJobConfig = {
val configuration = env.configuration
.getOptionalWithFileSupport[Configuration]("otoroshi.anonymous-reporting")
.getOrElse(Configuration.empty)
AnonymousReportingJobConfig(
enabled = configuration.getOptionalWithFileSupport[Boolean]("enabled").getOrElse(default.enabled),
redirect = configuration.getOptionalWithFileSupport[Boolean]("redirect").getOrElse(default.redirect),
url = configuration.getOptionalWithFileSupport[String]("url").getOrElse(default.url),
timeout = configuration
.getOptionalWithFileSupport[Long]("timeout")
.map(v => FiniteDuration(v, TimeUnit.MILLISECONDS))
.getOrElse(default.timeout),
tlsConfig = NgTlsConfig.fromLegacy(
MtlsConfig(
certs = configuration.getOptionalWithFileSupport[Seq[String]]("tls.certs").getOrElse(Seq.empty),
trustedCerts = configuration.getOptionalWithFileSupport[Seq[String]]("tls.trustedCerts").getOrElse(Seq.empty),
loose = configuration.getOptionalWithFileSupport[Boolean]("tls.loose").getOrElse(false),
trustAll = configuration.getOptionalWithFileSupport[Boolean]("tls.trustAll").getOrElse(false),
mtls = configuration.getOptionalWithFileSupport[Boolean]("tls.enabled").getOrElse(false)
)
),
proxy = configuration
.getOptionalWithFileSupport[Boolean]("proxy.enabled")
.filter(identity)
.map { _ =>
DefaultWSProxyServer(
host = configuration.getOptionalWithFileSupport[String]("proxy.host").getOrElse("localhost"),
port = configuration.getOptionalWithFileSupport[Int]("proxy.port").getOrElse(1055),
principal = configuration.getOptionalWithFileSupport[String]("proxy.principal"),
password = configuration.getOptionalWithFileSupport[String]("proxy.password"),
ntlmDomain = configuration.getOptionalWithFileSupport[String]("proxy.ntlmDomain"),
encoding = configuration.getOptionalWithFileSupport[String]("proxy.encoding"),
nonProxyHosts = None
)
}
)
}
}
object AnonymousReportingJob {
private def avgDouble(value: Double, extractor: StatsView => Double, stats: Seq[StatsView]): Double = {
(if (value == Double.NaN || value == Double.NegativeInfinity || value == Double.PositiveInfinity) {
0.0
} else {
stats.map(extractor).:+(value).fold(0.0)(_ + _) / (stats.size + 1)
}).applyOn {
case Double.NaN => 0.0
case Double.NegativeInfinity => 0.0
case Double.PositiveInfinity => 0.0
case v if v.toString == "NaN" => 0.0
case v if v.toString == "Infinity" => 0.0
case v => v
}
}
private def sumDouble(value: Double, extractor: StatsView => Double, stats: Seq[StatsView]): Double = {
if (value == Double.NaN || value == Double.NegativeInfinity || value == Double.PositiveInfinity) {
0.0
} else {
stats.map(extractor).:+(value).fold(0.0)(_ + _)
}.applyOn {
case Double.NaN => 0.0
case Double.NegativeInfinity => 0.0
case Double.PositiveInfinity => 0.0
case v if v.toString == "NaN" => 0.0
case v if v.toString == "Infinity" => 0.0
case v => v
}
}
def buildReport(globalConfig: GlobalConfig)(implicit env: Env, ec: ExecutionContext): Future[JsValue] = {
(for {
members <- env.datastores.clusterStateDataStore.getMembers()
calls <- env.datastores.serviceDescriptorDataStore.globalCalls()
dataIn <- env.datastores.serviceDescriptorDataStore.globalDataIn()
dataOut <- env.datastores.serviceDescriptorDataStore.globalDataOut()
rate <- env.datastores.serviceDescriptorDataStore.globalCallsPerSec()
duration <- env.datastores.serviceDescriptorDataStore.globalCallsDuration()
overhead <- env.datastores.serviceDescriptorDataStore.globalCallsOverhead()
dataInRate <- env.datastores.serviceDescriptorDataStore.dataInPerSecFor("global")
dataOutRate <- env.datastores.serviceDescriptorDataStore.dataOutPerSecFor("global")
concurrentHandledRequests <- env.datastores.requestsDataStore.asyncGetHandledRequests()
} yield {
val (usesNew, usesNewFull) = NewEngine.enabledRawFromConfig(globalConfig, env)
val membersStats = members.map(_.statsView)
val now = System.currentTimeMillis()
val routePlugins: Seq[String] = env.proxyState.allRoutes().flatMap { route =>
route.plugins.slots.map(slot => slot.plugin)
}
val scriptPlugins: Seq[String] =
if (globalConfig.scripts.enabled)
Seq(
globalConfig.scripts.transformersRefs,
globalConfig.scripts.validatorRefs,
globalConfig.scripts.preRouteRefs,
globalConfig.scripts.sinkRefs,
globalConfig.scripts.jobRefs
).flatten
else Seq.empty
val pluginsPlugins = if (globalConfig.plugins.enabled) globalConfig.plugins.refs else Seq.empty
val plugins = routePlugins ++ scriptPlugins ++ pluginsPlugins
val counting = plugins.groupBy(identity).mapValues(v => JsNumber(v.size))
Json.obj(
"@timestamp" -> play.api.libs.json.JodaWrites.JodaDateTimeNumberWrites.writes(DateTime.now()),
"timestamp_str" -> DateTime.now().toString(),
"@id" -> IdGenerator.uuid,
"otoroshi_cluster_id" -> globalConfig.otoroshiId,
"otoroshi_version" -> env.otoroshiVersion,
"otoroshi_version_sem" -> env.otoroshiVersionSem.json,
"java_version" -> env.theJavaVersion.json,
"os" -> env.os.json,
"datastore" -> env.datastoreKind,
"env" -> env.env,
"features" -> Json.obj(
"snow_monkey" -> globalConfig.snowMonkeyConfig.enabled,
"clever_cloud" -> globalConfig.cleverSettings.isDefined,
"kubernetes" -> JsBoolean(
(globalConfig.plugins.enabled && globalConfig.plugins.refs.exists(l => l.toLowerCase().contains("kube"))) ||
(globalConfig.scripts.enabled && globalConfig.scripts.jobRefs.exists(l => l.toLowerCase().contains("kube")))
),
"elastic_read" -> globalConfig.elasticReadsConfig.isDefined,
"lets_encrypt" -> globalConfig.letsEncryptSettings.enabled,
"auto_certs" -> globalConfig.autoCert.enabled,
"wasmo" -> globalConfig.wasmoSettings.isDefined,
"wasm_manager" -> globalConfig.wasmoSettings.isDefined,
"backoffice_login" -> globalConfig.backOfficeAuthRef.isDefined
),
"stats" -> Json.obj(
"calls" -> calls,
"data_in" -> dataIn,
"data_out" -> dataOut,
"rate" -> sumDouble(rate, _.rate, membersStats),
"duration" -> avgDouble(duration, _.duration, membersStats),
"overhead" -> avgDouble(overhead, _.overhead, membersStats),
"data_in_rate" -> sumDouble(dataInRate, _.dataInRate, membersStats),
"data_out_rate" -> sumDouble(dataOutRate, _.dataOutRate, membersStats),
"concurrent_requests" -> sumDouble(
concurrentHandledRequests.toDouble,
_.concurrentHandledRequests.toDouble,
membersStats
).toLong
),
"engine" -> Json.obj(
"uses_new" -> usesNew,
"uses_new_full" -> usesNewFull
),
"cluster" -> Json.obj(
"mode" -> env.clusterConfig.mode.name,
"all_nodes" -> members.size,
"alive_nodes" -> members.count(m => (m.lastSeen.toDate.getTime + m.timeout.toMillis) > now),
"leaders_count" -> members.count(mv => mv.memberType == ClusterMode.Leader),
"workers_count" -> members.count(mv => mv.memberType == ClusterMode.Worker),
"nodes" -> JsArray(members.map(_.json).map { json =>
Json.obj(
"id" -> json.select("id").asValue,
"os" -> json.select("os").asValue,
"java_version" -> json.select("javaVersion").asValue,
"version" -> json.select("version").asValue,
"type" -> json.select("type").asValue,
"cpu_usage" -> json.select("stats").select("cpu_usage").asValue,
"load_average" -> json.select("stats").select("load_average").asValue,
"heap_used" -> json.select("stats").select("heap_used").asValue,
"heap_size" -> json.select("stats").select("heap_size").asValue,
"relay" -> JsBoolean(json.select("relay").select("enabled").asOpt[Boolean].getOrElse(false)),
"tunnels" -> JsNumber(BigDecimal(json.select("tunnels").asOpt[JsArray].map(_.value.size).getOrElse(0)))
// "live_threads" -> json.select("stats").select("live_threads").asValue,
// "live_peak_threads" -> json.select("stats").select("live_peak_threads").asValue,
// "daemon_threads" -> json.select("stats").select("daemon_threads").asValue,
// "location" -> json.select("location").asValue,
// "http_port" -> json.select("httpPort").asValue,
// "https_port" -> json.select("httpsPort").asValue,
// "internal_http_port" -> json.select("internalHttpPort").asValue,
// "internal_https_port" -> json.select("internalHttpsPort").asValue,
)
})
),
"entities" -> Json.obj(
"scripts" -> Json.obj(
"count" -> env.proxyState.allScripts().size,
"by_kind" -> env.proxyState.allScripts().foldLeft(Json.obj()) { case (obj, script) =>
val key = script.`type`.name
obj ++ Json.obj(key -> (obj.select(key).asOpt[Int].getOrElse(0) + 1))
}
),
"routes" -> Json.obj(
"count" -> env.proxyState.allRawRoutes().size,
"plugins" -> Json.obj(
"min" -> env.proxyState.allRawRoutes().map(_.plugins.slots.size).theMin(0),
"max" -> env.proxyState.allRawRoutes().map(_.plugins.slots.size).theMax(0),
"avg" -> env.proxyState.allRawRoutes().avgBy(_.plugins.slots.size)
)
),
"router_routes" -> Json.obj(
"count" -> env.proxyState.allRoutes().size,
"http_clients" -> Json.obj(
"ahc" -> env.proxyState.allRoutes().count(_.useAhcClient),
"akka" -> env.proxyState.allRoutes().count(_.useAkkaHttpClient),
"netty" -> env.proxyState.allRoutes().count(_.useNettyClient),
"akka_ws" -> env.proxyState.allRoutes().count(_.useAkkaHttpWsClient)
),
"plugins" -> Json.obj(
"min" -> env.proxyState.allRoutes().map(_.plugins.slots.size).theMin(0),
"max" -> env.proxyState.allRoutes().map(_.plugins.slots.size).theMax(0),
"avg" -> env.proxyState.allRoutes().avgBy(_.plugins.slots.size)
)
),
"route_compositions" -> Json.obj(
"count" -> env.proxyState.allRouteCompositions().size,
"plugins" -> Json.obj(
"min" -> env.proxyState
.allRouteCompositions()
.map(v => v.plugins.slots.size + v.routes.foldLeft(0)((a, b) => a + b.plugins.slots.size))
.theMin(0),
"max" -> env.proxyState
.allRouteCompositions()
.map(v => v.plugins.slots.size + v.routes.foldLeft(0)((a, b) => a + b.plugins.slots.size))
.theMax(0),
"avg" -> env.proxyState
.allRouteCompositions()
.avgBy(v => v.plugins.slots.size + v.routes.foldLeft(0)((a, b) => a + b.plugins.slots.size))
),
"by_kind" -> env.proxyState.allRouteCompositions().foldLeft(Json.obj()) { case (obj, rc) =>
val localPlugins = rc.routes.exists(_.plugins.slots.nonEmpty)
val overridesPlugins = rc.routes.exists(v => v.plugins.slots.nonEmpty && v.overridePlugins)
val key = if (localPlugins) {
if (overridesPlugins) "local_with_override" else "local"
} else {
"global"
}
obj ++ Json.obj(
key -> (obj.select(key).asOpt[Int].getOrElse(0) + 1)
)
}
),
"apikeys" -> Json.obj(
"count" -> env.proxyState.allApikeys().size,
"by_kind" -> Json.obj(
"disabled" -> env.proxyState.allApikeys().filterNot(_.enabled).size,
"with_rotation" -> env.proxyState.allApikeys().count(_.rotation.enabled),
"with_read_only" -> env.proxyState.allApikeys().count(_.readOnly),
"with_client_id_only" -> env.proxyState.allApikeys().count(_.allowClientIdOnly),
"with_constrained_services" -> env.proxyState.allApikeys().count(_.constrainedServicesOnly),
"with_meta" -> env.proxyState.allApikeys().count(_.metadata.nonEmpty),
"with_tags" -> env.proxyState.allApikeys().count(_.tags.nonEmpty)
),
"authorized_on" -> Json.obj(
"min" -> env.proxyState.allApikeys().map(_.authorizedEntities.size).theMin(0),
"max" -> env.proxyState.allApikeys().map(_.authorizedEntities.size).theMax(0),
"avg" -> env.proxyState.allApikeys().avgBy(_.authorizedEntities.size)
)
),
"jwt_verifiers" -> Json.obj(
"count" -> env.proxyState.allJwtVerifiers().size,
"by_strategy" -> env.proxyState.allJwtVerifiers().foldLeft(Json.obj()) { case (obj, verifier) =>
val key = verifier.strategy.name
obj ++ Json.obj(key -> (obj.select(key).asOpt[Int].getOrElse(0) + 1))
},
"by_alg" -> env.proxyState.allJwtVerifiers().foldLeft(Json.obj()) { case (obj, verifier) =>
val key = verifier.algoSettings.name
obj ++ Json.obj(key -> (obj.select(key).asOpt[Int].getOrElse(0) + 1))
}
),
"certificates" -> Json.obj(
"count" -> env.proxyState.allCertificates().size,
"by_kind" -> env.proxyState.allCertificates().foldLeft(Json.obj()) { case (obj, certificate) =>
obj ++ Json
.obj()
.applyOnIf(certificate.autoRenew) { o =>
o ++ Json.obj("auto_renew" -> (obj.select("auto_renew").asOpt[Int].getOrElse(0) + 1))
}
.applyOnIf(certificate.client) { o =>
o ++ Json.obj("client" -> (obj.select("client").asOpt[Int].getOrElse(0) + 1))
}
.applyOnIf(certificate.keypair) { o =>
o ++ Json.obj("keypair" -> (obj.select("keypair").asOpt[Int].getOrElse(0) + 1))
}
.applyOnIf(certificate.exposed) { o =>
o ++ Json.obj("exposed" -> (obj.select("exposed").asOpt[Int].getOrElse(0) + 1))
}
.applyOnIf(certificate.revoked) { o =>
o ++ Json.obj("revoked" -> (obj.select("revoked").asOpt[Int].getOrElse(0) + 1))
}
}
),
"auth_modules" -> Json.obj(
"count" -> env.proxyState.allAuthModules().size,
"by_kind" -> env.proxyState.allAuthModules().foldLeft(Json.obj()) { case (obj, module) =>
val key = module.`type`
obj ++ Json.obj(key -> (obj.select(key).asOpt[Int].getOrElse(0) + 1))
}
),
"service_descriptors" -> Json.obj(
"count" -> env.proxyState.allServices().size,
"plugins" -> Json.obj(
"old" -> env.proxyState
.allServices()
.filter { v =>
v.preRouting.enabled || v.accessValidator.enabled || v.transformerRefs.nonEmpty
}
.count(v =>
v.preRouting.refs.nonEmpty || v.accessValidator.refs.nonEmpty || v.transformerRefs.nonEmpty
),
"new" -> env.proxyState.allServices().filter(_.plugins.enabled).count(_.plugins.refs.size > 0)
),
"by_kind" -> Json.obj(
"disabled" -> env.proxyState.allServices().count(!_.enabled),
"fault_injection" -> env.proxyState.allServices().count(_.chaosConfig.enabled),
"health_check" -> env.proxyState.allServices().count(_.healthCheck.enabled),
"gzip" -> env.proxyState.allServices().count(_.gzip.enabled),
"jwt" -> env.proxyState.allServices().count(_.jwtVerifier.enabled),
"cors" -> env.proxyState.allServices().count(_.cors.enabled),
"auth" -> env.proxyState.allServices().count(_.privateApp),
"protocol" -> env.proxyState.allServices().count(_.enforceSecureCommunication),
"restrictions" -> env.proxyState.allServices().count(_.restrictions.enabled)
)
),
"teams" -> Json.obj("count" -> env.proxyState.allTeams().size),
"tenants" -> Json.obj("count" -> env.proxyState.allTenants().size),
"service_groups" -> Json.obj("count" -> env.proxyState.allServiceGroups().size),
//"error_templates" -> Json.obj("count" -> env.proxyState.allErrorTemplates().size),
"data_exporters" -> Json.obj(
"count" -> env.proxyState.allDataExporters().size,
"by_kind" -> env.proxyState.allDataExporters().foldLeft(Json.obj()) { case (obj, exporter) =>
val key = exporter.typ.name
obj ++ Json.obj(key -> (obj.select(key).asOpt[Int].getOrElse(0) + 1))
}
),
"otoroshi_admins" -> Json.obj(
"count" -> env.proxyState.allOtoroshiAdmins().size,
"by_kind" -> env.proxyState.allOtoroshiAdmins().foldLeft(Json.obj()) { case (obj, admin) =>
val key = admin.typ.name.toLowerCase()
obj ++ Json.obj(key -> (obj.select(key).asOpt[Int].getOrElse(0) + 1))
}
),
"backoffice_sessions" -> Json.obj(
"count" -> env.proxyState.allBackofficeSessions().size,
"by_kind" -> env.proxyState.allBackofficeSessions().foldLeft(Json.obj()) { case (obj, session) =>
val key = env.proxyState
.authModule(session.authConfigId)
.map(_.`type`)
.getOrElse(if (session.simpleLogin) "simple" else session.authConfigId)
obj ++ Json.obj(key -> (obj.select(key).asOpt[Int].getOrElse(0) + 1))
}
),
"private_apps_sessions" -> Json.obj(
"count" -> env.proxyState.allPrivateAppsSessions().size,
"by_kind" -> env.proxyState.allPrivateAppsSessions().foldLeft(Json.obj()) { case (obj, session) =>
val key = env.proxyState.authModule(session.authConfigId).map(_.`type`).getOrElse("unknown")
obj ++ Json.obj(key -> (obj.select(key).asOpt[Int].getOrElse(0) + 1))
}
),
"tcp_services" -> Json.obj("count" -> env.proxyState.allTcpServices().size)
),
"plugins_usage" -> counting
// "metrics" -> env.metrics.jsonRawExport(None),
)
})
}
}
class AnonymousReportingJob extends Job {
private val logger = Logger("otoroshi-jobs-anonymous-reporting")
private val showLog = new AtomicBoolean(true)
override def categories: Seq[NgPluginCategory] = Seq.empty
override def uniqueId: JobId = JobId("io.otoroshi.core.jobs.AnonymousReportingJob")
override def name: String = "Otoroshi anonymous reporting"
override def defaultConfig: Option[JsObject] = None
override def description: Option[String] =
s"""This job will send anonymous Otoroshi usage metrics to the Otoroshi teams in order to define the future of the product more accurately.
|This job may also capture your current operating system name/version and your current jvm name/version.
|No personal or sensible data are sent here, your otoroshi configuration is still safe.
|you can check what is sent on our github repository (https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/jobs/reporting.scala).
|Of course you can disable this job from config. file, config. env. variables and danger zone.
|""".stripMargin.some
override def jobVisibility: JobVisibility = JobVisibility.Internal
override def kind: JobKind = JobKind.ScheduledEvery
override def starting: JobStarting = JobStarting.Automatically
override def instantiation(ctx: JobContext, env: Env): JobInstantiation =
JobInstantiation.OneInstancePerOtoroshiCluster
override def initialDelay(ctx: JobContext, env: Env): Option[FiniteDuration] = 10.seconds.some
override def interval(ctx: JobContext, env: Env): Option[FiniteDuration] = 6.hour.some
override def predicate(ctx: JobContext, env: Env): Option[Boolean] = None
private def displayYouCanDisableLog(): Unit = {
logger.info("Anonymous reporting is ENABLED. Thank you for your help !")
logger.info(
"You can find more about anonymous reporting at https://maif.github.io/otoroshi/manual/topics/anonymous-reporting.html"
)
}
private def displayPleaseEnableLog(): Unit = {
logger.info(
"Anonymous reporting is DISABLED. It would help us a lot to activate it (Features > Danger zone > Send anonymous reports)."
)
logger.info(
"You can find more about anonymous reporting at https://maif.github.io/otoroshi/manual/topics/anonymous-reporting.html"
)
}
override def jobRun(ctx: JobContext)(implicit env: Env, ec: ExecutionContext): Future[Unit] = {
val globalConfig = env.datastores.globalConfigDataStore.latest()
val config = AnonymousReportingJobConfig.fromEnv(env)
if (config.enabled && globalConfig.anonymousReporting) {
if (showLog.compareAndSet(true, false)) {
displayYouCanDisableLog()
}
AnonymousReportingJob.buildReport(globalConfig).flatMap { report =>
if (env.isDev) logger.debug(report.prettify)
val req = if (config.tlsConfig.enabled) {
env.MtlsWs.url(config.url, config.tlsConfig.legacy)
} else {
env.Ws.url(config.url)
}
req
.withFollowRedirects(config.redirect)
.withRequestTimeout(config.timeout)
.applyOnWithOpt(config.proxy) { case (r, proxy) =>
r.withProxyServer(proxy)
}
.post(report)
.map { resp =>
if (resp.status != 200 && resp.status != 201 && resp.status != 204) {
logger.error(s"error while sending anonymous reports: ${resp.status} - ${resp.body}")
}
}
.recover { case e: Throwable =>
logger.error("error while sending anonymous reports", e)
()
}
}
} else {
displayPleaseEnableLog()
().vfuture
}
}.recover { case e: Throwable =>
logger.error("error job anonymous reports", e)
()
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy