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

com.netflix.atlas.webapi.GraphRequestActor.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.io.ByteArrayOutputStream
import java.time.Duration

import akka.actor.Actor
import akka.actor.ActorLogging
import akka.actor.ActorRef
import com.netflix.atlas.akka.DiagnosticMessage
import com.netflix.atlas.chart.AxisDef
import com.netflix.atlas.chart.LineStyle
import com.netflix.atlas.chart.PlotDef
import com.netflix.atlas.chart.SeriesDef
import com.netflix.atlas.core.model._
import com.netflix.atlas.core.stacklang.Interpreter
import com.netflix.atlas.core.stacklang.StandardVocabulary
import com.netflix.atlas.core.util.PngImage
import com.netflix.atlas.core.util.Strings
import com.netflix.atlas.json.Json
import com.netflix.spectator.api.Spectator
import spray.can.Http
import spray.http.MediaTypes._
import spray.http._


class GraphRequestActor extends Actor with ActorLogging {

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

  private def queryInterpreter = new Interpreter(QueryVocabulary.allWords ::: StandardVocabulary.allWords)

  private val errorId = Spectator.registry().createId("atlas.graph.errorImages")

  private val dbRef = context.actorSelection("/user/db")

  private var request: Request = _
  private var responseRef: ActorRef = _

  def receive = {
    case v => try innerReceive(v) catch {
      case t: Exception if request != null && request.shouldOutputImage =>
        // When viewing a page in a browser an error response is not rendered. To make it more
        // clear to the user we return a 200 with the error information encoded into an image.
        sendErrorImage(t, request.flags.width, request.flags.height, sender())
      case t: Throwable =>
        DiagnosticMessage.handleException(sender())(t)
    }
  }

  def innerReceive: Receive = {
    case req: Request =>
      request = req
      responseRef = sender()
      dbRef.tell(req.toDbRequest, self)
    case DataResponse(data) => sendImage(data)
    case ev: Http.ConnectionClosed =>
      log.info("connection closed")
      context.stop(self)
  }

  private def sendErrorImage(t: Throwable, w: Int, h: Int, responder: ActorRef) {
    val simpleName = t.getClass.getSimpleName
    Spectator.registry().counter(errorId.withTag("error", simpleName)).increment()
    val msg = s"$simpleName: ${t.getMessage}"
    val image = HttpEntity(`image/png`, PngImage.error(msg, w, h).toByteArray)
    responder ! HttpResponse(status = StatusCodes.OK, entity = image)
  }

  private def sendImage(data: Map[DataExpr, List[TimeSeries]]): Unit = {
    val ts = data.values.flatten

    val seriesList = request.exprs.flatMap { s =>
      val ts = s.expr.eval(request.evalContext, data).data
      val exprStr = s.expr.toString
      val tmp = ts.map { t =>
        val offset = Strings.toString(Duration.ofMillis(s.offset))
        val newT = t.withTags(t.tags + (TagKey.offset -> offset))
        val series = new SeriesDef
        series.data = newT
        series.query = exprStr
        series.label = s.legend(newT)
        series.axis = s.axis
        series.color = s.color
        series.alpha = s.alpha
        series.style = s.lineStyle.fold(LineStyle.LINE)(s => LineStyle.valueOf(s.toUpperCase))
        series.lineWidth = s.lineWidth
        series.palette = if (s.offset > 0L) "bw" else request.flags.palette
        series
      }
      tmp.sortWith(_.label < _.label)
    }

    val graphDef = request.newGraphDef

    //default axis
    val axisDef = new AxisDef
    graphDef.axis = Map(0 -> axisDef)

    //add default axis definitions on the right side of the graph for any
    //series which specified axis but did not specify a corresponding axis definition
    seriesList.foreach { seriesDef =>
      val yaxis = seriesDef.axis.getOrElse(0)
      if (!graphDef.axis.contains(yaxis)) {
        val ax = new AxisDef
        ax.rightSide = true
        graphDef.axis += (yaxis -> ax)
      }
    }

    for ((yax, axdef) <- graphDef.axis) {
      val axis = request.flags.axes(yax)
      axdef.min = axis.lower
      axdef.max = axis.upper
      axdef.label = axis.ylabel
      axdef.logarithmic = axis.logarithmic
      axdef.stack = axis.stack
    }

    val plotDef = new PlotDef
    plotDef.series = seriesList
    graphDef.plots = List(plotDef)

    val baos = new ByteArrayOutputStream
    request.engine.write(graphDef, baos)

    val entity = HttpEntity(request.contentType, baos.toByteArray)
    responseRef ! HttpResponse(StatusCodes.OK, entity)
    context.stop(self)
  }

  private def sendJson(data: AnyRef): Unit = {
    val entity = HttpEntity(`application/json`, Json.encode(data))
    responseRef ! HttpResponse(StatusCodes.OK, entity)
    context.stop(self)
  }

  private def sendText(data: String): Unit = {
    val entity = HttpEntity(`text/plain`, data)
    responseRef ! HttpResponse(StatusCodes.OK, entity)
    context.stop(self)
  }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy