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

com.wavesplatform.ResponsivenessLogs.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform

import java.io.{FileOutputStream, PrintWriter}
import java.time.LocalDate
import java.time.format.DateTimeFormatter

import scala.collection.mutable
import scala.concurrent.duration.Duration
import scala.util.Try

import com.wavesplatform.account.Address
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.lang.ValidationError
import com.wavesplatform.metrics.Metrics
import com.wavesplatform.transaction.{AuthorizedTransaction, Transaction, TxValidationError}
import com.wavesplatform.transaction.smart.InvokeScriptTransaction
import com.wavesplatform.utils.ScorexLogging
import org.influxdb.dto.Point

private class ResponsivenessLogs(csvPrefix: String, metricName: String) extends ScorexLogging {
  import ResponsivenessLogs.TxEvent

  //noinspection ScalaStyle
  private[this] case class MetricSnapshot(point: Point.Builder = null, nano: Long = System.nanoTime(), millis: Long = System.currentTimeMillis())

  private[this] case class TxState(
      received: Long,
      lastReceived: Long,
      firstMined: Option[MetricSnapshot],
      lastMined: Option[MetricSnapshot],
      failed: Option[MetricSnapshot],
      miningAttempt: Int,
      height: Int
  )
  private[this] val stateMap = mutable.AnyRefMap.empty[ByteStr, TxState]

  def writeEvent(
      height: Int,
      tx: Transaction,
      eventType: TxEvent,
      reason: Option[ValidationError] = None,
      writeCsv: Boolean = false,
      retentionPolicy: String = ""
  ): Unit =
    Try(synchronized {
      val reasonClass = reason match {
        case Some(value)                       => value.getClass.getSimpleName
        case _ if eventType == TxEvent.Expired => "Expired"
        case _                                 => "Unknown"
      }

      val isAlreadyInTheState = eventType == TxEvent.Invalidated && reasonClass != "AlreadyInTheState"

      def writeMetrics(): Unit = {
        def toMillis(ns: Long) = Duration.fromNanos(ns).toMillis
        val nowNanos           = System.nanoTime()

        if (eventType == TxEvent.Received)
          stateMap(tx.id()) = stateMap.get(tx.id()) match {
            case None =>
              TxState(nowNanos, nowNanos, None, None, None, 0, height)

            case Some(state) =>
              state.copy(
                lastReceived = nowNanos,
                lastMined = None,
                miningAttempt = if (state.lastMined.nonEmpty) state.miningAttempt + 1 else state.miningAttempt
              )
          }

        val basePoint = Point
          .measurement(metricName)
          .tag("id", tx.id().toString)
          .tag("event", eventType.toString.toLowerCase)
          .addField("type", tx.tpe.id)
          .addField("height", height)

        if (eventType == TxEvent.Mined) {
          stateMap.get(tx.id()).foreach {
            case TxState(received, lastReceived, firstMined, _, _, attempt, _) =>
              val delta     = toMillis(nowNanos - received)
              val lastDelta = toMillis(nowNanos - lastReceived)
              log.trace(s"Neutrino mining time for ${tx.id()} (attempt #$attempt): $delta ms ($lastDelta from last recv)")

              val snapshot = MetricSnapshot(basePoint.addField("time-to-mine", delta).addField("time-to-last-mine", lastDelta), nowNanos)
              stateMap(tx.id()) = TxState(
                received,
                lastReceived,
                firstMined.orElse(Some(snapshot)),
                Some(snapshot),
                None,
                attempt,
                height
              )
          }
        } else if (eventType == TxEvent.Expired || (eventType == TxEvent.Invalidated && !isAlreadyInTheState)) {
          stateMap.get(tx.id()).foreach {
            case st @ TxState(received, lastReceived, firstMined, _, _, _, _) =>
              val delta     = toMillis(nowNanos - received)
              val lastDelta = toMillis(nowNanos - lastReceived)
              log.trace(s"Neutrino fail time for ${tx.id()}: $delta ms")

              val baseFailedPoint = basePoint
                .tag("reason", reasonClass)
                .addField("time-to-fail", delta)
                .addField("time-to-last-fail", lastDelta)

              val failedPoint = firstMined match {
                case Some(ms) =>
                  val ffDelta    = toMillis(nowNanos - ms.nano)
                  val firstDelta = toMillis(ms.nano - received)
                  baseFailedPoint
                    .addField("time-to-first-mine", firstDelta)
                    .addField("time-to-finish-after-first-mining", ffDelta)

                case None =>
                  baseFailedPoint
              }

              stateMap(tx.id()) = st.copy(failed = Some(MetricSnapshot(failedPoint)))
          }
        }

        def writeMinedPoint(received: Long, firstMined: Option[MetricSnapshot], mined: MetricSnapshot) = {
          val ffDelta    = toMillis(firstMined.fold(0L)(ms => mined.nano - ms.nano))
          val firstDelta = toMillis(firstMined.fold(0L)(ms => ms.nano - received))
          val finalPoint = mined.point
            .addField("time-to-first-mine", firstDelta)
            .addField("time-to-finish-after-first-mining", ffDelta)
          log.trace(s"Writing mined responsiveness point: ${finalPoint.build()}")
          Metrics.write(finalPoint, mined.millis)
        }

        stateMap.toVector.collect {
          case (txId, TxState(received, _, firstMined, Some(mined), _, _, h)) if (h + 5) <= height =>
            writeMinedPoint(received, firstMined, mined)
            stateMap -= txId

          case (txId, TxState(received, _, firstMined, Some(mined), Some(MetricSnapshot(point, _, millis)), _, h)) if (h + 100) <= height =>
            log.trace(s"Writing failed responsiveness point: ${point.build()}")
            Metrics.write(point, millis)
            writeMinedPoint(received, firstMined, mined)
            stateMap -= txId

          case (txId, st) if (st.height + 1000) < height =>
            stateMap -= txId
        }
      }

      def writeCsvLog(prefix: String): Unit = {
        def escape(s: String): String = s.replaceAll("\\r", "\\\\r").replaceAll("\\n", "\\\\n")

        val date       = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)
        val fileStream = new FileOutputStream(s"${sys.props("waves.directory")}/$prefix-events-$date.csv", true)
        val pw         = new PrintWriter(fileStream)
        val reasonEscaped = reason match {
          case Some(see: TxValidationError.ScriptExecutionError)        => s"ScriptExecutionError(${escape(see.message)})"
          case Some(_: TxValidationError.TransactionNotAllowedByScript) => "TransactionNotAllowedByScript"
          case Some(err)                                                => escape(err.toString)
          case None                                                     => ""
        }
        val txType    = tx.tpe.id
        val timestamp = System.currentTimeMillis()
        val txJson    = if (eventType == TxEvent.Expired || eventType == TxEvent.Invalidated) tx.json().toString() else ""
        val logLine   = s"${tx.id()};$eventType;$height;$txType;$timestamp;$reasonClass;$reasonEscaped;$txJson"
        // log.info(logLine)
        try pw.println(logLine)
        finally pw.close()
      }

      Metrics.withRetentionPolicy(retentionPolicy)(writeMetrics())
      if (writeCsv && !isAlreadyInTheState) writeCsvLog(csvPrefix)
    }).failed.foreach(log.error("Error writing responsiveness metrics", _))
}

object ResponsivenessLogs {
  var enableMetrics   = false
  var enableCsv       = false
  var retentionPolicy = ""

  private[this] val neutrino = new ResponsivenessLogs("neutrino", "neutrino")
  private[this] val ordinary = new ResponsivenessLogs("tx", "blockchain-responsiveness")

  type TxEvent = TxEvent.Value
  object TxEvent extends Enumeration {
    val Received, Mined, Expired, Invalidated = Value
  }

  def isNeutrino(tx: Transaction): Boolean = {
    val txAddrs = tx match {
      case is: InvokeScriptTransaction =>
        Seq(is.senderAddress) ++ (is.dApp match {
          case a: Address => Seq(a)
          case _          => Nil
        })
      case a: AuthorizedTransaction => Seq(a.sender.toAddress)
      case _                        => Nil
    }

    val neutrinoAddrs = Set(
      "3PC9BfRwJWWiw9AREE2B3eWzCks3CYtg4yo",
      "3PG2vMhK5CPqsCDodvLGzQ84QkoHXCJ3oNP",
      "3P5Bfd58PPfNvBM2Hy8QfbcDqMeNtzg7KfP",
      "3P4PCxsJqMzQBALo8zANHtBDZRRquobHQp7",
      "3PNikM6yp4NqcSU8guxQtmR5onr2D4e8yTJ"
    )

    txAddrs.map(_.toString).exists(neutrinoAddrs)
  }

  def writeEvent(height: Int, tx: Transaction, eventType: TxEvent, reason: Option[ValidationError] = None): Unit =
    if (!enableMetrics) ()
    else {
      if (isNeutrino(tx)) neutrino.writeEvent(height, tx, eventType, reason, enableCsv, retentionPolicy)
      ordinary.writeEvent(height, tx, eventType, reason, enableCsv, retentionPolicy)
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy