app/com/gu/contentapi/sanity/support/CloudWatchReportingSupport.scala (85 lines of code) (raw):
package com.gu.contentapi.sanity.support
import org.scalatest.{Failed, Outcome, Succeeded, TestSuite}
import org.slf4j.LoggerFactory
import play.api.{Configuration, Logger}
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.cloudwatch.model.{MetricDatum, PutMetricDataRequest, PutMetricDataResponse}
import software.amazon.awssdk.services.cloudwatch.{CloudWatchAsyncClient, CloudWatchClient, CloudWatchClientBuilder}
import java.util.concurrent.CompletableFuture
import scala.concurrent.ExecutionContext.Implicits.global
import scala.jdk.FutureConverters
import scala.jdk.FutureConverters._
import scala.util.{Failure, Success, Try}
trait CloudWatchReportingSupport extends TestSuite {
def cloudWatchReporter: CloudWatchReporter
override protected def withFixture(test: NoArgTest): Outcome = {
val outcome = super.withFixture(test)
outcome match {
case Failed(e) => cloudWatchReporter.reportFailedTest()
case Succeeded => cloudWatchReporter.reportSuccessfulTest()
case _ => // do nothing
}
outcome
}
}
trait CloudWatchReporter {
def reportSuccessfulTest(): Unit
def reportFailedTest(): Unit
def reportTestRunComplete(): Unit
}
object CloudWatchReporter {
private val logger = LoggerFactory.getLogger(getClass)
def apply(config: Configuration): CloudWatchReporter = {
def cfg(key: String) = config.getOptional[String](s"content-api-sanity-tests.cloudwatch-$key")
val reporter = for {
namespace <- cfg("namespace")
testRunsMetric <- cfg("test-runs-metric")
successfulTestsMetric <- cfg("successful-tests-metric")
failedTestsMetric <- cfg("failed-tests-metric")
} yield {
logger.info(s"Will report metrics to CloudWatch. namespace=$namespace, metrics=($testRunsMetric, $successfulTestsMetric, $failedTestsMetric)")
new RealCloudWatchReporter(namespace, testRunsMetric, successfulTestsMetric, failedTestsMetric)
}
reporter getOrElse {
logger.info("Will not report any metrics to CloudWatch")
DoNothingCloudWatchReporter
}
}
}
class RealCloudWatchReporter(namespace: String,
testRunsMetric: String,
successfulTestsMetric: String,
failedTestsMetric: String) extends CloudWatchReporter {
private val logger = LoggerFactory.getLogger(getClass)
lazy val cloudwatch = CloudWatchAsyncClient.builder().region(Region.EU_WEST_1).build()
private def loggingAsyncHandler[A](input:Try[A]) = input match {
case Success(_)=>
logger.info("CloudWatch PutMetricData request - success")
case Failure(exception)=>
logger.warn(s"CloudWatch PutMetricDataRequest error: ${exception.getMessage}}")
}
/**
* Convenience method to keep the old behaviour.
* Takes a function which returns a Java style future, and wraps this into a Scala future with some basic
* success/failure logging on it.
* @param f function to run. This must return some kind of CompletableFuture
*/
private def loggedAsyncCall[T](f: ()=>CompletableFuture[T]) = {
f().asScala.onComplete(loggingAsyncHandler)
}
private def put(metricName: String, value: Double): Unit = {
val metric = MetricDatum.builder()
.value(value)
.metricName(metricName)
.build()
val request = PutMetricDataRequest.builder()
.namespace(namespace)
.metricData(metric)
.build()
loggedAsyncCall { ()=>
cloudwatch.putMetricData(request)
}
}
override def reportSuccessfulTest(): Unit = put(successfulTestsMetric, 1)
override def reportFailedTest(): Unit = put(failedTestsMetric, 1)
override def reportTestRunComplete(): Unit = put(testRunsMetric, 1)
}
object DoNothingCloudWatchReporter extends CloudWatchReporter {
override def reportSuccessfulTest(): Unit = {}
override def reportFailedTest(): Unit = {}
override def reportTestRunComplete(): Unit = {}
}