All Downloads are FREE. Search and download functionalities are using the official Maven repository.

greenscore.ecometrics.scala Maven / Gradle / Ivy

package otoroshi.greenscore

import com.codahale.metrics.UniformReservoir
import otoroshi.cluster.ClusterLeaderUpdateMessage.RouteCallIncr
import otoroshi.greenscore.EcoMetrics.{colorFromScore, letterFromScore, MAX_GREEN_SCORE_NOTE}
import otoroshi.utils.cache.types.UnboundedTrieMap
import otoroshi.utils.syntax.implicits.{BetterJsValue, BetterSyntax}
import play.api.libs.json._

import java.util.{Timer => _}
import scala.collection.concurrent.TrieMap

class ThresholdsRegistry {
  private val routesScore: UnboundedTrieMap[String, RouteReservoirs] = TrieMap.empty

  def updateRoute(routeCallIncr: RouteCallIncr) = {
    routesScore
      .getOrElseUpdate(routeCallIncr.routeId, RouteReservoirs())
      .update(routeCallIncr)
  }

  def route(routeId: String): Option[RouteReservoirs] = routesScore.get(routeId)

  def json(routeId: String) = routesScore.get(routeId).map(_.json()).getOrElse(RouteReservoirs().json())
}

case class DynamicTripleBounds(
    overhead: TripleBounds = TripleBounds(),
    duration: TripleBounds = TripleBounds(),
    backendDuration: TripleBounds = TripleBounds(),
    calls: TripleBounds = TripleBounds(),
    dataIn: TripleBounds = TripleBounds(),
    dataOut: TripleBounds = TripleBounds(),
    headersOut: TripleBounds = TripleBounds(),
    headersIn: TripleBounds = TripleBounds()
) {

  def json() = Json.obj(
    "overhead"        -> overhead.json(),
    "duration"        -> duration.json(),
    "backendDuration" -> backendDuration.json(),
    "calls"           -> calls.json(),
    "dataIn"          -> dataIn.json(),
    "dataOut"         -> dataOut.json(),
    "headersOut"      -> headersOut.json(),
    "headersIn"       -> headersIn.json()
  )

  def +(other: DynamicTripleBounds) = copy(
    overhead = overhead + other.overhead,
    duration = duration + other.duration,
    backendDuration = backendDuration + other.backendDuration,
    calls = calls + other.calls,
    dataIn = dataIn + other.dataIn,
    headersOut = headersOut + other.headersOut,
    headersIn = headersIn + other.headersIn,
    dataOut = dataOut + other.dataOut
  )

  def from(reservoirs: RouteReservoirs, thresholds: Thresholds) = {
    DynamicTripleBounds(
      overhead.incr(reservoirs.overhead.getSnapshot.getMean.toInt, thresholds.overhead),
      duration.incr(reservoirs.duration.getSnapshot.getMean.toInt, thresholds.duration),
      backendDuration.incr(reservoirs.backendDuration.getSnapshot.getMean.toInt, thresholds.backendDuration),
      calls.incr(reservoirs.calls.getSnapshot.getMean.toInt, thresholds.calls),
      dataIn.incr(reservoirs.dataIn.getSnapshot.getMean.toInt, thresholds.dataIn),
      dataOut.incr(reservoirs.dataOut.getSnapshot.getMean.toInt, thresholds.dataOut),
      headersOut.incr(reservoirs.headersOut.getSnapshot.getMean.toInt, thresholds.headersOut),
      headersIn.incr(reservoirs.headersIn.getSnapshot.getMean.toInt, thresholds.headersIn)
    )
  }
}

case class ScalingRouteReservoirs(
    overhead: Float = 0,
    duration: Float = 0,
    backendDuration: Float = 0,
    calls: Float = 0,
    dataIn: Float = 0,
    dataOut: Float = 0,
    headersOut: Float = 0,
    headersIn: Float = 0
) {
  def json() = Json.obj(
    "overhead"        -> overhead,
    "duration"        -> duration,
    "backendDuration" -> backendDuration,
    "calls"           -> calls,
    "dataIn"          -> dataIn,
    "dataOut"         -> dataOut,
    "headersOut"      -> headersOut,
    "headersIn"       -> headersIn
  )

  def merge(other: ScalingRouteReservoirs) = copy(
    overhead = overhead + other.overhead,
    duration = duration + other.duration,
    backendDuration = backendDuration + other.backendDuration,
    calls = calls + other.calls,
    dataIn = dataIn + other.dataIn,
    headersOut = headersOut + other.headersOut,
    headersIn = headersIn + other.headersIn,
    dataOut = dataOut + other.dataOut
  )

  def mean(length: Int) = copy(
    overhead = overhead / length,
    duration = duration / length,
    backendDuration = backendDuration / length,
    calls = calls / length,
    dataIn = dataIn / length,
    headersOut = headersOut / length,
    headersIn = headersIn / length,
    dataOut = dataOut / length
  )
}

object ScalingRouteReservoirs {
  private def scalingReservoir(value: Double, limit: Int): Float = {
    if (value > limit)
      1
    else
      (value / limit).toFloat
  }

  def from(reservoirs: RouteReservoirs) = {
    ScalingRouteReservoirs(
      reservoirs.overhead.getSnapshot.getMean.toLong,
      reservoirs.duration.getSnapshot.getMean.toLong,
      reservoirs.backendDuration.getSnapshot.getMean.toLong,
      reservoirs.calls.getSnapshot.getMean.toLong,
      reservoirs.dataIn.getSnapshot.getMean.toLong,
      reservoirs.dataOut.getSnapshot.getMean.toLong,
      reservoirs.headersOut.getSnapshot.getMean.toLong,
      reservoirs.headersIn.getSnapshot.getMean.toLong
    )
  }

  def from(reservoirs: RouteReservoirs, thresholds: Thresholds) = {
    ScalingRouteReservoirs(
      overhead = 1 - this.scalingReservoir(reservoirs.overhead.getSnapshot.getMean, thresholds.overhead.poor),
      duration = 1 - this.scalingReservoir(reservoirs.duration.getSnapshot.getMean, thresholds.duration.poor),
      backendDuration =
        1 - this.scalingReservoir(reservoirs.backendDuration.getSnapshot.getMean, thresholds.backendDuration.poor),
      calls = 1 - this.scalingReservoir(reservoirs.calls.getSnapshot.getMean, thresholds.calls.poor),
      dataIn = 1 - this.scalingReservoir(reservoirs.dataIn.getSnapshot.getMean, thresholds.dataIn.poor),
      dataOut = 1 - this.scalingReservoir(reservoirs.dataOut.getSnapshot.getMean, thresholds.dataOut.poor),
      headersOut = 1 - this.scalingReservoir(reservoirs.headersOut.getSnapshot.getMean, thresholds.headersOut.poor),
      headersIn = 1 - this.scalingReservoir(reservoirs.headersIn.getSnapshot.getMean, thresholds.headersIn.poor)
    )
  }
}

case class RouteReservoirs(
    overhead: UniformReservoir = new UniformReservoir(),
    duration: UniformReservoir = new UniformReservoir(),
    backendDuration: UniformReservoir = new UniformReservoir(),
    calls: UniformReservoir = new UniformReservoir(),
    dataIn: UniformReservoir = new UniformReservoir(),
    headersOut: UniformReservoir = new UniformReservoir(),
    dataOut: UniformReservoir = new UniformReservoir(),
    headersIn: UniformReservoir = new UniformReservoir()
) {
  def update(routeCallIncr: RouteCallIncr) = {
    overhead.update(routeCallIncr.overhead.get())
    duration.update(routeCallIncr.duration.get())
    backendDuration.update(routeCallIncr.backendDuration.get())

    dataIn.update(routeCallIncr.dataIn.get())
    dataOut.update(routeCallIncr.dataOut.get())
    headersIn.update(routeCallIncr.headersIn.get())
    headersOut.update(routeCallIncr.headersOut.get())
    calls.update(routeCallIncr.calls.get())
  }

  def json(): JsValue = Json.obj(
    "overhead"        -> overhead.getSnapshot.getMean,
    "duration"        -> duration.getSnapshot.getMean,
    "backendDuration" -> backendDuration.getSnapshot.getMean,
    "dataIn"          -> dataIn.getSnapshot.getMean,
    "dataOut"         -> dataOut.getSnapshot.getMean,
    "headersIn"       -> headersIn.getSnapshot.getMean,
    "headersOut"      -> headersOut.getSnapshot.getMean,
    "calls"           -> calls.getSnapshot.getMean
  )
}

sealed trait Score {
  def color: String
  def letter: String
}

object SectionScore {
  def from(o: JsValue) = SectionScore(
    score = o.select("score").as[Double],
    scalingScore = o.select("scaling_score").as[Double]
  )
}

object Score {
  case class Excellent(color: String = "#2ecc71", letter: String = "A")     extends Score
  case class Acceptable(color: String = "#27ae60", letter: String = "B")    extends Score
  case class Sufficient(color: String = "#f1c40f", letter: String = "C")    extends Score
  case class Poor(color: String = "#d35400", letter: String = "D")          extends Score
  case class ExtremelyPoor(color: String = "#c0392b", letter: String = "E") extends Score

  def avgDouble(value: Double): Double = {
    (if (value == Double.NaN || value == Double.NegativeInfinity || value == Double.PositiveInfinity) {
       0.0
     } else {
       value
     }).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
    }
  }
}

case class SectionScore(score: Double = 0.0, scalingScore: Double = 0.0) {
  def json() = {
    Json.obj(
      "score"         -> score,
      "scaling_score" -> Score.avgDouble(scalingScore)
    )
  }

  def merge(other: SectionScore): SectionScore = SectionScore(
    score = score + other.score,
    scalingScore = scalingScore + other.scalingScore
  )

  def merge(other: RouteScoreByDateAndSection): SectionScore = this.merge(other.score)
}

case class RouteDynamicValues(routeId: String, groupId: String, dynamicValues: Dynamicvalues) {
  def json() = Json.obj(
    "id"             -> routeId,
    "group_id"       -> groupId,
    "dynamic_values" -> dynamicValues.json()
  )
}

case class RouteScore(
    groupId: String,
    sectionsScoreByDate: Seq[RouteScoreByDateAndSection],
    dynamicValues: Dynamicvalues
) {
  def json(): JsObject = Json.obj(
    "group_id"               -> groupId,
    "sections_score_by_date" -> sectionsScoreByDate.map(_.json()),
    "dynamic_values"         -> dynamicValues.json()
  )
}

case class Dynamicvalues(
    scaling: ScalingRouteReservoirs = ScalingRouteReservoirs(),
    raw: ScalingRouteReservoirs = ScalingRouteReservoirs(),
    counters: DynamicTripleBounds = DynamicTripleBounds()
) {
  def json() = Json.obj(
    "scaling"  -> scaling.json(),
    "raw"      -> raw.json(),
    "counters" -> counters.json()
  )

  def merge(other: Dynamicvalues) = copy(
    scaling = scaling.merge(other.scaling),
    raw = raw.merge(other.raw),
    counters = counters + other.counters
  )

  def mean(length: Int) = copy(
    scaling = scaling.mean(length),
    raw = raw.mean(length)
  )
}

case class GroupScore(
    dynamicValuesByRoutes: Seq[RouteDynamicValues],
    dynamicValues: Dynamicvalues,
    routeScoreByDate: Seq[RouteScoreAtDate]
) {
  def json() = Json.obj(
    "dynamic_values_by_routes" -> dynamicValuesByRoutes.map(_.json()),
    "dynamic_values"           -> dynamicValues.json(),
    "score_by_route"           -> routeScoreByDate.map(_.json())
  )
}

object EcoMetrics {
  val MAX_GREEN_SCORE_NOTE = 6000

  private def scoreToColor(rank: Double, max: Double): Score = {
    if (rank >= max) {
      Score.Excellent()
    } else if (rank >= max / 2) {
      Score.Acceptable()
    } else if (rank >= max / 3) {
      Score.Sufficient()
    } else if (rank >= max / 6) {
      Score.Poor()
    } else // rank < max/6
      Score.ExtremelyPoor()
  }

  def letterFromScore(rank: Double, max: Double = MAX_GREEN_SCORE_NOTE): String = {
    scoreToColor(rank, max).letter
  }

  def colorFromScore(rank: Double, max: Double = MAX_GREEN_SCORE_NOTE): String = {
    scoreToColor(rank, max).color
  }
}

object RouteScoreByDateAndSection {
  def from(o: JsValue) = RouteScoreByDateAndSection(
    date = o.select("date").as[Long],
    section = o.select("section").as[String],
    sectionWeight = o.select("section_weight").as[Double],
    score = SectionScore.from(o.select("score").as[JsValue]),
    letter = o.select("letter").as[String],
    color = o.select("color").as[String]
  )
}

case class RouteScoreByDateAndSection(
    date: Long,
    section: String = "",
    sectionWeight: Double = 0.0,
    score: SectionScore = SectionScore(),
    letter: String = "",
    color: String = ""
) {
  def json() = Json.obj(
    "date"           -> date,
    "section"        -> section,
    "section_weight" -> sectionWeight,
    "score"          -> score.json,
    "letter"         -> letter,
    "color"          -> color
  )

  def processRoute(): RouteScoreByDateAndSection = {
    copy(
      letter = letterFromScore(score.score, max = (sectionWeight / 100) * MAX_GREEN_SCORE_NOTE),
      color = colorFromScore(score.score, max = (sectionWeight / 100) * MAX_GREEN_SCORE_NOTE),
      score = score.copy(
        scalingScore = if (score.score == 0) 0 else score.score / ((sectionWeight / 100) * MAX_GREEN_SCORE_NOTE)
      )
    )
  }
}

case class RouteScoreAtDateItem(groupId: String, routeId: String, scores: Seq[RouteScoreByDateAndSection]) {
  def json() = Json.obj(
    "group_id" -> groupId,
    "id"       -> routeId,
    "sections" -> scores.map(_.json())
  )
}

object RouteScoreAtDateItem {
  def from(json: JsValue) = RouteScoreAtDateItem(
    groupId = json.select("group_id").as[String],
    routeId = json.select("id").as[String],
    scores = json.select("sections").as[JsArray].value.map(RouteScoreByDateAndSection.from)
  )
}

case class RouteScoreAtDate(date: Long, routes: Seq[RouteScoreAtDateItem]) {
  def json() = Json.obj(
    "date"   -> date,
    "routes" -> routes.map(_.json())
  )
}

object RouteScoreAtDate {
  def from(o: JsValue) = RouteScoreAtDate(
    date = o.select("date").as[Long],
    routes = o.select("routes").as[JsArray].value.map(RouteScoreAtDateItem.from)
  )
}

class EcoMetrics {

  private val registry = new ThresholdsRegistry()

  private def calculateRulesByDate(rules: RulesRouteConfiguration): Seq[RouteScoreByDateAndSection] = {
    val enrichedRules = rules.states
      .sortBy(_.date)(Ordering.Long)
      .zipWithIndex
      .foldLeft(Seq.empty[RuleStateRecord]) { case (acc, (item, i)) =>
        acc :+ item.copy(
          states = rules.states
            .slice(0, i + 1)
            .flatMap(_.states)
            .foldLeft(RulesManager.rules.map(r => RuleState(r.id, enabled = false))) { case (acc, i) =>
              acc.map(p => if (p.id == i.id) i else p)
            }
        )
      }

    enrichedRules
      .foldLeft(Seq.empty[RouteScoreByDateAndSection]) { case (acc, state) =>
        acc ++ state.states
          .foldLeft(Seq.empty[RouteScoreByDateAndSection]) { case (acc, rule) =>
            val ruleWeight = RulesManager.rules.find(r => r.id == rule.id).get
            val value      =
              if (rule.enabled) MAX_GREEN_SCORE_NOTE * (ruleWeight.sectionWeight / 100) * (ruleWeight.weight / 100)
              else 0

            acc.find(score => score.section == ruleWeight.section) match {
              case None       =>
                acc :+ RouteScoreByDateAndSection(
                  date = state.date,
                  section = ruleWeight.section,
                  sectionWeight = ruleWeight.sectionWeight,
                  score = SectionScore(score = value)
                )
              case Some(item) =>
                acc.filter(score => !(score.section == ruleWeight.section)) :+ RouteScoreByDateAndSection(
                  date = state.date,
                  section = ruleWeight.section,
                  sectionWeight = ruleWeight.sectionWeight,
                  score = SectionScore(score = item.score.score + value)
                )
            }
          }
      }
      .map(_.processRoute())
  }

  private def calculateRulesAtDate(
      rules: RulesRouteConfiguration,
      date: Long
  ): Option[Seq[RouteScoreByDateAndSection]] = {
    if (
      rules.states
        .sortBy(_.date)(Ordering.Long)
        .headOption
        .exists(createdAt => createdAt.date <= date)
    ) {

      val record: RuleStateRecord = RuleStateRecord(
        date = date,
        states = rules.states
          .sortBy(_.date)(Ordering.Long)
          .flatMap(r => r.states.map(state => (r.date, state)))
          .foldLeft(RulesManager.rules.map(r => RuleState(r.id, enabled = false))) {
            case (acc, (recordState, state)) => {
              if (recordState <= date) {
                acc.map(p => if (p.id == state.id) state else p)
              } else {
                acc
              }
            }
          }
      )

      record.states
        .foldLeft(Seq.empty[RouteScoreByDateAndSection]) { case (acc, rule) =>
          val ruleWeight = RulesManager.rules.find(r => r.id == rule.id).get
          val value      =
            if (rule.enabled) MAX_GREEN_SCORE_NOTE * (ruleWeight.sectionWeight / 100) * (ruleWeight.weight / 100) else 0

          acc.find(score => score.section == ruleWeight.section) match {
            case None       =>
              acc :+ RouteScoreByDateAndSection(
                date = date,
                section = ruleWeight.section,
                sectionWeight = ruleWeight.sectionWeight,
                score = SectionScore(score = value)
              )
            case Some(item) =>
              acc.filter(score => !(score.section == ruleWeight.section)) :+ RouteScoreByDateAndSection(
                date = date,
                section = ruleWeight.section,
                sectionWeight = ruleWeight.sectionWeight,
                score = SectionScore(score = item.score.score + value)
              )
          }
        }
        .map(_.processRoute())
        .some
    } else {
      None
    }
  }

  private def mergeRoutesScoreByDateAndSection(routes: Seq[(RouteRules, RouteScore)]) = {
    val dates = routes
      .flatMap(item => item._2.sectionsScoreByDate.map(_.date))
      .sorted(Ordering.Long)
      .toSet

    dates.foldLeft(Seq.empty[RouteScoreAtDate]) { case (acc, date) =>
      acc :+ RouteScoreAtDate(
        date = date,
        routes = routes
          .map(route => {
            val rules = calculateRulesAtDate(route._1.rulesConfig, date)
            (route._2.groupId, route._1.routeId, rules)
          })
          .filter(item => item._3.nonEmpty)
          .map(item => RouteScoreAtDateItem(item._1, item._2, item._3.get))
      )
    }
  }

  private def mergeRouteDynamicValues(dynamicScore: Seq[Dynamicvalues]): Dynamicvalues = {
    if (dynamicScore.isEmpty) {
      Dynamicvalues()
    } else {
      val result = dynamicScore
        .foldLeft(Dynamicvalues()) { case (acc, item) => acc.merge(item) }

      result.mean(dynamicScore.length)
    }
  }

  def calculateGlobalScore(groups: Seq[GreenScoreEntity]): GroupScore = {
    val routes: Seq[(RouteRules, RouteScore)] =
      groups.flatMap(group => group.routes.map(route => calculateRouteScore(route, group)))

    // x date -> n routes -> 4 sections
    val routesAtDate: Seq[RouteScoreAtDate] = mergeRoutesScoreByDateAndSection(routes)

    val dynamicValuesByRoutes =
      routes.map(route => RouteDynamicValues(route._1.routeId, route._2.groupId, route._2.dynamicValues))

    GroupScore(
      dynamicValuesByRoutes = dynamicValuesByRoutes,
      dynamicValues = mergeRouteDynamicValues(routes.map(score => score._2.dynamicValues)),
      routeScoreByDate = routesAtDate
    )
  }

  private def calculateRouteScore(route: RouteRules, group: GreenScoreEntity) = {
    val sectionsScoreByDate: Seq[RouteScoreByDateAndSection] = calculateRulesByDate(route.rulesConfig)

    val routeScore = registry.route(route.routeId).getOrElse(RouteReservoirs())

    (
      route,
      RouteScore(
        groupId = group.id,
        sectionsScoreByDate = sectionsScoreByDate,
        dynamicValues = Dynamicvalues(
          scaling = ScalingRouteReservoirs.from(routeScore, group.thresholds),
          raw = ScalingRouteReservoirs.from(routeScore),
          counters = DynamicTripleBounds().from(routeScore, group.thresholds)
        )
      )
    )
  }

  def json(routeId: String): JsValue = registry.json(routeId)

  def updateRoute(routeCallIncr: RouteCallIncr): Unit = {
    registry.updateRoute(routeCallIncr)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy