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

com.netflix.atlas.webapi.GraphApi.scala Maven / Gradle / Ivy

/*
 * Copyright 2015 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.webapi

import java.time.Instant
import java.time.ZoneId

import akka.actor.ActorRefFactory
import akka.actor.Props
import com.netflix.atlas.akka.WebApi
import com.netflix.atlas.chart.GraphDef
import com.netflix.atlas.chart.GraphEngine
import com.netflix.atlas.chart.VisionType
import com.netflix.atlas.core.model.DataExpr
import com.netflix.atlas.core.model.EvalContext
import com.netflix.atlas.core.model.Extractors
import com.netflix.atlas.core.model.StyleExpr
import com.netflix.atlas.core.model.StyleVocabulary
import com.netflix.atlas.core.model.TimeSeries
import com.netflix.atlas.core.stacklang.Interpreter
import com.netflix.atlas.core.stacklang.StandardVocabulary
import com.netflix.atlas.core.util.Step
import com.netflix.atlas.core.util.Strings
import spray.http.HttpRequest
import spray.http.MediaType
import spray.http.Uri
import spray.routing.RequestContext


class GraphApi(implicit val actorRefFactory: ActorRefFactory) extends WebApi {

  import com.netflix.atlas.webapi.GraphApi._

  def routes: RequestContext => Unit = {
    path("api" / "v1" / "graph") {
      get { ctx =>
        try {
          val reqHandler = actorRefFactory.actorOf(Props(new GraphRequestActor))
          reqHandler.tell(toRequest(ctx.request), ctx.responder)
        } catch handleException(ctx)
      }
    }
  }

  private def toRequest(req: HttpRequest): Request = {
    val params = req.uri.query
    val id = "default"

    import com.netflix.atlas.chart.GraphConstants._
    val axes = (0 to MaxYAxis).map(i => i -> newAxis(params, i)).toMap

    val vision = params.get("vision").map(v => VisionType.valueOf(v))

    val flags = ImageFlags(
      title = params.get("title"),
      fontSize = params.get("font_size").map(_.toInt),
      width = params.get("w").fold(ApiSettings.width)(_.toInt),
      height = params.get("h").fold(ApiSettings.height)(_.toInt),
      axes = axes,
      axisPerLine = params.get("axis_per_line").contains("1"),
      showLegend = !params.get("no_legend").contains("1"),
      showLegendStats = !params.get("no_legend_stats").contains("1"),
      showBorder = !params.get("no_border").contains("1"),
      showOnlyGraph = params.get("only_graph").contains("1"),
      vision = vision.getOrElse(VisionType.normal),
      palette = params.get("palette").getOrElse(ApiSettings.palette)
    )

    val q = params.get("q")
    if (!q.isDefined) {
      throw new IllegalArgumentException("missing required parameter 'q'")
    }

    Request(
      query = q.get,
      start = params.get("s"),
      end = params.get("e"),
      timezone = params.get("tz"),
      step = params.get("step"),
      flags = flags,
      format = params.get("format").getOrElse("png"),
      numberFormat = params.get("number_format").getOrElse("%f"),
      id = id,
      isBrowser = false,
      isAllowedFromBrowser = true)
  }

  private def newAxis(params: Uri.Query, id: Int): Axis = {
    Axis(
      upper = params.get(s"u.$id").orElse(params.get("u")).map(_.toDouble),
      lower = params.get(s"l.$id").orElse(params.get("l")).map(_.toDouble),
      logarithmic = params.get(s"o.$id").orElse(params.get("o")) == Some("1"),
      stack = params.get(s"stack.$id").orElse(params.get("stack")) == Some("1"),
      ylabel = params.get(s"ylabel.$id").orElse(params.get("ylabel")))
  }

}

object GraphApi {

  private val interpreter = new Interpreter(StyleVocabulary.allWords ::: StandardVocabulary.allWords)

  private val engines = ApiSettings.engines.map(e => e.name -> e).toMap

  private val contentTypes = engines.map { case (k, e) =>
    k -> MediaType.custom(e.contentType)
  }

  case class Request(
      query: String,
      start: Option[String],
      end: Option[String],
      timezone: Option[String],
      step: Option[String],
      flags: ImageFlags,
      format: String,
      numberFormat: String,
      id: String,
      isBrowser: Boolean,
      isAllowedFromBrowser: Boolean) {

    def shouldOutputImage: Boolean = (format == "png")

    val tz: ZoneId = ZoneId.of(timezone.getOrElse(ApiSettings.timezone))

    // Resolved start and end time
    val (resStart, resEnd) = timeRange(
      start.getOrElse(ApiSettings.startTime), end.getOrElse(ApiSettings.endTime), tz)

    val stepSize = {
      val datapointWidth = math.min(ApiSettings.maxDatapoints, flags.width)

      val stepDuration = step.map(Strings.parseDuration)
      val stepMillis = ApiSettings.stepSize
      val stepParam = stepDuration.fold(stepMillis)(s => Step.round(stepMillis, s.toMillis))
      Step.compute(stepParam, datapointWidth, resStart.toEpochMilli, resEnd.toEpochMilli)
    }

    // Final start and end time rounded to step boundaries
    val fstart = roundToStep(resStart)
    val fend = roundToStep(resEnd)

    private def timeRange(s: String, e: String, tz: ZoneId): (Instant, Instant) = {
      if (Strings.isRelativeDate(s, true)) {
        require(!Strings.isRelativeDate(e, true), "start and end are both relative")
        val end = Strings.parseDate(e, tz)
        val start = Strings.parseDate(end, s, tz)
        start.toInstant -> end.toInstant
      } else {
        val start = Strings.parseDate(s, tz)
        val end = Strings.parseDate(start, e, tz)
        start.toInstant -> end.toInstant
      }
    }

    private def roundToStep(t: (Instant, Instant)): (Instant, Instant) = {
      roundToStep(t._1) -> roundToStep(t._2)
    }

    private def roundToStep(i: Instant): Instant = {
      Instant.ofEpochMilli(i.toEpochMilli / stepSize * stepSize)
    }

    def engine: GraphEngine = engines(format)

    def contentType: MediaType = contentTypes(format)

    val evalContext: EvalContext = {
      EvalContext(fstart.toEpochMilli, fend.toEpochMilli + stepSize, stepSize)
    }

    def exprs: List[StyleExpr] = {
      interpreter.execute(query).stack.reverse.flatMap {
        case Extractors.PresentationType(s) => s.perOffset
      }
    }

    def toDbRequest: DataRequest = {
      val dataExprs = exprs.flatMap(_.expr.dataExprs)
      val deduped = dataExprs.toSet.toList
      DataRequest(evalContext, deduped)
    }

    def newGraphDef: GraphDef = {
      val graphDef = new GraphDef
      graphDef.title = flags.title
      graphDef.timezone = tz
      graphDef.startTime = fstart
      graphDef.endTime = fend
      graphDef.step = stepSize
      graphDef.width = flags.width
      graphDef.height = flags.height
      graphDef.axisPerLine = flags.axisPerLine
      graphDef.showLegend = flags.showLegend
      graphDef.showLegendStats = flags.showLegendStats
      graphDef.showBorder = flags.showBorder
      graphDef.onlyGraph = flags.showOnlyGraph
      graphDef.fontSize  = flags.fontSize
      graphDef.numberFormat = numberFormat
      graphDef.visionType = flags.vision
      graphDef
    }
  }

  case class DataRequest(context: EvalContext, exprs: List[DataExpr])

  case class DataResponse(ts: Map[DataExpr, List[TimeSeries]])

  case class Axis(
      upper: Option[Double] = None,
      lower: Option[Double] = None,
      logarithmic: Boolean = false,
      stack: Boolean = false,
      ylabel: Option[String] = None)

  case class ImageFlags(
      title: Option[String],
      fontSize: Option[Int],
      width: Int,
      height: Int,
      axes: Map[Int, Axis],
      axisPerLine: Boolean,
      showLegend: Boolean,
      showLegendStats: Boolean,
      showBorder: Boolean,
      showOnlyGraph: Boolean,
      vision: VisionType,
      palette: String)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy