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

com.netflix.atlas.eval.graph.SimpleLegends.scala Maven / Gradle / Ivy

/*
 * Copyright 2014-2024 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.atlas.eval.graph

import com.netflix.atlas.core.model.DataExpr
import com.netflix.atlas.core.model.MathExpr
import com.netflix.atlas.core.model.Query
import com.netflix.atlas.core.model.Query.KeyValueQuery
import com.netflix.atlas.core.model.StyleExpr
import com.typesafe.scalalogging.StrictLogging

/**
  * Helper to analyze a set of expressions and try to automatically set a reasonable
  * human readable legend.
  */
object SimpleLegends extends StrictLogging {

  def generate(exprs: List[StyleExpr]): List[StyleExpr] = {
    try {
      // Extract key/value pairs from all expressions
      val kvs = exprs
        .map(e => e -> extractKeyValues(e))
        .filterNot(_._2.isEmpty)
        .toMap

      if (kvs.isEmpty) {
        exprs
      } else {
        // Figure out the unique set
        val common = kvs.values.reduce(intersect)
        exprs.map { expr =>
          if (hasExplicitLegend(expr)) {
            expr
          } else if (kvs.contains(expr)) {
            val kv = kvs(expr)
            val uniq = diff(kv, common)
            if (uniq.nonEmpty)
              generateLegend(expr, uniq)
            else if (common.nonEmpty)
              generateLegend(expr, common)
            else
              expr
          } else {
            expr
          }
        }
      }
    } catch {
      // This is a nice to have presentation detail. In case it fails for any reason we
      // just fallback to the defaults. Under normal usage this is not expected to ever
      // be reached.
      case e: Exception =>
        logger.warn("failed to generate simple legend, using default", e)
        exprs
    }
  }

  private def hasExplicitLegend(expr: StyleExpr): Boolean = {
    expr.settings.contains("legend")
  }

  private def withLegend(expr: StyleExpr, legend: String): StyleExpr = {
    val label = if (expr.offset > 0L) s"$legend (offset=$$atlas.offset)" else legend
    expr.copy(settings = expr.settings + ("legend" -> label))
  }

  private def keyValues(query: Query): Map[String, String] = {
    query match {
      case Query.And(q1, q2)            => keyValues(q1) ++ keyValues(q2)
      case Query.Equal(k, v)            => Map(k -> v)
      case Query.LessThan(k, v)         => Map(k -> v)
      case Query.LessThanEqual(k, v)    => Map(k -> v)
      case Query.GreaterThan(k, v)      => Map(k -> v)
      case Query.GreaterThanEqual(k, v) => Map(k -> v)
      case Query.Regex(k, v)            => Map(k -> v)
      case Query.RegexIgnoreCase(k, v)  => Map(k -> v)
      case Query.Not(q: KeyValueQuery)  => keyValues(q).map(t => t._1 -> s"!${t._2}")
      case _                            => Map.empty
    }
  }

  private def generateLegend(expr: StyleExpr, kv: Map[String, String]): StyleExpr = {
    if (expr.expr.isGrouped) {
      val fmt = expr.expr.finalGrouping.mkString("$", " $", "")
      withLegend(expr, fmt)
    } else if (kv.contains("name")) {
      withLegend(expr, kv("name"))
    } else {
      val legend = kv.toList.sortWith(_._1 < _._1).map(_._2).mkString(" ")
      withLegend(expr, legend)
    }
  }

  private def extractKeyValues(expr: StyleExpr): Map[String, String] = {
    val dataExprs = removeNamedRewrites(expr).expr.dataExprs
    if (dataExprs.isEmpty)
      Map.empty
    else
      dataExprs.map(de => keyValues(de.query)).reduce(intersect)
  }

  private def removeNamedRewrites(expr: StyleExpr): StyleExpr = {
    // Custom averages like dist-avg and node-avg are done with rewrites that can
    // lead to confusing legends. For the purposes here those can be rewritten to
    // a simple aggregate like sum based on the display expression.
    expr
      .rewrite {
        case MathExpr.NamedRewrite(n, q: Query, evalExpr, _, _) if n.endsWith("-avg") =>
          val aggr = DataExpr.Sum(q)
          if (evalExpr.isGrouped) DataExpr.GroupBy(aggr, evalExpr.finalGrouping) else aggr
      }
      .asInstanceOf[StyleExpr]
  }

  private def intersect(m1: Map[String, String], m2: Map[String, String]): Map[String, String] = {
    m1.toSet.intersect(m2.toSet).toMap
  }

  private def diff(m1: Map[String, String], m2: Map[String, String]): Map[String, String] = {
    m1.toSet.diff(m2.toSet).toMap
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy