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

com.netflix.atlas.lwcapi.ExpressionApi.scala Maven / Gradle / Ivy

The newest version!
/*
 * 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.lwcapi

import org.apache.pekko.http.scaladsl.model.HttpEntity
import org.apache.pekko.http.scaladsl.model.HttpHeader
import org.apache.pekko.http.scaladsl.model.HttpResponse
import org.apache.pekko.http.scaladsl.model.MediaTypes
import org.apache.pekko.http.scaladsl.model.StatusCodes
import org.apache.pekko.http.scaladsl.model.headers.*
import org.apache.pekko.http.scaladsl.server.Directives.*
import org.apache.pekko.http.scaladsl.server.Route
import org.apache.pekko.util.ByteString
import com.github.benmanes.caffeine.cache.CacheLoader
import com.github.benmanes.caffeine.cache.Caffeine
import com.netflix.atlas.core.util.FastGzipOutputStream
import com.netflix.atlas.core.util.Strings
import com.netflix.atlas.json.Json
import com.netflix.atlas.json.JsonSupport
import com.netflix.atlas.pekko.CustomDirectives.*
import com.netflix.atlas.pekko.WebApi
import com.netflix.spectator.api.Registry
import com.typesafe.scalalogging.StrictLogging

import java.io.ByteArrayOutputStream
import java.time.Duration
import java.util.zip.CRC32
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.util.Using

case class ExpressionApi(
  sm: StreamSubscriptionManager,
  registry: Registry
) extends WebApi
    with StrictLogging {

  import ExpressionApi.*

  private implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.global

  private val expressionCount = registry.distributionSummary("atlas.lwcapi.expressions.count")

  private val responseCache = Caffeine
    .newBuilder()
    .expireAfterWrite(Duration.ofSeconds(10))
    .build[Option[String], EncodedExpressions](
      new CacheLoader[Option[String], EncodedExpressions] {

        override def load(key: Option[String]): EncodedExpressions = {
          val expressions = key match {
            case Some(cluster) => sm.subscriptionsForCluster(cluster).map(_.metadata)
            case None          => sm.subscriptions.map(_.metadata)
          }
          encode(expressions)
        }
      }
    )

  /** Helper for testing to force the recomputation of encoded expressions. */
  private[lwcapi] def clearCache(): Unit = {
    responseCache.invalidateAll()
  }

  def routes: Route = {
    endpointPathPrefix("lwc" / "api" / "v1" / "expressions") {
      optionalHeaderValueByName("If-None-Match") { etags =>
        get {
          pathEndOrSingleSlash {
            complete(Future(handleList(etags)))
          } ~
          path(Segment) { cluster =>
            complete(Future(handleGet(etags, cluster)))
          }
        }
      }
    }
  }

  private def handleList(receivedETags: Option[String]): HttpResponse = {
    handle(receivedETags, responseCache.get(None))
  }

  private def handleGet(receivedETags: Option[String], cluster: String): HttpResponse = {
    handle(receivedETags, responseCache.get(Some(cluster)))
  }

  private def handle(
    receivedETags: Option[String],
    expressions: EncodedExpressions
  ): HttpResponse = {
    expressionCount.record(expressions.size)
    val tag = expressions.etag
    val headers: List[HttpHeader] = List(RawHeader("ETag", tag))
    val recvTags = receivedETags.getOrElse("")
    if (recvTags.contains(tag)) {
      HttpResponse(StatusCodes.NotModified, headers = headers)
    } else {
      val entity = HttpEntity(MediaTypes.`application/json`, expressions.data)
      HttpResponse(StatusCodes.OK, `Content-Encoding`(HttpEncodings.gzip) :: headers, entity)
    }
  }
}

object ExpressionApi {

  case class Return(expressions: List[ExpressionMetadata]) extends JsonSupport

  case class EncodedExpressions(etag: String, data: ByteString, size: Int)

  private val streams = new ThreadLocal[ByteArrayOutputStream]

  /** Use thread local to reuse byte array buffers across calls. */
  private def getOrCreateStream: ByteArrayOutputStream = {
    var baos = streams.get
    if (baos == null) {
      baos = new ByteArrayOutputStream
      streams.set(baos)
    } else {
      baos.reset()
    }
    baos
  }

  private[lwcapi] def encode(expressions: List[ExpressionMetadata]): EncodedExpressions = {
    val baos = getOrCreateStream
    Using.resource(new FastGzipOutputStream(baos)) { out =>
      Using.resource(Json.newJsonGenerator(out)) { gen =>
        Return(expressions.sorted).encode(gen)
      }
    }
    val bytes = baos.toByteArray

    val crc = new CRC32
    crc.update(bytes)
    crc.getValue
    val hash = Strings.zeroPad(crc.getValue, 16)
    val etag = s""""$hash""""

    val data = ByteString.fromArrayUnsafe(baos.toByteArray)
    EncodedExpressions(etag, data, expressions.size)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy