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

akka.projection.internal.Telemetry.scala Maven / Gradle / Ivy

There is a newer version: 1.5.0-M4
Show newest version
/*
 * Copyright (C) 2020-2023 Lightbend Inc. 
 */

package akka.projection.internal

import java.util

import scala.collection.immutable

import akka.NotUsed
import akka.actor.typed.ActorSystem
import akka.annotation.InternalApi
import akka.annotation.InternalStableApi
import akka.projection.ProjectionId
import akka.util.ccompat.JavaConverters._

/**
 * Service Provider Interface (SPI) for collecting metrics from projections.
 *
 * Implementations must include a single constructor with two arguments: [[ProjectionId]]
 * and [[ActorSystem]].  To setup your implementation, add a setting on your `application.conf`:
 *
 * {{{
 * akka.projection.telemetry.implementations += com.example.MyMetrics
 * }}}
 */
@InternalStableApi
trait Telemetry {

  /** Invoked when a projection is stopped. The reason for stopping is unspecified, can be a
   * graceful stop or a failure (see [[Telemetry.failed]]).
   */
  def stopped(): Unit

  /**
   * Invoked when a projection processing an envelope fails (even after all retry attempts).  The
   * projection may then be restarted by a supervisor.
   *
   * @param cause exception thrown by the errored envelope handler.
   */
  def failed(cause: Throwable): Unit

  /**
   * Invoked as soon as the envelope is read, deserialised and ready to be processed.
   *
   * @param envelope          the envelope that's ready for processing.  The type `Envelope` will always
   *                          represent a single item as stored in the event log.
   * @param creationTimeInMillis Timestamp (in millis-since-epoch) of the instant when the envelope was created. The
   *                             meaning of "when the envelope was created" is implementation specific and could be
   *                             an instant on the producer machine, or the instant when the database persisted the
   *                             envelope, or other.
   * @return an externally-provided context that will propagate with the envelope until [[Telemetry.afterProcess]]
   */
  def beforeProcess[Envelope](envelope: Envelope, creationTimeInMillis: Long): AnyRef

  /**
   * Invoked after processing an event such that it is visible by the read-side threads (data is
   * committed).  This method is granted to be invoked after the envelope handler has committed but
   * may or may not happen after the offset was committed (depending on the projection semantics).
   *
   * @param externalContext the context produced by [[Telemetry.beforeProcess]] and attached to the processed envelope.
   */
  def afterProcess(externalContext: AnyRef): Unit

  /**
   * Invoked when the offset is committed.
   *
   * @param numberOfEnvelopes number of envelopes marked as committed when committing this offset.  This takes
   *                  into consideration both batched processing (only commit one offset every N
   *                  envelopes) and grouped handling (user code processes multiple envelopes at
   *                  once).
   */
  def onOffsetStored(numberOfEnvelopes: Int): Unit

  /**
   * Invoked when processing an envelope errors.  When using a [[akka.projection.HandlerRecoveryStrategy]] that
   * retries, this method will be invoked as many times as retries.  If the error propagates and
   * causes the projection to fail [[Telemetry.failed]] will be invoked.
   *
   * @param cause exception thrown by the errored envelope handler.
   */
  def error(cause: Throwable): Unit
}

/**
 * INTERNAL API
 */
@InternalApi private[projection] object TelemetryProvider {
  def start(projectionId: ProjectionId, system: ActorSystem[_]): Telemetry = {
    if (!system.settings.config.hasPath("akka.projection.telemetry.implementations")) {
      NoopTelemetry
    } else {
      val telemetryFqcns: util.List[String] =
        system.settings.config.getStringList("akka.projection.telemetry.implementations")

      telemetryFqcns.size() match {
        case 0 =>
          NoopTelemetry
        case 1 =>
          val fqcn = telemetryFqcns.get(0)
          create(projectionId, system, fqcn)
        case _ =>
          new EnsembleTelemetry(telemetryFqcns.asScala.toSeq, projectionId, system)
      }
    }
  }

  def create(projectionId: ProjectionId, system: ActorSystem[_], fqcn: String) = {
    val dynamicAccess = system.dynamicAccess
    dynamicAccess
      .createInstanceFor[Telemetry](
        fqcn,
        immutable.Seq((classOf[ProjectionId], projectionId), (classOf[ActorSystem[_]], system)))
      .get
  }
}

/**
 * INTERNAL API
 */
@InternalApi private[projection] object NoopTelemetry extends Telemetry {
  override def failed(cause: Throwable): Unit = {}

  override def stopped(): Unit = {}

  override def beforeProcess[Envelope](envelope: Envelope, creationTimeInMillis: Long): AnyRef = NotUsed

  override def afterProcess(externalContext: AnyRef): Unit = {}

  override def onOffsetStored(numberOfEnvelopes: Int): Unit = {}

  override def error(cause: Throwable): Unit = {}

}

/**
 * INTERNAL API
 */
@InternalApi private[projection] class EnsembleTelemetry(
    telemetryFqcns: Seq[String],
    projectionId: ProjectionId,
    system: ActorSystem[_])
    extends Telemetry {

  val telemetries = telemetryFqcns.map(fqcn => TelemetryProvider.create(projectionId, system, fqcn))

  override def stopped(): Unit = telemetries.foreach(_.stopped())
  override def failed(cause: Throwable): Unit = telemetries.foreach(_.failed(cause))
  override def beforeProcess[Envelope](envelope: Envelope, creationTimeInMillis: Long): AnyRef =
    telemetries.map(_.beforeProcess(envelope, creationTimeInMillis))
  override def afterProcess(externalContext: AnyRef): Unit = {
    val contexts = externalContext.asInstanceOf[Seq[AnyRef]]
    contexts.zip(telemetries).foreach { case (ctx, telemetry) => telemetry.afterProcess(ctx) }
  }
  override def onOffsetStored(numberOfEnvelopes: Int): Unit = telemetries.foreach(_.onOffsetStored(numberOfEnvelopes))
  override def error(cause: Throwable): Unit = telemetries.foreach(_.error(cause))

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy