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

com.wavesplatform.metrics.HttpSpanLogger.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform.metrics

import java.time.{Duration, Instant, LocalDateTime, ZoneId}
import akka.http.scaladsl.model.StatusCodes
import com.typesafe.config.Config
import com.typesafe.scalalogging.LazyLogging
import kamon.instrumentation.tag.TagKeys
import kamon.metric.PeriodSnapshot
import kamon.module.{CombinedReporter, ModuleFactory}
import kamon.tag.Lookups
import kamon.trace.Span
import play.api.libs.json.{JsObject, Json}

class HttpSpanLoggerFactory extends ModuleFactory {
  override def create(settings: ModuleFactory.Settings): HttpSpanLogger = new HttpSpanLogger
}

class HttpSpanLogger extends CombinedReporter with LazyLogging {
  import HttpSpanLogger.*
  override def reportSpans(spans: Seq[Span.Finished]): Unit = logger.whenInfoEnabled {
    for (span <- spans if span.isAkkaHttpServer) {
      val code = StatusCodes.getForKey(span.statusCode).fold("")(c => c.toString())

      val processingStart   = span.marks.find(_.key == ProcessingStartMark).map(_.instant)
      val akkaQueueDuration = processingStart.map(millisBetween(span.from, _))
      val processDuration =
        for {
          from <- processingStart
          to   <- span.marks.find(_.key == ResponseEndMark).map(_.instant)
        } yield millisBetween(from, to)

      val extraFields = akkaQueueDuration.toSeq.map("akka_queue_duration" -> Json.toJson(_)) ++
        processDuration.toSeq.map("process_duration" -> Json.toJson(_))
      val json = Json.obj(
        "@timestamp" -> LocalDateTime.ofInstant(span.from, ZoneId.systemDefault()),
        "trace_id"   -> span.trace.id.string,
        "method"     -> span.method,
        "path"       -> span.operation,
        "code"       -> code,
        "duration"   -> millisBetween(span.from, span.to)
      ) ++ JsObject(extraFields)

      logger.info(json.toString)
    }
  }

  override def reportPeriodSnapshot(snapshot: PeriodSnapshot): Unit = {
    val timeInQueue = snapshot.timers.filter(_.name == TimeInQueueMetricKey)
    val distributions = timeInQueue
      .flatMap(_.instruments)
      .collect {
        case snap if snap.tags.get(Lookups.plain("name")) == "heavy-request-executor" && snap.value.count > 0 =>
          snap.value
      }
    distributions.foreach { dist =>
      logger.info(
        Json
          .obj(
            "@timestamp"                    -> LocalDateTime.ofInstant(snapshot.from, ZoneId.systemDefault()),
            "executor_queue_duration_max"   -> dist.max,
            "executor_queue_duration_min"   -> dist.min,
            "executor_queue_duration_sum"   -> dist.sum,
            "executor_queue_duration_count" -> dist.count,
            "executor_queue_duration_avg"   -> dist.sum.toDouble / dist.count
          )
          .toString
      )
    }
  }

  override def stop(): Unit = {}

  override def reconfigure(newConfig: Config): Unit = {}
}

object HttpSpanLogger {
  val ProcessingStartMark = "processing.start"
  val ResponseEndMark     = "http.response.ready"
  val ExecutorEnqueueMark = "executor.enqueue"
  val ExecutorStartMark   = "executor.start"

  val TimeInQueueMetricKey = "executor.time-in-queue"

  case class Mark(key: String, durationMillis: Long)

  def millisBetween(from: Instant, to: Instant): Long = Duration.between(from, to).toMillis
  implicit class FinishedSpanExt(val span: Span.Finished) extends AnyVal {
    def isAkkaHttpServer: Boolean = span.metricTags.get(Lookups.option("component")).contains("akka.http.server")
    def method: String            = span.metricTags.get(Lookups.plain(TagKeys.HttpMethod))
    def statusCode: Int           = span.metricTags.get(Lookups.plainLong(TagKeys.HttpStatusCode)).toInt
    def operation: String         = span.metricTags.get(Lookups.plain("operation"))
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy