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

org.gfccollective.aws.cloudwatch.CloudWatchMetricsClient.scala Maven / Gradle / Ivy

The newest version!
package org.gfccollective.aws.cloudwatch

import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient
import software.amazon.awssdk.services.cloudwatch.model.{MetricDatum, PutMetricDataRequest}
import org.gfccollective.concurrent.JavaConverters._
import org.gfccollective.concurrent.{ExecutorService, SameThreadExecutionContext}
import org.gfccollective.logging.OpenLoggable

import scala.collection.JavaConverters._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Success
import scala.util.control.NonFatal


/** A type class of things that can be converted to CloudWatch Metrics. */
trait ToCloudWatchMetricsData[A] {
  /** A function to convert type A to CW metrics. */
  def toMetricData(a: A): Seq[MetricDatum]
}

/**
 * Push selected metrics to AWS CloudWatch.
 * Some are meant to be 'system health' metrics, like our Future-based RPC call times.
 * Some are meant to be 'business' metrics, like number of logins.
 *
 * Trait exists mainly to make it possible to inject this as a dependency.
 */
trait CloudWatchMetricsClient {

  /** Event namespace in CloudWatch will be a concatenation of all of these.
    * Every time you enter a namespace it'll get appended to the current one, e.g. "a / b / c".
    */
  def enterNamespace( n: String
                    ): CloudWatchMetricsClient


  /** Run a closure with a namespaced CW metrics client, just a convenience function.
    *
    * @param n    namespace to enter
    * @param run  closure to run
    * @tparam R   closure result type
    * @return     closure result
    */
  def withNamespace[R]( n: String
                     )( run: (CloudWatchMetricsClient) => R
                      ): R = {
    run(this.enterNamespace(n))
  }


  /** Requests metric data to be sent asynchronously.
    * It will block only to schedule async tasks, shouldn't be noticeable under low contention.
    * HTTP service calls are asynchronous.
    *
    * http://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricData.html
    *
    * @param a                object to send metrics data for
    * @param tcwmdEv          evidence that ToCloudWatchMetricsData implementation exists for type A
    * @tparam A               type of the object we convert to metrics data
    */
  def putMetricData[A]( a: A
                     )( implicit tcwmdEv: ToCloudWatchMetricsData[A]
                      ): Unit


  /** Brackets a block of code and puts a given metric if an exception occurs.
    *
    * @param t2a       construct metric from throwable (e.g. you may want to report different exceptions differently)
    * @param run       block of code to run
    * @param tcwmdEv   evidence that ToCloudWatchMetricsData implementation exists for type A
    * @tparam A        type of metric
    * @tparam R        type of result
    * @return          result of the execution of the given closure
    */
  def putExceptionMetricData[A,R]( t2a: (Throwable) => A
                                )( run: => R
                                )( implicit tcwmdEv: ToCloudWatchMetricsData[A]
                                 ): R = {
    try {
      run
    } catch {
      case NonFatal(e) =>
        this.putMetricData(t2a(e)) // report exception and re-throw it
        throw e
    }
  }


  /** Brackets a block of asynchronous code (Future result) and puts a given metric if an exception occurs
    * either synchronously or wrapped in the result of the Future.
    *
    * @param t2a       construct metric from throwable (e.g. you may want to report different exceptions differently)
    * @param run       block of code to run
    * @param tcwmdEv   evidence that ToCloudWatchMetricsData implementation exists for type A
    * @tparam A        type of metric
    * @tparam R        type of result
    * @return          result of the execution of the given closure
    */
  def putAsynchronousExceptionMetricData[A,R]( t2a: (Throwable) => A
                                            )( run: => Future[R]
                                            )( implicit tcwmdEv: ToCloudWatchMetricsData[A]
                                            ): Future[R] = {

    val safeFuture: Future[R] = this.putExceptionMetricData(t2a)(run) // if run() explodes before returning a future we capture it here

    implicit val ec = SameThreadExecutionContext // putMetricData should be lightweight

    safeFuture.onComplete {
      case Success(NonFatal(e)) =>
        this.putMetricData(t2a(e))
      case _ =>
        ()
    }

    safeFuture
  }
}


object CloudWatchMetricsClient {

  /** Creates default implementation of the CloudWatchMetricsClient.
    *
    * http://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricData.html
    *
    * @param metricNamespace CloudWatch metric namespace
    * @return CloudWatchMetricsClient instance
    */
  def apply( metricNamespace: String
           , awsCloudWatch: CloudWatchAsyncClient = CloudWatchAsyncClient.create()
           ): CloudWatchMetricsClient = CloudWatchMetricsClientImpl(metricNamespace, awsCloudWatch)

}


/** Default implementation of CloudWatchMetricsClient, trait is for dependency injection. */
private[cloudwatch]
object CloudWatchMetricsClientImpl {

  private
  val Logger = new OpenLoggable {}

  private
  val awsClient = CloudWatchAsyncClient.create


  private
  val executor: ExecutorService = {
    import java.util.concurrent._

    new ThreadPoolExecutor(
      1 // core pool size
    , 2 * Runtime.getRuntime.availableProcessors // max pool size
    , 60L, TimeUnit.SECONDS, // keep alive
      new LinkedBlockingQueue[Runnable]()
    )
  }.asScala

  private
  implicit val executionContext = ExecutionContext.fromExecutor(executor)
}


private[cloudwatch]
case class CloudWatchMetricsClientImpl (
    namespace: String
  , awsClient: CloudWatchAsyncClient
) extends CloudWatchMetricsClient {

  import CloudWatchMetricsClientImpl._
  import scala.compat.java8.FutureConverters._

  override
  def enterNamespace( n: String
                    ): CloudWatchMetricsClient = {
    this.copy(namespace = s"${this.namespace} / ${n}")
  }

  override
  def putMetricData[A]( a: A
                      )( implicit tcwmdEv: ToCloudWatchMetricsData[A]
                      ): Unit = {
    awsClient.putMetricData(
      PutMetricDataRequest.builder
        .namespace(namespace)
        .metricData(tcwmdEv.toMetricData(a).asJava)
        .build
    ).toScala.recover {
      case NonFatal(e) =>
        Logger.error(e.getMessage, e)
    }
  }
}







© 2015 - 2024 Weber Informatics LLC | Privacy Policy