backend/app/services/MetricsService.scala (78 lines of code) (raw):
package services
import com.amazonaws.auth.AWSCredentialsProvider
import com.amazonaws.services.cloudwatch.{AmazonCloudWatch, AmazonCloudWatchClientBuilder}
import com.amazonaws.services.cloudwatch.model.{Dimension, MetricDatum, PutMetricDataRequest}
import utils.{AwsCredentials, Logging}
import java.util.Date
import scala.util.control.NonFatal
case class MetricUpdate(name: String, value: Double)
object Metrics {
val namespace = "PFI"
val itemsIngested = "IngestedItems"
val itemsFailed = "IngestItemsFailed"
val batchesIngested = "IngestBatchesIngested"
val batchesFailed = "IngestBatchesFailed"
val failureToResultMapper = "ErrorsInGiantFailureToResultMapper"
val usageEvents = "UsageEvents"
val searchInFolderEvents = "SearchInFolderEvents"
def metricDatum(name: String, dimensions: List[Dimension], value: Double): MetricDatum = {
new MetricDatum()
.withMetricName(name)
.withTimestamp(new Date())
.withDimensions(dimensions: _*)
.withValue(value)
}
}
trait MetricsService {
def updateMetrics(metrics: List[MetricUpdate]): Unit
def updateMetric(metricName: String, metricValue: Double = 1): Unit
def recordUsageEvent(username: String): Unit
def recordSearchInFolderEvent(username: String): Unit
}
class NoOpMetricsService() extends MetricsService {
def updateMetrics(metrics:List[MetricUpdate]): Unit = ()
def updateMetric(metricName: String, metricValue: Double = 1): Unit = ()
def recordUsageEvent(username: String): Unit = ()
def recordSearchInFolderEvent(username: String): Unit = ()
}
class CloudwatchMetricsService(config: AWSDiscoveryConfig) extends MetricsService with Logging {
private val credentials: AWSCredentialsProvider = AwsCredentials()
private val cloudwatch: AmazonCloudWatch = AmazonCloudWatchClientBuilder.standard()
.withCredentials(credentials)
.withRegion(config.region)
.build()
// These must be exactly the same as in the alarm, without any additional dimensions.
// CloudWatch will not aggregate custom metrics
val defaultDimensions = List(("Stack", config.stack), ("Stage", config.stage))
def updateMetrics(metrics:List[MetricUpdate]): Unit = {
val dimensions = defaultDimensions
updateMetrics(metrics, dimensions)
}
private def updateMetrics(metrics:List[MetricUpdate], dimensionValues: List[(String, String)] = List()): Unit = {
val dimensions = dimensionValues.map{
case (name, value) => new Dimension().withName(name).withValue(value)
}
val metricsData = metrics.map(m => Metrics.metricDatum(m.name, dimensions, m.value))
try {
val request = new PutMetricDataRequest()
.withNamespace(Metrics.namespace)
.withMetricData(
// As above, the metric must have the same unit as the alarm, in this case no unit
metricsData : _*
)
cloudwatch.putMetricData(request)
} catch {
case NonFatal(e) =>
logger.warn(s"Unable to report ${metrics.map(_.name).mkString(",")} to Cloudwatch", e)
}
}
def updateMetric(metricName: String, metricValue: Double = 1): Unit =
updateMetrics(List(MetricUpdate(metricName, metricValue)))
def recordUsageEvent(userEmail: String): Unit = {
val standardisedStage = if (config.stack == "pfi-giant") "PROD" else "CODE"
val dimensions = List(("App", "Giant"), ("Stage", standardisedStage), ("UserEmail", userEmail))
updateMetrics(List(MetricUpdate(Metrics.usageEvents, 1)), dimensions)
}
def recordSearchInFolderEvent(userEmail: String): Unit = {
val standardisedStage = if (config.stack == "pfi-giant") "PROD" else "CODE"
val dimensions = List(("App", "Giant"), ("Stage", standardisedStage), ("UserEmail", userEmail))
updateMetrics(List(MetricUpdate(Metrics.searchInFolderEvents, 1)), dimensions)
}
}